日韩久久久精品,亚洲精品久久久久久久久久久,亚洲欧美一区二区三区国产精品 ,一区二区福利

Linux 多線程應(yīng)用中如何編寫安全的信號處理函數(shù)

系統(tǒng) 2142 0

在開發(fā)多線程應(yīng)用時(shí),開發(fā)人員一般都會(huì)考慮線程安全,會(huì)使用 pthread_mutex 去保護(hù)全局變量。如果應(yīng)用中使用了信號,而且信號的產(chǎn)生不是因?yàn)槌绦蜻\(yùn)行出錯(cuò),而是程序邏輯需要,譬如 SIGUSR1、SIGRTMIN 等,信號在被處理后應(yīng)用程序還將正常運(yùn)行。在編寫這類信號處理函數(shù)時(shí),應(yīng)用層面的開發(fā)人員卻往往忽略了信號處理函數(shù)執(zhí)行的上下文背景,沒有考慮編寫安全的信號處理函數(shù)的一些規(guī)則。本文首先介紹編寫信號處理函數(shù)時(shí)需要考慮的一些規(guī)則;然后舉例說明在多線程應(yīng)用中如何構(gòu)建模型讓因?yàn)槌绦蜻壿嬓枰a(chǎn)生的異步信號在指定的線程中以同步的方式處理。

線程和信號

Linux 多線程應(yīng)用中,每個(gè)線程可以通過調(diào)用 pthread_sigmask() 設(shè)置本線程的信號掩碼。一般情況下,被阻塞的信號將不能中斷此線程的執(zhí)行,除非此信號的產(chǎn)生是因?yàn)槌绦蜻\(yùn)行出錯(cuò)如 SIGSEGV;另外不能被忽略處理的信號 SIGKILL 和 SIGSTOP 也無法被阻塞。

當(dāng)一個(gè)線程調(diào)用 pthread_create() 創(chuàng)建新的線程時(shí),此線程的信號掩碼會(huì)被新創(chuàng)建的線程繼承。

POSIX.1 標(biāo)準(zhǔn)定義了一系列線程函數(shù)的接口,即 POSIX threads(Pthreads)。Linux C 庫提供了兩種關(guān)于線程的實(shí)現(xiàn):LinuxThreads 和 NPTL(Native POSIX Threads Library)。LinuxThreads 已經(jīng)過時(shí),一些函數(shù)的實(shí)現(xiàn)不遵循POSIX.1 規(guī)范。NPTL 依賴 Linux 2.6 內(nèi)核,更加遵循 POSIX..1 規(guī)范,但也不是完全遵循。

基于 NPTL 的線程庫,多線程應(yīng)用中的每個(gè)線程有自己獨(dú)特的線程 ID,并共享同一個(gè)進(jìn)程ID。應(yīng)用程序可以通過調(diào)用 kill(getpid(),signo) 將信號發(fā)送到進(jìn)程,如果進(jìn)程中當(dāng)前正在執(zhí)行的線程沒有阻礙此信號,則會(huì)被中斷,線號處理函數(shù)會(huì)在此線程的上下文背景中執(zhí)行。應(yīng)用程序也可以通過調(diào)用 pthread_kill(pthread_t thread, int sig) 將信號發(fā)送給指定的線程,則線號處理函數(shù)會(huì)在此指定線程的上下文背景中執(zhí)行。

基于 LinuxThreads 的線程庫,多線程應(yīng)用中的每個(gè)線程擁有自己獨(dú)特的進(jìn)程 ID, getpid() 在不同的線程中調(diào)用會(huì)返回不同的值,所以無法通過調(diào)用 kill(getpid(),signo) 將信號發(fā)送到整個(gè)進(jìn)程。

下文介紹的在指定的線程中以同步的方式處理異步信號是基于使用了 NPTL 的 Linux C 庫。請參考“ Linux 線程模型的比較:LinuxThreads 和 NPTL ”和“ pthreads(7) - Linux man page ”進(jìn)一步了解 Linux 的線程模型,以及不同版本的 Linux C 庫對 NPTL 的支持。


編寫安全的異步信號處理函數(shù)

信號的產(chǎn)生可以是:

  • 用戶從控制終端終止程序運(yùn)行,如 Ctrk + C 產(chǎn)生 SIGINT;
  • 程序運(yùn)行出錯(cuò)時(shí)由硬件產(chǎn)生信號,如訪問非法地址產(chǎn)生 SIGSEGV;
  • 程序運(yùn)行邏輯需要,如調(diào)用 kill raise 產(chǎn)生信號。

因?yàn)樾盘柺钱惒绞录葱盘柼幚砗瘮?shù)執(zhí)行的上下文背景是不確定的,譬如一個(gè)線程在調(diào)用某個(gè)庫函數(shù)時(shí)可能會(huì)被信號中斷,庫函數(shù)提前出錯(cuò)返回,轉(zhuǎn)而去執(zhí)行信號處理函數(shù)。對于上述第三種信號的產(chǎn)生,信號在產(chǎn)生、處理后,應(yīng)用程序不會(huì)終止,還是會(huì)繼續(xù)正常運(yùn)行,在編寫此類信號處理函數(shù)時(shí)尤其需要小心,以免破壞應(yīng)用程序的正常運(yùn)行。關(guān)于編寫安全的信號處理函數(shù)主要有以下一些規(guī)則:

  • 信號處理函數(shù)盡量只執(zhí)行簡單的操作,譬如只是設(shè)置一個(gè)外部變量,其它復(fù)雜的操作留在信號處理函數(shù)之外執(zhí)行;
  • errno 是線程安全,即每個(gè)線程有自己的 errno ,但不是異步信號安全。如果信號處理函數(shù)比較復(fù)雜,且調(diào)用了可能會(huì)改變 errno 值的庫函數(shù),必須考慮在信號處理函數(shù)開始時(shí)保存、結(jié)束的時(shí)候恢復(fù)被中斷線程的 errno 值;
  • 信號處理函數(shù)只能調(diào)用可以重入的 C 庫函數(shù);譬如不能調(diào)用 malloc(),free() 以及標(biāo)準(zhǔn) I/O 庫函數(shù)等;
  • 信號處理函數(shù)如果需要訪問全局變量,在定義此全局變量時(shí)須將其聲明為 volatile, 以避免編譯器不恰當(dāng)?shù)膬?yōu)化。

從整個(gè) Linux 應(yīng)用的角度出發(fā),因?yàn)閼?yīng)用中使用了異步信號,程序中一些庫函數(shù)在調(diào)用時(shí)可能被異步信號中斷,此時(shí)必須根據(jù) errno 的值考慮這些庫函數(shù)調(diào)用被信號中斷后的出錯(cuò)恢復(fù)處理,譬如socket 編程中的讀操作:

                 rlen = recv(sock_fd, buf, len, MSG_WAITALL); 
     if ((rlen == -1) && (errno == EINTR)){
         // this kind of error is recoverable, we can set the offset change 
         //‘rlen’ as 0 and continue to recv
     }

          

在指定的線程中以同步的方式處理異步信號

如上文所述,不僅編寫安全的異步信號處理函數(shù)本身有很多的規(guī)則束縛;應(yīng)用中其它地方在調(diào)用可被信號中斷的庫函數(shù)時(shí)還需考慮被中斷后的出錯(cuò)恢復(fù)處理。這讓程序的編寫變得復(fù)雜,幸運(yùn)的是,POSIX.1 規(guī)范定義了 sigwait()、 sigwaitinfo() pthread_sigmask() 等接口,可以實(shí)現(xiàn): sigwait

  • 以同步的方式處理異步信號;
  • 在指定的線程中處理信號。

這種在指定的線程中以同步方式處理信號的模型可以避免因?yàn)樘幚懋惒叫盘柖o程序運(yùn)行帶來的不確定性和潛在危險(xiǎn)。

sigwait() 提供了一種等待信號的到來,以串行的方式從信號隊(duì)列中取出信號進(jìn)行處理的機(jī)制。 sigwait( )只等待函數(shù)參數(shù)中指定的信號集,即如果新產(chǎn)生的信號不在指定的信號集內(nèi),則 sigwait() 繼續(xù)等待。對于一個(gè)穩(wěn)定可靠的程序,我們一般會(huì)有一些疑問:

  • 多個(gè)相同的信號可不可以在信號隊(duì)列中排隊(duì)?
  • 如果信號隊(duì)列中有多個(gè)信號在等待,在信號處理時(shí)有沒有優(yōu)先級規(guī)則?
  • 實(shí)時(shí)信號和非實(shí)時(shí)信號在處理時(shí)有沒有什么區(qū)別?

筆者寫了一小段測試程序來測試 sigwait 在信號處理時(shí)的一些規(guī)則。


清單 1. sigwait_test.c

            #include <signal.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void sig_handler(int signum)
{
    printf("Receive signal. %d/n", signum);
}

void* sigmgr_thread()
{
    sigset_t   waitset, oset;
    int        sig;
    int        rc;
    pthread_t  ppid = pthread_self();

    pthread_detach(ppid);

    sigemptyset(&waitset);
    sigaddset(&waitset, SIGRTMIN);
    sigaddset(&waitset, SIGRTMIN+2);
    sigaddset(&waitset, SIGRTMAX);
    sigaddset(&waitset, SIGUSR1);
    sigaddset(&waitset, SIGUSR2);

    while (1)  {
        rc = sigwait(&waitset, &sig);
        if (rc != -1) {
            sig_handler(sig);
        } 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;

    sigemptyset(&bset);
    sigaddset(&bset, SIGRTMIN);
    sigaddset(&bset, SIGRTMIN+2);
    sigaddset(&bset, SIGRTMAX);
    sigaddset(&bset, SIGUSR1);
    sigaddset(&bset, SIGUSR2);

    if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
        printf("!! Set pthread mask failed/n");

    kill(pid, SIGRTMAX);
    kill(pid, SIGRTMAX);
    kill(pid, SIGRTMIN+2);
    kill(pid, SIGRTMIN);
    kill(pid, SIGRTMIN+2);
    kill(pid, SIGRTMIN);
    kill(pid, SIGUSR2);
    kill(pid, SIGUSR2);
    kill(pid, SIGUSR1);
kill(pid, SIGUSR1);

    // Create the dedicated thread sigmgr_thread() which will handle signals synchronously
    pthread_create(&ppid, NULL, sigmgr_thread, NULL);

    sleep(10);

    exit (0);
}

          

程序編譯運(yùn)行在 RHEL4 的結(jié)果如下:


圖 1. sigwait 測試程序執(zhí)行結(jié)果

從以上測試程序發(fā)現(xiàn)以下規(guī)則:

  • 對于非實(shí)時(shí)信號,相同信號不能在信號隊(duì)列中排隊(duì);對于實(shí)時(shí)信號,相同信號可以在信號隊(duì)列中排隊(duì)。
  • 如果信號隊(duì)列中有多個(gè)實(shí)時(shí)以及非實(shí)時(shí)信號排隊(duì),實(shí)時(shí)信號并不會(huì)先于非實(shí)時(shí)信號被取出,信號數(shù)字小的會(huì)先被取出:如 SIGUSR1(10)會(huì)先于 SIGUSR2 (12),SIGRTMIN(34)會(huì)先于 SIGRTMAX (64), 非實(shí)時(shí)信號因?yàn)槠湫盘枖?shù)字小而先于實(shí)時(shí)信號被取出。

sigwaitinfo() 以及 sigtimedwait() 也提供了與 sigwait() 函數(shù)相似的功能。

Linux 多線程應(yīng)用中的信號處理模型

在基于 Linux 的多線程應(yīng)用中,對于因?yàn)槌绦蜻壿嬓枰a(chǎn)生的信號,可考慮調(diào)用 sigwait() 使用同步模型進(jìn)行處理。其程序流程如下:

  1. 主線程設(shè)置信號掩碼,阻礙希望同步處理的信號;主線程的信號掩碼會(huì)被其創(chuàng)建的線程繼承;
  2. 主線程創(chuàng)建信號處理線程;信號處理線程將希望同步處理的信號集設(shè)為 sigwait() 的第一個(gè)參數(shù)。
  3. 主線程創(chuàng)建工作線程。


圖 2. 在指定的線程中以同步方式處理異步信號的模型

代碼示例

以下為一個(gè)完整的在指定的線程中以同步的方式處理異步信號的程序。

主線程設(shè)置信號掩碼阻礙 SIGUSR1 和 SIGRTMIN 兩個(gè)信號,然后創(chuàng)建信號處理線程 sigmgr_thread() 和五個(gè)工作線程 worker_thread() 。主線程每隔10秒調(diào)用 kill() 對本進(jìn)程發(fā)送 SIGUSR1 和 SIGTRMIN 信號。信號處理線程 sigmgr_thread() 在接收到信號時(shí)會(huì)調(diào)用信號處理函數(shù) sig_handler()

程序編譯: gcc -o signal_sync signal_sync.c -lpthread

程序執(zhí)行: ./signal_sync

從程序執(zhí)行輸出結(jié)果可以看到主線程發(fā)出的所有信號都被指定的信號處理線程接收到,并以同步的方式處理。


清單 2. signal_sync.c

            #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);
}

          

注意事項(xiàng)

在基于 Linux 的多線程應(yīng)用中,對于因?yàn)槌绦蜻壿嬓枰a(chǎn)生的信號,可考慮使用同步模型進(jìn)行處理;而對會(huì)導(dǎo)致程序運(yùn)行終止的信號如 SIGSEGV 等,必須按照傳統(tǒng)的異步方式使用 signal() sigaction() 注冊信號處理函數(shù)進(jìn)行處理。這兩種信號處理模型可根據(jù)所處理的信號的不同同時(shí)存在一個(gè) Linux 應(yīng)用中:

  • 不要在線程的信號掩碼中阻塞不能被忽略處理的兩個(gè)信號 SIGSTOP 和 SIGKILL。
  • 不要在線程的信號掩碼中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS。
  • 確保 sigwait() 等待的信號集已經(jīng)被進(jìn)程中所有的線程阻塞。
  • 在主線程或其它工作線程產(chǎn)生信號時(shí),必須調(diào)用 kill() 將信號發(fā)給整個(gè)進(jìn)程,而不能使用 pthread_kill() 發(fā)送某個(gè)特定的工作線程,否則信號處理線程無法接收到此信號。
  • 因?yàn)? sigwait() 使用了串行的方式處理信號的到來,為避免信號的處理存在滯后,或是非實(shí)時(shí)信號被丟失的情況,處理每個(gè)信號的代碼應(yīng)盡量簡潔、快速,避免調(diào)用會(huì)產(chǎn)生阻塞的庫函數(shù)。

小結(jié)

在開發(fā) Linux 多線程應(yīng)用中, 如果因?yàn)槌绦蜻壿嬓枰胄盘枺?在信號處理后程序仍將繼續(xù)正常運(yùn)行。在這種背景下,如果以異步方式處理信號,在編寫信號處理函數(shù)一定要考慮異步信號處理函數(shù)的安全; 同時(shí), 程序中一些庫函數(shù)可能會(huì)被信號中斷,錯(cuò)誤返回,這時(shí)需要考慮對 EINTR 的處理。另一方面,也可考慮使用上文介紹的同步模型處理信號,簡化信號處理函數(shù)的編寫,避免因?yàn)樾盘柼幚砗瘮?shù)執(zhí)行上下文的不確定性而帶來的風(fēng)險(xiǎn)。

Linux 多線程應(yīng)用中如何編寫安全的信號處理函數(shù)


更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯(lián)系: 360901061

您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。

【本文對您有幫助就好】

您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描上面二維碼支持博主2元、5元、10元、自定義金額等您想捐的金額吧,站長會(huì)非常 感謝您的哦!!!

發(fā)表我的評論
最新評論 總共0條評論
主站蜘蛛池模板: 鄢陵县| 新化县| 泌阳县| 深水埗区| 安顺市| 星子县| 德昌县| 黄陵县| 宜兰县| 康保县| 察雅县| 梧州市| 敦化市| 靖宇县| 青海省| 开封县| 桐庐县| 尼勒克县| 长丰县| 西贡区| 稷山县| 会同县| 昔阳县| 双峰县| 理塘县| 陵水| 汝州市| 泸州市| 福海县| 神池县| 两当县| 洛浦县| 盘锦市| 巴彦淖尔市| 泰和县| 乾安县| 武定县| 靖宇县| 宝兴县| 莎车县| 邵东县|