linux 時序競態

pause函數

調用該函數可以造成進程主動掛起,等待信號喚醒。調用該系統調用的進程將處於阻塞狀態(主動放棄cpu) 直到有信號遞達將其喚醒。

int pause(void);	

返回值:-1 並設置errno爲EINTR
返回值:
① 如果信號的默認處理動作是終止進程,則進程終止,pause函數沒有機會返回。
② 如果信號的默認處理動作是忽略,進程繼續處於掛起狀態,pause函數不返回。
③ 如果信號的處理動作是捕捉,則調用完信號處理函數之後,pause返回-1,errno設置爲EINTR,表示“被信號中斷”。
④ pause收到的信號不能被屏蔽,如果被屏蔽,那麼pause就不能被喚醒。

例:使用pause和alarm來實現sleep函數。

#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
void sig_deal(int signo)
{
}

unsigned int mysleep(unsigned int n)
{
    struct sigaction newact, oldact;
    unsigned int ret;

    newact.sa_handler = &sig_deal;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);//SIGALRM信號回調函數

    alarm(n); 
	
    int pause_ret=pause();
	if(pause_ret==-1&&errno==EINTR)
		printf("pause success\n");

    ret = alarm(0);//取消定時器,返回舊鬧鐘餘下秒數。
    sigaction(SIGALRM, &oldact, NULL);//恢復SIGALRM信號舊有的處理方式

    return ret;
}


int main(void)
{
    while(1){
        mysleep(2);
        printf("2s passed\n");
    }

    return 0;
}

在這裏插入圖片描述

時序問題分析

回顧,藉助pause和alarm實現的mysleep函數。設想如下時序:
1.註冊SIGALRM信號處理函數(sigaction…)
2.調用alarm(1) 函數設定鬧鐘1秒。
3.函數調用剛結束,開始倒計時1秒。當前進程失去cpu,內核調度優先級更高的進程(有多個)取代當前進程。當前進程無法獲得cpu,進入就緒態等待cpu。
4.1秒後,鬧鐘超時,內核向當前進程發送SIGALRM信號(自然定時法,與進程狀態無關),高優先級進程尚未執行完,當前進程仍處於就緒態,信號無法處理(未決)
5.優先級高的進程執行完,當前進程獲得cpu資源,內核調度回當前進程執行。SIGALRM信號遞達,信號設置捕捉,執行處理函數sig_deal。
6.信號處理函數執行結束,返回當前進程主控流程,pause()被調用掛起等待。(欲等待alarm函數發送的SIGALRM信號將自己喚醒)
7.SIGALRM信號已經處理完畢,pause永遠不會等到。

解決時序問題

可以通過設置屏蔽SIGALRM的方法來控制程序執行邏輯,但無論如何設置,程序都有可能在“解除信號屏蔽”與“掛起等待信號”這個兩個操作間隙失去cpu資源。除非將這兩步驟合併成一個“原子操作”。sigsuspend函數具備這個功能。在對時序要求嚴格的場合下都應該使用sigsuspend替換pause。

int sigsuspend(const sigset_t *mask);	

sigsuspend函數調用期間,進程信號屏蔽字由其參數mask指定。
可將某個信號(如SIGALRM)從臨時信號屏蔽字mask中刪除,這樣在調用sigsuspend時將解除對該信號的屏蔽,然後掛起等待,當sigsuspend返回時,進程的信號屏蔽字恢復爲原來的值。

例:改進後的mysleep。

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

void sig_alrm(int signo)
{
    /* nothing to do */
}

unsigned int mysleep(unsigned int n)
{
    struct sigaction newact, oldact;
    sigset_t newmask, oldmask, suspmask;
    unsigned int unslept;

    //1.爲SIGALRM設置捕捉函數,一個空函數
    newact.sa_handler = sig_alrm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);

    //2.設置阻塞信號集,阻塞SIGALRM信號
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGALRM);
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);   //信號屏蔽字,阻塞SIGALRM信號

    //3.定時n秒,到時後可以產生SIGALRM信號
    alarm(n);

    /*4.構造一個調用在sigsuspend函數執行過程中臨時有效的阻塞信號集suspmask,
     *  在臨時阻塞信號集裏解除SIGALRM的阻塞*/
    suspmask = oldmask;
    sigdelset(&suspmask, SIGALRM);

    /*5.sigsuspend調用期間,採用臨時阻塞信號集suspmask替換原有阻塞信號集
     *  這個信號集中不包含SIGALRM信號,同時掛起等待,
     *  當sigsuspend被信號喚醒返回時,恢復原有的阻塞信號集*/
    sigsuspend(&suspmask); //原子操作

    unslept = alarm(0);
    //6.恢復SIGALRM原有的處理動作
    sigaction(SIGALRM, &oldact, NULL);

    //7.解除對SIGALRM的阻塞
    sigprocmask(SIG_SETMASK, &oldmask, NULL);

    return unslept;
}

int main(void)
{
    while(1)
	{
        mysleep(2);
        printf("2s passed\n");
    }

    return 0;
}

整理一下,其實解決該問題的核心思路就是先把SIGALRM信號屏蔽,然後利用sigsuspend函數(系統調用是原子操作)把解除SIGALRM信號屏蔽和執行該信號的回調函數這兩步一氣呵成。

但是我認爲,這個函數可以解決時序競態,但是定時的時間肯定就不準了(如果信號發出的時候,當前進程沒有拿到cpu,那麼雖然該信號被阻塞了,但是當前進程還是要等待cpu,接下來等到當前進程拿到cpu,然後sigsuspend也順利完成了任務,但是總時間變長了)。

總結

競態條件,跟系統負載有很緊密的關係,體現出信號的不可靠性。系統負載越嚴重,信號不可靠性越強。
不可靠由其實現原理所致。信號是通過軟件方式實現(跟內核調度高度依賴,延時性強),每次系統調用結束後,或中斷處理處理結束後,需通過掃描PCB中的未決信號集,來判斷是否應處理某個信號。當系統負載過重時,會出現時序混亂。
這種意外情況只能在編寫程序過程中,提早預見,主動規避,而無法通過gdb程序調試等其他手段彌補。且由於該錯誤不具規律性,後期捕捉和重現十分困難。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章