信號的捕捉在Linux下機制:如圖
由此可知此機制下發生了四次的模式切換:用戶態--->內核態、內核態--->用戶態、用戶態--->內核態、內核態--->用戶態,從中也可以看出進程處理信號的時機是從內核態切回到用戶態時候,若這時有信號可以遞達則去執行其自定義處理動作,具體執行完成以後則在返回到用戶態main函數繼續上下文執行,若處理動作爲退出則就直接退出。
注意:自定義捕捉函數和main函數使用不同的堆棧空間,它們之間不存在調用和被調用的關係,是兩個獨立的控制流程。
信號捕捉函數:
#include <signal.h>
int sigaction(int signo,const struct sigaction* act,struct sigaction* oact); //讀取或修改與指定信號相關聯的處理動作
參數:
signo:信號編號
act:若act指針非空,則根據act修改該信號的處理動作
oact:若oact指針非空,則通過oact傳出該信號原來的處理動作
act和oact指向sigaction結構體:
struct sigaction
{
void (*sa_handler)(int); //函數指針指向自定義捕捉函數或賦值與默認或忽略動作
sigset_t sa_mask; //通過此信號集參數可屏蔽當前進程中別的信號
sa_flags; //默認爲0
void (*sa_sigaction)(int,siginfo_t*,void*); //實時信號函數指針
};
返回值:
調用成功則返回0,出錯則返回- 1;
注意:
將sa_handler賦值爲常數SIG_IGN傳給sigaction表示忽略信號,賦值爲常數SIG_DFL表示執行系統默認動作,賦值爲一個函數指針表示用自定義函數捕捉信號;
當某個信號的處理函數被調用時,內核自動將當前信號加入進程的信號屏蔽字,當信號處理函數返回時自動恢復原來的信號屏蔽字,這樣就保證了在處理某個信號時,如果這種信號再次產生,那麼它會被阻塞到當前處理結束爲止;
如果在調用信號處理函數時,除了當前信號被自動屏蔽之外,還希望自動屏蔽另外一些信號,則用sa_mask字段說明這些需要額外屏蔽的信號,當信號處理函數返回時自動恢復原來的信號屏蔽字.
pause函數:
#include <unistd.h>
int pause(void);
說明:使當前進程掛起直至收到一個自定義捕捉信號出錯返回;
若收到信號的處理動作是終止進程,則進程終止,pause函數沒有機會返回;如果信號的處理動作是忽略,則進程繼續於掛起狀態,pause不返回;如果信號的處理動作是捕捉,則調用了信號處理函數之後pause返回-1,errno設置爲EINTR, 所以pause只有出錯的返回值。
運用以上函數與以前知識模擬實現sleep函數:
代碼如下:
mysleep.c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void myhandler(int sig)
{
}
int mysleep(int seconds)
{
struct sigaction act;
struct sigaction oact;
act.sa_handler=myhandler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGALRM,&act,&oact); //用此函數自定義信號SIGALRM捕捉函數
alarm(seconds); //設定鬧鐘函數
pause(); //掛起操作直至收到自定義捕捉函數的信號
sigaction(SIGALRM,&oact,&act); //恢復SIGALRM信號以前動作
int ret=alarm(0); //取消鬧鐘
return ret; //返回以前設定鬧鐘的剩餘秒數
}
int main()
{
while(1)
{
int ret=mysleep(3); //自定義實現sleep()函數
printf("mysleep done......%d\n",ret);
}
}
運行結果如下:
在以上實現中雖然實現了sleep功能,但其有一個問題:
1.當在代碼中調用alarm(seconds)時,這時並沒有執行下一條指令pause()使進程掛起;
2.這時若有內核調度優先級更高的進程取代當前進程執行,並且優先級更高的進程有很多個,每個都要執行很長時間;
3.當seconds秒之後鬧鐘超時了,內核發送SIGALRM信號給被取代切出的進程,其信號處於未決狀態;
4.當優先級更高的進程執行完了,內核調度會使被取代切出進程執行,這時SIGALRM信號遞達,執行其自定義捕捉函數之後再次進入內核;
5.最後返回這個進程的主控制流程,alarm(seconds)返回,再調用pause()掛起等待;
但這時由於別的進程優先級高影響了此進程調度時間,使alarm()函數seconds秒發送的SIGALRM信號已經處理完成,所以pause使進程掛起後將不會在收到自定義捕捉的信號。
競態條件:以上alarm()緊接着的下一行就是pause(),但是無法保證pause()一定會在調用alarm()之後的seconds秒之內被調用,是由於異步事件在任何時候都有可能發生(這裏異步事件指出現更高優先級的進程),從而可能由於時序問題而導致錯誤,這叫做競態條件.
解決以上問題:可以使在調用pause()之前一直使SIGALRM信號阻塞(即使當pause()調用alarm()之後的seconds秒之內沒有被調用,但由於信號阻塞它也不會遞達),直至在調用pause時取消其阻塞並使進程掛起(合成原子操作)
用以下函數實現:
int sigsuspend(const sigset_t *sigmask);//解除對某信號屏蔽並使當前進程掛起
參數:
sigmask:進程的信號屏蔽字由sigmask參數指定,可以通過指定sigmask來臨時解除當前進程中對某個信號的屏蔽,然後掛起等待,當sigsuspend返回時,進程的信號屏蔽字恢復爲原來的值(即原來對該信號是屏蔽的,從sigsuspend返回後仍然是屏蔽的).
返回值:
與pause相同,sigsuspend沒有成功返回值,只有執行了一個信號處理函數之後sigsuspend才返回,返回值爲-1,errno設置爲EINTR。
依此來實現sleep函數解除競態條件問題的優化:
SafeMysleep.c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
//實現解決時序問題引起的競態條件問題的sleep()函數
void myhandler(int sig)
{
}
int mysleep(int seconds)
{
struct sigaction act;
struct sigaction oact;
act.sa_handler=myhandler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGALRM,&act,&oact); //定義信號SIGALRM的自定義捕捉函數
sigset_t newset,oldset,sigmask;
sigemptyset(&newset);
sigemptyset(&oldset);
sigaddset(&newset,SIGALRM);
sigprocmask(SIG_BLOCK,&newset,&oldset); //阻塞信號SIGALRM
alarm(seconds); //設定鬧鐘
sigmask=oldset;
sigdelset(&sigmask,SIGALRM);
sigsuspend(&sigmask); //將信號SIGALRM取消阻塞並掛起當前進程
//此函數調用完後SIGALRM恢復阻塞
sigaction(SIGALRM,&oact,&act); //恢復SIGALRM信號以前動作
sigprocmask(SIG_SETMASK,&oldset,&newset); //恢復當前系統舊的阻塞集
int ret=alarm(0); //取消鬧鐘
return ret;
}
int main()
{
while(1)
{
int ret=mysleep(3);
printf("mysleep done......%d\n",ret);
}
}
結果如下: