APUE——信號與線程

APUE原文
Linux 多線程應用中如何編寫安全的信號處理函數
LinuxThread與NPTL
多線程下慎用sigwait
linux中使用信號–sigwait()和pthread_sigmask()

1. 信號與線程三個函數

利用sigwait,sigwaitinfo,pthread_sigmask函數可以解決如下問題

  1. 以線程同步的方式,處理異步信號
  2. 在指定的線程中處理信號
    信號與線程主要涉及如下三個函數:
#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,否則返回錯誤編號
  1. Linux 多線程應用中,每個線程可以通過調用 pthread_sigmask() 設置本線程的信號掩碼。一般情況下,被阻塞的信號將不能中斷此線程的執行,除非此信號的產生是因爲程序運行出錯如SIGSEGV;另外不能被忽略處理的信號 SIGKILL 和 SIGSTOP 也無法被阻塞。
  2. 利用線程信號屏蔽集的繼承關係,新建的線程會繼承主線程的信號屏蔽集!當一個線程調用 pthread_create() 創建新的線程時,此線程的信號掩碼會被新創建的線程繼承。
#include <signal.h>
int pthread_kill(pthread_t thread, int signo);
返回值:若成功則返回0,否則返回錯誤編號

2. sigwait函數分析

在這裏插入圖片描述

  1. sigwait函數是將隊列中處於pending狀態的信號(已經產生,並未被處理的信號,可能被阻塞)移除,但是並不解除原來信號的屏蔽狀態
  2. 由於進程中,如果接收到一個信號,如果運行的線程沒有屏蔽該信號,如果signal或者sigaction已經設置了信號處理函數,那麼該線程會執行信號處理函數,那麼調用sigwait函數的線程並不能獲得pending狀態的信號,所以在調用sigwait前需要所有線程均屏蔽該信號
  3. 在一個進程中,signal和sigaction是所有線程共享的信號處理函數,所以如果在一個線程中使用signal,那麼會改變所有線程的信號處理,所以多線程中儘量使用sigwait等
  4. sigpromask用在單線程的場景,如果是多線程則必須使用pthread_sigmask
  5. 正常情況下sigwait不會被信號打斷,如果已經將該信號加入信號屏蔽集,但是也有例外,比如SIGSEGV,SIGKILL 和 SIGSTOP等!

3. linux多線程模型分析

多線程下信號處理函數建立流程;

  1. 主線程設置信號掩碼,阻礙希望同步處理的信號;主線程的信號掩碼會被其創建的線程繼承;
  2. 主線程創建信號處理線程;信號處理線程將希望同步處理的信號集設爲 sigwait()的第一個參數。
  3. 主線程創建工作線程。
    在這裏插入圖片描述

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 應用中:

  1. 不要在線程的信號掩碼中阻塞不能被忽略處理的兩個信號 SIGSTOP 和 SIGKILL。
  2. 不要在線程的信號掩碼中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS。
  3. 確保 sigwait() 等待的信號集已經被進程中所有的線程阻塞。
  4. 在主線程或其它工作線程產生信號時,必須調用 kill() 將信號發給整個進程,而不能使用 pthread_kill() 發送某個特定的工作線程,否則信號處理線程無法接收到此信號。
  5. 因爲 sigwait()使用了串行的方式處理信號的到來,爲避免信號的處理存在滯後,或是非實時信號被丟失的情況,處理每個信號的代碼應儘量簡潔、快速,避免調用會產生阻塞的庫函數。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章