
1. Linux进程信号基础概念信号是Linux系统中进程间通信的一种基本机制它本质上是一个软件中断用于通知进程发生了某个特定事件。就像我们在日常生活中收到手机通知一样进程接收到信号后会中断当前正在执行的操作转而处理这个信号。1.1 信号的产生与传递信号可以由多种情况触发硬件异常如除零错误、非法内存访问终端产生的信号如CtrlC产生的SIGINT软件条件如管道破裂、定时器到期其他进程通过kill系统调用发送内核负责信号的传递工作。当信号产生时内核会在目标进程的进程控制块(PCB)中设置相应的信号标志位。只有当进程从内核态返回用户态时才会检查并处理这些待处理的信号。1.2 常见信号类型Linux系统定义了多种标准信号以下是一些最常用的信号编号信号名称默认动作说明1SIGHUP终止终端挂断2SIGINT终止键盘中断(CtrlC)3SIGQUIT终止核心转储键盘退出(Ctrl)9SIGKILL终止强制终止信号15SIGTERM终止终止信号17SIGCHLD忽略子进程状态改变19SIGSTOP停止进程停止信号20SIGTSTP停止进程终端停止信号(CtrlZ)注意SIGKILL(9)和SIGSTOP(19)是两种特殊的信号它们不能被捕获、阻塞或忽略这保证了系统管理员始终有办法控制进程。2. 信号的处理方式2.1 默认处理方式每个信号都有一个默认的处理动作通常是以下三种之一终止进程(Term)忽略信号(Ign)终止并生成核心转储文件(Core)停止进程(Stop)继续运行被停止的进程(Cont)例如SIGINT的默认动作是终止进程这就是为什么在终端按CtrlC可以终止正在运行的程序。2.2 自定义信号处理Linux允许我们通过signal()或更现代的sigaction()系统调用为信号注册自定义处理函数。这就像为手机通知设置自定义铃声一样。#include signal.h #include stdio.h #include unistd.h void sig_handler(int signo) { if (signo SIGINT) { printf(收到SIGINT信号!\n); } } int main() { if (signal(SIGINT, sig_handler) SIG_ERR) { printf(无法设置信号处理程序\n); } while(1) { sleep(1); } return 0; }编译并运行这个程序后按CtrlC不会终止程序而是会打印收到SIGINT信号!。2.3 忽略信号除了自定义处理函数外我们还可以选择完全忽略某些信号signal(SIGINT, SIG_IGN); // 忽略SIGINT信号但要注意如前所述SIGKILL和SIGSTOP不能被忽略。3. 信号处理的高级话题3.1 可靠信号与不可靠信号Linux信号分为可靠信号(编号34-64)和不可靠信号(编号1-31)。主要区别在于可靠信号支持排队不会丢失不可靠信号不支持排队连续发送可能丢失可靠信号有更丰富的附加信息在现代编程中应优先使用可靠信号。3.2 信号屏蔽与阻塞有时我们需要暂时阻止某些信号的传递这可以通过信号屏蔽(阻塞)实现sigset_t newmask, oldmask; sigemptyset(newmask); sigaddset(newmask, SIGINT); // 阻塞SIGINT信号 sigprocmask(SIG_BLOCK, newmask, oldmask); // 这段代码执行期间不会收到SIGINT // ... // 恢复原来的信号屏蔽字 sigprocmask(SIG_SETMASK, oldmask, NULL);3.3 信号处理函数的注意事项编写信号处理函数时需要特别小心可重入性只能使用异步信号安全的函数避免死锁不要在信号处理函数中获取锁保持简单尽量只设置标志变量在主循环中处理注意全局变量使用volatile修饰可能被信号处理函数修改的变量4. 实际应用案例4.1 优雅地关闭服务进程在开发服务端程序时我们通常希望收到SIGTERM信号时能优雅地关闭#include signal.h #include stdio.h #include stdlib.h #include unistd.h volatile sig_atomic_t shutdown_flag 0; void handle_shutdown(int sig) { shutdown_flag 1; } int main() { struct sigaction sa; sa.sa_handler handle_shutdown; sigemptyset(sa.sa_mask); sa.sa_flags 0; if (sigaction(SIGTERM, sa, NULL) -1) { perror(sigaction); exit(EXIT_FAILURE); } while (!shutdown_flag) { // 正常处理业务逻辑 printf(服务运行中...\n); sleep(1); } // 清理资源 printf(收到关闭信号正在清理资源...\n); sleep(2); // 模拟清理过程 printf(服务已优雅关闭\n); return 0; }4.2 防止僵尸进程通过处理SIGCHLD信号可以避免产生僵尸进程#include signal.h #include sys/wait.h #include stdio.h #include stdlib.h #include unistd.h void sigchld_handler(int sig) { int saved_errno errno; while (waitpid(-1, NULL, WNOHANG) 0) { continue; } errno saved_errno; } int main() { struct sigaction sa; sa.sa_handler sigchld_handler; sigemptyset(sa.sa_mask); sa.sa_flags SA_RESTART | SA_NOCLDSTOP; if (sigaction(SIGCHLD, sa, NULL) -1) { perror(sigaction); exit(EXIT_FAILURE); } pid_t pid fork(); if (pid 0) { // 子进程 printf(子进程运行中...\n); sleep(2); exit(EXIT_SUCCESS); } else { // 父进程 printf(父进程等待中...\n); while (1) { sleep(1); } } return 0; }5. 常见问题与调试技巧5.1 信号处理函数没有被调用可能原因信号被阻塞了检查sigprocmask信号处理设置失败检查signal/sigaction返回值处理的是不可靠信号且信号丢失5.2 程序意外终止检查是否收到了未处理的信号使用strace跟踪系统调用信号处理函数中调用了不安全的函数信号处理函数执行时间过长5.3 多线程程序中的信号处理在多线程环境中信号处理是进程范围的所有线程共享但信号可以定向到特定线程使用pthread_kill每个线程有自己的信号屏蔽字建议专门创建一个线程处理所有信号使用sigwait而不是信号处理函数void *signal_thread(void *arg) { sigset_t set; int sig; sigemptyset(set); sigaddset(set, SIGINT); sigaddset(set, SIGTERM); while (1) { sigwait(set, sig); printf(线程收到信号: %d\n, sig); // 处理信号 } return NULL; }6. 性能考量与最佳实践6.1 信号处理的开销信号处理会引入一定的性能开销上下文切换用户态-内核态-用户态可能打断关键路径上的操作增加代码复杂度优化建议尽量减少信号处理函数的执行时间避免频繁发送信号考虑使用其他IPC机制如管道、消息队列替代频繁信号6.2 信号与系统调用的交互当信号中断了阻塞的系统调用时默认情况下系统调用会返回EINTR错误可以设置SA_RESTART标志自动重启被中断的系统调用某些系统调用不支持自动重启如poll、select正确处理方式while ((n read(fd, buf, size)) -1 errno EINTR) { continue; // 被信号中断重试 } if (n -1) { // 真正的错误 perror(read); }6.3 信号安全编程原则保持信号处理函数尽可能简单只使用异步信号安全的函数如write、read、kill等避免在信号处理函数中修改全局状态对共享变量使用sig_atomic_t类型或适当的同步机制考虑使用自管道技巧(self-pipe trick)将信号事件转换为I/O事件自管道技巧示例int selfpipe[2]; void sig_handler(int sig) { int saved_errno errno; write(selfpipe[1], X, 1); errno saved_errno; } int main() { pipe(selfpipe); fcntl(selfpipe[0], F_SETFL, O_NONBLOCK); fcntl(selfpipe[1], F_SETFL, O_NONBLOCK); signal(SIGINT, sig_handler); while (1) { fd_set rfds; FD_ZERO(rfds); FD_SET(selfpipe[0], rfds); select(selfpipe[0]1, rfds, NULL, NULL, NULL); if (FD_ISSET(selfpipe[0], rfds)) { char buf[256]; read(selfpipe[0], buf, sizeof(buf)); printf(收到信号处理中...\n); } } }在实际项目中我发现信号处理最容易出问题的地方在于对可重入性的忽视。曾经在一个高并发服务中信号处理函数调用了printf记录日志结果在某些极端情况下导致了死锁。后来我们改用write系统调用直接输出到标准错误问题才得以解决。这个教训让我深刻认识到信号处理函数的特殊性——它们可能在任何时间点打断主程序的执行因此必须格外小心。