8、线程和信号
本来在进程中信号处理就已经很麻烦了,再引入线程使得这个问题变得更加麻烦。
每个线程都有它自己的signal mask,但是整个进程中的信号的处理特性(动作等)却被所有的线程共享。也就是说,单个的一个线程就可以阻塞信号,但是如果修改了信号的处理动作,那么所有的线程的信号处理动作都变了。所以,一个线程忽略了一个信号,那么另一个线程可以通过为这个信号设置默认动作或者安装信号处理函数来取消忽略。
信号会被发送给进程中的单个线程,如果信号和硬件相关或者超时相关,那么信号会被发送给导致这个事件的线程。而其它的信号会被发送给任意一个线程。
前面讲述了进程使用sigprocmask来阻塞信号发送的方法。然而这个函数并不适用于多线程环境,线程应该使用pthread_sigmask来替代这个函数。
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
这个函数如果成功返回0,如果失败返回错误号码。
此函数和sigprocmask的功能是一样的,不同的只是它应该被线程使用并且失败的时候返回错误号码,而不想sigprocmask失败的时候返回-1并且设置errno.
线程使用sigwait来等待一个或者多个信号的发生。
#include <signal.h>
int sigwait(const sigset_t *restrict set, int *restrict signop);
如果成功返回0,如果失败返回错误号码。
参数set指定线程等待的信号的集合,返回的时候,signop指向的整数包含被发送的信号的号码。
如果在调用sigwait的时候,set中的某个信号处于提交状态,那么sigwait会立即返回不会阻塞。返回之前,会将这个信号从进程的处于提交状态的信号集合中移走。为了防止出现错误的情况,sigwait被调用之前线程应该阻塞它所等待的信号,然后sigwait会自动解锁这些信号并且等待它们中的一个发生。返回之前sigwait会恢复线程的signal mask。而如果调用sigwait信号没有被阻塞的话会出现一个时间窗口,导致被等待的信号在发生的时候,sigwait还没有来得及完成它的调用。
使用sigwait的一个优点就是,它可以通过让我们使用一种同步的方式来对待异步产生的信号,这样简化信号的处理方式。我们可以通过将信号加入到线程的signal mask中来阻止信号打断线程的执行。然后我们可以指定特定的线程来处理信号,这些被指定的线程可以使用函数调用并且不用担心哪些函数从信号处理中被调用是安全的,因为它们是在一个普通的线程上下文中进行调用,而不是在打断线程执行的那个信号处理函数中了。
如果多个线程调用sigwait阻塞在一个信号上面,那么当这个信号被发送的时候,只有一个线程会从sigwait中返回。如果一个信号被捕获(例如进程通过sigaction为信号建立起来了一个信号处理函数)并且有一个线程调用了sigwait等待这个信号,那么如何处理发送信号的方法取决于系统实现。这时候,系统实现可以允许sigwait返回,或者调用信号处理函数,但是不能两者都做。
我们使用kill给进程发送信号,使用pthread_kill来给线程发送信号。
#include <signal.h>
int pthread_kill(pthread_t thread, int signo);
如果成功返回0,如果失败返回错误号码。
我们可以给signo赋值为0,来检测线程是否存在。如果我们使用pthread_kill来给一个线程发送信号,并且信号的默认处理动作是终止整个进程,那么结果仍然会导致整个进程被终止。
注意时钟计时器(alarm timers)是一个进程资源,所有的线程共享同样的一套时钟。所以,如果多线程在进程中使用时钟,不可能对其他的线程不造成影响(也就是说多个线程中使用时钟计时那么需要它们之间有一定的协调)。
举例
前面我们曾经举过一个例子,一个程序当它捕获信号的时候会在信号处理函数中设置一个标记标志是否需要退出,然后主程序检测这个标记,来确定程序是否应该退出。因为这个程序的线程只有一个主线程,还有相应的信号处理函数,所以我们只要阻塞相应的信号就不会丢失对那个标记的修改。然而在多个线程中我们就需要使用互斥量来保护这个标记了。
本书这一节给出了一个例子,具体参见参考资料。这里只大致说明一下:这个例子使用一个单独的线程来设置标记而不是使用信号量处理函数,它在互斥量的保护下修改那个标记,这样主线程不会丢失pthread_cond_signal时候的唤醒动作,我们在主线程中检查这个标记的时候也使用同样的互斥量来访问这个标记(在等待条件的时候当然会自动释放这个信号量具体参考前面讲述的条件变量)。
还有一个需要注意的地方就是我们在主线程的开始阻塞了那些(可能导致标记被修改的)信号,然后创建子线程来捕获那些信号(使用sigwait函数),捕获到信号的时候就可能会修改标记(取决于具体实现)。这样,只有一个线程会接收到相应的信号,我们也不用担心主线程会被那个信号所打断了。
Linux把线程作为一个独立的进程来实现,进程之间分享相应的资源,使用clone(2)来创建这样的进程。因为这个原因,在涉及到信号处理的时候,linux上面的线程的行为就和其他实现上的行为不一样了。在POSIX.1的线程模型中,异步信号会被发送给一个进程,然后选择进程中的一个独立的线程来接收这个信号,这基于那个线程当前没有阻塞这个信号。在Linux上面,一个异步的信号会被发送给一个特定的线程,并且因为每个线程做为一个独立的进程运行,这样系统也无法选择当前没有阻塞这个信号的线程。这样的结果就是线程可能不会注意到这个信号。因此像本例子中的程序,如果信号是从终端驱动中生成的,那么就会好用,因为会发送信号给整个进程组(这样无论线程进程都会收到信号),但是当你使用kill想要发送一个信号给进程的时候,在Linux上面它就不会如你所期望的那样工作了(一个进程就是一个线程而不是一组线程的集合的原因吗?)。
网友评论