Linux開發--時序競態與解決辦法
一、訊號引起的競態
競態是指裝置或系統出現不恰當的執行時序,而得到不正確的結果,由於時間片,或其他因素,導致該到達並響應的訊號沒有被響應,這就是由訊號引起的競態。
假設我們要寫一個sleep函式,其中利用到了訊號,編寫的過程如下:
1.註冊一個訊號signal(SIGALRM,handler)。接收核心給出的一個訊號。
2.呼叫alarm()函式。
3.pause()掛起程序。(int pause(void)使呼叫程序掛起,直到有訊號遞達,如果遞達訊號是忽略,則繼續掛起) 實現如下:
#include <unistd.h> #include <signal.h> #include <stdio.h> void sig_alrm(int signo) {//訊號處理函式中什麼都不做 /* nothing to do */ } unsigned int mysleep(unsigned int nsecs) { //newact用於儲存新的訊號處理動作,oldact用於儲存原有訊號處理動作 struct sigaction newact, oldact; unsigned int unslept; newact.sa_handler = sig_alrm; //設定訊號處理函式 sigemptyset(&newact.sa_mask); newact.sa_flags = 0; sigaction(SIGALRM, &newact, &oldact); //設定新sigaction,儲存舊sigaction alarm(nsecs); //設定定時 pause(); //掛起程序 unslept = alarm(0); sigaction(SIGALRM, &oldact, NULL); //將原有sigaction設定回去 return unslept; //返回還未睡夠的時間 } int main(void) { while(1){ mysleep(2); printf("Two seconds passed\n"); } return 0; }
上述述程式的執行過程就是:
1、先註冊SIGALRM訊號的處理函式(在訊號處理函式中什麼都不做),並將原有sigaction儲存起來。
2、開始定時,掛起程序。
3、2秒之後傳送SIGALRM訊號,程序響應訊號,被喚起,進入什麼都不做的訊號處理函式。
4、執行完訊號處理函式之後恢復原有sigaction,結束sleep
5、列印Two seconds passed 但是在這裡有一個問題,如果在“alarm(nsecs); 設定定時”和 “pause(); 掛起程序”之間由於時間片被用完,導致該程序被調出(這時alarm函式已經被執行,核心已經開始計時),而且在該系統中程序數很多,導致“飢餓現象”,所以該程序很久沒有被掉入,假設三秒都沒有被掉入(我們設定的定時是2秒),這時在核心中其實在它被掉入前已經倒數完畢併發出SIGALRM訊號,所以執行它的訊號處理函式(什麼都不做),然後它在第三秒後被掉入,繼續執行,這時執行的函式是pause(),程序被掛起,但是又由於alarm發出的SIGALRM訊號在之前已經被執行,所以pause()會永遠等待不到SIGALRM訊號的到達,那麼該程序會永遠的被掛起!
#include <signal.h> int sigsuspend(const sigset_t *mask) 函式的執行過程是: 1.以通過指定mask來臨時解除對某個訊號的遮蔽, 2.然後掛起等待, 3.當被訊號喚醒sigsuspend返回時,程序的訊號遮蔽字恢復為原來的值
改進之後的例子:
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void sig_alrm(int signo)
{
/* do nothing */
}
unsigned int mysleep(unsigned int nsecs)
{
struct sigaction newact, oldact; //newact用於儲存新的訊號處理動作,oldact用於儲存原有訊號處理動作
//newmask將要新增的阻塞訊號集,oldmask原有訊號集
//suspmask為sigsuspend所用阻塞訊號集
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;//儲存未睡夠的時間
/* 設定SIGALRM訊號處理函式,並儲存原有訊號處理函式 */
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
/* 阻塞SIGALRM訊號,並儲存當前的阻塞訊號集 */
sigemptyset(&newmask);//初始化訊號集(全部置0),防止原有垃圾值
sigaddset(&newmask, SIGALRM);//阻塞SIGALRM訊號
//註冊訊號遮蔽字
//設原有的訊號遮蔽字為mask,則由於以SIG_BLOCK方式
//首先將原有的儲存到oldmask中,現在的阻塞訊號字為mask=mask | newmask)
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
alarm(nsecs);//呼叫時鐘,nsecs秒後向該程序傳送SIGALRM訊號,程式繼續執行
/* 如果這個時候cpu被搶佔>nsecs秒,由於SIGALRM訊號被阻塞,所以即使時間到了傳送了SIGALRM訊號,也不會被處理,也就不會發生時序競爭 */
suspmask = oldmask;//將原有訊號集賦給sigsuspend所用的訊號集
sigdelset(&suspmask, SIGALRM);//在sigsuspend中不阻塞SIGALRM訊號
//掛起
sigsuspend(&suspmask);
//只要有任何訊號被捕捉就會被喚醒繼續執行下面的程式
//並將訊號集恢復為該程序的訊號集:sigprocmask之後的訊號集
// 如果在正常的SIGALRM訊號到來之前,接收到其他訊號,則計算未睡夠的時間
unslept = alarm(0);
sigaction(SIGALRM, &oldact, NULL); //恢復願來SIGALRM訊號的動作
sigprocmask(SIG_SETMASK, &oldmask, NULL);//恢復最初訊號集
return(unslept);//返回未睡夠的時間
}
int main(void)
{
while(1)
{
mysleep(2);
printf("Two seconds passed\n");
}
return 0;
}