1. 程式人生 > >淺談Linux中的訊號處理機制(二)

淺談Linux中的訊號處理機制(二)

      首先謝謝 @小堯弟 這位朋友對我昨天夜裡寫的一篇《淺談Linux中的訊號處理機制(一)》的指正,之前的題目我用的“淺析”一詞,給人一種要剖析核心的感覺。本人自知功力不夠,尚且不能對著Linux核心原始碼評頭論足。以後的路還很長,我還是一步一個腳印的慢慢走著吧,Linux核心這座山,我才剛剛抵達山腳下。

      好了,言歸正傳,我接著昨天寫下去。如有錯誤還請各位看官指正,先此謝過。

      上篇末尾,我們看到了這樣的現象:send程序總共傳送了500次SIGINT訊號給rcv程序,但是實際過程中rcv只接受/處理了13次SIGINT的訊號處理函式(signal-handler function)。究竟是rcv程序接受了500次SIGINT訊號只執行了13次訊號處理函式,還是rcv程序只接受了13次SIGINT訊號然後執行了13次訊號處理函式呢。我們不禁要問:訊號去了哪兒呢?要搞清這個問題之前,我們還需瞭解一個叫做做訊號集和訊號遮蔽的知識點。

訊號集

      在處理訊號相關的函式時,我們時常需要一種的特殊的資料結構來表示一組訊號的集合,這樣的集合我們稱之為訊號集,其資料型別表示為sigset_t,通常是用位掩碼的形式來實現的。我的環境是CentOS7,其定義在/usr/include/bits/sigset.h中,具體如下:

/* A `sigset_t' has a bit for each signal. */

# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

#endif

在sigset.h同時也提供了一組函式(實際上用巨集來實現的,感興趣可以查閱sigset.h),用以實現對sigset_t型別資料的操作。其原型如下:

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

除此之外Glibc還提供了另外三個非標準規定的函式:

int sigisemptyset(const sigset_t* set);

int sigandset(sigset_t* dest,sigset_t* left,sigset_t* right);

int sigorset(sigset_t* dest,sigset_t* left,sigset_t* right);

基本上看了原型之後這些函式的用法也就一目瞭然了,不需要浪費篇幅了。除此之外,我覺得的這些函式的實現還是值得一讀的,是C語言中位運算學習的一個不錯的demo。

訊號遮蔽

      在瞭解了訊號集的基本概念之後,我們就可以知道繼續瞭解其他與訊號集相關的概念了,首先是訊號遮蔽字。它定義了要阻塞遞送到當前程序的訊號集,每一個程序都有一個訊號遮蔽字(signal mask)。如果你知道什麼是許可權遮蔽(umask)那麼訊號遮蔽字也很好理解。sigprocmask()函式可以檢測和更改當前程序的訊號遮蔽字。其原型:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

當oldset是一個非空指標的話,呼叫sigprocmask之後,oldset便返回了之前的訊號遮蔽字。set引數會結合how引數對當前的訊號遮蔽字做出修改。(和之前一節提到過的一樣有兩個特殊的訊號,你不可以遮蔽它們是:SIGKILL和SIGSTOP)具體規則是:

how 行為
SIG_BLOCK 設定程序的訊號遮蔽字為當期訊號遮蔽字和set的並集。set是新增的要遮蔽的訊號集。
SIG_UNBLOCK 設定當前程序的訊號遮蔽字為當前訊號遮蔽字和set補集的交集,也就是當前訊號遮蔽字減去set中的要解除遮蔽的訊號集。set中是要解除遮蔽的訊號集。
SIG_SETMASK 設定當前程序的訊號遮蔽字為set訊號集。

然而當set指向一個NULL時,那麼how也就沒有作用了。通常我們讓set設定為NULL時,通過oldset獲取當前的訊號遮蔽字。

    如果某個或多個訊號在程序遮蔽了該訊號的期間來到過一次或者多次,我們稱這樣的訊號叫做未決的(pending)訊號。那麼在呼叫sigprocmask()解除這個訊號遮蔽之後,該訊號會在sigprocmask ()返回之前,遞送給(SUSv3 規定至少傳遞一個訊號)當前程序

    程序維護了一個數據結構來儲存未決的訊號,我們可以通過sigpending()來獲取哪些訊號是未決的:

int sigpending(sigset_t *set);//return 0 on success,or -1 on error

set引數返回的便是未決的訊號集。之後便可以通過使用sigismember()來判斷,set中包含哪些訊號。

      到這裡我們就可以解釋上一篇末尾的問題了。因為Linux上signal()註冊的訊號處理函式在執行時,會自動的將當前的訊號新增到程序的訊號遮蔽字當中。當訊號處理函式返回時,會恢復之前的訊號遮蔽字。這意味著,當訊號處理函式執行時,它不會遞迴的中斷自身。

實時訊號

      早期Unix系統只定義了32種訊號。POSIX.1b定義了一組額外的實時訊號(為了相容之前的應用,而不是修改以前的傳統訊號)。實時訊號的特點,《Linux系統程式設計手冊》上有一段總結的很是全面:

  • Realtime signals provide an increased range of signals that can be used for application-defined purposes. Only two standard signals are freely available for application-defined purposes: SIGUSR1 and SIGUSR2.
  • Realtime signals are queued. If multiple instances of a realtime signal are sent to a process, then the signal is delivered multiple times. By contrast, if we send further instances of a standard signal that is already pending for a process, that signal is delivered only once.
  • When sending a realtime signal, it is possible to specify data (an integer or pointer value) that accompanies the signal. The signal handler in the receiving process can retrieve this data.
  • The order of delivery of different realtime signals is guaranteed. If multiple different realtime signals are pending, then the lowest-numbered signal is delivered first. In other words, signals are prioritized, with lower-numbered signals having higher priority. When multiple signals of the same type are queued, they are delivere—along with their accompanying data—in the order in which they were sent.

根據第二點,我們可以將上篇的部落格末尾的SIGINT改成SIGRTMIN+5(當然這裡隨意,只要是實時訊號,Linux上kill()也是可以傳送實時訊號的),然後重複昨天的測試,我們會驚喜的發現,rcv程序“不出意外”地接受並處理了500次訊號處理函式。

     那麼如何通過傳送實時訊號時傳遞資料呢?彆著急,還得掌握一個系統呼叫sigaction()。

sigaction()系統呼叫

      之前我們已經解除了signal()函式,sigaction()是另外一種選擇,它功能更加強大,相容性更好,任何時候我們都應優先考慮使用sigaction(),即使signal()更加簡單靈活。其函式原型:

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);//Return 0 on success,or -1 on error

與sigprocmask類似地,oldact返回之前的訊號設定,act用來設定新的訊號處理。signum自然不用解釋,這是要處理的訊號。這個函式的關鍵之處就是struct sigaction這個和函式同名的結構體。當然要使用sigaction()還是得從struct sigaction入手,它的定義:

struct sigaction {

    union {
        void (*sa_handler)(int);                                 
        void (*sa_sigaction)(int, siginfo_t *, void *);   

    }__sigaction_handler;                                  //Address of handler
    sigset_t sa_mask;                                        //Signals blocked during the handler invocation
    int sa_flags;                                                //Flags controlling handler invocation
    void (*sa_restorer)(void);                             //Restore,not use
};

sa_mask是一組訊號集,當呼叫訊號處理函式之前會將這組訊號集新增到程序的訊號遮蔽字中,直到訊號處理函式返回。利用sa_mask引數,我們可以指定一組訊號,讓我們的訊號處理函式不被這些訊號打斷。與前面的signal()一樣,預設還是會把引發訊號處理函式的訊號,自動的新增到程序的訊號遮蔽字中的。sa_flags引數,如果有經驗的話,我們不難猜到這肯定是一組選項,畢竟身經百戰了嘛。那我們就來看看這組選項是什麼意思:

sa_flags 說明
SA_INTERRUPT 由此訊號中斷的系統呼叫不會自動重啟。
SA_NOCLDSTOP

 當signum為SIGCHLD時,當因接受一訊號的子程序停止或者恢復時,將不會產生此訊號(有點繞).但是子程序終止時,仍會產生此訊號。

(If sig is SIGCHLD, don’t generate this signal when a child process is stopped or resumed as a consequence of receiving a signal.)

 SA_NOCLDWAIT 當signum為SIGCHLD時,子程序終止時不會轉化為殭屍程序。此時呼叫wait(),則阻塞到所有子程序都終止,才返回-1,errno被視之為ECHILD。 
 SA_NODEFER 捕獲該訊號的時候,不會在執行訊號處理函式之前將該訊號自動新增到程序的訊號遮蔽字中。 
 SA_ONSTACK 呼叫訊號處理函式時,使用sigaltstack()安裝的備用棧。 
 SA_RESETHAND  當捕獲該訊號時,會在呼叫訊號處理函式之前將訊號處理函式設定為預設值SIG_DFL,並清除SA_SIGINFO標誌。
 SA_RESTART  被此訊號中斷的系統呼叫,會自動重啟。
SA_SIGINFO 呼叫訊號處理函式時附帶了額外的資料要處理,具體見下文。

sa_restorer和名字一樣為保留引數,不需要使用。最後我們要看的是__sigaction_handler,這是一個聯合體(當然啦,這是廢話)。sa_handler和sa_sigaction都是訊號處理函式的指標,所以一次只能選擇兩者中的一個。如果sa_mask中設定了SA_SIGINFO位那麼就按照void (*sa_sigaction)(int, siginfo_t *, void *)的形式的函式呼叫訊號處理函式,否則使用 void (*sa_handler)(int)這樣的函式。下面我們再來看一看sa_sigaction這個函式:

void sa_sigaction(int signum, siginfo_t* info, void* context);

siginfo_t是一個結構體,其結構和實現相關,我的CentOS7系統上是這樣的:

siginfo_t {
    int si_signo; /* Signal number */
    int si_errno; /* An errno value */
    int si_code; /* Signal code */
    int si_trapno; /* Trap number that caused hardware-generated signal (unused on most architectures) */
    pid_t si_pid; /* Sending process ID */
    uid_t si_uid; /* Real user ID of sending process */
    int si_status; /* Exit value or signal */
    clock_t si_utime; /* User time consumed */
    clock_t si_stime; /* System time consumed */
    sigval_t si_value; /* Signal value */
    int si_int; /* POSIX.1b signal */
    void *si_ptr; /* POSIX.1b signal */
    int si_overrun; /* Timer overrun count; POSIX.1b timers */
    int si_timerid; /* Timer ID; POSIX.1b timers */
    void *si_addr; /* Memory location which caused fault */
    long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */
    int si_fd; /* File descriptor */
    short si_addr_lsb; /* Least significant bit of address (since Linux 2.6.32) */
}

每個欄位的含義後邊都加了清晰的註釋,但是還有一個引數使我們需要特別注意的,其中si_value欄位用來接收伴隨著訊號傳送過來的資料,其型別是一個sigval_t的聯合體,其定義(我的系統是在路徑/usr/include/bits/siginfo.h 上):

# define __have_sigval_t 1

/* Type for data associated with a signal. */
typedef union sigval
{
    int     sival_int;
    void* sival_ptr;
} sigval_t;
#endif

在實際程式設計中,到底選擇sival_int還是sival_ptr欄位,還是取決於你的應用程式。但是由於指標的作用範圍只能在程序的內部,如果傳送一個指標到另一個程序一般沒有什麼實際的意義。

      基本上寫到這裡,我們就可以使用sigaction()進行訊號處理的demo了,但是這裡我們先不急著寫,留到下一節一併寫了。

使用sigqueue()

      之前我們提到了傳送實時訊號時可以附帶資料,kill(),raise()等函式的引數註定他們無法附帶更多的資料,這裡我們要認識一個新的函式sigqueue()專門用於在傳送訊號的時候,附加傳遞額外的資料。

int sigqueue(pid_t pid, int sig, const union sigval value);//Return 0 on success ,or -1 on error

前兩個引數和kill()一致,但是不同於kill(),這裡不能將pid只能是單個程序,而不像kill()那樣豐富的用法。value的型別便是在上邊提及的sigval_t,於是就清晰了:傳送程序在這裡傳送的value在接受程序中通過訊號處理函式sa_sigaction中的siginfo_t info引數就可以拿到了。

一個處理實時訊號訊號簡單的demo,處理訊號端程式碼catch.c:

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

void sighandler(int sig,siginfo_t* info,void* context)
{
    printf("Send process pid = %ld,receive a data :%d\n",info->si_pid,info->si_value.sival_int);
}

int main()
{
    printf("pid = %ld\n",(long)getpid());
    struct sigaction act;
    act.sa_flags = SA_SIGINFO;
    sigemptyset(&act.sa_mask);
    act.sa_sigaction = sighandler;
    if(sigaction(SIGRTMIN+5,&act,0) == -1)
        exit(-1);
    pause();
}

傳送訊號端send.c:

#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>

int main(int argc,char* argv[])
{
    printf("Send process pid = %ld\n",(long)getpid());
    union sigval value;
    value.sival_int = 5435620;
    pid_t pid = (pid_t)atol(argv[1]);
    sigqueue(pid,SIGRTMIN+5,value);
}

  執行結果如圖所示,在sa_sigaction中成功拿到了傳送程序的程序id以及傳送的資料:

當然由於夜深了,這個demo寫的還是比較簡單的,基本我們使用已經沒有任何障礙了。

      準備把有關訊號的知識點總結完的,一寫出來,才發現訊號這部分的知識點真是多,而且牽扯到好多細節方面的東西,看來這個任務今晚完不成了,明天繼續吧。

      如果您發現我的博文有錯誤之處,煩請您指正,我先在此謝過!聯絡郵箱[email protected]