歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

linux系統編程之信號(六) 競態條件與sigsuspend函數

一、利用pause和alarm函數實現sleep函數

#include <unistd.h>

int pause(void);

pause函數使調用進程掛起直到有信號遞達。如果信號的處理動作是終止進程,則進程終止,pause函數沒有機會返回;如果信號的處理動作是忽略,則進程繼續處於掛起狀態,pause不返回;如果信號的處理動作是捕捉,則調用了信號處理函數之後pause返回-1,errno設置為EINTR,所以pause只有出錯的返回值。錯誤碼EINTR表示“被信號中斷”。

alarm函數可以參考這裡:http://blog.csdn.net/simba888888/article/details/8944647

下面使用pause和alarm實現sleep(3)函數,稱為mysleep:

#include<stdio.h>
#include<signal.h>
#include<unistd.h>
    
void sig_alrm(int signo)
{
    /* nothing to do */
}
    
    
unsigned int mysleep(unsigned int nsecs)
{
    struct sigaction newact, oldact;
    unsigned int unslept;
    
    newact.sa_handler = sig_alrm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);
    
    alarm(nsecs);
    pause();
    
    unslept = alarm(0);
    sigaction(SIGALRM, &oldact, NULL);
    
    return unslept;
}
    
int main(void)
{
    while (1)
    {
        mysleep(2);
        printf("Two seconds passed\n");
    }
    return 0;
}

1. main函數調用mysleep函數,後者調用sigaction注冊了SIGALRM信號的處理函數sig_alrm。

2. 調用alarm(nsecs)設定鬧鐘。

3. 調用pause等待,內核切換到別的進程運行。

4. nsecs秒之後,鬧鐘超時,內核發SIGALRM給這個進程。

5. 從內核態返回這個進程的用戶態之前處理未決信號,發現有SIGALRM信號,其處理函數是sig_alrm。

6. 切換到用戶態執行sig_alrm函數,進入sig_alrm函數時SIGALRM信號被自動屏蔽,從sig_alrm函數返回時SIGALRM信號自動解除屏蔽。然後自動執行系統調用sigreturn再次進入內核,再返回用戶態繼續執行進程的主控制流程(main函數調用的mysleep函數)。

7. pause函數返回-1,然後調用alarm(0)取消鬧鐘,調用sigaction恢復SIGALRM信號以前的處理動作。

需要注意的是雖然sig_alrm函數什麼都沒干,但還是得注冊作為SIGALRM的處理函數,因為SIGALRM信號的默認處理是終止進程,這也是在mysleep函數返回時要恢復SIGALRM信號原來的sigaction的原因。此外,mysleep函數的返回值表示“未睡到”的時間,即unslept,當尚未計時到nsecs而pause函數先被其他信號處理函數所中斷返回,在外界看來就是在sleep期間被其他信號處理函數中斷了,則mysleep返回非0值,即unslept。如sleep(3)的man 手冊寫的返回值:

RETURN VALUE: Zero if the requested time has elapsed, or the number of seconds left to sleep, if the call was interrupted  by  a signal handler.

當然如果是被SIGALRM handler所中斷,則表示睡眠時間到,mysleep返回值為0。

二、競態條件與sigsuspend函數

現在重新審視上面的mysleep函數,設想這樣的時序:

1. 注冊SIGALRM信號的處理函數。

2. 調用alarm(nsecs)設定鬧鐘。

3. 內核調度優先級更高的進程取代當前進程執行,並且優先級更高的進程有很多個,每個都要執行很長時間

4. nsecs秒鐘之後鬧鐘超時了,內核發送SIGALRM信號給這個進程,處於未決狀態。

5. 優先級更高的進程執行完了,內核要調度回這個進程執行。SIGALRM信號遞達,執行處理函數sig_alrm之後再次進入內核。

6. 返回這個進程的主控制流程,alarm(nsecs)返回,調用pause()掛起等待。

7. 可是SIGALRM信號已經處理完了,還等待什麼呢?

出現這個問題的根本原因是系統運行的時序(Timing)並不像我們寫程序時所設想的那樣。雖然alarm(nsecs)緊接著的下一行就是pause(),但是無法保證pause()一定會在調用alarm(nsecs)之後的nsecs秒之內被調用。由於異步事件在任何時候都有可能發生(這裡的異步事件指出現更高優先級的進程),如果我們寫程序時考慮不周密,就可能由於時序問題而導致錯誤,這叫做競態條件(Race Condition)。

如何解決上述問題呢?我們可能會想到,在調用pause之前屏蔽SIGALRM信號使它不能提前遞達就可以了。看看以下方法可行嗎?

1. 屏蔽SIGALRM信號;

2. alarm(nsecs);

3. 解除對SIGALRM信號的屏蔽;

4. pause();

從解除信號屏蔽到調用pause之間存在間隙,SIGALRM仍有可能在這個間隙遞達。要消除這個間隙,我們把解除屏蔽移到pause後面可以嗎?

1. 屏蔽SIGALRM信號;

2. alarm(nsecs);

3. pause();

4. 解除對SIGALRM信號的屏蔽;

這樣更不行了,還沒有解除屏蔽就調用pause,pause根本不可能等到SIGALRM信號。要是“解除信號屏蔽”和“掛起等待信號”這兩步能合並成一個原子操作就好了,這正是sigsuspend函數的功能。sigsuspend包含了pause的掛起等待功能,同時解決了競態條件的問題,在對時序要求嚴格的場合下都應該調用sigsuspend而不是pause。

#include <signal.h>

int sigsuspend(const sigset_t *sigmask);

和pause一樣,sigsuspend沒有成功返回值,只有執行了一個信號處理函數之後sigsuspend才返回,返回值為-1,errno設置為EINTR。

調用sigsuspend時,進程的信號屏蔽字由sigmask參數指定,可以通過指定sigmask來臨時解除對某個信號的屏蔽,然後掛起等待,當sigsuspend返回時,進程的信號屏蔽字恢復為原來的值,如果原來對該信號是屏蔽的,從sigsuspend返回後仍然是屏蔽的。

以下用sigsuspend重新實現mysleep函數:

/*************************************************************************
    > File Name: mysleep.c
    > Author: Simba
    > Mail: [email protected]
    > Created Time: 2012年12月16日 星期日 21時30分42秒
 ************************************************************************/
    
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
    
void sig_alrm(int signo)
{
    /* nothing to do */
}
    
unsigned int mysleep(unsigned int nsecs)
{
    struct sigaction newact, oldact;
    sigset_t newmask, oldmask, suspmask;
    unsigned int unslept;
    
    /* set our handler, save previous information */
    newact.sa_handler = sig_alrm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);
    
    /* block SIGALRM and save current signal mask */
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGALRM);
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);
    
    alarm(nsecs);
    
    suspmask = oldmask;
    sigdelset(&suspmask, SIGALRM); /* make sure SIGALRM isn't block */
    
    sigsuspend(&suspmask); /* wait for any signal to be caught */
    
    /* some signal has been caught. SIGALRM is now blocked */
    unslept = alarm(0);
    sigaction(SIGALRM, &oldact, NULL); /* reset previous action */
    
    /* reset signal mask, which unblocks SIGALRM */
    sigprocmask(SIG_SETMASK, &oldmask, NULL);
    return(unslept);
}
    
int main(void)
{
    while (1)
    {
        mysleep(2);
        printf("Two seconds passed\n");
    }
    return 0;
}

如果在調用mysleep函數時SIGALRM信號沒有屏蔽:

1. 調用sigprocmask(SIG_BLOCK, &newmask, &oldmask);時屏蔽SIGALRM。

2. 調用sigsuspend(&suspmask);時解除對SIGALRM的屏蔽,然後掛起等待待。

3. SIGALRM遞達後suspend返回,自動恢復原來的屏蔽字,也就是再次屏蔽SIGALRM。

4. 調用sigprocmask(SIG_SETMASK, &oldmask, NULL);時再次解除對SIGALRM的屏蔽。

Copyright © Linux教程網 All Rights Reserved