在另一篇文章Linux信号中,介紹了信号的産生與處理方式,以及一系列信号集函數的使用。
本文使用信号機制,模拟實作sleep函數并了解競态條件。
在此之前先介紹一波需要用到的函數。
sigaction函數
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
可以讀取和修改于指定信号相關聯的處理動作。
參數 signnum 為指定信号的編号。
若act指針非空,則根據 act 修改信号的處理動作,oldact可以為空,或者傳出原來的處理動作。act和oldact都指向下面的結構體:
struct sigaction
{
void (*sa_handler) (int) // 信号處理函數 SIG_IGN 表示忽略 SIG_DFL 表示預設動作
sigset_t sa_mask; // 額外要屏蔽信号集
int sa_flags; // 一般為0
void (*sa_sigaction)(int, siginfo_t *, void *); // 實時信号處理函數
}
當某個信号的處理函數被調用時,核心自動将目前信号加入程序的信号屏蔽字,當信号處理函數傳回時自動恢複原來的信号屏蔽字。保證在處理某個信号時,如果這種信号再次産生,那麼它會被阻塞到目前處理結束為止。如果在調動信号處理函數時,還需要屏蔽别的信号,則可以通過sa_mask 指定。
pause 函數
#include <unistd.h>
int pause(void);
挂起調用程序直到有信号遞達。 如果信号的處理動作是終止程序,則程序終止。如果信号的處理動作是忽略,則程序繼續處于挂起狀态。隻有信号的處理動作是捕捉pause函數才會傳回。
mysleep1
首先來實作 sleep 版本1:
1. 調用
sigaction()
捕捉信号SIGALRM
2. 調用
alarm()
設定鬧鐘
3. 調用
pause()
挂起等待
4. 取消鬧鐘
5. 恢複捕捉動作
#include <stdio.h>
#include <signal.h>
// 什麼事情也不做
void handler(int signum)
{}
unsigned mysleep(unsigned seconds)
{
// 1. 捕捉信号 SIGALRM
struct sigaction act, oldact;
act.sa_handler = handler;
act.sa_flags = ;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, &oldact); // 設定act 并傳出原來的act
// 2. 設定鬧鐘
alarm(seconds);
// 3. 挂起等待信号遞達
pause();
// 4. 取消鬧鐘
unsigned ret = alarm();
// 恢複捕捉
sigaction(SIGALRM, &oldact, NULL);
return ret;
}
int main()
{
while()
{
printf(" 我正在睡覺 zzz \n");
mysleep();
}
return ;
}
關于mysleep1有幾個問題:
Q1:信号處理函數handler函數什麼都不幹,為什麼還要注冊它作為SIGALRM的處理函數?不注冊信号處理函數可以嗎*?
答:不可以。因為pause() 函數使程序挂起等待,直到有信号遞達并且要執行自定義的信号處理函數才有機會傳回。
Q2:為什麼在mysleep函數傳回前要恢複SIGALRM信号原來的sigaction?
答:main函數作為調用者隻想睡一下覺,沒叫你在它睡覺的時候把它打的鼻青臉腫的,是以調用之前什麼樣就給恢複成什麼樣子。
Q3:mysleep函數的傳回值表示什麼含義? 什麼情況下傳回非0值?
答:mysleep 傳回值表示鬧鐘剩餘時間。 在取消鬧鐘時,上一個鬧鐘傳回為0時。
mysleep2
重新審視上面代碼,會發現有一個bug,假如在設定鬧鐘後,出現大量優先級較高的程序需要執行,測試該程序就會被切出去,即CPU資源被配置設定給了别的程序,如果時間很長的話,當該程序再次或者CPU資源的時候,鬧鐘時間已過,而pause永遠不會傳回。
出現這個問題的原因是系統執行代碼的時序是不确定的。如果在寫程式時考慮不周密,可能由于時序問題而導緻錯誤,這叫做競态條件(Race Condition)。
我們可以在設定鬧鐘之前,屏蔽信号 SIGALRM, 在讓程序挂起時,解除對該程序的屏蔽,然後在讓程序挂起等待信号遞達。可以利用 sigsuspend函數 幫我們圓夢。
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
和pause 函數一樣,該函數沒有成傳回值,隻有執行一個信号處理函數之後,才會傳回。
調用sigsuspend時,程序的信号屏蔽字由sigmask參數指定,可以通過指定sigmask來臨時解除對某個信号的屏蔽,然後挂起等待,傳回時,程序的信号屏蔽字恢複為原來的值。
下面的從新實作的mysleep2:
1. 捕捉SIGALRM信号
2. 屏蔽SIGALRM信号,讓該信号處于未決狀态,直接解除對該信号的屏蔽
3. 調用
alarm()
設定鬧鐘
4. 調用
sigsuspend( )
臨時解除型号,并挂起等待執行完信号處理函數傳回
5. 取消鬧鐘
6. 恢複信号捕捉
7. 恢複信号屏蔽
#include <stdio.h>
#include <signal.h>
// 什麼事情也不做
void handler(int signum)
{}
unsigned mysleep(unsigned seconds)
{
struct sigaction act, oldact;
sigset_t newmask, oldmask, suspmask;
// 1. 信号捕捉
act.sa_handler = handler;
act.sa_flags = ;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, &oldact);
// 2. 信号屏蔽
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
// 3. 設定鬧鐘
alarm(seconds);
// 4. 臨時解除屏蔽并挂起等待
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM); // 確定SIGALRM 不會被阻塞
sigsuspend(&suspmask);
// 5. 取消鬧鐘
unsigned ret = alarm();
// 6. 恢複捕捉動作
sigaction(SIGALRM, &oldact, NULL);
// 7. 恢複信号屏蔽
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return ret;
}
int main()
{
while()
{
printf("我正在睡覺。。\n");
mysleep();
}
return ;
}