信號的捕捉與模擬實現sleep函數

信號的捕捉:

如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這稱爲捕捉信號。
信號的處理有三種方式。
那麼它是在什麼時候處理信號的呢?
我們來看一張圖。
這裏寫圖片描述
0,一張圖,兩半,上爲用戶態(運行態),下面爲內核態(管理態)。
1, 上圖爲信號的捕捉,處理流程。
2,圖中3,4 是爲了處理用戶自定義的句柄。
3,圖中有4個內核與用戶的切換。
4,用戶處理信號的時機:從內核態切回用戶態時。

mysleep

在模擬實現sleep之前先介紹三個函數。
一,sigaction

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct  sigaction *oact);

sigaction函數可以讀取和修改與指定信號相關聯的處理動作。
調用成功則返回0,出錯則 返回- 1。
signo是指定信號的編號。
若act指針非空,則根據act修改該信號的處理動作。
若oact指針非空,則通過oact傳出該信號原來的處理動作。
act和oact指向sigaction結構體:

struct sigaction
{
     void (*sa_handler)(int);
     sigset_t sa_mask;
     int sa_flags;
     void (*sa_sigaction)(int, siginfo_t *, void*);
}

1,關於sa_handler
將sa_handler賦值爲常數SIG _IGN傳給sigaction表示忽略信號,
賦值爲常數SIG_DFL 表示執行系統默認動作,
賦值爲一個函數指針表示用自定義函數捕捉信號,或者說向內核註冊 了一個信號處理函 數,該函數返回值爲void,可以帶一個int參數,通過參數可以得知當前信 號的編號,這樣就可以用同一個函數處理多種信號。
顯然,這也是一個回調函數,不是被main 函數調用,而是被系統所調用。
2,當某個信號的處理函數被調用時,內核自動將當前信號加入進程的信號屏蔽字,當信號處理函數返回時自動恢復原來的信號屏蔽字,這樣就保證了在處理某個信號時,如果這種信號再次產 生,那麼它會被阻塞到當前處理結束爲止。(防止多次信號)
3,如果在調用信號處理函數時,除了當前信號被自動屏蔽之外,還希望自動屏蔽另外一些信號,則 用sa_mask字段說明這些需要額外屏蔽的信號,當信號處理函數返回時自動恢復原來的信號 屏蔽字。
二:pause

#include <unistd.h>
int pause(void);

pause函數使調用進程掛起直到有信號遞達。
如果信號的處理動作是終止進程,則進程終止,pause函數沒有機會返回;
如果信號的處理動作是忽略,則進程繼續處於掛起狀態,pause 不返回;
如果信號的處理動作是捕捉,則調用了信號處理函數之後pause返回-1,errno設置爲 EINTR, 所以pause只有出錯的返回值。錯誤碼 EINTR表示“被信號中斷”。
三:alarm用法:
alarm和SIGALRM

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

作用:調用alarm函數可以設定一個鬧鐘,也就是告訴內核在seconds秒之後給當前進程發 SIGALRM信號, 該信號的默認處理動作是終止當前進程。
返回值:這個函數的返回值是0或者是以前設定的鬧鐘時間還餘下的秒數。
四,模擬實現mysleep
1,main函數調用mysleep函數,後者調用sigaction註冊了SIGALRM信號的處理函數 sig_alrm。
2,調用alarm(nsecs)設定鬧鐘。
3,調用pause等待,內核切換到別的進程運行。

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

void myhandler(int sec)
{}

int mysleep(int second)
{
    int ret;
    int test = 100;  //這句爲了驗證pause 幹了什麼。
    sigset_t sig;  //前兩句符合C語言編程規範,把需要定義的變量放前面。
    struct sigaction new, old;  //設置兩個處理動作,爲後面的註冊處理函數而設值的。
    new.sa_handler = myhandler;  //設置結構體(內讀到信號時所)調用處理方式(注意指針類型)。
    new.sa_flags = 0;   //默認給0
    sigemptyset(&sig); //初始化信號集,因爲不需要屏蔽任何信號。
    new.sa_mask = sig; // (然後把這個不屏蔽任何信號的 )信號集給這個結構體內

    sigaction(SIGALRM, &new, &old);   //註冊 處理函數動作
    alarm(second);      //設置一個鬧鐘,秒數已經給定,繼續跑,在結束發信號SIGALRM
    test = pause();    //跑到這一步,程序就掛起了,就需要等待信號。
    printf("%d\n", test);// -1,如果不給結束的信號,處理動作向下執行。證明這裏接受的是SIGALRM
    ret = alarm(0);   /*取消鬧鐘。//在這個程序裏面,取消這個鬧鐘是可以跑通的,但是如果我們在其他地方加代碼呢?比如說在有鬧鐘的同時,我們在跑其他的代碼,鬧鐘的異常會引起很多問題,爲了編寫優良,我們是需要加上取消鬧鐘的。*/
    sigaction(SIGALRM, &old, NULL);//恢復默認的處理動作。
    return ret;  //0 
}
int main()
{
    while(1)
    {
        printf("oh,no,wait me 3s more!\n");
        mysleep(3);
    }
    return 0;
}

五:advance_mysleep
question:
系統頻繁的切換進程的時候是很容易出現錯誤的!
如果有一個優先級更高的線程在 我們這個程序的alarm和pause 之間跑起來呢?
這個線程裏面有一個時間很長的alarm, 我們這個程序的鬧鐘超時了(但是在此期間並沒有響應),內核發送SIGALRM信號給這個進程,處於未決狀態。
優先級高的線程執行結束,內核要調度回這個進程開始執行。SIGALRM信號遞達,執行處理函數之後再次進入內核; 返回這個進程的主控制流。我們的SIGALRM剛出來就處理了。然後再pause,這個時候就是機場等一艘輪船。
這裏寫圖片描述
也就是說 雖然alarm(times)緊接着的下一行就是pause(),但是無法保證pause()一定會在調用alarm(times)之後的secs秒之內被調用。如果接受不到他的信號那麼它將永遠掛起。
由於異步事件在任何時候都有可能發生 (這裏的異步事件指出現更優 先級的進程),如果寫程序時考慮不周密,就可能由於時序問題而導致錯誤,這叫做競態條件。
改善:
在調用pause之前屏蔽SIGALRM信號使它不能提前遞達就可以了。
但是你屏蔽了就要解除,解除後如果再等待,也可能發生異步的問題。
引入函數:
sigsuspend(包含了pause的掛起等待功能,同時解決了競態條件的問題),在存在時序問題的場合下都應該調用sigsuspend而不是pause。

#include <signal.h>
int sigsuspend(const sigset_t *sigmask);

和pause一樣,sigsuspend沒有成功返回值,只有一個執行了一個信號處理函數之後sigsuspend才返回,返回值爲-1,errno設置爲EINTR。
調用sigsuspend時,進程的信號屏蔽字由sigmask參數指定。
可以通過sigmask來臨時解除對某個信號的屏蔽,然後掛起等待,當sigsuspend返回時,進程的信號屏蔽字回覆爲原來的值。如果原來對信號是屏蔽的,從sigsuspend返回後仍然是屏蔽的。 (恢復原狀態)
這裏寫圖片描述
1、調用sigprocmask(SIG_BLOCK, &newmask, &omask);屏蔽SIGALRM
2、調用sigsuspend(&smask);解除對SIGALRM的屏蔽,然後掛起等待
3、SIGALRM遞達後suspend返回,自動回覆原來的屏蔽字,也就是再次屏蔽SIGALRM
4、調用sigprocmask(SIG_SETMASK, &omask, NULL);再次解除對SIGALRM的屏蔽

發佈了115 篇原創文章 · 獲贊 48 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章