信號的捕捉

信號的捕捉在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);
	}
}
結果如下:






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