ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

系统api-信号

2026/8/10 22:54:41 拓冰建站 浏览量
系统api-信号 常见信号及其产生条件(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用户自定义execexec运行的进程中exec执行前进程的信号处理设置失效fork产生新进程新进程继承父进程对信号的处理设置异步信号安全可重入函数进程在执行指令序列中每个CPU指令执行完毕时都有可能被信号中断转而执行信号处理函数的指令序列在信号处理函数指令序列执行中每个CPU指令执行完毕时也可能再次产生新的信号转而执行对应的信号处理函数的指令序列一个信号处理函数执行完毕会返回被其打断的上一层次指令序列打断处打断处下一指令接着执行如果程序正在执行malloc的指令序列这时被信号打断执行信号的处理信号处理中也执行了malloc我们直到malloc是基于一个进程全局资源来维护的在上一个malloc执行到一半时又执行一个malloc会对全局资源的维护造成混乱和错误所以称这样的函数malloc即为不可重入的对这样的函数一般不再信号处理函数中进行调用若调用则可能存在上述调用时序下让程序发生混乱的可能但也有一些函数比如执行一些基于函数内本地变量的计算返回结果等这些函数即使正常执行被打断在插入的处理函数中再次执行因为函数并不操作全局数据每次函数操作的都是栈数据所以这样的调用不会有让程序发生混乱和错误的风险称这样的函数为可重入的异步线程安全一般在信号处理函数中若要调用函数我们应调用可重入的函数来保证我们的信号处理函数在任何执行序列下均不会有让程序混乱出错的可能另外一个值得注意的是一般系统调用出错均设置出错码而出错码又是一个全局变量针对此特殊情况采取的策略一般是信号处理函数开始时用本地变量记录此时的出错码信号处理函数返回时恢复进入时的出错码以下整理了一些可重入的系统调用可以安全的在信号处理函数中调用这些函数信号信号产生时内核记录信号类型目标进程若目标进程未阻塞此信号则设置目标进程的信号标记目标进程将下接下来中断返回时执行相应的信号处理若目标进程阻塞了此信号则设置目标进程的信号标记但在解除对其阻塞前目标进程不会在中断返回时执行相应的信号处理对非实时信号若内核在已经记录了某(信号进程)信息下又收到重叠的(信号进程)消息则仍然只对此(信号进程)维持一个记录不排队killraise// pid:// 0 进程ID为pid的进程// 0 和调用进程同组的所有进程// -1 所有进程// 0 属于进程组|pid|的所有进程// 不发给系统进程集中进程需对目标进程有发送权限// 调用进程是否可向目标进程发送成功需要权限检查通过intkill(pid_tpid,intsigno);alarmpause// 指定时间后给进程发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);sigactionintsigaction(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_handlerfunc;sigemptyset(act.sa_mask);act.sa_flags0;#ifdefSA_INTERRUPTct.sa_flags|SA_INTERRUPT;#endifif(sigaction(signo,act,oact)0){returnSIG_ERR;}return(oact.sa_handler);}sigsetjmpsiglongjmp// 参数非时首次调sigsetjmp会存储进程信号屏蔽字到env供后续siglongjmp恢复intsigsetjmp(sigjmp_buf env,intsavemask);intsiglongjmp(sigjmp_buf env,intval);引入背景setjmplongjmp也用来实现非局部转移但longjmp在信号处理中实现非局部转移时有的实现在转移后恢复对信号处理关联信号的阻塞为处理前有的实现不处理sigsetjmp, siglongjmp允许显式指定在env中记录进程的信号屏蔽字用于siglongjmp时恢复sig_atomic_t是ISO C定义的变量类型特点是写此类型变量不会被中断volatile用于高速编译器变量在内存中每次获取变量都要从内存读其值而不要将其优化到寄存器两者一般联合起来使用sigsuspendintsigsuspend(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终端控制信号会发给前台进程组所有进程如只想让特定信号发给指定进程则需对前台进程组进行相关信号的阻塞等额外处理abortvoidabort(void);发SIGABRT给调用进程作业控制信号(1).SIGCHLD子进程停止终止时给父进程发(2).SIGCONT发给停止的子进程让其继续(3).SIGSTOP不可被捕捉或忽略(4).SIGTSTP(5).SIGTTIN后台进程组成员读控制终端(6).SIGTTOU后台进程组成员写控制终端strsignalchar*strsignal(intsigno);信号处理范例#includeapue.hstaticvoidsig_alrm(intsigno){}// 通过alarm实现sleepalarm 处理SIGALRMunsignedintsleep(unsignedintseconds){structsigactionnewact,oldact;sigset_tnewmask,oldmask,suspmask;unsignedintunslept;// 设置好对SIGALRM处理方式newact.sa_handlersig_alrm;sigemptyset(newact.sa_mask);newact.sa_flags0;sigaction(SIGALRM,newact,oldact);// 为何要提前阻塞SIGALRM// 这样可以避免在alarm后进入阻塞等待SIGALRM打断这一时间段内收到SIGALRM// 造成后续阻塞无法被此SIGALRM所打断sigemptyset(newmask);sigaddset(newmask,SIGALRM);sigprocmask(SIG_BLOCK,newmask,oldmask);alarm(seconds);suspmaskoldmask;sigdelset(suspmask,SIGALRM);// 以原子方式整个执行序列不会被信号打断// 设置可接收SIGALRM进入阻塞等待sigsuspend(suspmask);// 剩余时间获取unsleptalarm(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#includepthread.h#includesignal.h#includestdio.h#includestdlib.h#includeunistd.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){intretsigwait(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(inti0;i3;i){pthread_create(workers[i],NULL,worker_thread,(void*)(long)i);}// 主线程也进入工作循环或等待pthread_join(sig_tid,NULL);return0;}针对异步信号linux下使用i/o复用下主流方案为Linux 特有的 signalfd 可以将信号转换为文件描述符上的可读事件完美融入 epoll/select 事件循环。#includesys/signalfd.hintsetup_signalfd(void){sigset_t mask;sigemptyset(mask);sigaddset(mask,SIGUSR1);sigaddset(mask,SIGTERM);// 所有线程屏蔽这些信号pthread_sigmask(SIG_BLOCK,mask,NULL);// 创建 signalfdintfdsignalfd(-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() 等