信号与线程
Unix的信号机制在诞生之初,生活在只有进程(process)的相对单纯的环境中。自从Unix世界有了线程(thread)的概念,信号就被赋予了发往进程中某个特定线程的能力,当然,这也增加了整个信号机制实现的复杂度。本系列的前面三篇文章都是基于进程进行的信号实现机制的讨论,本文将着重介绍Linux中信号和线程之间的交互。
发送信号给线程
无论是kill()还是sigqueue(),都只能向进程发送信号,在Linux中,要向进程内的线程发送信号,需要使用tkill()或者tgkill():
int
两个函数中,"tid"都是代表目标线程的PID,但tgkill()比tkill()多了一个"tgid"的参数。"tgid"是目标线程所在进程的PID,它可以用来防止向错误的线程发送信号。
发送方给目标线程发送信号时,可能目标线程已经因为某种原因退出了,按照Linux中PID的分配规则,退出线程/进程的PID可被分配给其他的线程/进程使用。这种情况下,如果使用tkill(),就可能出现将信号发送到不相干的线程上。引入"tgid"可以帮助进行目标线程所在进程的校验,这样出现错发的可能性就被大大地降低了。
线程对信号的接收
根据POSIX标准的定义,进程内的所有线程共享进程的信号处理函数,当进程内的一个线程为某个信号注册了处理函数,另一个线程可以更改这个处理函数。在Linux的实现中,线程作为独立的调度实体也有自己的task_struct,同一进程的不同线程的task_struct的"sighand"将指向同一个包含信号处理函数列表的sighand_struct。
但是,每个线程可以有单独的pending位图/队列和block位图。如果一个信号是发送给线程的,那么内核在递送该信号时,会将它放入线程私有的pending位图/队列中,之后根据目标线程的block位图的设置,直接由目标线程处理就可以了。
如果信号是发送给一个进程的,那么该信号在递送时将被内核放入进程的pending位图/队列中,由进程内的所有线程共享。接下来,内核会从进程的各个线程中,挑选一个block位图中没有屏蔽该信号的线程,来执行对应的信号处理函数,其中,进程的主线程将被内核优先选择。
但是有一些信号是需要进程内的全体线程都做出响应的,比如前面提到的令进程闻风丧胆的SIGKILL,它一旦到来,就不会留下一个活口。
当一个线程即将被内核调度执行,而该线程私有的penging位图/队列和所在进程共享的penging位图/队列上都有待处理的信号时,内核将优先向线程递送私有的penging位图/队列上的信号:
int
作为Unix时代的产物,前面讲到的sigprocmask()函数最初是用来设置进程的block位图的,但是同pending位图/队列不同的是,并没有一个所谓的线程共享的block位图的概念,所以在多线程环境下,sigprocmask()的语义也就变成了设置线程的block位图。当然,为了语义更加明显,你可以使用POSIX线程库提供的pthread_procmask()函数,两者的参数和行为都是一样的。
参考:
- 《Linux环境编程:从应用到内核》第6章
- Understanding the Linux Kernel 第3版第11章
- UNIX Internals: The New Frontiers 第4章
- http://kernel.meizu.com/linux-signal.html
- https://www.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html?ca=drs-
原创文章,转载请注明出处。