1. 程式人生 > >Linux開發--時序競態與解決辦法

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訊號的到達,那麼該程序會永遠的被掛起! 

  以上的情況就是一種由訊號引起的競態,這種情況是不可預知的,而且如若出現後果很嚴重!  所以,要改變上述情況,可以使用sigsuspend函式,這個函式有一個特點就是以下的第1步和第2步是一個原子操作,這時,在使用alarm()函式之前先阻塞SIGALRM訊號,在執行sigsuspend時解除阻塞(以下的第一步就是該函式的作用),這樣就不會引起競態!就可以解決上述問題了!

#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;
}