如何让信号处理程序仅执行一次并终止所有进程?
我有一个包含1个父进程和N个子进程的程序,编写了信号处理函数来处理SIGINT和SIGTERM信号,用于清理信号量和共享内存。相关代码如下:
信号处理函数及初始化代码
void unlink_semaphores(){ sem_unlink(SEM_NAME1) < 0; // perror("sem_unlink(3) failed"); sem_unlink(SEM_NAME2) < 0; //perror("sem_unlink(3) failed"); sem_unlink(SEM_NAME3) < 0; // perror("sem_unlink(3) failed"); sem_unlink(SEM_NAME4) < 0; // perror("sem_unlink(3) failed"); sem_unlink(SEM_NAME5) < 0; //perror("sem_unlink(3) failed"); sem_unlink(SEM_NAME6) < 0; //perror("sem_unlink(3) failed"); sem_unlink(SEM_NAME7) < 0; //perror("sem_unlink(3) failed"); if (shm_unlink(SHMOBJ_PATH1) != 0) { // perror("In shm_unlink() of buffer 1"); exit(1); } } void sinyal_handler(int signo) { if (signo == SIGINT || signo == SIGTERM) { if(signo == SIGINT) printf("received Ctrl+C\n"); if(signo == SIGTERM) printf("received SIGTERM \n"); printf("closing semaphores and giving shared maps...\n"); unlink_semaphores(); _exit(EXIT_FAILURE); } }
在fork子进程前,我设置了信号处理:
signal(SIGINT,&sinyal_handler); signal(SIGTERM,&sinyal_handler);
子进程会一直运行在while(1)循环中,直到收到Ctrl+C信号后由信号处理程序终止并清理资源。
当前问题
按下Ctrl+C时,程序会多次调用信号处理函数,重复打印信息并尝试多次解除链接文件。请问该如何解决?
你遇到的这个问题其实很常见——当按下Ctrl+C时,操作系统会把SIGINT信号发送给整个进程组里的所有进程,也就是你的父进程加上所有子进程,所以每个进程都会触发自己的信号处理函数,自然就会重复执行清理逻辑了。下面给你几个靠谱的解决思路:
方法1:父进程统一处理信号,子进程忽略信号
这是最推荐的方案,逻辑清晰且容易维护。让子进程忽略SIGINT和SIGTERM,只让父进程捕获信号,由父进程统一完成清理工作,同时父进程可以主动给所有子进程发送终止信号,确保它们正常退出。
修改后的fork代码示例:
// 先定义一个数组保存子进程PID,方便后续管理 pid_t child_pids[PROCESS_N]; for (i = 0; i < PROCESS_N; i++) { pid_t pid = fork(); if (pid == 0) { // 子进程忽略SIGINT和SIGTERM,交给父进程处理 signal(SIGINT, SIG_IGN); signal(SIGTERM, SIG_IGN); // 子进程的业务逻辑 while(1){ // ... 你的子进程业务代码 } } else if (pid > 0) { // 父进程保存子进程PID child_pids[i] = pid; } else { // fork失败的错误处理 perror("fork failed"); exit(EXIT_FAILURE); } }
然后修改父进程的信号处理函数,先终止所有子进程再执行清理:
void sinyal_handler(int signo) { if (signo == SIGINT || signo == SIGTERM) { if(signo == SIGINT) printf("received Ctrl+C\n"); if(signo == SIGTERM) printf("received SIGTERM \n"); // 先终止所有子进程 for (int j = 0; j < PROCESS_N; j++) { if (child_pids[j] > 0) { // 给子进程发送终止信号 kill(child_pids[j], SIGTERM); // 等待子进程退出,避免变成僵尸进程 waitpid(child_pids[j], NULL, 0); } } printf("closing semaphores and giving shared maps...\n"); unlink_semaphores(); // 父进程用exit而不是_exit,确保标准IO等资源正确清理 exit(EXIT_FAILURE); } }
方法2:在信号处理中临时屏蔽信号(辅助方案)
如果担心单个进程可能被多次触发信号处理(比如信号还在处理时又收到同个信号),可以在进入信号处理函数后临时屏蔽SIGINT和SIGTERM,防止重复触发。不过这个方法不能解决多进程重复执行的问题,建议和方法1配合使用。
示例代码:
void sinyal_handler(int signo) { // 屏蔽SIGINT和SIGTERM,防止处理过程中再次收到信号 sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); sigprocmask(SIG_BLOCK, &mask, NULL); if (signo == SIGINT || signo == SIGTERM) { if(signo == SIGINT) printf("received Ctrl+C\n"); if(signo == SIGTERM) printf("received SIGTERM \n"); printf("closing semaphores and giving shared maps...\n"); unlink_semaphores(); _exit(EXIT_FAILURE); } // 恢复信号屏蔽(不过这里_exit已经退出,这行不会执行) sigprocmask(SIG_UNBLOCK, &mask, NULL); }
方法3:用原子标记确保清理只执行一次
可以设置一个全局原子变量,标记是否已经执行过清理操作。每次进入信号处理函数时先检查这个标记,只有第一个进入的进程会执行清理逻辑,其他进程直接退出。
示例代码:
#include <stdatomic.h> // 原子变量,初始为false,表示未执行清理 atomic_bool cleanup_done = ATOMIC_VAR_INIT(false); void sinyal_handler(int signo) { if (signo == SIGINT || signo == SIGTERM) { // 原子交换操作:把cleanup_done设为true,返回之前的值 if (!atomic_exchange(&cleanup_done, true)) { // 只有第一个进程会进入这里执行清理 if(signo == SIGINT) printf("received Ctrl+C\n"); if(signo == SIGTERM) printf("received SIGTERM \n"); printf("closing semaphores and giving shared maps...\n"); unlink_semaphores(); } // 所有进程都退出 _exit(EXIT_FAILURE); } }
这个方法能避免重复执行unlink操作,但多个进程还是会触发信号处理函数,只是只有一个会执行清理。相比之下,方法1的逻辑更清晰,是更优的选择。
内容的提问来源于stack exchange,提问作者itsfreeandtakesminute

