前言
Linux中的信号是向进程异步发送的事件通知,通知进程有事件(硬件异常、程序执行异常、外部发出信号)发生。当信号产生时,内核向进程发送信号(在进程所在的进程表项的信号域设置对应于该信号的位)。内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时,当一个进程在内核态运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理,进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。内核为每个进程维护一个(未处理)的信号队列,信号产生后首先被放入到未决队列中,如果进程选择阻塞信号,那么如果某个信号发生多次,未决队列中仅保留相同的信号(不可靠信号类型)中的一个,而可靠信号则会被保留。
一、进程信号处理
1 2 3 4 5 6 7 |
int pause( void ); //将调用进程/线程 挂起sleep,直到有信号产生且在信号处理函数完成后返回
|
1 2 3 4 5 |
sighandler_t signal ( int signum, sighandler_t handler);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int sigaction( int signum, const struct sigaction *act, struct sigaction *oldact);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
int sigpending(sigset_t *set); //获取当前阻塞的信号集
|
二、多线程信号处理
多线程信号处理跟单线程的程序最大的区别就是所有的线程共享信号处理函数,每个线程对信号处理函数的修改,都会同步到其他线程。linux环境下线程是通过轻量级进程(有兴趣可以查资料)实现的,因此内核为每个线程维护一个未决信号队列。创建新的线程时,新线程继承主线程的信号屏蔽字,但是新线程的未决信号队列被清空(防止同一信号被多个线程处理)。各个线程的信号屏蔽字(sigmask)是独立的,可以通过pthread_sigmask函数来控制线程级别的sigmask。
如果是硬件故障(如SIGBUS/SIGFPE/SIGILL/SIGSEGV)或定时器超时触发的信号,该信号会发往引起该事件的线程;其余的所有情况产生的信号都会发送到主线程。因此要想让特定线程处理信号,需要主线程将这些信号屏蔽。
1 2 3 4 |
int pthread_sigmask( int how, const sigset_t *set, sigset_t *oldset); //线程级别的sigprocmask
|
三、踩坑教训
1、在一个多线程程序中,线程A中会设置定时器,如果超时就会触发SIGALRM的信号处理函数sig_alarm_func,该函数执行了pthread_cancel(A);pthread_create(B);的操作。在测试过程中发现进程中同时存在A, B两个线程。查看pthread_cancel 说明,phtread_cancel是个异步的,需要等到线程A执行到cancellation point才能结束退出。利用gdb查看A的函数调用栈发现,阻塞到了信号处理函数sig_alarm_func中,即发生了“自己取消自己”的问题。根据第二部分讲到的信号通告机制,定时器信号被发往了调用定时器的线程,因而信号处理函数也是在调用线程的上下文中执行,所以出现了异常。
解决方法:单独设置一个信号处理线程,阻塞除该线程外的其他所有线程的信号。在信号处理线程中,利用while+sigwait 对信号进行同步处理代替注册信号处理函数的异步处理方式。
2、在处理一个程序堆栈时,发现程序在malloc函数中发生了死锁。进一步分析发现信号处理函数在保存函数调用堆栈时调用了malloc,而信号产生时正好也在执行malloc操作。通过查看malloc的相关文档发现,malloc在申请内存的时候,有加锁操作。
解决方法:信号处理函数中取消malloc这类不可重入的有锁函数。以后编写信号处理函数的时候,在函数内部尽少做一些耗时处理尽快返回,在调用函数时必须调用可重入(reentrant)函数(即不可以有static、global等全局变量,不可以分配、释放内存,不要修改errno等)。