信號(signal)

一 信號的基本概念

信號機制是進程間相互傳遞消息的一種方法,信號全稱軟中斷信號,也有人稱作軟中斷,從它的命名可以看出,它的使用很像中斷,所以,信號是進程控制的一部分。

(1)進程之間可以通過系統調用kill發送軟中斷信號

(2)內核也可以因爲內部事件而給進程發送信號,通知進程發生了某個事件

注:信號指示通知給進程發生了什麼事,並不給進程傳遞數據。

爲了理解信號,我們從熟悉的場景說起

  1. 用戶輸入指令,在shell下啓動一個前臺進程。

  2. 用戶按下Ctrl+C,此時硬盤驅動產生一箇中斷給Linux內核。

  3. 如果cpu當前正在執行這個進程的代碼,則該進程的用戶空間暫停執行,CPU從用戶態切換到內核態處理硬件中斷。

  4. 終端驅動程序將Ctrl+C解釋成一個SIGINT信號,記在該進程的PCB中(也可以說發送一個SIGINT信號給該進程)

  5. 當某個信號從內核返回到該用戶空間代碼繼續執行之前,首先處理PCB中(也可以說發送一個SIGINT信號給該進程)

    kill  -l 命令可查看系統定義的信號列表

    wKioL1eeqo-BxhbfAACmFbko8j4647.png-wh_50

信號產生的條件主要有:

1.用戶在終端下按下某些鍵時,終端驅動程序會發送信號給前臺進程,例如Ctrl+C產生SIGINT信號,Ctrl+\產生SIGQUIT信號,Ctrl+Z產生SIGSTOP信號(可使前臺進程終止)

2.硬件異常產生信號,這些條件由硬件檢測並通知內核,然後內核向當前進程發送適當的信號,例如當前進程執行了除以0的指令,CPU運算單元會產生異常,內核將這個異常解釋爲SIGFPE信號發送給該進程,再比如當前進程訪問了非法內存地址,MMU會產生異常,內核將這個異常解釋爲SIGSEGV信號發送給該進程。

3.一個進程調用kill(2)函數可以發送信號給另一個進程,可以用kill(1)給某個進程,kill(1)也是調用kill(2)函數實現的,如果不明確指定信號則發送SIGTERM信號,該信號默認處理動作是終止進程。當內核檢測到某種軟件條件發生時也可以通過信號通知進程,例如鬧鐘超時產生SIGALRM信號,向讀端已關閉的管道寫數據時產生SIGPIPE信號。如果不想按默認動作處理信號,用戶進程可以調用sigaction(2)函數告訴進程應該如何處理某種信號。

可選的信號處理有一下三個動作:

  1. 忽略此信號

  2. 執行信號的默認處理動作

  3. 提供一個信號處理函數,要求內核在處理該信號時切換到用戶態執行這個處理函數,這種方式稱爲捕捉一個信號。

產生信號的方式:

  1. 通過終端鍵產生信號(Ctrl+C等)

  2. 調用系統函數向進程發信號

  3. 由軟件條件產生信號(如鬧鐘alarm函數)

  4. unsigned int alarm(unsigned int seconds);

    調用alarm函數可以設定一個鬧鐘,也就是告訴內核在second秒之後給當前進程發送SIGALARM信號,該信號默認處理動作是終止當前進程,這個函數返回值是0或者是以前鬧鐘時間還餘下的秒數。

二 阻塞信號

信號在內核中的表示

    信號產生有各種原因,而實際信號的處理動作稱爲信號的遞達(Delivery),信號從產生到遞達之間的狀態,稱爲信號未決(Pending)。進程可以選擇阻塞(Block)某個信號,被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞才執行遞達動作,注意:阻塞和忽略是不同的,只要信號被阻塞就不會被遞達,而忽略是遞達之後可選的一種處理動作。

wKioL1eewJijWEcVAABqg8KLIvQ743.png-wh_50

每個信號都有兩個標誌位分別表示阻塞和未決,還有一個函數指針表示處理動作,信號產生時,內核在進程控制塊中設置信號未決標誌,直到信號遞達才處理這個標誌。

  1. SIGHUP信號未阻塞也未產生過,當它遞達是才默認處理動作。

  2. SIGINT信號產生過,但正在被阻塞,所以暫時不能遞達,雖然它的處理動作是忽略的,但沒有接觸阻塞之前不能忽略這個信號,因爲進程仍有機會改變處理動作之後再解除阻塞。

  3. SIGQUIT信號未產生過,一旦產生SIGQUIT信號將被阻塞,它的處理動作是用戶自定義函數sighandler。

如果進程在解除對某信號的阻塞之前這種信號產生過多次,將如何處理?

允許系統遞送信號一次或多次,Linux是這樣實現的:常規信號在遞送之前產生只計一次,而實時信號在遞送之前產生多次可依次放在隊列裏。
從上圖看來,每個信號只有一個bit的未決標誌,非0即1,不記錄信號產生了多少次,阻塞標誌也是這樣表示的。因此未決和阻塞標誌可用相同的數據類型sigset_t來存儲,sigset_t稱爲信號集,這個類型可以表示每個信號“有效”或“無效”狀態。信號阻塞集也叫作當前進程的信號屏蔽字(Signal Mask).

#include<stdio.h>
#include<signal.h>
void catch()
{}

int my_sleep(int timeout)
{
    signal(SIGALRM,catch);
    alarm(timeout);
    pause();
    int ret = alarm(0);
    signal(SIGALRM,SIG_DFL);
    return ret;

}
int main()
{
    while(1)
    {
        printf("testing...\n");
        my_sleep(1);
    }
    return 0;
}

wKioL1efZYKjXbE7AAATUeHjBcY823.png-wh_50設置一個鬧鐘,使得每一秒輸出一次。

現在重新審視“mysleep”程序,設想這樣的時序:
1. 註冊SIGALRM信號的處理函數。
2. 調用alarm(nsecs)設定鬧鐘。
3. 內核調度優先級更高的進程取代當前進程執行,並且優先級更高的進程有很多個,每個
都要 執行很長時間
4. nsecs秒鐘之後鬧鐘超時了,內核發送SIGALRM信號給這個進程,處於未決狀態。
5. 優先級更高的進程執行完了,內核要調度回這個進程執行。SIGALRM信號遞達,執行處
理函 數sig_alrm之後再次進入內核。
6. 返回這個進程的主控制流程,alarm(nsecs)返回,調用pause()掛起等待。

7. 可是SIGALRM信號已經處理完了,還等待什麼呢?
出現這個問題的根本原因是系統運行的時序(Timing)並不像我們寫程序時所設想的那樣。
雖然alarm(nsecs)緊接着的下一行就是pause(),但是無法保證pause()一定會在調用
alarm(nsecs)之 後的nsecs秒之內被調用。由於異步事件在任何時候都有可能發生(這裏
的異步事件指出現更高優 先級的進程),如果我們寫程序時考慮不周密,就可能由於時序問題
而導致錯誤,這叫做競態條件 (Race Condition)。
如何解決上述問題呢?讀者可能會想到,在調用pause之前屏蔽SIGALRM信號使它不能提前遞
達就可 以了。看看以下方法可行嗎?

  1. 屏蔽SIGALRM信號;
    2. alarm(nsecs);
    3. 解除對SIGALRM信號的屏蔽;
    4. pause();

  2. 從解除信號屏蔽到調用pause之間存在間隙,SIGALRM仍有可能在這個間隙遞達。要消除這
    個間隙, 我們把解除屏蔽移到pause後面可以嗎?
    1. 屏蔽SIGALRM信號;
    2. alarm(nsecs);
    3. pause();
    4. 解除對SIGALRM信號的屏蔽;
    這樣更不行了,還沒有解除屏蔽就調用pause,pause根本不可能等到SIGALRM信號。要是
    “解除信號屏蔽”和“掛起等待信號”這兩步能合併成一個原子操作就好了,這正是sigsuspend
    函數的功 能。sigsuspend包含了pause的掛起等待功能,同時解決了競態條件的問題,在對
    時序要求嚴格的場合下都應該調用sigsuspend而不是pause。


三 信號捕捉

測試31種信號哪種可以捕捉?哪種不能捕捉?

如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這稱爲捕捉信號。由於信號處理函數的代碼是在用戶空間的,處理過程比較複雜,舉例如下:

  1. 用戶程序註冊了SIGQUIT信號的處理函數sighandler。
    2. 當前正在執行main函數,這時發生中斷或異常切換到內核態。
    3. 在中斷處理完畢後要返回用戶態的main函數之前檢查到有信號SIGQUIT遞達。
    4. 內核決定返回用戶態後不是恢復main函數的上下文繼續執行,而是執行sighandler函
    數,sighandler和main函數使用不同的堆棧空間,它們之間不存在調用和被調用的關係,
    是 兩個獨立的控制流程。
    5. sighandler函數返回後自動執行特殊的系統調用sigreturn再次進入內核態。
    6. 如果沒有新的信號要遞達,這次再返回用戶態就是恢復main函數的上下文繼續執行了。

程序:

#include<stdio.h>
#include<signal.h>
void header(int sig)
{
    printf("catch %d sig\n",sig);
}
int main()
{
    int i =1;
    for(i=10;i<=31;++i)
    {
        signal(i,header);
        while(1)
        {
            printf("testing...");
            sleep(1);
            kill(getpid(),i);
            break;
        }
    }
    return 0;
}

wKioL1eisIXi9gbZAAAOEmOpVZk230.png-wh_50

wKiom1eisKTRDAfdAAARhsdmbtU982.png-wh_50

wKioL1eisL7wioMZAAAOEmOpVZk320.png-wh_50


用到的幾個函數:

int pause(void)

pause函數使進程掛起,直到有信號遞達,如果信號處理動作是終止進程,則進程終止,pause函數沒有機會返回,如果信號處理動作是忽略,則進程繼續掛起,pause不返回,如果信號處理動作是捕捉,則

調用信號處理函數之後,pause返回-1.

signal(參數1,參數2)

例如:signal(SIGALARM,SIG_DFL)  指對某信號採用默認處理方式。

unsigned int alarm(unsigned int seconds)

調用alarm函數可以設定一個鬧鐘,也就是告訴內核在seconds秒之後給當前進程發SIGALRM信號, 該信號的默認處理動作是終止當前進程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章