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()使用了串行的方式處理信號的到來,爲避免信號的處理存在滯後,或是非實時信號被丟失的情況,處理每個信號的代碼應儘量簡潔、快速,避免調用會產生阻塞的庫函數。