常见信号及其产生条件
(1).SIGABRTabort产生
(2).SIGALRMalarm设置的时间到达
(3).SIGCHLD
子进程状态改变(终止/停止).
这里的停止可以理解为暂停的意思.暂停即为不继续执行,但仍有恢复执行可能(通过继续).
这里的终止可理解为停止的意思.停止了则意味着执行结束(正常或异常).没有恢复执行可能(重新启动不算恢复).
(4).SIGCONT
子进程由暂停变为继续
(5).SIGHUP
控制终端断开时,发此信号给终端所关联的会话首进程(可能是后台进程,也可能为前台进程).
正常下,终端产生的信号,总是传递给关联到此终端的前台进程.
会话首进程终止时,也产生SIGHUP,但将其送给会话的前台进程组内所有进程.
守护进程不关联终端,也不是前台进程,绝对不可能收到此消息.
但可人为向守护进程发此消息,让其做自定义工作.
(6).SIGINT
终端中断
(7).SIGIO
异步I/O
(8).SIGKILL
用于杀死进程,其响应方式不可更改
(9).SIGPIPE
管道没有进程以读方式打开时,对其执行写产生.SOCK_STREAM套接字来自对端的RST已经收到,后续本地对起执行write时,产生.
(10).SIGQUIT
终端退出(终端上按退出键).
(11).SIGSEGV
无效内存引用.
(12).SIGSTOP
停止一个进程,这里的停止可以理解为暂停的意思.暂停即为不继续执行,但仍有恢复执行可能(通过继续).
(13).SIGSYS
无效系统调用
(14).SIGTERM
让进程有机会在被终止前做好清理工作,系统关机时,init进程通常给所有进程发SIGTERM信号(默认是终止进程),隔一段固定时间后,给所有仍在运行的进程发SIGKILL.
(15).SIGTSTP
终端停止符(终端上按挂起键).
(16).SIGTTIN
后台进程读其控制终端时
(17).SIGTTOU
后台进程写控制终端时
(18).SIGURG
套接字的紧急情况
(19).SIGUSR1
用户自定义
(20).SIGUSR2
用户自定义
exec
exec运行的进程中,exec执行前进程的信号处理设置失效.fork产生新进程,新进程继承父进程对信号的处理设置.
异步信号安全/可重入函数
进程在执行指令序列中,每个CPU指令执行完毕时,都有可能被信号中断,转而执行信号处理函数的指令序列.
在信号处理函数指令序列执行中,每个CPU指令执行完毕时,也可能再次产生新的信号,转而执行对应的信号处理函数的指令序列.
一个信号处理函数执行完毕,会返回被其打断的上一层次指令序列打断处/打断处下一指令接着执行.
如果程序正在执行malloc的指令序列,这时被信号打断,执行信号的处理,信号处理中也执行了malloc我们直到malloc是基于一个进程全局资源来维护的.在上一个malloc执行到一半时,又执行一个malloc,会对全局资源的维护造成混乱和错误.所以,称这样的函数malloc即为不可重入的.对这样的函数一般不再信号处理函数中进行调用,若调用则可能存在上述调用时序下让程序发生混乱的可能.
但也有一些函数,比如执行一些基于函数内本地变量的计算返回结果等,这些函数即使正常执行被打断,在插入的处理函数中再次执行,因为函数并不操作全局数据,每次函数操作的都是栈数据,所以,这样的调用不会有让程序发生混乱和错误的风险.称这样的函数为可重入的/异步线程安全.
一般,在信号处理函数中若要调用函数,我们应调用可重入的函数.来保证我们的信号处理函数在任何执行序列下均不会有让程序混乱,出错的可能.另外一个值得注意的是,一般系统调用出错均设置出错码,而出错码又是一个全局变量.针对此特殊情况,采取的策略一般是:信号处理函数开始时,用本地变量记录此时的出错码.信号处理函数返回时,恢复进入时的出错码.
以下整理了一些可重入的系统调用,可以安全的在信号处理函数中调用这些函数:
信号
信号产生时,内核记录信号类型,目标进程.
若目标进程未阻塞此信号,则设置目标进程的信号标记.目标进程将下接下来中断返回时执行相应的信号处理.
若目标进程阻塞了此信号,则设置目标进程的信号标记,但在解除对其阻塞前,目标进程不会在中断返回时执行相应的信号处理.
对非实时信号,若内核在已经记录了某(信号,进程)信息下,又收到重叠的(信号,进程)消息,则仍然只对此(信号,进程)维持一个记录(不排队).
kill,raise
// pid:// > 0 进程ID为pid的进程// = 0 和调用进程同组的所有进程// -1 所有进程// < 0 属于进程组|pid|的所有进程// 不发给系统进程集中进程,需对目标进程有发送权限// 调用进程是否可向目标进程发送成功,需要权限检查通过intkill(pid_tpid,intsigno);alarm,pause
// 指定时间后给进程发SIGALRMunsignedintalarm(unsignedintseconds);intpause();信号集
intsigemptyset(sigset_t*set);intsigfillset(sigset_t*set);intsigaddset(sigset_t*set,intsigno);intsigdelset(sigset_t*set,intsigno);// 若真,返回1。若假,返回0。intsigismember(constsigset_t*set,intsigno);sigprocmask
// SIG_BLOCK 增加集合位// SIG_UNBLOCK 减少集合位// SIG_SETMASK 设置集合位intsigprocmask(inthow,constsigset_t*restrict set,sigset_t*restrict oset);sigpending
// set中获取进程此时所有阻塞信号构成的信号集合intsigpending(sigset_t*set);sigaction
intsigaction(intsigno,conststructsigaction*restrict act,structsigaction*restrict oact);structsigaction{// fun addr/SIG_IGN/SIG_DFLvoid(*sa_handler)(int);// signals to blocksigset_tsa_mask;// 设置了SA_SIGINFO标志时的处理函数void(*sa_sigaction)(int,siginfo_t*,void*);intsa_flags;};可靠信号语义:
(1). 信号中断产生,让进程执行信号处理程序期间.此中断信号自动被屏蔽.
(2).sigaction中,信号处理程序为sa_handler.信号处理期间sa_mask与中断信号构成的集合被屏蔽.信号处理结束,恢复信号屏蔽为中断前状态.
(3). 信号处理一旦用sigaction进行设置,此后一直有效.
(4). 关于信号是否会中断系统调用,即使设置了SA_RESTART有些系统也仍然会中断.可统一按会中断系统调用处理.
typedefvoid(*Sigfunc)(int);Sigfuncsignal(intsigno,Sigfunc func){structsigactionact,oact;act.sa_handler=func;sigemptyset(&act.sa_mask);act.sa_flags=0;#ifdefSA_INTERRUPTct.sa_flags|=SA_INTERRUPT;#endifif(sigaction(signo,&act,&oact)<0){returnSIG_ERR;}return(oact.sa_handler);}sigsetjmp,siglongjmp
// 参数2非0时,首次调sigsetjmp会存储进程信号屏蔽字到env,供后续siglongjmp恢复intsigsetjmp(sigjmp_buf env,intsavemask);intsiglongjmp(sigjmp_buf env,intval);引入背景:setjmp,longjmp也用来实现非局部转移.但longjmp在信号处理中实现非局部转移时,有的实现在转移后,恢复对信号处理关联信号的阻塞为处理前,有的实现不处理.sigsetjmp, siglongjmp允许显式指定在env中记录进程的信号屏蔽字,用于siglongjmp时恢复
sig_atomic_t是ISO C定义的变量类型,特点是写此类型变量不会被中断.volatile用于高速编译器变量在内存中,每次获取变量都要从内存读其值,而不要将其优化到寄存器.两者一般联合起来使用.
sigsuspend
intsigsuspend(constsigset_t*sigmask);以原子方式[执行不会被信号中断]执行
(1). 设置进程的信号屏蔽字为sigmask
(2). 进程挂起
进程继续条件:
捕捉到一个信号,且从该信号的信号处理返回,则sigsuspend返回-1,进程继续
引入背景:
若以单独语句实现
(1). 设置进程信号屏蔽字为sigmask
(2). 进程挂起
则存在以下可能:在(1)执行前进程阻塞了信号X,且(1)执行时,针对此进程的信号X已经产生,但由于进程阻塞了它,故未发给进程.
执行(1)后,如sigmask中解除了对X的阻塞.则在执行2前,信号X被进程接收,执行其处理.
执行(2)时,进程阻塞等待被信号中断.但此前处理的X,此时,不会再引起(2)中阻塞的中断.
如用sigsuspend.(1),(2)以原子方式执行.可以保证不会产生上述现象.
信号的应用:用于实现父子进程间的同步
利用sigsuspend实现阻塞.利用kill实现解除阻塞.
另一种父子同步是,父亲用wait阻塞.子进程终止通过发SIGCHLD给父亲,解除阻塞.(父进程需忽略SIGCHLD信号处理,如不忽略,此信号默认处理将终止父进程.不用显式处理是因为父进程需用wait阻塞等待同步.而不是异步信号来时,执行一次wait).
终端控制信号会发给前台进程组所有进程,如只想让特定信号发给指定进程,则需对前台进程组进行相关信号的阻塞等额外处理.
abort
voidabort(void);发SIGABRT给调用进程
作业控制信号
(1).SIGCHLD
子进程停止/终止时,给父进程发.
(2).SIGCONT
发给停止的子进程,让其继续.
(3).SIGSTOP
不可被捕捉或忽略
(4).SIGTSTP
(5).SIGTTIN
后台进程组成员读控制终端
(6).SIGTTOU
后台进程组成员写控制终端
strsignal
char*strsignal(intsigno);信号处理范例
#include"apue.h"staticvoidsig_alrm(intsigno){}// 通过alarm实现sleep--alarm + 处理SIGALRMunsignedintsleep(unsignedintseconds){structsigactionnewact,oldact;sigset_tnewmask,oldmask,suspmask;unsignedintunslept;// 设置好对SIGALRM处理方式newact.sa_handler=sig_alrm;sigemptyset(&newact.sa_mask);newact.sa_flags=0;sigaction(SIGALRM,&newact,&oldact);// 为何要提前阻塞SIGALRM// 这样可以避免在alarm后,进入阻塞等待SIGALRM打断这一时间段内收到SIGALRM// 造成后续阻塞无法被此SIGALRM所打断sigemptyset(&newmask);sigaddset(&newmask,SIGALRM);sigprocmask(SIG_BLOCK,&newmask,&oldmask);alarm(seconds);suspmask=oldmask;sigdelset(&suspmask,SIGALRM);// 以原子方式[整个执行序列不会被信号打断]// 设置可接收SIGALRM+进入阻塞等待sigsuspend(&suspmask);// 剩余时间获取unslept=alarm(0);// 恢复对SIGALRM处理,恢复信号阻塞sigaction(SIGALRM,&oldact,NULL);sigprocmask(SIG_SETMASK,&oldmask,NULL);return(unslept);}多线程下处理方案选择
- SIGSEGV, SIGFPE, SIGILL 等同步信号必须通过 sigaction 捕获,因为它们精确投递到出错线程,无法被 sigwait 捕获。
- 针对SIGINT/SIGTERM/SIGUSR1/SIGALRM等异步信号,默认行为是选择一个不屏蔽此信号的线程,进行投递,在此线程上下文执行信号回调。推荐的做法是:所有线程通过 pthread_sigmask() 阻塞目标异步信号,然后创建一个专用线程调用 sigwait() 或 sigwaitinfo() 同步接收并处理信号。
#define_GNU_SOURCE#include<pthread.h>#include<signal.h>#include<stdio.h>#include<stdlib.h>#include<unistd.h>// 工作线程:完全屏蔽 SIGUSR1 和 SIGTERMvoid*worker_thread(void*arg){//sigset_t set;//sigemptyset(&set);//sigaddset(&set, SIGUSR1);//sigaddset(&set, SIGTERM);//pthread_sigmask(SIG_BLOCK, &set, NULL); // 阻塞信号while(1){// 正常工作,完全不用担心被信号中断printf("Worker %ld running...\n",(long)arg);sleep(2);}returnNULL;}// 专用信号线程:同步等待信号void*signal_thread(void*arg){sigset_t set;intsig;sigemptyset(&set);sigaddset(&set,SIGUSR1);sigaddset(&set,SIGTERM);// 该线程也屏蔽这些信号,但 sigwait 会绕过屏蔽直接接收while(1){intret=sigwait(&set,&sig);// 同步阻塞等待if(ret!=0)continue;switch(sig){caseSIGUSR1:printf("[Signal Thread] Got SIGUSR1, safe to use locks here\n");break;caseSIGTERM:printf("[Signal Thread] Got SIGTERM, initiating graceful shutdown...\n");// 可以安全地加锁、修改全局状态、通知条件变量exit(0);}}returnNULL;}intmain(){pthread_t workers[3],sig_tid;// 关键:在创建任何线程前,主线程先屏蔽信号// 这样后续 pthread_create 的新线程会继承这个信号掩码sigset_t set;sigemptyset(&set);sigaddset(&set,SIGUSR1);sigaddset(&set,SIGTERM);pthread_sigmask(SIG_BLOCK,&set,NULL);// 创建专用信号线程(必须在屏蔽之后,这样它也继承屏蔽集)pthread_create(&sig_tid,NULL,signal_thread,NULL);// 创建工作线程(它们继承屏蔽集,因此不会收到异步信号)for(inti=0;i<3;i++){pthread_create(&workers[i],NULL,worker_thread,(void*)(long)i);}// 主线程也进入工作循环或等待pthread_join(sig_tid,NULL);return0;}- 针对异步信号,linux下使用i/o复用下,主流方案为:
Linux 特有的 signalfd 可以将信号转换为文件描述符上的可读事件,完美融入 epoll/select 事件循环。
#include<sys/signalfd.h>intsetup_signalfd(void){sigset_t mask;sigemptyset(&mask);sigaddset(&mask,SIGUSR1);sigaddset(&mask,SIGTERM);// 所有线程屏蔽这些信号pthread_sigmask(SIG_BLOCK,&mask,NULL);// 创建 signalfdintfd=signalfd(-1,&mask,SFD_CLOEXEC);returnfd;}// 在事件循环中使用:// struct signalfd_siginfo fdsi;// read(fd, &fdsi, sizeof(fdsi));// 根据 fdsi.ssi_signo 判断信号类型sigaction + SA_SIGINFO 的局限性
即使使用 sigaction 注册处理函数,它仍然是异步中断。在处理函数中:
- ❌ 不能调用 printf()(非异步信号安全)
- ❌ 不能调用 malloc()/free()
- ❌ 不能操作大部分 pthread_mutex_lock()(如果该锁正被其他线程持有,会死锁)
- ✅ 只能调用异步信号安全函数(write(), _exit(), sem_post() 等)