多线程环境的信号递送
在一个单进程中,信号会打断进程的执行,并且递送到进程中处理,而对于多线程环境,信号会递送给其中的一个线程,这个被递送的线程是不确定的。每个线程存在自己的信号屏蔽字,可以通过如下函数设置:
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
该函数设置线程对应的信号屏蔽字,被屏蔽的信号会被挂起从而无法递送到对应的线程。这里有一点需要特别注意,线程的信号屏蔽字是可以向下继承的,主线程设置的信号屏蔽字可以被后续创建的线程继承。
信号的阻塞
进程环境中sigprocmask可以用来阻塞进程中信号的递送,而pthread_sigmask可以阻塞线程中的信号递送,阻塞信号的可以阻塞几乎所有的信号,但是有两个信号除外:SIGKILL和SIGSTOP。
另外为了保证程序的健壮性,一些必要的错误信号最好在程序编写时也不要阻塞,比如:
TSIGFPE,SIGILL,SIGSEGV,SIGBUS
这些信号的到来都表示此进程已经处于不可控的状态,比如算术运算错误或者内存引用错误,或者是硬件错误等。
sigwait同步处理
由于每个线程都可能接收到信号,当进程收到信号时,如果是多线程的情况,我们是无法确定是哪一个线程处理这个信号,因此多线程的异步信号处理就显得很复杂,而sigwait可以实现信号的同步处理。sigwait是从进程被阻塞而挂起的信号中,接收指定的信号,多线程环境一般都会设置一个线程sigwait专门处理信号,那么所有线程都必须要先屏蔽要处理的信号,可以利用继承关系方便达到此目的。
#include <signal.h>
int sigwait(const sigset_t *set, int *sig);
需要注意的是sigwait不改变当前线程的信号屏蔽字,只是从pending的信号中取出信号去处理,因而不会触发进程的signal handler。sigwait的第一个参数是一个信号集,指的含义是该函数所等待的信号集,它不会修改信号屏蔽字。
如果信号集中不存在的信号,即使它是处于pending状态,sigwait依然不会返回。
系统调用的中断
在多线程环境中,只有接收了信号的线程,阻塞的系统调用才会中断返回,而未接受的线程,系统调用不受影响,因此我们使用sigwait专门线程处理信号,会使系统调用的使用更加简单,工作线程肯定不会接收信号,因此不用担心被信号中断。
信号的两种处理方式
多线程环境下,信号处理有两种方式:
- 依然使用进程的信号处理程序处理,对于多线程环境下,signal和sigaction的处理函数是所有线程共享的,在任一个线程修改,都会影响整个进程。如果有信号递送到了该进程,它依然会有效触发,只是它所打断的线程是不确定的。
- sigwait来处理,如果信号被sigwait所接收,那么它肯定是挂起的,也就是说此信号一定是被阻塞递送的,因此signal或者sigaction处理函数不会处理它。
假想如下场景:多线程环境中,如果有一个信号,我们不想使用sigwait来处理,而使用进程默认的signal handler去处理,而其他信号都想被一个线程sigwait同步处理,那要如果实现呢?
- 该信号想要触发signal handler,一定要能够被递送到进程,或者说一定要存在一个线程允许递送它,也就是不能完全阻塞。
- 想要被sigwait接收的信号,一定是pending信号,那么一定要先阻塞对应的信号。
- 为了防止工作线程系统调用被打断,创建一个线程处理信号,保证signal handler还是sigwait都在该特定线程运行。
示例代码:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/resource.h>
#include <pthread.h>
#include "include/debug.h"
void alarm_func(int signo)
{
pr_info("SIGALRM receive!\n");
}
void *thr_fn(void *arg)
{
int err, signo;
sigset_t set,mask;
signal(SIGALRM, alarm_func);
sigfillset(&mask);
sigdelset(&mask, SIGALRM);
if ((err = pthread_sigmask(SIG_SETMASK, &mask, NULL)) != 0)
err_exit("SIG_BLOCK error\n");
sigfillset(&set);
sigdelset(&set, SIGALRM);
for (;;) {
err = sigwait(&set, &signo);
if (err != 0) {
err_exit("sigwait failed\n");
}
switch (signo) {
case SIGTERM:
pr_info("Catch SIGTERM; exiting\n");
break;
case SIGQUIT:
pr_info("Catch SIGQUIT; exiting\n");
exit(0);
break;
case SIGINT:
pr_info("Catch SIGINT; exiting\n");
break;
default:
pr_info("Unexpected signal %d\n", signo);
}
}
return(0);
}
pthread_t setup_signal_thread(void)
{
int err;
struct sigaction sa;
sigset_t mask;
pthread_t tid;
/*
* Block all signals in main thread
*/
sigfillset(&mask);
if ((err = pthread_sigmask(SIG_SETMASK, &mask, NULL)) != 0)
err_exit("SIG_BLOCK error\n");
/*
* Create a child thread to handle SIGHUP and SIGTERM.
*/
err = pthread_create(&tid, NULL, thr_fn, 0);
if (err != 0)
err_exit("can't create thread\n");
return tid;
}
int main()
{
char buf[10];
pthread_t tid;
void *tret;
tid = setup_signal_thread();
pr_info("signal thread created\n");
read(STDIN_FILENO, buf, 10);
pr_info("read was interrupted!\n");
}
该示例在工作线程(主线程中)阻塞了所有的信号,这样后续创建的线程都会继承该屏蔽字。新创建的信号线程处理两种情况:
- SIGALRM 信号使用signal handler来处理,因此该线程的信号屏蔽字必须没有SIGALRM。
- 其他信号使用sigwait来处理,sigwait接收的信号必须是线程阻塞的信号。
运行结果如下:
$ ./syscall_intr
[5216] INFO: signal thread created
[5216] INFO: SIGALRM receive!
[5216] INFO: SIGALRM receive!
[5216] INFO: SIGALRM receive!
[5216] INFO: SIGALRM receive!
[5216] INFO: SIGALRM receive!
[5216] INFO: SIGALRM receive!
[5216] INFO: SIGALRM receive!
[5216] INFO: SIGALRM receive!
[5216] INFO: SIGALRM receive!
[5216] INFO: SIGALRM receive!
[5216] INFO: Unexpected signal 6
[5216] INFO: Unexpected signal 6
[5216] INFO: Unexpected signal 6
[5216] INFO: Catch SIGQUIT; exiting