线程和信号
程序员文章站
2022-04-03 13:37:11
...
每个线程都有自己的信号屏蔽字,但是信号的处理是进程中的所有线程共享的。这意味着单个线程可以阻止某些信号,但当某个线程修改了与某个给定信号相关的处理行为以后,所有的线程都必须共享这个处理行为的改变。这样,如果一个线程选择忽略某个给定信号,那么另一个线程就可以通过以下两种方式撤销上述线程的信号选择:恢复信号的默认处理行为,或者为信号设置一个新的信号处理程序。
进程中的信号是递送到单个线程的。如果一个信号与硬件故障相关,那么该信号一般会被发送到引起该事件的线程中去,而其他的信号则被发送到任意一个线程。
在信号基础函数一节中讨论了进程如何使用 sigprocmask 函数来阻止信号发送,但该函数的行为在多线程的进程中并没有定义,线程中必须使用 pthread_sigmask。线程还可以通过 sigwait 等待一个或多个信号的出现,以及调用 pthread_kill 来向线程发送信号。
pthread_sigmask 的参数同 sigprocmask,在此就不再赘述,只是该函数是工作在线程中,而且失败时返回错误码。
sigwait 的 set 参数指定了线程等待的信号集。返回时,signop 指向的整数将包含发送信号的编号。如果信号集中的某个信号在 sigwait 调用时处于挂起状态,那么 sigwait 将无阻塞地返回。在返回之前,sigwait 将从进程中移除那些处于挂起等待状态的信号。如果具体实现支持排队信号,并且信号的多个实例被挂起,那么 sigwait 将会移除该信号的一个实例,其他的实例还要继续排队。
为避免错误行为发生,线程在调用 sigwait 之前,必须阻塞那些它正在等待的信号。sigwait 会原子地取消信号集的阻塞状态,直到有新的信号被递送。在返回前,sigwait 将恢复线程的信号屏蔽字。如果信号在 sigwait 被调用时没有被阻塞,那么在线程完成对 sigwait 的调用之前会出现一个时间窗,在该时间窗中,信号就可以被发送给线程。
使用 sigwait 的好处在于它可以简化信号处理,允许把异步产生的信号用同步的方式处理。为防止信号中断线程,可以把信号加到每个线程的信号屏蔽字中,然后可以安排专用线程处理信号。这些专用线程可以调用函数,不需要担心在信号处理程序中调用哪些函数是安全的,因为这些函数调用来自正常的线程上下文,而非会中断线程正常执行的传统信号处理程序。
如果多个线程在 sigwait 的调用中因等待同一个信号而阻塞,那么在信号递送的时候,就只有一个线程可以从 sigwait 中返回。如果一个信号被捕获,而一个线程正在 sigwait 调用中等待同一信号,那么这时将由操作系统实现来决定以何种方式递送信号:操作系统可以让 sigwait 返回,也可以激活信号处理程序,但这两种情况不会同时发生。
kill 函数可以把信号发送给进程,而要把信号发送给线程,可以调用 pthread_kill。可以传一个 0 值的 signo 来检查线程是否存在。注意,如果信号的默认动作是终止该进程,那么把信号传递给某个线程仍然会杀死整个进程。另外,由于闹钟定时器是进程资源,并且所有的线程共享相同的闹钟。所以进程中的多个线程不可能互不干扰地使用闹钟定时器。
在前面sigsuspend 函数和 abort 函数中介绍 sigsuspend 函数时,曾给出一个在信号处理程序中设置标志来表明主程序应该退出的例子,那个例子中唯一可运行的控制线程就是主线程和信号处理程序,所以阻塞信号足以避免错失标志修改。在多线程中,需要使用互斥量来保护标志,如下例程序所示。
程序运行结果同前面的那一个。这个程序中不用依赖信号处理程序中断主线程,有专门的独立线程进行信号处理。在互斥量的保护下改动 quitflag 的值,这样主线程就不会在 pthread_cond_signal 时错失唤醒调用。
这里需要注意的是,在主线程开始时阻塞 SIGINT 和 SIGQUIT。当创建线程进行信号处理程序时,新建线程继承了现有的信号屏蔽字。因为 sigwait 会解除信号的阻塞状态,所以只有一个线程可以用于信号的接收。这使得对主线程进行编码时不必担心来自这些信号的中断。
进程中的信号是递送到单个线程的。如果一个信号与硬件故障相关,那么该信号一般会被发送到引起该事件的线程中去,而其他的信号则被发送到任意一个线程。
在信号基础函数一节中讨论了进程如何使用 sigprocmask 函数来阻止信号发送,但该函数的行为在多线程的进程中并没有定义,线程中必须使用 pthread_sigmask。线程还可以通过 sigwait 等待一个或多个信号的出现,以及调用 pthread_kill 来向线程发送信号。
#include <signal.h> int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset); int sigwait(const sigset_t *restrict set, int *restrict signop); int pthread_kill(pthread_t thread, int signo); /* 所有函数返回值:若成功,返回 0;否则,返回错误编号 */
pthread_sigmask 的参数同 sigprocmask,在此就不再赘述,只是该函数是工作在线程中,而且失败时返回错误码。
sigwait 的 set 参数指定了线程等待的信号集。返回时,signop 指向的整数将包含发送信号的编号。如果信号集中的某个信号在 sigwait 调用时处于挂起状态,那么 sigwait 将无阻塞地返回。在返回之前,sigwait 将从进程中移除那些处于挂起等待状态的信号。如果具体实现支持排队信号,并且信号的多个实例被挂起,那么 sigwait 将会移除该信号的一个实例,其他的实例还要继续排队。
为避免错误行为发生,线程在调用 sigwait 之前,必须阻塞那些它正在等待的信号。sigwait 会原子地取消信号集的阻塞状态,直到有新的信号被递送。在返回前,sigwait 将恢复线程的信号屏蔽字。如果信号在 sigwait 被调用时没有被阻塞,那么在线程完成对 sigwait 的调用之前会出现一个时间窗,在该时间窗中,信号就可以被发送给线程。
使用 sigwait 的好处在于它可以简化信号处理,允许把异步产生的信号用同步的方式处理。为防止信号中断线程,可以把信号加到每个线程的信号屏蔽字中,然后可以安排专用线程处理信号。这些专用线程可以调用函数,不需要担心在信号处理程序中调用哪些函数是安全的,因为这些函数调用来自正常的线程上下文,而非会中断线程正常执行的传统信号处理程序。
如果多个线程在 sigwait 的调用中因等待同一个信号而阻塞,那么在信号递送的时候,就只有一个线程可以从 sigwait 中返回。如果一个信号被捕获,而一个线程正在 sigwait 调用中等待同一信号,那么这时将由操作系统实现来决定以何种方式递送信号:操作系统可以让 sigwait 返回,也可以激活信号处理程序,但这两种情况不会同时发生。
kill 函数可以把信号发送给进程,而要把信号发送给线程,可以调用 pthread_kill。可以传一个 0 值的 signo 来检查线程是否存在。注意,如果信号的默认动作是终止该进程,那么把信号传递给某个线程仍然会杀死整个进程。另外,由于闹钟定时器是进程资源,并且所有的线程共享相同的闹钟。所以进程中的多个线程不可能互不干扰地使用闹钟定时器。
在前面sigsuspend 函数和 abort 函数中介绍 sigsuspend 函数时,曾给出一个在信号处理程序中设置标志来表明主程序应该退出的例子,那个例子中唯一可运行的控制线程就是主线程和信号处理程序,所以阻塞信号足以避免错失标志修改。在多线程中,需要使用互斥量来保护标志,如下例程序所示。
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <signal.h> static int quitflag; // set nonzero by thread sigset_t mask; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t waitloc = PTHREAD_COND_INITIALIZER; void *thr_fn(void *arg){ int signo, err; for(;;){ if((err=sigwait(&mask, &signo)) != 0){ printf("sigwait error\n"); exit(1); } switch(signo){ case SIGINT: printf("interrupt\n"); break; case SIGQUIT: pthread_mutex_lock(&lock); quitflag = 1; pthread_mutex_unlock(&lock); pthread_cond_signal(&waitloc); return (void *)0; default: printf("unexpected signal: %d\n", signo); exit(1); } } } int main(void){ sigset_t oldmask; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGQUIT); pthread_sigmask(SIG_BLOCK, &mask, &oldmask); pthread_t tid; if(pthread_create(&tid, NULL, thr_fn, NULL) != 0){ printf("failed to create thread\n"); exit(1); } pthread_mutex_lock(&lock); while(quitflag == 0) pthread_cond_wait(&waitloc, &lock); pthread_mutex_unlock(&lock); quitflag = 0; // SIGQUIT has been caught and is blocked sigprocmask(SIG_SETMASK, &oldmask, NULL); exit(0); }
程序运行结果同前面的那一个。这个程序中不用依赖信号处理程序中断主线程,有专门的独立线程进行信号处理。在互斥量的保护下改动 quitflag 的值,这样主线程就不会在 pthread_cond_signal 时错失唤醒调用。
这里需要注意的是,在主线程开始时阻塞 SIGINT 和 SIGQUIT。当创建线程进行信号处理程序时,新建线程继承了现有的信号屏蔽字。因为 sigwait 会解除信号的阻塞状态,所以只有一个线程可以用于信号的接收。这使得对主线程进行编码时不必担心来自这些信号的中断。
上一篇: java线程的生命周期