1. 程式人生 > >linux訊號處理 (訊號產生 訊號阻塞 訊號集)

linux訊號處理 (訊號產生 訊號阻塞 訊號集)

1.0 定義

訊號(signal)是Linux程序間通訊的一種機制,全稱為軟中斷訊號,也被稱為軟中斷。訊號本質上是在軟體層次上對硬體中斷機制的一種模擬。它提供了一種處理非同步事件的方法,也是程序間惟一的非同步通訊方式。體現為作業系統修改了目標程序的PCB內容,即為對其傳送了訊號。

2.0 訊號的產生

(1)硬體方式

        a.當用戶在終端上按下某鍵時,將產生訊號。如按下<Ctral + C>組合鍵後將產生一個SIGINT訊號。

        b.硬體異常產生訊號:除資料、無效的儲存訪問等。這些事件通常由硬體(如:CPU)檢測到,並將其通知給Linux作業系統核心,然後核心生成相應的訊號,並把訊號傳送給該事件發生時正在進行的程式。

 (2) 軟體方式

        c.使用者在終端下呼叫kill命令向程序傳送任務訊號。

        d.程序呼叫kill或sigqueue函式傳送訊號。

        e.當檢測到某種軟體條件已經具備時發出訊號,如由alarm或settimer設定的定時器超時時將生成SIGALRM訊號。

  

3.0 訊號的處理

  在Linux系統下,訊號的處理方式有3種:

一是   忽略訊號;                       --------------------------------------如signal(訊號,

SIG_IGN);   

二是   按照系統提供的預設處理規則進行處理;   ---------------如signal(訊號,SIG_DFN);   或不作任何註冊

三是   捕捉訊號,在程式中定義自己的訊號處理函式,在訊號處理函式中完成相應的功能。-----註冊一個訊號函式,自己處理  

Linux系統分別為前兩種方式提供了相應的巨集定義:SIG_IGN和SIG_DFN。

下面列出幾個重要訊號及其處理方式(用第二種方式預設處理)。

-----------------------------------------------------------------------------------------------------------------

訊號名稱          預設處理方式                                訊號產生原因/說明

-----------------------------------------------------------------------------------------------------------------

SIGHUP          終止程序                                        控制終端掛起或者退出

SIGINT            終止程序                                        <Ctrl>+<C>

SIGQUIT         終止程序並進行核心映像轉儲         <Ctrl>+<\>

SIGKILL          終止程序                                        不能阻塞、忽略、捕捉

SIGALRM        終止程序                                        定時器超時訊號 

SIGTERM        終止程序                                        可以阻塞、捕捉

SIGCHLD        混略訊號                                        子程序退出時向父程序傳送該訊號

SIGSTOP        暫停程序執行                                 不能阻塞、忽略、捕捉

-----------------------------------------------------------------------------------------------------------------

3.1 訊號處理函式的實現

 訊號處理函式是程序接收到訊號後要執行的函式,該函式應該儘量簡潔,一般不要執行過多的程式碼。最好只是改變一個外部標誌變數的值,而在另外的程式中不斷的檢測該變數,繁雜的工作都留給那些程式去做。在定義訊號處理函式時,應該特別注意以下幾點。

1)如果訊號處理程式中需要存取某個全域性變數,則應該在程式中使用關鍵字volatile宣告此變數。通知編譯器,在編譯過程中不要對該變數進行優化。

2)如果在訊號處理函式中呼叫某個函式,那麼那麼該函式必須是可重入的,或者保證在訊號處理函式執行期間不會有訊號到達程序。Linux系統下存在許多不可重入的函式,如malloc、gethostbyname等。

        在訊號處理函式裡,有時需要用到長跳轉的操作。所謂長跳轉,就是從訊號處理函式直接跳轉到函式體外指定的程式碼位置繼續執行。Linux系統提供了兩個函式實現該功能:設定跳轉點的sigsetjmp執行跳轉siglongjmp。sigsetjmp用來設定跳轉點,在成功呼叫後,sigsetjmp語句所在的位置就是跳轉點,這個位置指標將被儲存到sigsetjmp的第一個引數中。這個兩個函式的原型為:

#include <setjmp.h>

int sigsetjmp (struct __jmp_buf_tag env[1], int savemask);

void siglongjmp(sigjmp_buf env,int val);

引數說明:

             1)env[1]:輸出引數,該引數實際上是一個結構體的指標。該結構體中包含了長跳轉指標,是否儲存訊號掩碼及儲存的訊號掩碼值等資訊。對於應用人員來說,該結構是透明的。

           2)env:輸入引數,等效於env[1]。

           3)savemask:是否儲存訊號掩碼。如果該引數非零,則在呼叫sigsetjmp後,當前程序的訊號掩碼將被儲存;在呼叫siglongjmp時,將恢復由sigsetjmp儲存的訊號掩碼。

          4)val:當由siglongjmp呼叫sigsetjmp時,該引數將會被隱含傳給sigsetjmp作為返回值。如果val等於0,那麼sigsetjmp函式將忽略該引數而返回其他非零值。

返回值:

1)sigsetjmp函式:若返回0,表明sigsetjmp不是由siglongjmp呼叫的;若返回非零值,則是由siglongjmp呼叫而返回。

 程式設計實現捕捉SIGINT訊號,在訊號處理函式中用長跳轉跳轉至主程式。     

#include <stdio.h>
#include <signal.h>
#include <setjmp.h>

//全域性變數,用於儲存跳轉點及其現場
static sigjmp_buf jmpbuff;

//SIGINT訊號處理函式
void CbSigInt(int signo)
{
    //輸出訊號的值
    printf("\nreceive signal %d\n",signo);
    //長跳轉到jmpbuff(即sigsetjmp函式入口處),並平衡堆疊
    siglongjmp(jmpbuff,88);
}

void main()
{
    int res;
    //安裝SIGINT訊號
    signal(SIGINT,CbSigInt);

    //設定跳轉點
    res=sigsetjmp(jmpbuff,1);
    //第一次呼叫sigsetjmp時將返回0

    if(res==0)
        printf("First call sigsetjmp!\n");
    //從訊號處理函式中跳轉過來時,sigsetjmp將返回非零值
    else
    {
        //輸出提示資訊後退出程序
        printf("res=%d\n",res);
        printf("sigsetjmp is called by siglongjmp!\n");
        return;
    }
    //暫停執行等待訊號
    pause();
}

編譯執行該程式,在程序pause期間,按下<Ctrl>+<C>鍵向程序傳送SIGINT訊號,驗證訊號處理函式是否跳轉到指定的位置

3.2   訊號的阻塞處理

        訊號的阻塞    就是通知系統核心暫時停止向程序傳送指定的訊號,而是由核心對程序接收到的相應訊號進行快取排隊,直到程序解除對相應訊號的阻塞為止。一旦程序解除對該訊號的阻塞,則快取的訊號將被髮送到相應的程序。

        訊號在幾種情況下會進入阻塞狀態。

1)系統自動阻塞:在訊號的處理函式執行過程中,該訊號將被阻塞,直到訊號處理函式執行完畢,該阻塞將會解除。這種機制的作用主要是避免訊號的巢狀。

2)通過sigaction實現人為阻塞:在使用sigaction安裝訊號時,如果設定了sa_mask阻塞訊號集,則該訊號集中的訊號在訊號處理函式執行期間將會阻塞。這種情況下進行訊號阻塞的主要原因是:一個訊號處理函式在執行過程中,可能會有其他訊號到來。此時,當前的訊號處理函式就會被中斷。而這往往是不希望發生的。此時,可以通過sigaction系統呼叫的訊號阻塞掩碼對相關訊號進行阻塞。通過這種方式阻塞的訊號,在訊號處理函式執行結束後就會解除。

3)通過sigprocmask實現人為阻塞:可以通過sigprocmask系統呼叫指定阻塞某個或者某幾個訊號。這種情況下進行訊號阻塞的原因較多,一個典型的情況是:某個訊號的處理函式與程序某段程式碼都要某個共享資料區進行讀寫。如果當程序正在讀寫共享資料區的過程中,一個訊號過來,則程序的讀寫過程將被中斷轉而執行訊號處理函式,而訊號處理函式也要對該共享資料區進行讀寫,這樣共享資料區就會發生混亂。這種情況下,需要在程序讀寫共享資料區前阻塞該訊號,在讀寫完成後再解除該訊號的阻塞。

        提示:在訊號的接收過程中可能存在這樣的情況:若干個相同的訊號同時到達。通過上面的介紹可以知道,當訊號處理函式正在執行時,同類訊號將被阻塞處理。但是,如果此時訊號處理函式還沒有來得及執行,那麼該同類訊號就不會阻塞,在這種情況下,將會發生一種成為“訊號合併”的現象。同時到達的同類訊號將被合併處理,就像只有一個訊號到達一樣。

        被阻塞的訊號的集合成為當前程序的訊號掩碼。每個程序都有惟一的訊號掩碼。為了對訊號進行阻塞或者解除阻塞,Linux提供了專門的系統呼叫sigprocmask完成這一任務。該函式的原型為:

#include <signal.h>

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

引數說明:

1)how:輸入引數,設定訊號阻塞掩碼的方式。可以包括3種方式對訊號的掩碼進行設定,分別是阻塞訊號的SIG_BLOCK、解除阻塞的SIG_UNBLOCK和設定阻塞掩碼的SIG_SETMASK。

2)set:輸入引數,阻塞訊號集。當引數how為SIG_BLOCK時,該引數表明要阻塞的訊號集;當how引數為SIG_UNBLOCK時,該引數表明要解除阻塞的訊號集;當how引數為SIG_SETMASK時,該引數表明要阻塞的訊號集。

3)oset:輸出引數,原阻塞訊號集。

返回值:

若成功,返回0;若失敗,返回-1。

例:程式設計實現下面功能:為程序安裝SIGINT訊號,先阻塞該訊號,休眠10秒,再解除該訊號的阻塞。


程式碼如下:
#include <stdio.h>
#include <signal.h>

//SIGINT訊號處理函式
void CbSigInt(int signo)
{
    //輸出訊號的值
    printf("receive signal %d\n",signo);
}
void main()
{
    //訊號掩碼結構變數,用於指定新的訊號掩碼
    sigset_t mask;
    //訊號掩碼結構變數,用於儲存原來的訊號處理掩碼
    sigset_t omask;

    //安裝SIGINT訊號
    signal(SIGINT,CbSigInt);

    //清空訊號掩碼變數
    sigemptyset(&mask);

    //向掩碼結構中增加訊號SIGINT
    sigaddset(&mask,SIGINT);

    //阻塞SIGINT訊號
    sigprocmask(SIG_BLOCK,&mask,&omask);

    //休眠10秒
    sleep(10);

    //解除SIGINT訊號的阻塞
    sigprocmask(SIG_SETMASK,&omask,NULL);
}
編譯執行該程式,在程序休眠期間,按下<Ctrl>+<C>鍵向程序傳送SIGINT訊號,注意觀看訊號是否被阻塞。在休眠結束後,驗證剛才被阻塞的SIGINT訊號是否被重新發送。

提示:在建立新的子程序時,子程序將繼承父程序的訊號掩碼

訊號集

       我們已經知道,我們可以通過訊號來終止程序,也可以通過訊號來在程序間進行通訊,程式也可以通過指定訊號的關聯處理函式來改變訊號的預設處理方式,也可以遮蔽某些訊號,使其不能傳遞給程序。那麼我們應該如何設定我們需要處理的訊號,我們不需要處理哪些訊號等問題呢?訊號集函式就是幫助我們解決這些問題的。

       訊號的遞送、阻塞和未決

  實際執行訊號的處理動作稱為訊號遞送(Delivery),訊號從產生到遞送之間的狀態,稱為訊號未決(Pending)。程序可以選擇阻塞(Block)某個訊號,SIGKILL 和 SIGSTOP 不能被阻塞。被阻塞的訊號產生時將保持在未決狀態,直到程序解除對此訊號的阻塞,才執行遞送的動作。
  每個程序都有一個訊號掩碼,它實際上是一個訊號集,位於該訊號集中的訊號一旦產生,並不會被遞送給相應的程序,而是會被阻塞在未決狀態。在訊號處理函式執行期間,這個正在被處理的訊號總是處於訊號掩碼中,如果又有該訊號產生,則會被阻塞,直到上一個針對該訊號的處理過程結束以後才會被遞送。

å¨è¿éæå¥å¾çæè¿°

å¨è¿éæå¥å¾çæè¿°

 

訊號集的操作

通過上節對訊號阻塞的介紹可以知道,訊號的阻塞實際上是對一個集合的操作。這個集合中可能包含多種訊號,這就是訊號集。訊號集的資料型別為sigset_t,實際上是個結構體,它的定義如下所示。

typedef struct

{

    unsigned long sig[_NSIG_WORDS];

}sigset_t;


Linux系統提供了一系列函式對訊號集進行操作。這些函式的原型如下所示。


#include <signal.h>

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

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

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

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

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

引數說明:

    1)set:輸入引數,訊號集。

    2)signo:輸入引數,要增加或刪除或判斷的訊號。

返回值:

    1)對於sigismember函式:返回1表示訊號屬於訊號集;返回0表示訊號不屬於訊號集。

    2)對於其他函式:若成功,返回0;若失敗,返回-1。

未決訊號的處理

        訊號的未決是訊號產生後的一種狀態,是指從訊號產生後,到訊號被接收程序處理之前的一種過渡狀態。由於訊號的未決狀態時間非常短,所以通常情況下,處於未決狀態的訊號非常少。如果程式中使用了sigprocmask阻塞了某種訊號,則向程序傳送的這種訊號將處於未決狀態。Linux提供了專門的函式sigpending獲取當前程序中處於未決狀態的訊號。該函式的原型為:

#include <signal.h>

int sigpending(sigset_t *set);

引數說明:

          1)set:輸出引數,處於未決狀態的訊號集。

返回值:

           若成功,返回0;若失敗,返回-1。

程式設計實現下面功能:為程序安裝SIGINT訊號,先阻塞該訊號,休眠10秒,最後檢視當前程序未決的訊號。

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

void main()
{
    //訊號掩碼結構變數,用於指定新的訊號掩碼
    sigset_t mask;

    //訊號掩碼結構變數,用於儲存原來的訊號處理掩碼
    sigset_t omask;

    //訊號掩碼結構變數,用於儲存未決的訊號集
    sigset_t pendmask;

    //清空訊號掩碼變數
    sigemptyset(&mask);

    //向掩碼結構中增加訊號SIGINT
    sigaddset(&mask,SIGINT);

    //阻塞SIGINT訊號
    sigprocmask(SIG_BLOCK,&mask,&omask);

    //休眠10秒
    sleep(10);

    //獲取當前未決的訊號集
    if(sigpending(&pendmask)<0)
    {
        perror("sigpending");

        //解除SIGINT訊號的阻塞
        sigprocmask(SIG_SETMASK,&omask,NULL);
        return;
    }

    //判斷SIGINT是否在未決訊號集中
    if(sigismember(&pendmask,SIGINT))
        printf("SIGINT signal is pending.\n");
    else
        printf("SIGINT signal is not pending.\n");

    //解除SIGINT訊號的阻塞
    sigprocmask(SIG_SETMASK,&omask,NULL);
}
//編譯執行該程式,在程序休眠期間,按下<Ctrl>+<C>鍵向程序傳送SIGINT訊號,驗證該訊號是否處於未決狀態。

等待訊號

在有些情況下,程式需要暫停執行,進入休眠狀態,以等待訊號的到來。這時可以使用pause系統呼叫。pause一旦被呼叫,則程序將進入休眠狀態。之後,只有在程序接收到訊號後,pause才會返回。pause的原型為:

#include <unistd.h>

         int pause();

返回值:

        pause的返回值永遠是-1,錯誤碼errno為EINTR。

用pause程式設計實現等待SIGINT訊號到來的功能。

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

//SIGINT訊號處理函式
void CbSigInt(int signo)
{
    //輸出訊號的值
    printf("receive signal %d\n",signo);
}

void main()
{
    //安裝SIGINT訊號
    signal(SIGINT,CbSigInt);
    //等待訊號
    pause();
}

//編譯執行該程式,在程序pause期間,按下<Ctrl>+<C>鍵向程序傳送SIGINT訊號,驗證該訊號是否被處理。
//注意:由於此測試程式沒有遮蔽其他訊號,因此任何一個訊號的到來都能喚醒pause。

 sigsuspend

        pause系統呼叫可以實現暫停程序的執行等待某個訊號的到來,但是,如果在pause被呼叫之前,指定的訊號到達程序,那麼,在隨後的pause呼叫中,假定不再有訊號到來,則程序將進入無限期的等待中。為此Linux提供了功能更強大的sigsuspend以滿足這種需求。sigsuspend的工作過程如下:

         1)設定程序的訊號掩碼並阻塞程序。

         2)收到訊號,恢復原來的訊號掩碼。

        3)呼叫程序設定的訊號處理函式。

         4)等待訊號處理函式返回後,sigsuspend返回。

        上述四個步驟是一次性完成的,作業系統保證操作過程的原子性。特別需要注意的是第三步呼叫訊號處理函式是由sigsuspend完成的。

sigsuspend的原型為:

#include <signal.h>

int sigsuspend(const sigset_t *set);

引數說明:

             1)set:輸入引數,執行sigsuspend過程中需要被阻塞的訊號集。

返回值:

            sigsuspend的返回值永遠是-1,錯誤碼errno為EINTR。

用sigsuspend程式設計實現等待SIGINT訊號到來的功能。

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

//SIGINT訊號處理函式
void CbSigInt(int signo)
{
    //輸出訊號的值
    printf("receive signal %d\n",signo);
}

void main()
{
    //訊號掩碼結構變數,用於指定新的訊號掩碼
    sigset_t mask;

    //安裝SIGINT訊號
    signal(SIGINT,CbSigInt);

    //設定訊號集為所有訊號,準備阻塞所有訊號
    sigfillset(&mask);

    //從訊號集中刪除SIGINT訊號,該訊號為目標訊號,不能被阻塞。
    sigdelset(&mask,SIGINT);

    //等待SIGINT訊號
    sigsuspend(&mask);
}

//編譯執行該程式,在程序suspend期間,按下<Ctrl>+<C>鍵向程序傳送SIGINT訊號,驗證該訊號是否被處理。
//注意:由於此測試程式遮蔽了除SIGINT外的所有其他訊號,因此只有在收到SIGINT訊號後進程才會退出。

提示:

        一個阻塞式系統呼叫在執行過程中如果沒有符合條件的資料,將進入休眠狀態,直到有符合條件的資料到來。比較典型的例子是從網路連線上讀取資料,如果沒有資料到來,那麼這個讀操作將會阻塞。此時有兩種情況可以中斷該讀操作的執行:一是網路上有資料到來,則讀操作將獲取到所需要的資料後返回;二是當前程序收到了某個訊號,此時,讀操作將被中斷並返回失敗,錯誤碼errno為EINTR。

 

abort  

abort()函式首先解除程序對SIGABRT訊號的阻止,然後向呼叫程序傳送該訊號。

abort()函式會導致程序的異常終止除非SIGABRT訊號被捕捉並且訊號處理控制代碼沒有返回。