信号用于进程间信息的传递,对信号的处理属于异步过程,同时会中断进程当前的执行,类似硬件中总线的中断机制,所以信号也叫作“软件中断”。
信号的生成与传递时机
- 从进程的角度来说:信号一般为异步产生,程序无法预测信号何时到来;但有个例外,使用
raise
、kill
向进程自身发送的信号是同步的,即在这些调用返回时,信号必定已经传递到。 - 从内核的角度看:信号会在由内核态转为用户态时被处理:要么进程被重新调度,要么进程从系统调用中返回。所以,在信号处理函数中进行的系统调用有可能导致另一个信号处理函数的调用。
信号处理函数
大多数信号可以有用户自定义函数,这些函数在信号到来时被调用,使用signal
或者sigaction
系统调用来设置。
由于信号处理函数调用时机的特殊性,信号处理函数的要求比较严格,必须为“异步信号可重入”函数,以保证访问的安全。“异步信号可重入”函数被定义为:一个函数要么可重入,要么不可被信号中断。UNIX的几个标准(POSIX、SUS)定义了一个异步信号可重入的系统调用的列表,但Linux并未完全实现这些函数的可重入性。
信号处理函数的嵌套调用
如上所述:如果在A信号处理函数中没有阻塞某个信号B,那么在A的处理函数中,如果有系统调用,那么在系统调用返回时信号B的到来是会打断A的处理函数的执行,而去执行B的处理函数。注意,此时A的处理函数仍未执行完,在信号A上设置的阻塞掩码仍未被撤销,信号B的处理函数中的掩码是AB掩码的与。
信号处理函数中要注意的几点
- 要么不使用不是“异步信号可重入”的函数,要么在程序不安全的地方中阻塞该信号(这里的不安全的地方指:程序中使得该函数变得“不可重入”的部分);
- 由于信号处理函数在当前线程执行,因此
errno
变量也就变为不安全的了,所以,一般会在信号处理函数的出口和入口备份恢复errno
的值; - 在信号处理函数中使用
longjmp
进行非本地跳转时,需要注意的是,不是所有UNIX系统都保证longjmp
跳转出去时会取消对当前信号的阻塞(进入信号处理函数前会阻塞对应的信号),因此,需要使用sigsetjmp
和siglongjmp
函数进行非本地跳转;
特殊信号的特殊表现
SIGKILL
和SIGSTOP
信号均无法阻塞或者忽略,尝试修改默认行为则会出错;SIGCONT
信号可以被阻塞或者忽略,但是即使这样,被暂停的进程仍然会被唤醒。同时,进程收到SIGCONT
信号后,会将正处于等待状态的SIGSTOP
信号清除掉;同样的,收到SIGSTOP
会将等待的SIGCONT
信号清除;SIGCHLD
信号,默认在子进程结束或停止时触发。如果指定SA_NOCLDSTOP
标志,子进程因信号被停止(STOP
)的时候,不会触发该信号;如果指定SA_NOCLDWAIT
标志,子进程结束后会被系统直接清理,不会产生僵尸进程,但仍会触发父进程的信号处理函数;- 显式的将
SIGCHLD
信号忽略(SIG_IGN
)也可以达到第三点所述的效果。
实时信号
实时信号是标准信号以后的信号,大多数没有特定的意义,供程序自身使用。实时信号与标准信号相比,提供了队列化管理,具有特定顺序,并且可以附带少量数据(一个整型或一个指针)。实时信号最大和最小值定义为SIGRTMIN
、SIGRTMAX
。
信号的同步等待
sigsuspend
, sigwaitinfo
, signalfd
相关的系统调用提供了将信号同步处理的功能,详细介绍见下。
相关系统调用
signal
简单设置信号处理函数
sigaction
为一个信号设置信号处理函数,但是支持更多功能。
比如,支持在执行对应信号处理函数前自动阻塞信号自身与一组指定的信号,并在处理函数返回之后自动清除阻塞。与在信号处理函数中手动阻塞所需函数不同,sigaction
所做的阻塞是在处理函数被执行之前(或许是内核态时),所以可以保证阻塞操作的有效性不会受到信号异步性的影响。比如:
void
handler(int sig)
{
sigprocmask(SIG_SETMASK, &mask, &prevmask); // block some signal
/* DO SOMETHING*/
sigprocmask(SIG_SETMASK, &prevmask, NULL); // restore
}
如果要被阻塞的信号在第四行前来到,那么第四行就没起到它应有的作用。
另外,sigaction
还支持自动重启被中断的系统调用、不将子进程转为僵尸进程、使用自定义的栈执行信号处理函数等等功能。
sigprocmask
该系统调用用于设置和查看“阻塞信号列表”。
内核为进程(线程)维护了一组被阻塞的信号列表,处于该列表中的信号不会被进程接收到,而是等待进程停止阻塞。信号被阻塞时,对新到的信号处理方式有排队和不排队两种,标准信号不排队,实时信号排队。
sigpenging
用于查看由于阻塞而正在等待的信号。
sigsuspend
在更改信号掩码的之后,挂起进程直到新的信号到达,信号处理函数返回后,自动还原信号掩码。
有时候实现这样的功能:信号原本被阻塞,现在需要取消阻塞,直到第一个信号到达,也就是希望同步等待一个信号。如果按照如下方式写:
sigprocmask(SIG_SETMASK, &mask, &prevmask);
pause();
sigprocmask(SIG_SETMASK, &prevmask, NULL);
看起来能实现所需的功能:先设置新的阻塞掩码,然后等待信号到达,最后恢复。但是如果信号在pause
调用之前、第一个sigprocmask
之后到达,那么程序的表现就不正确了,之后的pause
返回并不是因为信号的第一次到达。实质上,这是一个竞态问题,竞态的双方为主程序和信号的处理函数。
sigsuspend
就可以解决这个问题,这个系统调用将上面三个操作封装成一个原子操作,以解决这里的竞态问题。如果sigsuspend
被信号打断(完成目标),则返回-1
,errno
设置为EINTR
。
sigwaitinfo
这是类似sigsuspend
的一个调用,也是实现信号的同步等待。通过参数mask
来指定需要等待的信号。
但是与sigsuspend
不同之处在于:
sigwaitinfo
并不会去调用信号处理函数,而是直接返回信号的编号和一个siginfo_t
结构体;sigwaitinfo
的参数mask
并不是用来替换当前的mask
的,而是指定需要等待的信号,相反,sigsuspend
的mask
参数会被用来替换当前进程的mask。
需要注意的是,在sigwaitinfo
一个信号前,需要显式地将这个信号阻塞(阻塞之后sigwaitinfo
仍然可以接收到信号的传递);如果sigwaitinfo
的时候没有对相应的信号进行阻塞,sigwaitinfo
的结果未定义。通常会在sigwaitinfo
之前阻塞所有的信号。
sigwaitinfo
有可能被其他未阻塞的信号打断,此时errno
为EINTR
。
signalfd
这也是用来使信号同步接收的一个调用,这个调用为信号的接受创建了一个文件描述符,当新的信号到达时,文件描述符变为可读状态。可以配合select
等完成同步。
sigset_t
信号集的数据类型,用于描述一组信号,实现方式取决于具体实现,可能为整型、数组或者结构体。
以下为glibc
提供的信号集操作函数:
sigemptyset
清空sigfillset
填满sigaddset
添加sigdelset
删除sigismember
查询