APUE原文
Linux 多线程应用中如何编写安全的信号处理函数
LinuxThread与NPTL
多线程下慎用sigwait
linux中使用信号–sigwait()和pthread_sigmask()
1. 信号与线程三个函数
利用sigwait,sigwaitinfo,pthread_sigmask函数可以解决如下问题
- 以线程同步的方式,处理异步信号
- 在指定的线程中处理信号
信号与线程主要涉及如下三个函数:
#include <signal.h>
int sigwait(const sigset_t *restrict set, int *restrict signop);
返回值:若成功则返回0,否则返回错误编号
set参数指出了线程等待的信号集,signop指向的整数将作为返回值,表明信号编号。
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
返回值:若成功则返回0,否则返回错误编号
- Linux 多线程应用中,每个线程可以通过调用 pthread_sigmask() 设置本线程的信号掩码。一般情况下,被阻塞的信号将不能中断此线程的执行,除非此信号的产生是因为程序运行出错如SIGSEGV;另外不能被忽略处理的信号 SIGKILL 和 SIGSTOP 也无法被阻塞。
- 利用线程信号屏蔽集的继承关系,新建的线程会继承主线程的信号屏蔽集!当一个线程调用 pthread_create() 创建新的线程时,此线程的信号掩码会被新创建的线程继承。
#include <signal.h>
int pthread_kill(pthread_t thread, int signo);
返回值:若成功则返回0,否则返回错误编号
2. sigwait函数分析
- sigwait函数是将队列中处于pending状态的信号(已经产生,并未被处理的信号,可能被阻塞)移除,但是并不解除原来信号的屏蔽状态
- 由于进程中,如果接收到一个信号,如果运行的线程没有屏蔽该信号,如果signal或者sigaction已经设置了信号处理函数,那么该线程会执行信号处理函数,那么调用sigwait函数的线程并不能获得pending状态的信号,所以在调用sigwait前需要所有线程均屏蔽该信号。
- 在一个进程中,signal和sigaction是所有线程共享的信号处理函数,所以如果在一个线程中使用signal,那么会改变所有线程的信号处理,所以多线程中尽量使用sigwait等
- sigpromask用在单线程的场景,如果是多线程则必须使用pthread_sigmask
- 正常情况下sigwait不会被信号打断,如果已经将该信号加入信号屏蔽集,但是也有例外,比如SIGSEGV,SIGKILL 和 SIGSTOP等!
3. linux多线程模型分析
多线程下信号处理函数建立流程;
- 主线程设置信号掩码,阻碍希望同步处理的信号;主线程的信号掩码会被其创建的线程继承;
- 主线程创建信号处理线程;信号处理线程将希望同步处理的信号集设为 sigwait()的第一个参数。
- 主线程创建工作线程。
3.1 例子1
主线程设置信号掩码阻碍 SIGUSR1 和 SIGRTMIN 两个信号,然后创建信号处理线程sigmgr_thread()和五个工作线程 worker_thread()。主线程每隔10秒调用 kill() 对本进程发送 SIGUSR1 和 SIGTRMIN 信号。信号处理线程 sigmgr_thread()在接收到信号时会调用信号处理函数 sig_handler()。
#include <signal.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void sig_handler(int signum)
{
static int j = 0;
static int k = 0;
pthread_t sig_ppid = pthread_self();
// used to show which thread the signal is handled in.
if (signum == SIGUSR1) {
printf("thread %d, receive SIGUSR1 No. %d\n", sig_ppid, j);
j++;
//SIGRTMIN should not be considered constants from userland,
//there is compile error when use switch case
} else if (signum == SIGRTMIN) {
printf("thread %d, receive SIGRTMIN No. %d\n", sig_ppid, k);
k++;
}
}
void* worker_thread()
{
pthread_t ppid = pthread_self();
pthread_detach(ppid);
while (1) {
printf("I'm thread %d, I'm alive\n", ppid);
sleep(10);
}
}
void* sigmgr_thread()
{
sigset_t waitset, oset;
siginfo_t info;
int rc;
pthread_t ppid = pthread_self();
pthread_detach(ppid);
sigemptyset(&waitset);
sigaddset(&waitset, SIGRTMIN);
sigaddset(&waitset, SIGUSR1);
while (1) {
rc = sigwaitinfo(&waitset, &info);
if (rc != -1) {
printf("sigwaitinfo() fetch the signal - %d\n", rc);
sig_handler(info.si_signo);
} else {
printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));
}
}
}
int main()
{
sigset_t bset, oset;
int i;
pid_t pid = getpid();
pthread_t ppid;
// Block SIGRTMIN and SIGUSR1 which will be handled in
//dedicated thread sigmgr_thread()
// Newly created threads will inherit the pthread mask from its creator
sigemptyset(&bset);
sigaddset(&bset, SIGRTMIN);
sigaddset(&bset, SIGUSR1);
if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
printf("!! Set pthread mask failed\n");
// Create the dedicated thread sigmgr_thread() which will handle
// SIGUSR1 and SIGRTMIN synchronously
pthread_create(&ppid, NULL, sigmgr_thread, NULL);
// Create 5 worker threads, which will inherit the thread mask of
// the creator main thread
for (i = 0; i < 5; i++) {
pthread_create(&ppid, NULL, worker_thread, NULL);
}
// send out 50 SIGUSR1 and SIGRTMIN signals
for (i = 0; i < 50; i++) {
kill(pid, SIGUSR1);
printf("main thread, send SIGUSR1 No. %d\n", i);
kill(pid, SIGRTMIN);
printf("main thread, send SIGRTMIN No. %d\n", i);
sleep(10);
}
exit (0);
}
3.2 例子2
#include<stdio.h>
#include<pthread.h>
#include<signal.h>
static void sig_alrm(int signo);
static void sig_init(int signo);
int
main()
{
sigset_t set;
int sig;
sigemptyset(&set);
sigaddset(&set, SIGALRM);
pthread_sigmask(SIG_SETMASK, &set, NULL);//阻塞SIGALRM信号
signal(SIGALRM, sig_alrm);
signal(SIGINT, sig_init);
sigwait(&set, &sig);//sigwait只是从未决队列中删除该信号,并不改变信号掩码。也就是,当sigwait函数返回,它监听的信号依旧被阻塞。
switch(sig){
case 14:
printf("sigwait, receive signal SIGALRM\n");
/*do the job when catch the sigwait*/
break;
default:
break;
}
sigdelset(&set, SIGALRM);
pthread_sigmask(SIG_SETMASK, &set, NULL);
for(;;)
{}
return 0;
}
static void
sig_alrm(int signo)
{
printf("after sigwait, catch SIGALRM\n");
fflush(stdout);
return ;
}
static void
sig_init(int signo)
{
printf("catch SIGINT\n");
return ;
}
注意,sigdelset(&set, SIGALRM);
pthread_sigmask(SIG_SETMASK, &set, NULL);需要解除屏蔽,因为sigwait不解除屏蔽!
3.3 例子3
#include "apue.h"
#include <pthread.h>
int quitflag; /* set nonzero by thread */
sigset_t mask;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t wait = PTHREAD_COND_INITIALIZER;
void *
thr_fn(void *arg)
{
int err, signo;
for(; ;)
{
err = sigwait(&mask, &signo);
if(err != 0)
err_exit(err, "sigwait failed");
switch(signo)
{
case SIGINT:
printf("\ninterrupt\n");
break;
case SIGQUIT:
pthread_mutex_lock(&lock);
quitflag = 1;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&wait);
return(0);
default:
printf("unexpected signal %d\n", signo);
exit(1);
}
}
}
int
main(void)
{
int err;
sigset_t oldmask;
pthread_t tid;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGQUIT);
if((err = pthread_sigmask(SIG_BLOCK, &mask, &oldmask)) != 0)
err_exit(err, "SIG_BLOCK error");
err = pthread_create(&tid, NULL, thr_fn, 0);
if(err != 0)
err_exit(err, "can't create thread");
pthread_mutex_lock(&lock);
while(quitflag == 0)
pthread_cond_wait(&wait, &lock);
pthread_mutex_unlock(&lock);
/* SIGQUIT has been caught and is now blocked; do whatever */
quitflag = 0;
/* reset signal mask which unblocks SIGQUIT */
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
exit(0);
}
interrupt 键入中断字符
interrupt 再次键入中断字符
interrupt 再一次
用结束字符终止
这里并不让信号处理程序中断主控线程,而是由专门的独立控制线程进行信号处理。改动quitflag的值是在互斥量的保护下进行的,这样主控线程不会在调用pthread_cond_signal时错失唤醒调用。在主控线程中使用相同的互斥量来检查标志的值,并且原子地释放互斥量,等待条件的发生。
注意在主线程开始时阻塞SIGINT和SIGQUIT。当创建线程进行信号处理时,新建线程继承了现有的信号屏蔽字。因为sigwait会解除信号的阻塞状态,所以只有一个线程可以用于信号的接收。这使得对主线程进行编码时不必担心来自这些信号的中断。
4.总结
在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑使用同步模型进行处理;而对会导致程序运行终止的信号如 SIGSEGV 等,必须按照传统的异步方式使用 signal()、 sigaction()注册信号处理函数进行处理。这两种信号处理模型可根据所处理的信号的不同同时存在一个 Linux 应用中:
- 不要在线程的信号掩码中阻塞不能被忽略处理的两个信号 SIGSTOP 和 SIGKILL。
- 不要在线程的信号掩码中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS。
- 确保 sigwait() 等待的信号集已经被进程中所有的线程阻塞。
- 在主线程或其它工作线程产生信号时,必须调用 kill() 将信号发给整个进程,而不能使用 pthread_kill() 发送某个特定的工作线程,否则信号处理线程无法接收到此信号。
- 因为 sigwait()使用了串行的方式处理信号的到来,为避免信号的处理存在滞后,或是非实时信号被丢失的情况,处理每个信号的代码应尽量简洁、快速,避免调用会产生阻塞的库函数。