# 6 信号机制 ## 6.1 完整概念讲解 ### 什么是信号 信号是Linux系统中最古老的进程间通信机制之一,本质上是一种**软件中断**。当某个事件发生时,内核会向目标进程发送一个信号,进程收到信号后会中断当前执行流程,转去执行预设的信号处理函数。 信号的核心特点: - **异步性**:信号的产生和处理是异步的,进程无法预知信号何时到来 - **简单性**:信号只携带一个整数编号,不携带额外数据 - **内核介入**:信号的发送和投递由内核完成 - **默认行为**:每个信号都有默认处理动作(终止、忽略、暂停、继续等) ### 为什么需要信号 1. **进程控制**:终止进程(SIGTERM/SIGKILL)、暂停进程(SIGSTOP/SIGTSTP) 2. **事件通知**:子进程退出(SIGCHLD)、定时器到期(SIGALRM) 3. **用户交互**:Ctrl+C(SIGINT)、Ctrl+Z(SIGTSTP) 4. **异常处理**:段错误(SIGSEGV)、除零错误(SIGFPE) 5. **资源限制**:CPU时间耗尽(SIGXCPU) ### 信号的生命周期 ``` 产生(Delivery) → 排队(Pending) → 抛递(Delivery) → 处理(Disposition) ``` - **产生**:硬件异常、内核事件、kill()调用、软件条件等 - **排队**:信号到达后进入pending状态(非实时信号只保留一次) - **抛递**:内核将信号投递给目标进程 - **处理**:执行信号处理函数或默认动作 ### 信号分类 | 类别 | 信号范围 | 特点 | | ---------- | -------- | ------------------------------ | | 非实时信号 | 1-31 | 不支持排队,多次发送只保留一次 | | 实时信号 | 34-64 | 支持排队,可携带额外数据 | ## 6.2 核心API/语法 ### signal() - 简单信号处理 ```c #include typedef void (*sighandler_t)(int signum); sighandler_t signal(int signum, sighandler_t handler); ``` | 参数 | 说明 | | ------- | ---------------------------------------------------- | | signum | 信号编号 | | handler | SIG_IGN(忽略)、SIG_DFL(默认)、自定义处理函数指针 | | 返回值 | 说明 | | ------ | ---------------------- | | 成功 | 之前的信号处理函数指针 | | 失败 | SIG_ERR | **注意**:signal()在不同Unix系统中行为不一致,Linux下会自动重启系统调用,但handler可能被重置为默认(不可靠)。 ### sigaction() - 可靠信号处理 ```c #include int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); ``` **struct sigaction 结构体**: ```c struct sigaction { union { void (*sa_handler)(int signum); // 简单处理函数 void (*sa_sigaction)(int signum, siginfo_t *info, void *context); // 高级处理函数 }; sigset_t sa_mask; // 信号屏蔽字,处理期间阻塞的信号 int sa_flags; // 选项标志 void (*sa_restorer)(void); // 已废弃,不要使用 }; ``` | sa_flags | 说明 | | ------------ | -------------------------------------------------- | | SA_SIGINFO | 使用sa_sigaction作为处理函数(可获取信号详细信息) | | SA_RESTART | 自动重启被中断的系统调用 | | SA_NOCLDSTOP | 子进程停止时不产生SIGCHLD | | SA_NOCLDWAIT | 子进程退出时不产生僵尸进程 | | SA_NODEFER | 处理期间不自动屏蔽当前信号 | | SA_RESETHAND | 处理一次后恢复为默认动作 | **siginfo_t 结构体**(需SA_SIGINFO标志): ```c typedef struct { int si_signo; // 信号编号 int si_code; // 信号来源 int si_errno; // 错误码 pid_t si_pid; // 发送进程PID uid_t si_uid; // 发送进程UID void *si_addr; // 内存地址(SIGSEGV) int si_status; // 退出状态(SIGCHLD) // ... 其他字段 } siginfo_t; ``` ### kill() 和 raise() ```c #include int kill(pid_t pid, int sig); // 发送信号给进程 int raise(int sig); // 发送信号给自己 int sigqueue(pid_t pid, int sig, const union sigval value); // 实时信号+数据 ``` **kill() 的 pid 参数**: | pid值 | 含义 | | --------- | ---------------------------- | | pid > 0 | 发送给指定PID的进程 | | pid == 0 | 发送给同进程组的所有进程 | | pid == -1 | 发送给所有有权限发送的进程 | | pid < -1 | 发送给进程组ID为-pid的进程组 | ### 信号屏蔽 ```c #include int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); int sigpending(sigset_t *set); int sigsuspend(const sigset_t *mask); ``` **sigprocmask 的 how 参数**: | how | 说明 | | ----------- | ------------------------- | | SIG_BLOCK | 将set中的信号添加到屏蔽字 | | SIG_UNBLOCK | 从屏蔽字中移除set中的信号 | | SIG_SETMASK | 将屏蔽字设置为set | **信号集操作函数**: ```c sigemptyset(sigset_t *set); // 清空信号集 sigfillset(sigset_t *set); // 填满信号集 sigaddset(sigset_t *set, int signum); // 添加信号 sigdelset(sigset_t *set, int signum); // 删除信号 sigismember(const sigset_t *set, int signum); // 检查信号是否在集合中 ``` ## 6.3 代码示例 ### 示例1:signal()基本用法 ```c #include #include #include void sigint_handler(int signum) { printf("收到SIGINT信号(%d),程序即将退出...\n", signum); _exit(0); } int main() { // 注册SIGINT信号处理函数 signal(SIGINT, sigint_handler); printf("程序运行中,按Ctrl+C测试...\n"); while(1) { sleep(1); } return 0; } ``` **编译命令**: ```bash gcc -o signal_demo signal_demo.c ./signal_demo ``` ### 示例2:sigaction()完整示例 ```c #include #include #include #include // 高级信号处理函数 void sig_handler(int signum, siginfo_t *info, void *context) { printf("信号编号: %d\n", signum); printf("发送进程PID: %d\n", info->si_pid); printf("发送进程UID: %d\n", info->si_uid); printf("信号来源代码: %d\n", info->si_code); if (signum == SIGSEGV) { printf("发生段错误,地址: %p\n", info->si_addr); } } int main() { struct sigaction sa; // 清空并设置sigaction结构体 sigemptyset(&sa.sa_mask); sa.sa_sigaction = sig_handler; sa.sa_flags = SA_SIGINFO; // 使用高级处理函数 // 注册SIGSEGV信号处理 if (sigaction(SIGSEGV, &sa, NULL) == -1) { perror("sigaction"); exit(1); } printf("程序运行中,触发段错误测试...\n"); // 触发段错误 int *p = NULL; *p = 100; return 0; } ``` **编译命令**: ```bash gcc -o sigaction_demo sigaction_demo.c ./sigaction_demo ``` ### 示例3:信号屏蔽与sigsuspend ```c #include #include #include volatile int received_sigusr1 = 0; void sigusr1_handler(int signum) { received_sigusr1 = 1; printf("收到SIGUSR1信号\n"); } int main() { struct sigaction sa; sigset_t block_set, old_set, wait_set; // 设置信号处理函数 sigemptyset(&sa.sa_mask); sa.sa_handler = sigusr1_handler; sa.sa_flags = 0; sigaction(SIGUSR1, &sa, NULL); // 创建要屏蔽的信号集 sigemptyset(&block_set); sigaddset(&block_set, SIGUSR1); // 阻塞SIGUSR1 sigprocmask(SIG_BLOCK, &block_set, &old_set); printf("PID: %d,等待SIGUSR1信号...\n", getpid()); // 等待信号(原子操作:解除阻塞并等待) sigemptyset(&wait_set); sigsuspend(&wait_set); printf("收到信号,继续执行...\n"); // 恢复原来的信号屏蔽字 sigprocmask(SIG_SETMASK, &old_set, NULL); return 0; } ``` **编译命令**: ```bash gcc -o signal_block signal_block.c ./signal_block & kill -USR1 ``` ### 示例4:SIGALRM定时器与SA_RESTART ```c #include #include #include #include #include volatile int alarm_count = 0; void alarm_handler(int signum) { alarm_count++; printf("ALRM: %d\n", alarm_count); // 每次触发后重新设置5秒定时器 if (alarm_count < 5) { alarm(5); } else { printf("定时器结束\n"); } } int main() { struct sigaction sa; char buf[100]; // 设置SIGALRM处理 sigemptyset(&sa.sa_mask); sa.sa_handler = alarm_handler; sa.sa_flags = SA_RESTART; // 自动重启被中断的read sigaction(SIGALRM, &sa, NULL); // 设置5秒定时器 alarm(5); printf("程序运行中,等待定时器...\n"); printf("也可以在终端输入内容测试EINTR:\n"); // 测试SA_RESTART效果 // 如果没有SA_RESTART,read会被信号中断返回-1 while (1) { printf("> "); fflush(stdout); if (read(STDIN_FILENO, buf, sizeof(buf)) == -1) { if (errno == EINTR) { printf("read被信号中断 (EINTR)\n"); continue; } perror("read"); break; } if (strncmp(buf, "quit", 4) == 0) { break; } printf("输入: %s", buf); } return 0; } ``` **编译命令**: ```bash gcc -o alarm_demo alarm_demo.c ./alarm_demo ``` ### 示例5:SIGCHLD处理子进程退出 ```c #include #include #include #include #include volatile int child_exited = 0; void sigchld_handler(int signum) { int status; pid_t pid; // 使用WNOHANG循环回收,避免遗漏 while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { if (WIFEXITED(status)) { printf("子进程%d正常退出,状态码: %d\n", pid, WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("子进程%d被信号%d终止\n", pid, WTERMSIG(status)); } child_exited = 1; } } int main() { struct sigaction sa; pid_t pid; // 设置SIGCHLD处理 sigemptyset(&sa.sa_mask); sa.sa_handler = sigchld_handler; sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // 不处理停止的子进程 sigaction(SIGCHLD, &sa, NULL); // 创建子进程 pid = fork(); if (pid == 0) { // 子进程 printf("子进程PID: %d,3秒后退出\n", getpid()); sleep(3); exit(42); // 退出状态码42 } // 父进程 printf("父进程PID: %d,子进程PID: %d\n", getpid(), pid); printf("等待子进程退出...\n"); while (!child_exited) { pause(); // 挂起等待信号 } printf("所有子进程已回收\n"); return 0; } ``` **编译命令**: ```bash gcc -o sigchld_demo sigchld_demo.c ./sigchld_demo ``` ### 示例6:实时信号与sigqueue ```c #include #include #include #include #include void sigrt_handler(int signum, siginfo_t *info, void *context) { printf("收到实时信号 %d, 数据值: %d\n", signum, info->si_value.sival_int); } int main() { struct sigaction sa; union sigval val; pid_t pid; // 设置实时信号处理 sigemptyset(&sa.sa_mask); sa.sa_sigaction = sigrt_handler; sa.sa_flags = SA_SIGINFO; // 必须使用SA_SIGINFO sigaction(SIGRTMIN, &sa, NULL); printf("PID: %d\n", getpid()); printf("等待实时信号...\n"); // 等待一下,然后发送测试信号 sleep(2); // 使用sigqueue发送实时信号+数据 val.sival_int = 12345; if (sigqueue(getpid(), SIGRTMIN, val) == -1) { perror("sigqueue"); } // 多次发送,实时信号会排队 for (int i = 0; i < 3; i++) { val.sival_int = 100 + i; sigqueue(getpid(), SIGRTMIN, val); } sleep(1); return 0; } ``` **编译命令**: ```bash gcc -o realtime_sig realtime_sig.c ./realtime_sig ``` ## 6.4 注意事项与易错点 | 问题 | 说明 | 解决方案 | | -------------------------------- | -------------------------------- | ---------------------------------- | | signal()不可靠 | handler可能被重置为SIG_DFL | 使用sigaction()替代 | | 信号处理函数中避免非异步安全函数 | printf、malloc、free等不可重入 | 使用write()、_exit(),或设置标志位 | | 信号屏蔽遗漏 | 信号处理期间收到相同信号导致递归 | 使用sa_mask自动屏蔽当前信号 | | 僵尸进程 | 子进程退出后父进程未回收 | 处理SIGCHLD或设置SA_NOCLDWAIT | | EINTR错误 | 系统调用被信号中断 | 使用SA_RESTART或检查errno | | 实时信号丢失 | 非实时信号只保留一次 | 使用SIGRTMIN及以上的实时信号 | | kill()权限不足 | 非root进程只能发给同用户进程 | 保证发送者和接收者属于同一用户 | | 信号处理函数阻塞 | 处理函数执行时间过长影响系统响应 | 尽量简短,设置标志位延迟处理 | | 多线程信号处理 | 信号默认投递给主线程 | 使用sigaction配合线程掩码 | ### 信号处理函数中的安全函数列表 **异步信号安全函数**(可在信号处理函数中调用): - `_exit()`、`_Exit()` - `read()`、`write()` - `sigaction()`、`sigprocmask()` - `kill()`、`raise()` - `alarm()`、`pause()`、`sleep()` - `getpid()`、`getuid()` - `pthread_sigmask()`(多线程) **不安全函数**(避免在信号处理函数中调用): - `printf()`、`fprintf()`、`malloc()`、`free()` - `exit()`、`abort()` - 任何使用全局变量的函数 ## 6.5 面试要点 ### Q1: signal()和sigaction()的区别? **答**: 1. signal()是简单API,sigaction()是更强大的替代方案 2. signal()在某些系统中handler会被重置为默认,sigaction()不会 3. sigaction()可以精确控制信号屏蔽、自动重启行为等 4. sigaction()支持SA_SIGINFO获取信号详细信息 5. **生产环境推荐使用sigaction()** ### Q2: 什么是EINTR?如何处理? **答**: - **EINTR**:系统调用被信号中断,返回-1并设置errno=EINTR - **产生场景**:read/write/select/poll等阻塞调用被信号打断 - **处理方式**: - 使用SA_RESTART标志自动重启 - 手动检查errno并重试 - 在循环中处理中断(推荐) ### Q3: 为什么信号处理函数中要避免使用printf? **答**: 1. printf()使用了全局的stdio缓冲区,在信号处理函数中调用可能导致死锁 2. printf()内部使用了malloc,malloc不是异步信号安全的 3. 多线程环境下,printf()需要获取锁,可能造成竞争 4. **安全替代**:使用write()系统调用 ### Q4: SIGCHLD的处理方式有哪些? **答**: 1. **忽略**:`signal(SIGCHLD, SIG_IGN)` 或设置`SA_NOCLDWAIT`(不产生僵尸进程) 2. **自定义处理**:使用sigaction()注册处理函数,在函数中调用waitpid()回收 3. **处理多个子进程**:使用`while(waitpid(-1, &status, WNOHANG) > 0)`循环回收 ### Q5: 实时信号和非实时信号的区别? **答**: | 特性 | 非实时信号(1-31) | 实时信号(34-64) | | -------- | -------------------------- | ---------------------------- | | 排队 | 不支持,多次发送只保留一次 | 支持,按发送顺序排队 | | 优先级 | 无 | 优先级从34递增 | | 携带数据 | 不能 | 可通过sigqueue传递联合体数据 | | 典型用途 | 传统Unix信号 | 用户自定义、实时系统 | ### Q6: 嵌入式开发中信号的典型应用场景? **答**: 1. **定时器**:SIGALRM实现软件定时器,用于超时检测、周期任务 2. **子进程管理**:SIGCHLD处理守护进程、监控进程的退出 3. **用户输入**:SIGINT处理Ctrl+C优雅退出 4. **看门狗**:SIGALRM定期喂狗,超时则复位 5. **进程间同步**:SIGUSR1/SIGUSR2实现简单的父子进程同步