1. 信号初探
实时 & 标准 信号
1
Linux supports both POSIX reliable signals (hereinafter "standard signals") and POSIX real-time signals.
| 特性 | 标准信号 | 实时信号 |
|---|---|---|
| 编号范围 | 1–31 | 32–64 (SIGRTMIN–SIGRTMAX) |
| 可靠性 | 可能丢失或合并 | 严格队列化,不丢失 |
| 优先级 | 无 | 低编号优先(如 SIGRTMIN > SIGRTMIN+1) |
| 数据传递 | 不支持 | 支持(通过 sigqueue + siginfo_t) |
| 典型用途 | 进程控制、错误处理 | 实时应用、线程通信 |
标准信号
- 信号丢失 如果同一信号在未处理期间多次到达,仅保留最后一次(如快速按多次 Ctrl+C 可能只触发一次 SIGINT)。
- 无优先级 所有标准信号平等,无传递顺序保证。
- 无附加信息 信号本身不携带数据(仅能通过全局变量传递额外信息)。
实时信号
信号队列化 同一信号多次到达时会按顺序排队,不会丢失(如连续发送 3 次 SIGRTMIN+1 会触发 3 次处理)。
优先级支持 低编号信号优先传递(如 SIGRTMIN 优先级高于 SIGRTMIN+1)。
携带附加数据 可通过 sigqueue() 发送信号时附加整数或指针数据。
kill & signal
1
kill 就是向进程发送对应信号 ~
1
2
3
4
5
// kill 命令
kill -l # 列出所有信号名称和编号
kill -SIGNAME PID # 发送指定信号(如 `SIGTERM`)
kill -s SIGNAME PID # 同上(兼容性写法)
kill -9 PID # 强制杀死进程(SIGKILL 不可捕获)
1
2
3
4
5
6
7
8
9
xm@hcss-ecs-4208:~$ man 7 signal
Signal Value Action Comment
──────────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal
or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
...
一些记载
某些信号不可被忽略
| 信号名称 | 编号 | 默认行为 | 说明 |
|---|---|---|---|
SIGKILL | 9 | 强制终止 | 立即终止目标进程(管理员最后的杀手锏,无法被阻塞、捕获或忽略)。 |
SIGSTOP | 19 | 强制暂停 | 暂停目标进程的执行(用于调试或作业控制,无法被捕获或忽略)。 |
2.signal 问题
橄榄
1
2
3
4
5
6
7
8
9
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
// 信号处理时阻塞 & 退出信号处理时解除阻塞
* If the disposition is set to a function, then first either the disposition is reset to SIG_DFL, or the signal is blocked (see Portability below), and then handler is called with argument signum. If invocation of the handler caused the signal to be blocked, then the signal is unblocked upon return from the handler.
几个问题
1. exec 将原先要捕捉的信号设为默认,其他信号则保持不变
1
2
3
4
5
6
7
8
9
10
11
12
int main() {
// 原程序捕获 SIGINT
signal(SIGINT, [](int) { puts("Ignored SIGINT"); });
// 忽略 SIGTERM
signal(SIGTERM, SIG_IGN);
// exec 后:
// - 如果原程序对某个信号设置了自定义处理函数(如 SIGINT),exec 后该信号的处理方式会恢复为默认行为(SIG_DFL)。
// - 如果原程序显式忽略了某个信号(如 SIGTERM),exec 后该信号仍会被忽略(SIG_IGN 状态保留)。
execl("/path/to/new_program", "new_program", NULL);
}
2. 如何安全地捕获 SIGINT 和 SIGQUIT
仅当信号未被忽略时才绑定自定义处理函数
1
2
3
4
5
6
7
8
9
10
11
void sig_int(int), sig_quit(int);
// 检查 SIGINT 是否被忽略,若未被忽略则绑定 handler
if (signal(SIGINT, SIG_IGN) != SIG_IGN) {
signal(SIGINT, sig_int);
}
// 检查 SIGQUIT 是否被忽略,若未被忽略则绑定 handler
if (signal(SIGQUIT, SIG_IGN) != SIG_IGN) {
signal(SIGQUIT, sig_quit);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1.再解释 信号处理的几种方式
信号处理的三种状态
每个信号都有三种可能的处理方式:
默认行为(SIG_DFL)
由系统预定义(如 SIGINT 默认终止进程,SIGQUIT 默认终止并生成core文件)。
忽略信号(SIG_IGN)
明确告诉系统:"这个信号来了不要做任何事"。
自定义处理函数
程序员自己写的函数,用于响应信号。
// 2.为什么不能直接覆盖?
假设有一个守护进程(daemon)启动时做了以下操作:
signal(SIGINT, SIG_IGN); // 显式忽略SIGINT
然后你的程序这样写:
void handler(int sig) { /*...*/ }
signal(SIGINT, handler); // 强行覆盖SIG_IGN
// 3. 会引发什么问题
破坏系统设计意图
守护进程忽略 SIGINT 是有原因的(比如防止被误杀),你的覆盖会导致它失去保护。
违反最小惊讶原则
其他程序员或工具可能默认认为 SIGINT 是被忽略的,你的修改会引入隐蔽的Bug。
// 4. 是否只针对 sigint & sigquit
否 针对所有信号
3. 子进程fork后
继承父进程的信号处理方式, 因为fork 会复制父进程的内存镜像, 处理函数在子进程中是有意义的。
不可靠信号
- 触发wilie sig_int_flag = 0
- 触发信号
- pause 挂起, 此时信号为不可靠信号 ~
- 此时sig_int_flag = 1 , 但是进程会一直挂死在pause这里。
1
pause() causes the calling process (or thread) to sleep until a signal is delivered that either terminates the process or causes the invocation of a signal-catching function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 这段代码并不准确 但是在大多数时候都能正常工作 发生问题难以排查
int sig_int_flag = 0; // 标志位,信号发生时设为非零
void sig_int(int signo) { // 信号处理函数
sig_int_flag = 1; // 设置标志位
signal(SIGINT, sig_int); // 重新绑定(应对某些系统的自动重置)
}
int main() {
signal(SIGINT, sig_int); // 注册信号处理函数
while (sig_int_flag == 0) { // 等待信号触发
pause(); // 挂起进程,直到信号到达
}
// 信号发生后执行的逻辑
printf("Signal received!\n");
return 0;
}
中断的系统调用
低速系统调用:指那些可能阻塞进程执行的系统调用,通常涉及I/O操作或进程间通信,其完成时间不确定,取决于外部事件(如数据到达、信号触发等)
- 低速系统调用中断
1
2
3
4
5
// 若进程收到信号且该信号未被忽略,系统调用会被中断(interrupted),立即返回错误,并设置 errno 为 EINTR(Interrupted system call)。
n = read(fd, buf, sizeof(buf));
if (n == -1 && errno == EINTR) {
// 系统调用被信号中断
}
- 非阻塞或高速系统调用(如 getpid, time) 通常不会被信号中断,因为这些调用能立即完成。
3. 可重入函数
可重入性与线程安全性:两种并发问题的防御策略
一、根本原因:两种并发威胁的差异
1. 可重入性要防御的是「自我入侵」
场景:
同一个执行流(如单线程)在函数执行完成前再次进入该函数。
典型情况:
- 递归调用
- 中断处理(硬件中断抢占当前执行流)
- 信号处理函数(信号可能打断正在执行的相同函数)
核心矛盾:
函数被自己”打断”时,如果依赖内部静态状态就会崩溃。
1
2
3
4
5
6
// static 为什么不可重入? 再次调用会复用原来的 static 变量
// 不可重入函数示例
char *strtok(char *str, const char *delim) {
static char *last; // ← 静态变量导致重入时状态被破坏
/* ... */
}
2. 线程安全性要防御的是「外部入侵」
场景:
多个线程同时调用同一函数。
典型情况:
- 多线程共享数据
- 异步任务处理
核心矛盾:
并发访问共享资源导致数据竞争。
1
2
3
4
5
// 非线程安全示例
void unsafe_counter() {
static int count = 0;
count++; // ← 多线程同时执行会导致计数错误
}
二、技术实现差异(防御策略不同)
| 防御策略 | 可重入函数 | 线程安全函数 |
|---|---|---|
| 状态存储位置 | 只允许栈变量或参数传递 | 允许全局变量+同步机制 |
| 同步方式 | 不需要锁(根本无共享状态) | 需要锁/原子操作 |
| 性能开销 | 零额外开销 | 有锁竞争开销 |
| 典型技术 | 线程局部存储(TLS) | 互斥锁(mutex)、信号量 |
类比理解:
- 可重入:像「无状态微服务」——每次调用自带完整上下文,不依赖外部存储。
- 线程安全:像「带门禁的共享仓库」——允许多工人进出,但每次只准一人操作货架。
三、必须分开设计的现实案例
案例1:信号处理函数(需可重入但无需线程安全)
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <signal.h>
#include <unistd.h>
// 信号处理函数必须是可重入的(可能打断正在执行的相同函数)
void handler(int sig) {
// 这里不能用printf(非可重入),只能用write
write(STDOUT_FILENO, "Signal!\n", 8);
}
int main() {
signal(SIGINT, handler);
while(1);
}
为什么: 若在handler执行时又收到信号,不可重入的函数会导致堆栈/状态混乱。 为什么“随时可能打断执行”要求信号处理函数是可重入的? 假设程序正在运行某段代码,突然收到了一个信号,内核立刻暂停当前执行,在原地直接跳转去执行你的 handler(信号处理函数)。 而 handler 函数的执行环境其实是与被中断的代码共用同一个栈、同一个内存空间、同一个线程上下文的! 例如,handler 中调用了 printf() 或 malloc(),而此时主程序也在调用这些函数。因为这些函数内部用了静态变量或全局状态(比如堆分配器的元数据),两者交错执行时可能:
- 锁丢失
- 内部状态错乱
- 崩溃、死循环、内存破坏
当你在一个信号处理函数(比如 SIGINT 的 handler)还没执行完时,如果系统又收到了同样的信号,且该信号没有被阻塞(blocked)或自动屏蔽(auto-masked),就可能会再次调用同一个 handler,导致递归执行或重入调用。
案例2:线程池计数器(需线程安全但不必可重入)
1
2
3
4
5
6
7
8
9
10
11
#include <pthread.h>
int counter = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
// 线程安全但不可重入(用锁保护全局变量)
void increment() {
pthread_mutex_lock(&lock);
counter++; // 受保护的全局状态
pthread_mutex_unlock(&lock);
}
为什么:
多线程并发调用时,锁能保证原子性,但若函数递归调用自身会死锁。
四、终极判断法则
当您不确定该用哪种时,问两个问题:
- 这个函数会被自己打断吗?(递归/信号/中断→需要可重入)
- 这个函数会被其他线程调用吗?(多线程→需要线程安全)
决策树:
1
2
3
4
5
graph TD
A[函数需要并发安全?] -->|是| B{会被自己打断吗?}
B -->|是| C[必须可重入]
B -->|否| D[只需线程安全]
A -->|否| E[无需特殊处理]
五、历史教训:混用的灾难
- OpenSSL早期漏洞:在信号处理函数中调用了非可重入的malloc,导致被攻击者利用引发崩溃。
- Linux内核竞态条件:未正确区分可重入和线程安全,导致中断上下文死锁。
总结
区分这两者是因为:
- 威胁来源不同:自己 vs 他人
- 防御成本不同:可重入需要从代码结构上根治,线程安全可通过后期加锁解决
- 适用场景不同:信号/中断必须可重入,多线程必须线程安全
就像「防病毒」和「防火」需要不同的措施,虽然都是安全防护,但解决的是不同维度的危险。
4. 可重入函数 和 异步信号安全的函数
概览
| 方面 | 可重入函数(Reentrant) | 异步信号安全函数(Async-Signal-Safe) |
|---|---|---|
| 定义 | 函数在中途被打断(例如中断/信号/递归)再次被调用,依然能正确运行 | 函数在信号处理函数中被调用是安全的 |
| 场景 | 中断、递归、线程、ISR 等可能“自我打断”的情况 | 信号处理程序里调用 |
| 标准 | 编程习惯/工程原则 | POSIX 标准定义了一组函数是 async-signal-safe 的 |
| 本质要求 | 不依赖全局或静态变量,内部状态不被共享 | 不使用不可重入函数,不调用非 signal-safe 的系统调用 |
| 举例 | strcpy()(如果不使用静态变量) | write(), _exit(), signal()(参见 POSIX 列表) |
- 所有 异步信号安全函数都必须是可重入的(信号处理可能随时中断)。
- 但 可重入函数不一定异步信号安全(可能内部调用了非信号安全函数,如 malloc())。
- 异步信号安全函数 不仅要避免共享状态,还必须只使用 POSIX 明确允许的系统调用。
- 可重入函数更宽泛,是通用并发下的设计原则。
1
2
3
4
5
6
7
8
9
10
11
12
13
一、什么是可重入函数
这个函数可以“被多次调用”而不会搞乱状态。
常见于:递归、多个线程同时调用。
只要你不给它共享数据,它就不会乱。
二、什么是异步信号
程序正跑着,突然被打断(比如来了个 SIGINT 信号)。
这时跳去执行信号处理函数。
异步信号安全指的是:就算被打断了,也能安全地调用这个函数。
异步信号安全函数
1
2
3
4
5
// 查看那些函数是异步线程安全的
man 7 signal-safety
An async-signal-safe function is one that can be safely called from within a signal handler. Many functions are not async-signal-safe. In particular, nonreentrant functions
are generally unsafe to call from a signal handler.
不可重入函数的几点
为什么静态数据结构(函数内部使用的 static 局部变量、 全局变量(global variables)、或者依赖 静态分配内存 的数据结构(如 malloc 管理的全局链表))会导致函数不可重入
- 这些变量在多次调用函数之间共享同一块内存,具有持久性,不会随着函数退出而销毁。
- 静态变量在多个调用间是共享的
- 如果一个函数执行到一半被中断,另一个“重入”的调用又改了同一个静态变量
- 那么第一个调用再恢复时状态已被篡改
静态数据结构使函数状态在不同调用之间共享,而可重入函数要求每次调用互相独立,因此两者本质冲突。
静态数据结构既不是可重入的,也不是线程安全的。多个线程同时访问同一块静态内存(如静态链表、全局计数器)
malloc printf 等标注IO 也是不可重入的
要注意errno ~ 哪怕函数是可重入函数 也会覆盖errno
举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void my_alarm(int signo)
{
struct passwd *rootptr;
printf("in signal handler\n");
if ((rootptr = getpwnam("root")) == NULL)
err_sys("getpwnam(root) error");
alarm(1);
}
int main(void)
{
struct passwd *ptr;
signal(SIGALRM, my_alarm);
alarm(1);
for(;;) {
if ((ptr = getpwnam("sar")) == NULL)
err_sys("getpwnam error");
if (strcmp(ptr->pw_name, "sar") != 0)
printf("return value corrupted, pw_name=%s\n", ptr->pw_name);
}
}
- getpwnam 会调用free
- my_alarm 是非可重入函数
- main -> getpwnam -> free && my_alarm -> getpwnam -> free 后冲突, 会出core.
- 在信号处理函数中调用非可重入函数 结局不可预期.
4. sigcld
SIGCHLD 或 SIGCLD 在子进程结束(无论是正常退出还是异常终止)时发送给父进程。这个信号的目的是告知父进程某个子进程的状态已经改变,并且父进程可以选择通过 wait() 或 waitpid() 等系统调用来获取子进程的退出状态。
当父进程接收到 SIGCHLD 信号时,操作系统确实会向父进程发送 SIGCHLD 信号,通知父进程某个子进程已终止。然而,操作系统不会自动调用 wait() 来回收子进程资源。wait() 需要由父进程显式调用来清理已经终止的子进程,防止它们成为僵尸进程。
父进程可以通过信号处理程序来定制如何处理 SIGCHLD 信号。常见的做法是捕获 SIGCHLD 信号,防止操作系统默认的行为(如暂停信号),然后通过 wait() 或 waitpid() 手动回收子进程的资源,避免产生僵尸进程。
父进程直接wait 也可以 , 为什么非要sigcld 信号? SIGCHLD 信号提供了一种更为灵活和非阻塞的方式来处理子进程的终止。父进程可以选择不被阻塞,直到有子进程退出时再去处理它。例如,父进程可以在信号处理程序中调用 wait() 或 waitpid() 来回收子进程资源,而不需要在主程序中同步阻塞等候。
如果父进程选择不显式处理 SIGCHLD 信号,操作系统会采取默认行为。通常情况下,操作系统会在父进程忽略 SIGCHLD 信号时自动清理子进程资源,避免僵尸进程的产生。
举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static void sig_cld(int); // 声明信号处理程序
int main()
{
pid_t pid;
if (signal(SIGCLD, sig_cld) == SIG_ERR) // 注册信号处理程序
perror("signal error");
if ((pid = fork()) < 0) // 创建子进程
perror("fork error");
else if (pid == 0) { /* child */
sleep(2); // 子进程等待
_exit(0); // 子进程退出
}
pause(); // 父进程暂停等待信号
exit(0); // 父进程退出
}
static void sig_cld(int signo) /* 中断 pause() 的信号处理程序 */
{
pid_t pid;
int status;
printf("SIGCLD received\n");
if (signal(SIGCLD, sig_cld) == SIG_ERR) // 重新设置信号处理程序
perror("signal error");
if ((pid = wait(&status)) < 0) // 父进程回收子进程资源
perror("wait error");
printf("pid = %d\n", pid);
}
为什么需要立即重新注册信号处理程序? 在 Unix/Linux 信号处理机制中,当信号触发并跳转到用户自定义的处理函数时,某些历史版本的信号实现(尤其是传统的 System V)会将该信号的处理方式自动重置为默认行为(SIG_DFL)。如果处理函数中不立即重新注册(即调用 signal() 重置),会导致以下问题:信号丢失风险 or 时间窗口问题。 某些历史实现(如System V)中,signal()注册的处理函数执行后会自动重置为SIG_DFL。(现代 linux 并不会,现代linux内核会自动将该信号添加到进程的信号屏蔽字中(阻塞该信号))
如何处理? 用 sigaction() 替代 signal(),通过标志位自动处理重置问题,避免手动注册的复杂性
signal(SIGCLD, handler); // 先重置信号处理程序问题标注 ```c // 在信号处理函数(如 SIGCLD 的处理函数)开头直接调用 signal() 重新注册信号,会导致: // 内核发现仍有子进程未处理 → 重复触发信号 → 处理函数被无限递归调用 → 最终栈溢出崩溃。
// 正确做法 // 必须在调用 wait() 回收子进程状态后,再调用 signal() 重新注册信号。这样内核确认子进程已处理,不会重复发送信号。 void handler(int sig) { while (wait(NULL) > 0); // 先回收所有终止的子进程 signal(sig, handler); // 再重新注册信号 }
1
2
3
4
5
6
7
8
9
10
11
12
13
## 4. 可靠信号的术语 和 语义
### 进程会自己给自己发信号吗?
进程可以通过系统调用(如 kill() 或 raise())主动向自己发送信号
```c
#include <signal.h>
int main() {
raise(SIGTERM); // 向当前进程发送 SIGTERM(等同于 kill(getpid(), SIGTERM))
return 0;
}
除以0 怎么操作的?
1
2
3
4
// 1. x86/ARM CPU 会抛出 #DE(Divide Error)异常(中断号 0)。
// 2. CPU 暂停当前进程的执行,切换到内核模式,由操作系统处理。
// 3. 内核向违规进程发送 SIGFPE(Signal Floating-Point Exception)
// 4. 信号默认行为:终止进程(Terminate) 并生成 Core Dump(如果启用)。
信号本质
从底层实现的角度来看,所有信号的发送最终都是由内核完成的,无论是进程间发送、进程给自己发送,还是内核主动发送。以下是更本质的解释:
1
2
3
4
5
6
7
8
9
10
11
//在 Unix/Linux 系统中:
//用户态进程无法直接向另一个进程发送信号,必须通过系统调用(如 kill())请求内核代为处理。
//内核是信号的中枢,负责:
//1. 验证发送者权限(如:是否有权向目标进程发送信号)。
//2. 将信号写入目标进程的信号队列。
//3. 在目标进程的上下文切换时,触发信号处理。
//关键结论
//所有信号发送的最终执行者都是内核,用户进程只是发起请求。
//你三种分类(A→B、A→A、内核→A)是逻辑上的分类,用于描述信号的触发场景,但底层均依赖内核。
信号过程
信号的生命周期:产生、未决与递送 信号在进程中的传递过程可分为三个阶段:
1. 信号产生(Generation)
- 触发来源: 内核(如硬件异常、子进程终止)、其他进程(通过 kill())或进程自身(如 raise())。
- 此时信号状态: 已生成但尚未被目标进程处理。
2. 信号未决(Pending)
- 定义: 从信号产生到实际递送给进程的中间状态。
- 信号被加入进程的 pending 队列,等待被处理。
- 可能阻塞: 若进程通过 sigprocmask() 屏蔽了该信号,信号会保持未决状态,直到解除屏蔽。
3. 信号递送(Delivery)
- 内核行为: 将信号从 pending 队列取出,执行以下操作之一:
- 调用用户注册的信号处理函数。
- 执行默认行为(如终止进程)。
- 忽略信号(若设置为 SIG_IGN)。
信号阻塞
在 Linux/Unix 系统中,进程可以阻塞(Block)某些信号,即暂时不接收这些信号,直到解除阻塞后才会处理。这是进程信号管理的重要机制之一。
作用:
- 延迟信号处理:进程可以选择性地屏蔽某些信号,先完成关键任务,再处理信号。 - 避免竞态条件:防止信号中断关键代码段(如共享资源操作)。 - 自定义信号处理逻辑:结合 sigprocmask() 和 sigpending() 实现更灵活的信号管理。
状态:
- Pending(待处理):信号已发送给进程,但尚未被处理(可能因为被阻塞)。 - Blocked(阻塞):进程主动屏蔽某些信号,它们会被加入 Pending 队列,直到解除阻塞。 - Delivered(已递送):信号已被进程处理(调用信号处理函数或默认行为)。
函数调用: | 函数 | 作用 | 参数说明 | 返回值 | |——————–|———————————————————————-|———————————————————————————————-|———————-| | sigprocmask() | 设置/修改进程的信号屏蔽字(Blocked 信号集) | - how: SIG_BLOCK(阻塞)、SIG_UNBLOCK(解除阻塞)、SIG_SETMASK(直接设置)
- set: 要操作的信号集
- oldset: 保存旧的信号集(可置 NULL) | 成功返回 0,失败返回 -1 | | sigpending() | 获取当前被阻塞且未处理的信号(Pending 信号集) | - set: 输出参数,存储待处理信号集 | 成功返回 0,失败返回 -1 | | sigaction() | 注册信号处理函数(可指定是否自动阻塞信号) | - signum: 信号编号(如 SIGINT)
- act: 新处理动作(含处理函数和标志位)
- oldact: 保存旧处理动作(可置 NULL) | 成功返回 0,失败返回 -1 |
5. 结合 alarm & pause 来谈谈信号处理的经典案例 sleep
// sleep 源码 第一版
1
2
3
4
5
6
7
8
9
10
static void sig_alm(int signo) { /* 仅唤醒pause,不处理逻辑 */ }
unsigned int sleep(unsigned int seconds) {
if (signal(SIGALRM, sig_alm) == SIG_ERR)
return seconds; // 信号处理注册失败时直接返回
alarm(seconds); // 启动定时器
pause(); // 挂起进程,等待SIGALRM
return alarm(0); // 关闭定时器并返回剩余时间
}
sleep 第一版问题
- 若调用sleep1前已设置闹钟,函数内的alarm(seconds)会覆盖原有闹钟,导致原定时器失效。
- sleep1修改了SIGALRM的信号处理函数,但未在返回前恢复原始配置,可能影响其他依赖该信号的代码。
- 若alarm在pause前超时,信号处理程序已执行,pause将永久挂起进程。
sleep 版本二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static jmp_buf env_alrm;
static void sig_alrm(int signo) {
longjmp(env_alrm, 1);
}
unsigned int sleep2(unsigned int seconds) {
if (signal(SIGALRM, sig_alrm) == SIG_ERR)
return seconds;
if (setjmp(env_alrm) == 0) {
alarm(seconds); // 启动定时器
pause(); // 挂起进程
}
return alarm(0); // 返回剩余时间
}
解决的问题:
- sleep2通过setjmp/longjmp强制跳转,即使pause未执行也能正确返回,避免了竞态条件。
仍然遗留的问题:
- 若SIGALRM中断了其他信号处理程序(如SIGINT),longjmp会强制终止该处理程序的执行,导致未完成的操作(如资源清理)被跳过。
1 2 3 4 5
volatile int k; // 防止编译器优化 void sigint_handler(int sig) { for (k = 0; k < 1000000000; k++); // 长时间循环 printf("SIGINT处理完成\n"); // 可能永远不会执行 }
- 不可重入:longjmp跳转后,被中断的信号处理程序可能遗留部分操作未完成(如文件未关闭、锁未释放),引发资源泄漏或状态不一致。
疑问:信号处理函数还会被打断吗
- 信号处理函数执行期间,相同信号会被屏蔽(不会被再次打断),但不同信号可能中断当前处理函数。例如:
- 若正在处理SIGALRM时再次收到SIGALRM,该信号会被挂起,直到处理结束。
- 若收到SIGINT则会立即中断SIGALRM的处理函数。
超时控制 版本一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void sig_alm(int);
int main(void) {
int n;
char line[MAXLINE];
if (signal(SIGALRM, sig_alm) == SIG_ERR)
err_sys("signal(SIGALRM) error");
alarm(10);
if ((n = read(STDIN_FILENO, line, MAXLINE)) < 0)
err_sys("read error");
alarm(0);
write(STDOUT_FILENO, line, n);
exit(0);
}
static void sig_alm(int signo) {
/* nothing to do, just return to interrupt the read */
}
问题:
- 竞争条件(Race Condition) ,在alarm()调用和read()调用之间存在时间窗口,若进程在这两个调用之间被内核阻塞(如进程调度),且阻塞时间超过闹钟设定时间,会导致:
- SIGALRM信号在read()开始前就已触发
- 后续read()调用将永久阻塞(无超时保护)
- 自动重启动(Automatic Restart)干扰, SIGALRM信号处理完成后,read会自动恢复执行而非被中断, 导致超时机制完全失效(即使触发信号,read仍继续阻塞)。
超时控制 版本二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static void sig_alrm(int);
static jmp_buf env_alrm;
int main(void)
{
int n;
char line[MAXLINE];
if (signal(SIGALRM, sig_alrm) == SIG_ERR)
err_sys("signal(SIGALRM) error");
if (setjmp(env_alrm) != 0)
err_quit("read timeout");
alarm(10);
if ((n = read(STDIN_FILENO, line, MAXLINE)) < 0)
err_sys("read error");
alarm(0);
write(STDOUT_FILENO, line, n);
exit(0);
}
static void sig_alrm(int signo)
{
longjmp(env_alrm, 1);
}
不管系统是否重新启动被中断的系统调用, 程序都会预期的那样工作。
6.信号集
概览
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <signal.h>
int sigemptyset(sigset_t *set); // 清空信号集(不含任何信号)
int sigfillset(sigset_t *set); // 填充所有信号到集合中
int sigaddset(sigset_t *set, int signum); // 添加指定信号(如 SIGINT)
int sigdelset(sigset_t *set, int signum); // 删除指定信号
int sigismember(const sigset_t *set, int signum); // 检查信号是否存在
/*
SIG_BLOCK:将 set 中的信号加入当前阻塞列表。
SIG_UNBLOCK:从阻塞列表中移除 set 中的信号。
SIG_SETMASK:直接设置阻塞列表为 set。
*/
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);// 阻塞信号
int sigpending(sigset_t *set); // 查看阻塞且未处理的信号(未决信号)
//sigaction 中指定处理信号时的阻塞行为 后续章节补充
struct sigaction sa;
sa.sa_mask = mask; // 设置处理函数执行期间阻塞的信号集
sigaction(SIGINT, &sa, NULL);
- 线程安全:在多线程环境中,应使用 pthread_sigmask 替代 sigprocmask。 2. 不可靠信号:某些早期信号(如 SIGKILL、SIGSTOP)无法被阻塞或忽略。 3. 信号集大小:sigset_t 的大小通常由系统决定(如 Linux 中为 1024 位,支持实时信号)。
信号集(sigset_t)的位操作
1
2
3
4
5
6
#define sigemptyset(ptr) (*(ptr) = 0) // 所有位清0
#define sigfillset(ptr) (*(ptr) = ~(signed int)0) // 所有位置1(注意逗号运算符返回0)
*set |= 1 << (signo - 1); // 位或操作开启特定位
*set &= ~(1 << (signo - 1)); // 位与操作关闭特定位
7. 经典api
sigprocmask
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Prototype for the glibc wrapper function */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
sigprocmask() is used to fetch and/or change the signal mask of the calling thread. The signal mask is the set of signals whose delivery is currently blocked for the caller
(see also signal(7) for more details).
SIG_BLOCK
The set of blocked signals is the union of the current set and the set argument.
SIG_UNBLOCK
The signals in set are removed from the current set of blocked signals. It is permissible to attempt to unblock a signal which is not blocked.
SIG_SETMASK
The set of blocked signals is set to the argument set.
If oldset is non-NULL, the previous value of the signal mask is stored in oldset.
If set is NULL, then the signal mask is unchanged (i.e., how is ignored), but the current value of the signal mask is nevertheless returned in oldset (if it is not NULL).
A set of functions for modifying and inspecting variables of type sigset_t ("signal sets") is described in sigsetops(3).
The use of sigprocmask() is unspecified in a multithreaded process; see pthread_sigmask(3).
应用举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void pr_mask(const char *str)
{
sigset_t sigset;
int errno_save;
errno_save = errno; /* we can be called by signal handlers */
if (sigprocmask(0, NULL, &sigset) < 0) {
err_sys("sigprocmask error");
} else {
printf("%s", str);
if (sigismember(&sigset, SIGINT))
printf(" SIGINT");
if (sigismember(&sigset, SIGQUIT))
printf(" SIGQUIT");
if (sigismember(&sigset, SIGUSR1))
printf(" SIGUSR1");
if (sigismember(&sigset, SIGALRM))
printf(" SIGALRM");
/* remaining signals can go here */
printf("\n");
}
errno = errno_save; /* restore errno */
}
sigpending
1
2
3
4
5
int sigpending(sigset_t *set);
DESCRIPTION
sigpending() returns the set of signals that are pending for delivery to the calling thread (i.e., the signals which have been raised while blocked). The mask of pending sig‐
nals is returned in set.
| 函数 | 查询内容 | 是否反映信号的实际到达 |
|---|---|---|
sigprocmask | 当前阻塞的信号集(哪些信号被屏蔽) | ❌ 不反映信号是否已到达 |
sigpending | 当前未决的信号集(已到达但被阻塞) | ✅ 反映信号是否已到达 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 举例说明 演示信号处理的全生命周期管理
// 1. 设置自定义处理函数 → 2. 临时阻塞信号 → 3. 检查未决信号 → 4. 解除阻塞并处理信号 → 5. 恢复默认行为。
static void sig_quit(int);
int main(void) {
sigset_t newmask, oldmask, pendmask;
if (signal(SIGQUIT, sig_quit) == SIG_ERR)
err_sys("can't catch SIGQUIT");
/* Block SIGQUIT and save current signal mask */
sigemptyset(&newmask);
sigaddset(&newmask, SIGQUIT);
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) // 阻塞 SIGQUIT,防止其立即中断程序。
err_sys("SIG_BLOCK error");
sleep(5); /* SIGQUIT here will remain pending */
if (sigpending(&pendmask) < 0)
err_sys("sigpending error");
if (sigismember(&pendmask, SIGQUIT)) // 验证在阻塞期间是否有 SIGQUIT 信号到达(如用户按下 Ctrl+\),但被暂存为未决状态。
printf("\nSIGQUIT pending\n");
/* Restore signal mask which unblocks SIGQUIT */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
printf("SIGQUIT unblocked\n");
sleep(5); /* SIGQUIT here will terminate with core file */
exit(0);
}
static void sig_quit(int signo) {
printf("caught SIGQUIT\n");
if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
err_sys("can't reset SIGQUIT");
}
1
2
3
4
5
6
7
8
// 结果
$ ./a.out
^\ # 第一次按Ctrl+\(阻塞期间,信号被暂存)
SIGQUIT pending # 发现未决信号
caught SIGQUIT # 解除阻塞后处理信号 // 关键行为:解除阻塞时,系统会检查是否有未决的 SIGQUIT 信号。如果有,则立即递送该信号给进程。
SIGQUIT unblocked
^\ # 第二次按Ctrl+\(默认行为生效) // 因处理函数中已重置为默认行为(SIG_DFL),直接终止进程。
QUIT (coredump) # 进程终止
几点问题说明:
多次信号被阻塞如何处理? 现象:在阻塞期间(如 sleep(5))多次触发同一信号(如连续按 Ctrl+\),系统仅保留一次未决信号。 示例:阻塞期间按10次 Ctrl+\,解除阻塞后仍只递送一次 SIGQUIT。
恢复信号屏蔽字后,是否会立即执行未决信号? 是的, 必须通过 sigprocmask(SIG_SETMASK, &oldmask, NULL) 恢复旧的屏蔽字(而非 SIG_UNBLOCK),确保精准解除阻塞。 若有未决信号,解除阻塞后内核会立即递送信号,触发注册的处理函数(如 sig_quit)。
哪些信号不可阻塞?所有信号都可阻塞吗? SIGKILL 和 SIGSTOP 绝对不可阻塞(无论通过 sigprocmask 或 signal/sigaction)。 理论上,除上述两种信号外,所有其他信号均可被阻塞(如 SIGINT、SIGQUIT、SIGTERM 等)。
sigaction
1
2
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
| 特性 | signal() | sigaction() |
|---|---|---|
| 标准 | 传统 UNIX(行为可能不一致) | POSIX 标准(行为一致) |
| 功能 | 简单,仅设置信号处理函数 | 更强大,支持信号阻塞、额外信息获取等 |
| 可移植性 | 不同系统行为可能不同(如是否自动重启) | 行为一致,推荐使用 |
| 信号嵌套控制 | 无明确控制 | 可通过 sa_mask 和 sa_flags 控制 |
| 信号信息获取 | 无法获取发送者 PID 等额外信息 | 可通过 SA_SIGINFO 获取详细信息 |
(1) 行为一致性
signal():- 不同 UNIX 系统(如 Linux、BSD、Solaris)可能有不同的实现。
- 例如,某些系统在信号处理后会自动恢复默认行为(类似
SA_RESETHAND),而有些不会。
sigaction():- 是 POSIX 标准接口,行为在所有兼容系统上一致。
- 通过
sa_flags明确控制是否恢复默认行为(如SA_RESETHAND)。
(2) 信号处理期间的阻塞
signal():- 默认情况下,信号处理期间不会阻塞相同信号,可能导致递归调用(如信号处理函数内再次触发相同信号)。
sigaction():- 默认阻塞相同信号(防止递归),但可通过
SA_NODEFER取消阻塞。 - 可通过
sa_mask指定其他需要阻塞的信号。
- 默认阻塞相同信号(防止递归),但可通过
(3) 系统调用中断与重启
signal():- 某些系统在信号中断系统调用后不会自动重启(返回
EINTR),需手动处理。
- 某些系统在信号中断系统调用后不会自动重启(返回
sigaction():- 可通过
SA_RESTART明确指定是否自动重启被中断的系统调用(如read()、write())。
- 可通过
(4) 信号信息获取
signal():- 处理函数为
void (*handler)(int),只能接收信号编号。
- 处理函数为
sigaction():- 若设置
SA_SIGINFO,处理函数为void (*handler)(int, siginfo_t *, void *),可获取:- 发送者 PID(
siginfo_t->si_pid)。 - 信号来源(如
kill()或硬件异常)。 - 用户态上下文(
ucontext_t)。
- 发送者 PID(
- 若设置
8. sigsetjmp
问题: 当信号处理函数被调用时:
- 内核会自动将该信号添加到进程的信号屏蔽字中(阻塞该信号)
- 这是为了防止同一信号递归中断处理程序
- 如果此时使用longjmp跳出信号处理函数,信号屏蔽字的状态会如何变化?
关键影响: 标准行为问题:
- 直接使用longjmp跳出会导致信号保持阻塞状态
- 这意味着后续的同类型信号将无法送达进程
POSIX 解决方案:
1
2
3
4
5
6
7
8
// 使用sigsetjmp/siglongjmp替代常规版本
// 当savemask非零时,会保存和恢复信号屏蔽字
#include <setjmp.h>
#include <signal.h>
int sigsetjmp(sigjmp_buf env, int savemask);
void siglongjmp(sigjmp_buf env, int val);
举例说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 这段代码展示了正确处理信号与非本地跳转交互的标准模式,是Unix系统编程中信号处理的经典范例。
// 不是很理解
static void sig_usr1(int);
static void sig_alrm(int);
static sigjmp_buf jmpbuf;
static volatile sig_atomic_t canjump;
int main(void)
{
if (signal(SIGUSR1, sig_usr1) == SIG_ERR)
err_sys("signal(SIGUSR1) error");
if (signal(SIGALRM, sig_alrm) == SIG_ERR)
err_sys("signal(SIGALRM) error");
pr_mask("starting main: ");
if (sigsetjmp(jmpbuf, 1)) {
pr_mask("ending main: ");
exit(0);
}
canjump = 1; /* now sigsetjmp() is OK */
for (;;)
pause();
}
static void sig_usr1(int signo)
{
time_t starttime;
if (canjump == 0)
return; /* unexpected signal, ignore */
pr_mask("starting sig_usr1: ");
alarm(3); /* SIGALRM in 3 seconds */
starttime = time(NULL);
for (;;) { /* busy wait for 5 seconds */
if (time(NULL) > starttime + 5)
break;
}
pr_mask("finishing sig_usr1: ");
canjump = 0;
siglongjmp(jmpbuf, 1); /* jump back to main, don't return */
}
static void sig_alrm(int signo)
{
pr_mask("in sig_alrm: ");
}