Linux 下 signal 機制

南丶煙發表於2015-06-30

【摘要】本文分析了Linux核心對於訊號的實現機制和應用層的相關處理。首先介紹了軟中斷訊號的本質及訊號的兩種不同分類方法尤其是不可靠訊號的原理。接著分析了核心對於訊號的處理流程包括訊號的觸發/註冊/執行及登出等。最後介紹了應用層的相關處理,主要包括訊號處理函式的安裝、訊號的傳送、遮蔽阻塞等,最後給了幾個簡單的應用例項。

 

【關鍵字】軟中斷訊號,signal,sigaction,kill,sigqueue,settimer,sigmask,sigprocmask,sigset_t

 

1       訊號本質

軟中斷訊號(signal,又簡稱為訊號)用來通知程式發生了非同步事件。在軟體層次上是對中斷機制的一種模擬,在原理上,一個程式收到一個訊號與處理器收到一箇中斷請求可以說是一樣的。訊號是程式間通訊機制中唯一的非同步通訊機制一個程式不必通過任何操作來等待訊號的到達,事實上,程式也不知道訊號到底什麼時候到達。程式之間可以互相通過系統呼叫kill傳送軟中斷訊號。核心也可以因為內部事件而給程式傳送訊號,通知程式發生了某個事件。訊號機制除了基本通知功能外,還可以傳遞附加資訊。

 

收到訊號的程式對各種訊號有不同的處理方法。處理方法可以分為三類:

第一種是類似中斷的處理程式,對於需要處理的訊號,程式可以指定處理函數,由該函式來處理。

第二種方法是,忽略某個訊號,對該訊號不做任何處理,就象未發生過一樣。

第三種方法是,對該訊號的處理保留系統的預設值,這種預設操作,對大部分的訊號的預設操作是使得程式終止。程式通過系統呼叫signal來指定程式對某個訊號的處理行為。

 

2       訊號的種類

可以從兩個不同的分類角度對訊號進行分類:

可靠性方面:可靠訊號與不可靠訊號;

與時間的關係上:實時訊號與非實時訊號。

 

2.1    可靠訊號與不可靠訊號

Linux訊號機制基本上是從Unix系統中繼承過來的。早期Unix系統中的訊號機制比較簡單和原始,訊號值小於SIGRTMIN的訊號都是不可靠訊號。這就是"不可靠訊號"的來源。它的主要問題是訊號可能丟失。

 

隨著時間的發展,實踐證明了有必要對訊號的原始機制加以改進和擴充。由於原來定義的訊號已有許多應用,不好再做改動,最終只好又新增加了一些訊號,並在一開始就把它們定義為可靠訊號,這些訊號支援排隊,不會丟失

 

訊號值位於SIGRTMIN和SIGRTMAX之間的訊號都是可靠訊號,可靠訊號克服了訊號可能丟失的問題。Linux在支援新版本的訊號安裝函式sigation()以及訊號傳送函式sigqueue()的同時,仍然支援早期的signal()訊號安裝函式,支援訊號傳送函式kill()。

 

訊號的可靠與不可靠只與訊號值有關,與訊號的傳送及安裝函式無關。目前linux中的signal()是通過sigation()函式實現的,因此,即使通過signal()安裝的訊號,在訊號處理函式的結尾也不必再呼叫一次訊號安裝函式。同時,由signal()安裝的實時訊號支援排隊,同樣不會丟失。

 

對於目前linux的兩個訊號安裝函式:signal()及sigaction()來說,它們都不能把SIGRTMIN以前的訊號變成可靠訊號(都不支援排隊,仍有可能丟失,仍然是不可靠訊號),而且對SIGRTMIN以後的訊號都支援排隊。這兩個函式的最大區別在於,經過sigaction安裝的訊號都能傳遞資訊給訊號處理函式,而經過signal安裝的訊號不能向訊號處理函式傳遞資訊。對於訊號傳送函式來說也是一樣的。

 

2.2    實時訊號與非實時訊號

早期Unix系統只定義了32種訊號,前32種訊號已經有了預定義值,每個訊號有了確定的用途及含義,並且每種訊號都有各自的預設動作。如按鍵盤的CTRL ^C時,會產生SIGINT訊號,對該訊號的預設反應就是程式終止。後32個訊號表示實時訊號,等同於前面闡述的可靠訊號。這保證了傳送的多個實時訊號都被接收。

 

非實時訊號都不支援排隊,都是不可靠訊號;實時訊號都支援排隊,都是可靠訊號。

 

3       訊號處理流程

 

 

對於一個完整的訊號生命週期(從訊號傳送到相應的處理函式執行完畢)來說,可以分為三個階段:

訊號誕生

訊號在程式中註冊

訊號的執行和登出

 

 

3.1    訊號誕生

訊號事件的發生有兩個來源:硬體來源(比如我們按下了鍵盤或者其它硬體故障);軟體來源,最常用傳送訊號的系統函式是kill, raise, alarm和setitimer以及sigqueue函式,軟體來源還包括一些非法運算等操作。

 

這裡按發出訊號的原因簡單分類,以瞭解各種訊號:

(1) 與程式終止相關的訊號。當程式退出,或者子程式終止時,發出這類訊號。

(2) 與程式例外事件相關的訊號。如程式越界,或企圖寫一個只讀的記憶體區域(如程式正文區),或執行一個特權指令及其他各種硬體錯誤。

(3) 與在系統呼叫期間遇到不可恢復條件相關的訊號。如執行系統呼叫exec時,原有資源已經釋放,而目前系統資源又已經耗盡。

(4) 與執行系統呼叫時遇到非預測錯誤條件相關的訊號。如執行一個並不存在的系統呼叫。

(5) 在使用者態下的程式發出的訊號。如程式呼叫系統呼叫kill向其他程式傳送訊號

(6) 與終端互動相關的訊號。如使用者關閉一個終端,或按下break鍵等情況。

(7) 跟蹤程式執行的訊號。

 

Linux支援的訊號列表如下。很多訊號是與機器的體系結構相關的

訊號值 預設處理動作 發出訊號的原因

SIGHUP 1 A 終端掛起或者控制程式終止

SIGINT 2 A 鍵盤中斷(如break鍵被按下)

SIGQUIT 3 C 鍵盤的退出鍵被按下

SIGILL 4 C 非法指令

SIGABRT 6 C 由abort(3)發出的退出指令

SIGFPE 8 C 浮點異常

SIGKILL 9 AEF Kill訊號

SIGSEGV 11 C 無效的記憶體引用

SIGPIPE 13 A 管道破裂: 寫一個沒有讀埠的管道

SIGALRM 14 A 由alarm(2)發出的訊號

SIGTERM 15 A 終止訊號

SIGUSR1 30,10,16 A 使用者自定義訊號1

SIGUSR2 31,12,17 A 使用者自定義訊號2

SIGCHLD 20,17,18 B 子程式結束訊號

SIGCONT 19,18,25 程式繼續(曾被停止的程式)

SIGSTOP 17,19,23 DEF 終止程式

SIGTSTP 18,20,24 D 控制終端(tty)上按下停止鍵

SIGTTIN 21,21,26 D 後臺程式企圖從控制終端讀

SIGTTOU 22,22,27 D 後臺程式企圖從控制終端寫

 

處理動作一項中的字母含義如下

A 預設的動作是終止程式

B 預設的動作是忽略此訊號,將該訊號丟棄,不做處理

C 預設的動作是終止程式並進行核心映像轉儲(dump core),核心映像轉儲是指將程式資料在記憶體的映像和程式在核心結構中的部分內容以一定格式轉儲到檔案系統,並且程式退出執行,這樣做的好處是為程式設計師提供了方便,使得他們可以得到程式當時執行時的資料值,允許他們確定轉儲的原因,並且可以除錯他們的程式。

D 預設的動作是停止程式,進入停止狀況以後還能重新進行下去,一般是在除錯的過程中(例如ptrace系統呼叫)

E 訊號不能被捕獲

F 訊號不能被忽略

 

3.2    訊號在目標程式中註冊

程式表的表項中有一個軟中斷訊號域,該域中每一位對應一個訊號。核心給一個程式傳送軟中斷訊號的方法,是在程式所在的程式表項的訊號域設定對應於該訊號的位。如果訊號傳送給一個正在睡眠的程式,如果程式睡眠在可被中斷的優先順序上,則喚醒程式;否則僅設定程式表中訊號域相應的位,而不喚醒程式。如果傳送給一個處於可執行狀態的程式,則只置相應的域即可。

 

程式的task_struct結構中有關於本程式中未決訊號的資料成員: struct sigpending pending:

struct sigpending{

        struct sigqueue *head, *tail;

        sigset_t signal;

};

 

第三個成員是程式中所有未決訊號集,第一、第二個成員分別指向一個sigqueue型別的結構鏈(稱之為"未決訊號資訊鏈")的首尾,資訊鏈中的每個sigqueue結構刻畫一個特定訊號所攜帶的資訊,並指向下一個sigqueue結構:

struct sigqueue{

        struct sigqueue *next;

        siginfo_t info;

}

 

訊號在程式中註冊指的就是訊號值加入到程式的未決訊號集sigset_t signal(每個訊號佔用一位)中,並且訊號所攜帶的資訊被保留到未決訊號資訊鏈的某個sigqueue結構中。只要訊號在程式的未決訊號集中,表明程式已經知道這些訊號的存在,但還沒來得及處理,或者該訊號被程式阻塞。

 

當一個實時訊號傳送給一個程式時,不管該訊號是否已經在程式中註冊,都會被再註冊一次,因此,訊號不會丟失,因此,實時訊號又叫做"可靠訊號"。這意味著同一個實時訊號可以在同一個程式的未決訊號資訊鏈中佔有多個sigqueue結構(程式每收到一個實時訊號,都會為它分配一個結構來登記該訊號資訊,並把該結構新增在未決訊號鏈尾,即所有誕生的實時訊號都會在目標程式中註冊)。

 

當一個非實時訊號傳送給一個程式時,如果該訊號已經在程式中註冊(通過sigset_t signal指示),則該訊號將被丟棄,造成訊號丟失。因此,非實時訊號又叫做"不可靠訊號"。這意味著同一個非實時訊號在程式的未決訊號資訊鏈中,至多佔有一個sigqueue結構

 

總之訊號註冊與否,與傳送訊號的函式(如kill()或sigqueue()等)以及訊號安裝函式(signal()及sigaction())無關,只與訊號值有關(訊號值小於SIGRTMIN的訊號最多隻註冊一次,訊號值在SIGRTMIN及SIGRTMAX之間的訊號,只要被程式接收到就被註冊)

 

 

3.3    訊號的執行和登出

核心處理一個程式收到的軟中斷訊號是在該程式的上下文中,因此,程式必須處於執行狀態。當其由於被訊號喚醒或者正常排程重新獲得CPU時,在其從核心空間返回到使用者空間時會檢測是否有訊號等待處理。如果存在未決訊號等待處理且該訊號沒有被程式阻塞,則在執行相應的訊號處理函式前,程式會把訊號在未決訊號鏈中佔有的結構卸掉。

 

對於非實時訊號來說,由於在未決訊號資訊鏈中最多隻佔用一個sigqueue結構,因此該結構被釋放後,應該把訊號在程式未決訊號集中刪除(訊號登出完畢);而對於實時訊號來說,可能在未決訊號資訊鏈中佔用多個sigqueue結構,因此應該針對佔用sigqueue結構的數目區別對待:如果只佔用一個sigqueue結構(程式只收到該訊號一次),則執行完相應的處理函式後應該把訊號在程式的未決訊號集中刪除(訊號登出完畢)。否則待該訊號的所有sigqueue處理完畢後再在程式的未決訊號集中刪除該訊號。

 

當所有未被遮蔽的訊號都處理完畢後,即可返回使用者空間。對於被遮蔽的訊號,當取消遮蔽後,在返回到使用者空間時會再次執行上述檢查處理的一套流程。

 

核心處理一個程式收到的訊號的時機是在一個程式從核心態返回使用者態時。所以,當一個程式在核心態下執行時,軟中斷訊號並不立即起作用,要等到將返回使用者態時才處理。程式只有處理完訊號才會返回使用者態,程式在使用者態下不會有未處理完的訊號

 

處理訊號有三種型別:程式接收到訊號後退出;程式忽略該訊號;程式收到訊號後執行使用者設定用系統呼叫signal的函式。當程式接收到一個它忽略的訊號時,程式丟棄該訊號,就象沒有收到該訊號似的繼續執行。如果程式收到一個要捕捉的訊號,那麼程式從核心態返回使用者態時執行使用者定義的函式。而且執行使用者定義的函式的方法很巧妙,核心是在使用者棧上建立一個新的層,該層中將返回地址的值設定成使用者定義的處理函式的地址,這樣程式從核心返回彈出棧頂時就返回到使用者定義的函式處,從函式返回再彈出棧頂時,才返回原先進入核心的地方。這樣做的原因是使用者定義的處理函式不能且不允許在核心態下執行(如果使用者定義的函式在核心態下執行的話,使用者就可以獲得任何許可權)。

 

4       訊號的安裝

如果程式要處理某一訊號,那麼就要在程式中安裝該訊號。安裝訊號主要用來確定訊號值及程式針對該訊號值的動作之間的對映關係,即程式將要處理哪個訊號;該訊號被傳遞給程式時,將執行何種操作。

 

linux主要有兩個函式實現訊號的安裝:signal()、sigaction()。其中signal()只有兩個引數,不支援訊號傳遞資訊,主要是用於前32種非實時訊號的安裝;而sigaction()是較新的函式(由兩個系統呼叫實現:sys_signal以及sys_rt_sigaction),有三個引數,支援訊號傳遞資訊,主要用來與 sigqueue() 系統呼叫配合使用,當然,sigaction()同樣支援非實時訊號的安裝。sigaction()優於signal()主要體現在支援訊號帶有引數。

 

4.1    signal()

#include <signal.h>

void (*signal(int signum, void (*handler))(int)))(int);

如果該函式原型不容易理解的話,可以參考下面的分解方式來理解:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler));

第一個引數指定訊號的值,第二個引數指定針對前面訊號值的處理,可以忽略該訊號(引數設為SIG_IGN);可以採用系統預設方式處理訊號(引數設為SIG_DFL);也可以自己實現處理方式(引數指定一個函式地址)。

如果signal()呼叫成功,返回最後一次為安裝訊號signum而呼叫signal()時的handler值;失敗則返回SIG_ERR。

傳遞給訊號處理例程的整數引數是訊號值,這樣可以使得一個訊號處理例程處理多個訊號。

 

#include <signal.h>

#include <unistd.h>

#include <stdio.h>

void sigroutine(int dunno)

{ /* 訊號處理例程,其中dunno將會得到訊號的值 */

        switch (dunno) {

        case 1:

        printf("Get a signal -- SIGHUP ");

        break;

        case 2:

        printf("Get a signal -- SIGINT ");

        break;

        case 3:

        printf("Get a signal -- SIGQUIT ");

        break;

        }

        return;

}

 

int main() {

        printf("process id is %d ",getpid());

        signal(SIGHUP, sigroutine); //* 下面設定三個訊號的處理方法

        signal(SIGINT, sigroutine);

        signal(SIGQUIT, sigroutine);

        for (;;) ;

}

 

其中訊號SIGINT由按下Ctrl-C發出,訊號SIGQUIT由按下Ctrl-發出。該程式執行的結果如下:

 

localhost:~$ ./sig_test

process id is 463

Get a signal -SIGINT //按下Ctrl-C得到的結果

Get a signal -SIGQUIT //按下Ctrl-得到的結果

//按下Ctrl-z將程式置於後臺

 [1]+ Stopped ./sig_test

localhost:~$ bg

 [1]+ ./sig_test &

localhost:~$ kill -HUP 463 //向程式傳送SIGHUP訊號

localhost:~$ Get a signal – SIGHUP

kill -9 463 //向程式傳送SIGKILL訊號,終止程式

localhost:~$

 

4.2    sigaction()

#include <signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

sigaction函式用於改變程式接收到特定訊號後的行為。該函式的第一個引數為訊號的值,可以為除SIGKILL及SIGSTOP外的任何一個特定有效的訊號(為這兩個訊號定義自己的處理函式,將導致訊號安裝錯誤)。第二個引數是指向結構sigaction的一個例項的指標,在結構sigaction的例項中,指定了對特定訊號的處理,可以為空,程式會以預設方式對訊號處理;第三個引數oldact指向的物件用來儲存返回的原來對相應訊號的處理,可指定oldact為NULL如果把第二、第三個引數都設為NULL,那麼該函式可用於檢查訊號的有效性

 

第二個引數最為重要,其中包含了對指定訊號的處理、訊號所傳遞的資訊、訊號處理函式執行過程中應遮蔽掉哪些訊號等等。

sigaction結構定義如下:

struct sigaction {

                       union{

                               __sighandler_t _sa_handler;

                               void (*_sa_sigaction)(int,struct siginfo *, void *);

                       }_u

            sigset_t sa_mask;

            unsigned long sa_flags;

}

 

1、聯合資料結構中的兩個元素_sa_handler以及*_sa_sigaction指定訊號關聯函式,即使用者指定的訊號處理函式。除了可以是使用者自定義的處理函式外,還可以為SIG_DFL(採用預設的處理方式),也可以為SIG_IGN(忽略訊號)。

 

2、由_sa_sigaction是指定的訊號處理函式帶有三個引數,是為實時訊號而設的(當然同樣支援非實時訊號),它指定一個3引數訊號處理函式。第一個引數為訊號值,第三個引數沒有使用,第二個引數是指向siginfo_t結構的指標,結構中包含訊號攜帶的資料值,引數所指向的結構如下:

siginfo_t {

                  int      si_signo;  /* 訊號值,對所有訊號有意義*/

                  int      si_errno;  /* errno值,對所有訊號有意義*/

                  int      si_code;   /* 訊號產生的原因,對所有訊號有意義*/

                               union{                               /* 聯合資料結構,不同成員適應不同訊號 */

                                       //確保分配足夠大的儲存空間

                                       int _pad[SI_PAD_SIZE];

                                       //對SIGKILL有意義的結構

                                       struct{

                                                      ...

                                                 }...

                                               ... ...

                                               ... ...                               

                                       //對SIGILL, SIGFPE, SIGSEGV, SIGBUS有意義的結構

                                  struct{

                                                      ...

                                                 }...

                                               ... ...

                                         }

}

 

前面在討論系統呼叫sigqueue傳送訊號時,sigqueue的第三個引數就是sigval聯合資料結構,當呼叫sigqueue時,該資料結構中的資料就將拷貝到訊號處理函式的第二個引數中。這樣,在傳送訊號同時,就可以讓訊號傳遞一些附加資訊。訊號可以傳遞資訊對程式開發是非常有意義的。

 

3、sa_mask指定在訊號處理程式執行過程中,哪些訊號應當被阻塞。預設情況下當前訊號本身被阻塞,防止訊號的巢狀傳送,除非指定SA_NODEFER或者SA_NOMASK標誌位。

注:請注意sa_mask指定的訊號阻塞的前提條件,是在由sigaction()安裝訊號的處理函式執行過程中由sa_mask指定的訊號才被阻塞

 

4、sa_flags中包含了許多標誌位,包括剛剛提到的SA_NODEFER及SA_NOMASK標誌位。另一個比較重要的標誌位是SA_SIGINFO,當設定了該標誌位時,表示訊號附帶的引數可以被傳遞到訊號處理函式中,因此,應該為sigaction結構中的sa_sigaction指定處理函式,而不應該為sa_handler指定訊號處理函式,否則,設定該標誌變得毫無意義。即使為sa_sigaction指定了訊號處理函式,如果不設定SA_SIGINFO,訊號處理函式同樣不能得到訊號傳遞過來的資料,在訊號處理函式中對這些資訊的訪問都將導致段錯誤(Segmentation fault)。

 

 

5       訊號的傳送

傳送訊號的主要函式有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

 

5.1    kill()

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid,int signo)

 

該系統呼叫可以用來向任何程式或程式組傳送任何訊號。引數pid的值為訊號的接收程式

pid>0 程式ID為pid的程式

pid=0 同一個程式組的程式

pid<0 pid!=-1 程式組ID為 -pid的所有程式

pid=-1 除傳送程式自身外,所有程式ID大於1的程式

 

Sinno是訊號值,當為0(即空訊號),實際不傳送任何訊號,但照常進行錯誤檢查,因此,可用於檢查目標程式是否存在,以及當前程式是否具有向目標傳送訊號的許可權root許可權的程式可以向任何程式傳送訊號,非root許可權的程式只能向屬於同一個session或者同一個使用者的程式傳送訊號)。

 

Kill()最常用於pid>0時的訊號傳送。該呼叫執行成功時,返回值為0;錯誤時,返回-1,並設定相應的錯誤程式碼errno。下面是一些可能返回的錯誤程式碼:

EINVAL:指定的訊號sig無效。

ESRCH:引數pid指定的程式或程式組不存在。注意,在程式表項中存在的程式,可能是一個還沒有被wait收回,但已經終止執行的僵死程式。

EPERM: 程式沒有權力將這個訊號傳送到指定接收訊號的程式。因為,一個程式被允許將訊號傳送到程式pid時,必須擁有root權力,或者是發出呼叫的程式的UID 或EUID與指定接收的程式的UID或儲存使用者ID(savedset-user-ID)相同。如果引數pid小於-1,即該訊號傳送給一個組,則該錯誤表示組中有成員程式不能接收該訊號。

 

5.2    sigqueue()

#include <sys/types.h>

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval val)

呼叫成功返回 0;否則,返回 -1。

 

sigqueue()是比較新的傳送訊號系統呼叫,主要是針對實時訊號提出的(當然也支援前32種),支援訊號帶有引數,與函式sigaction()配合使用。

sigqueue的第一個引數是指定接收訊號的程式ID,第二個引數確定即將傳送的訊號,第三個引數是一個聯合資料結構union sigval,指定了訊號傳遞的引數,即通常所說的4位元組值。

typedef union sigval {

               int  sival_int;

               void *sival_ptr;

}sigval_t;

 

sigqueue()比kill()傳遞了更多的附加資訊,但sigqueue()只能向一個程式傳送訊號,而不能傳送訊號給一個程式組。如果signo=0,將會執行錯誤檢查,但實際上不傳送任何訊號,0值訊號可用於檢查pid的有效性以及當前程式是否有許可權向目標程式傳送訊號。

 

在呼叫sigqueue時,sigval_t指定的資訊會拷貝到對應sig 註冊的3引數訊號處理函式的siginfo_t結構中,這樣訊號處理函式就可以處理這些資訊了。由於sigqueue系統呼叫支援傳送帶引數訊號,所以比kill()系統呼叫的功能要靈活和強大得多

 

5.3    alarm()

#include <unistd.h>

unsigned int alarm(unsigned int seconds)

系統呼叫alarm安排核心為呼叫程式在指定的seconds秒後發出一個SIGALRM的訊號。如果指定的引數seconds為0,則不再傳送 SIGALRM訊號。後一次設定將取消前一次的設定。該呼叫返回值為上次定時呼叫到傳送之間剩餘的時間,或者因為沒有前一次定時呼叫而返回0。

 

注意,在使用時,alarm只設定為傳送一次訊號,如果要多次傳送,就要多次使用alarm呼叫。

 

5.4    setitimer()

現在的系統中很多程式不再使用alarm呼叫,而是使用setitimer呼叫來設定定時器,用getitimer來得到定時器的狀態,這兩個呼叫的宣告格式如下:

int getitimer(int which, struct itimerval *value);

int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

在使用這兩個呼叫的程式中加入以下標頭檔案:

#include <sys/time.h>

 

該系統呼叫給程式提供了三個定時器,它們各自有其獨有的計時域,當其中任何一個到達,就傳送一個相應的訊號給程式,並使得計時器重新開始。三個計時器由引數which指定,如下所示:

TIMER_REAL:按實際時間計時,計時到達將給程式傳送SIGALRM訊號。

ITIMER_VIRTUAL:僅當程式執行時才進行計時。計時到達將傳送SIGVTALRM訊號給程式。

ITIMER_PROF:當程式執行時和系統為該程式執行動作時都計時。與ITIMER_VIR-TUAL是一對,該定時器經常用來統計程式在使用者態和核心態花費的時間。計時到達將傳送SIGPROF訊號給程式。

 

定時器中的引數value用來指明定時器的時間,其結構如下:

struct itimerval {

        struct timeval it_interval; /* 下一次的取值 */

        struct timeval it_value; /* 本次的設定值 */

};

 

該結構中timeval結構定義如下:

struct timeval {

        long tv_sec; /* 秒 */

        long tv_usec; /* 微秒,1秒 = 1000000 微秒*/

};

 

在setitimer 呼叫中,引數ovalue如果不為空,則其中保留的是上次呼叫設定的值。定時器將it_value遞減到0時,產生一個訊號,並將it_value的值設定為it_interval的值,然後重新開始計時,如此往復。當it_value設定為0時,計時器停止,或者當它計時到期,而it_interval 為0時停止。呼叫成功時,返回0;錯誤時,返回-1,並設定相應的錯誤程式碼errno:

EFAULT:引數value或ovalue是無效的指標。

EINVAL:引數which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一個。

下面是關於setitimer呼叫的一個簡單示範,在該例子中,每隔一秒發出一個SIGALRM,每隔0.5秒發出一個SIGVTALRM訊號:

 

#include <signal.h>

#include <unistd.h>

#include <stdio.h>

#include <sys/time.h>

int sec;

 

void sigroutine(int signo) {

        switch (signo) {

        case SIGALRM:

        printf("Catch a signal -- SIGALRM ");

        break;

        case SIGVTALRM:

        printf("Catch a signal -- SIGVTALRM ");

        break;

        }

        return;

}

 

int main()

{

        struct itimerval value,ovalue,value2;

        sec = 5;

 

        printf("process id is %d ",getpid());

        signal(SIGALRM, sigroutine);

        signal(SIGVTALRM, sigroutine);

 

        value.it_value.tv_sec = 1;

        value.it_value.tv_usec = 0;

        value.it_interval.tv_sec = 1;

        value.it_interval.tv_usec = 0;

        setitimer(ITIMER_REAL, &value, &ovalue);

 

        value2.it_value.tv_sec = 0;

        value2.it_value.tv_usec = 500000;

        value2.it_interval.tv_sec = 0;

        value2.it_interval.tv_usec = 500000;

        setitimer(ITIMER_VIRTUAL, &value2, &ovalue);

 

        for (;;) ;

}

 

該例子的螢幕拷貝如下:

localhost:~$ ./timer_test

process id is 579

Catch a signal – SIGVTALRM

Catch a signal – SIGALRM

Catch a signal – SIGVTALRM

Catch a signal – SIGVTALRM

Catch a signal – SIGALRM

Catch a signal –GVTALRM

 

5.5    abort()

#include <stdlib.h>

void abort(void);

向程式傳送SIGABORT訊號,預設情況下程式會異常退出,當然可定義自己的訊號處理函式。即使SIGABORT被程式設定為阻塞訊號,呼叫abort()後,SIGABORT仍然能被程式接收。該函式無返回值。

 

5.6    raise()

#include <signal.h>

int raise(int signo)

向程式本身傳送訊號,引數為即將傳送的訊號值。呼叫成功返回 0;否則,返回 -1。

 

6       訊號集及訊號集操作函式:

訊號集被定義為一種資料型別:

typedef struct {

                       unsigned long sig[_NSIG_WORDS];

} sigset_t

訊號集用來描述訊號的集合,每個訊號佔用一位。Linux所支援的所有訊號可以全部或部分的出現在訊號集中,主要與訊號阻塞相關函式配合使用。下面是為訊號集操作定義的相關函式:

 

#include <signal.h>

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

sigemptyset(sigset_t *set)初始化由set指定的訊號集,訊號集裡面的所有訊號被清空;

sigfillset(sigset_t *set)呼叫該函式後,set指向的訊號集中將包含linux支援的64種訊號;

sigaddset(sigset_t *set, int signum)在set指向的訊號集中加入signum訊號;

sigdelset(sigset_t *set, int signum)在set指向的訊號集中刪除signum訊號;

sigismember(const sigset_t *set, int signum)判定訊號signum是否在set指向的訊號集中。

 

7       訊號阻塞與訊號未決:

每個程式都有一個用來描述哪些訊號遞送到程式時將被阻塞的訊號集,該訊號集中的所有訊號在遞送到程式後都將被阻塞。下面是與訊號阻塞相關的幾個函式:

#include <signal.h>

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

int sigpending(sigset_t *set));

int sigsuspend(const sigset_t *mask));

 

sigprocmask()函式能夠根據引數how來實現對訊號集的操作,操作主要有三種:

SIG_BLOCK 在程式當前阻塞訊號集中新增set指向訊號集中的訊號

SIG_UNBLOCK 如果程式阻塞訊號集中包含set指向訊號集中的訊號,則解除對該訊號的阻塞

SIG_SETMASK 更新程式阻塞訊號集為set指向的訊號集

 

sigpending(sigset_t *set))獲得當前已遞送到程式,卻被阻塞的所有訊號,在set指向的訊號集中返回結果。

 

sigsuspend(const sigset_t *mask))用於在接收到某個訊號之前, 臨時用mask替換程式的訊號掩碼, 並暫停程式執行,直到收到訊號為止。sigsuspend 返回後將恢復呼叫之前的訊號掩碼。訊號處理函式完成後,程式將繼續執行。該系統呼叫始終返回-1,並將errno設定為EINTR。

 

 

8       訊號應用例項

linux下的訊號應用並沒有想象的那麼恐怖,程式設計師所要做的最多隻有三件事情:

安裝訊號(推薦使用sigaction());

實現三引數訊號處理函式,handler(int signal,struct siginfo *info, void *);

傳送訊號,推薦使用sigqueue()。

實際上,對有些訊號來說,只要安裝訊號就足夠了(訊號處理方式採用預設或忽略)。其他可能要做的無非是與訊號集相關的幾種操作。

 

例項一:訊號傳送及處理

實現一個訊號接收程式sigreceive(其中訊號安裝由sigaction())。

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

        struct sigaction act;  

        int sig;

        sig=atoi(argv[1]);

       

        sigemptyset(&act.sa_mask);

        act.sa_flags=SA_SIGINFO;

        act.sa_sigaction=new_op;

       

        if(sigaction(sig,&act,NULL) < 0)

        {

                printf("install sigal error\n");

        }

       

        while(1)

        {

                sleep(2);

                printf("wait for the signal\n");

        }

}

 

void new_op(int signum,siginfo_t *info,void *myact)

{

        printf("receive signal %d", signum);

        sleep(5);

}

說明,命令列引數為訊號值,後臺執行sigreceive signo &,可獲得該程式的ID,假設為pid,然後再另一終端上執行kill -s signo pid驗證訊號的傳送接收及處理。同時,可驗證訊號的排隊問題。

 

例項二:訊號傳遞附加資訊

主要包括兩個例項:

向程式本身傳送訊號,並傳遞指標引數

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

        struct sigaction act;  

        union sigval mysigval;

        int i;

        int sig;

        pid_t pid;         

        char data[10];

        memset(data,0,sizeof(data));

        for(i=0;i < 5;i++)

                data[i]='2';

        mysigval.sival_ptr=data;

       

        sig=atoi(argv[1]);

        pid=getpid();

       

        sigemptyset(&act.sa_mask);

        act.sa_sigaction=new_op;//三引數訊號處理函式

        act.sa_flags=SA_SIGINFO;//資訊傳遞開關,允許傳說引數資訊給new_op

        if(sigaction(sig,&act,NULL) < 0)

        {

                printf("install sigal error\n");

        }

        while(1)

        {

                sleep(2);

                printf("wait for the signal\n");

                sigqueue(pid,sig,mysigval);//向本程式傳送訊號,並傳遞附加資訊

        }

}

void new_op(int signum,siginfo_t *info,void *myact)//三引數訊號處理函式的實現

{

        int i;

        for(i=0;i<10;i++)

        {

                printf("%c\n ",(*( (char*)((*info).si_ptr)+i)));

        }

        printf("handle signal %d over;",signum);

}

 

這個例子中,訊號實現了附加資訊的傳遞,訊號究竟如何對這些資訊進行處理則取決於具體的應用。

 

不同程式間傳遞整型引數:

把1中的訊號傳送和接收放在兩個程式中,並且在傳送過程中傳遞整型引數。

訊號接收程式:

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

void new_op(int,siginfo_t*,void*);

int main(int argc,char**argv)

{

        struct sigaction act;

        int sig;

        pid_t pid;         

       

        pid=getpid();

        sig=atoi(argv[1]);     

       

        sigemptyset(&act.sa_mask);

        act.sa_sigaction=new_op;

        act.sa_flags=SA_SIGINFO;

        if(sigaction(sig,&act,NULL)<0)

        {

                printf("install sigal error\n");

        }

        while(1)

        {

                sleep(2);

                printf("wait for the signal\n");

        }

}

void new_op(int signum,siginfo_t *info,void *myact)

{

        printf("the int value is %d \n",info->si_int);

}

 

 

訊號傳送程式:

命令列第二個引數為訊號值,第三個引數為接收程式ID。

 

#include <signal.h>

#include <sys/time.h>

#include <unistd.h>

#include <sys/types.h>

main(int argc,char**argv)

{

        pid_t pid;

        int signum;

        union sigval mysigval;

        signum=atoi(argv[1]);

        pid=(pid_t)atoi(argv[2]);

        mysigval.sival_int=8;//不代表具體含義,只用於說明問題

        if(sigqueue(pid,signum,mysigval)==-1)

                printf("send error\n");

        sleep(2);

}

 

 

注:例項2的兩個例子側重點在於用訊號來傳遞資訊,目前關於在linux下通過訊號傳遞資訊的例項非常少,倒是Unix下有一些,但傳遞的基本上都是關於傳遞一個整數

 

例項三:訊號阻塞及訊號集操作

#include "signal.h"

#include "unistd.h"

static void my_op(int);

main()

{

        sigset_t new_mask,old_mask,pending_mask;

        struct sigaction act;

        sigemptyset(&act.sa_mask);

        act.sa_flags=SA_SIGINFO;

        act.sa_sigaction=(void*)my_op;

        if(sigaction(SIGRTMIN+10,&act,NULL))

                printf("install signal SIGRTMIN+10 error\n");

        sigemptyset(&new_mask);

        sigaddset(&new_mask,SIGRTMIN+10);

        if(sigprocmask(SIG_BLOCK, &new_mask,&old_mask))

                printf("block signal SIGRTMIN+10 error\n");

        sleep(10);

        printf("now begin to get pending mask and unblock SIGRTMIN+10\n");

        if(sigpending(&pending_mask)<0)

                printf("get pending mask error\n");

        if(sigismember(&pending_mask,SIGRTMIN+10))

                printf("signal SIGRTMIN+10 is pending\n");

        if(sigprocmask(SIG_SETMASK,&old_mask,NULL)<0)

                printf("unblock signal error\n");

        printf("signal unblocked\n");

        sleep(10);

}

 

static void my_op(int signum)

{

        printf("receive signal %d \n",signum);

}

 

編譯該程式,並以後臺方式執行。在另一終端向該程式傳送訊號(執行kill -s 42 pid,SIGRTMIN+10為42),檢視結果可以看出幾個關鍵函式的執行機制,訊號集相關操作比較簡單。

 

9       參考鳴謝:

linux訊號處理機制(詳解),http://www.zxbc.cn/html/20080712/61613.html

Linux環境程式間通訊(二): 訊號(上),鄭彥興 (mlinux@163.com)

signal、sigaction、kill等手冊,最直接而可靠的參考資料。

http://www.linuxjournal.com/modules.php?op=modload&name=NS-help&file=man提供了許多系統呼叫、庫函式等的線上指南。

http://www.opengroup.org/onlinepubs/007904975/可以在這裡對許多關鍵函式(包括系統呼叫)進行查詢,非常好的一個網址

程式間通訊訊號(上) http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html

程式間通訊訊號(下)http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index2.html

相關文章