一、利用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的屏蔽。