【Linux】訊號的產生、阻塞與捕捉
文章目錄
1.訊號的基本概念
1)理解什麼是訊號?
-
⽤戶輸⼊命令,在Shell下啟動⼀個前臺程序。
-
⽤戶按下Ctrl-C,這個鍵盤輸⼊產⽣⼀個硬體中斷。
-
如果CPU當前正在執⾏這個程序的程式碼,則該程序的⽤戶空間程式碼暫停執⾏,CPU從⽤戶態 切換到核心態處理硬體中斷。
-
終端驅動程式將Ctrl-C解釋成⼀個SIGINT訊號,記在該程序的PCB中(也可以說傳送了⼀ 個
SIGINT
訊號給該程序)。(說明PCB可記錄訊號,是引用點陣圖記錄,位置訊號的1/0表明訊號的存在與否)(只有OS有資格修改目標程序的點陣圖資訊也只有OS能傳送,發訊號其實更準確來說是寫訊號)。 -
當某個時刻要從核心返回到該程序的⽤戶空間程式碼繼續執⾏之前,⾸先處理PCB中記錄的訊號,發現有⼀個SIGINT訊號待處理,⽽這個訊號的預設處理動作是終⽌程序,所以直接終⽌程序⽽不再返回它的⽤戶空間程式碼執⾏。
注意 -
Ctrl-C產⽣的訊號只能發給
前臺程序
。⼀個命令 後⾯加個&
可以放到後臺運⾏,這樣Shell不必等待程序結束就可以接受新的命令,啟動新的程序。 -
Shell可以同時運⾏⼀個前臺程序和任意多個後臺程序,只有前臺程序才能接到像Ctrl-C這種控制鍵產⽣的訊號。
-
前臺程序在運⾏過程中⽤戶隨時可能按下Ctrl-C⽽產⽣⼀個訊號,也就是說該程序的⽤戶空間程式碼執⾏到任何地⽅都有可能收到SIGINT訊號⽽終⽌,所以
訊號相對於程序的控制流程來說是非同步(Asynchronous)的。
總結1.程序要處理訊號必須要認識訊號(點陣圖加規定)
2.訊號產生程序收到後有可能不立刻處理(先儲存)而在合適的時候
3.訊號機制的產生對程序來說是非同步的執行流並沒有關係
4.訊號處理的三種方法:執行預設訊號/忽略/自定義
5.如果當前訊號不能被處理就先記錄。
2)訊號列表
使用命令:kill -l
可以檢視系統定義的訊號列表。
每個訊號都有⼀個編號和⼀個巨集定義名稱,這些巨集定義可以在signal.h中找到,例如其中有定義 #define SIGINT 2 。 1-31為普通訊號,34-64為實時訊號。這些訊號各自在什麼條件下產生,預設的處理動作是什麼,在signal(7)中都有詳細說明:man 7 signal
2.訊號的產生
1)產生訊號的方法概述
《1》終端產生
Ctrl+c產生SIGINT訊號
Ctrl+\產生SIGQUIT訊號
Ctrl+Z產生SIGTSTP訊號
《2》硬體異常產生
,這些條件由硬體檢測到並通知核心,然後核心向當前程序傳送適當的信 號。例如當前程序執⾏了除以0的指令,CPU的運算單元會產⽣異常,核心將這個異常解釋 為SIGFPE訊號傳送給程序。再⽐如當前程序訪問了⾮法記憶體地址,MMU會產⽣異常,核心 將這個異常解釋為SIGSEGV訊號傳送給程序。
《3》kill產生
⼀個程序調⽤kill(2)函式可以傳送訊號給另⼀個程序。 可以⽤kill(1)命令傳送訊號給某個程序,kill(1)命令也是調⽤kill(2)函式實現的,如果不明確指定訊號則傳送SIGTERM訊號,該訊號的預設處理動作是終⽌程序。 當核心檢測到某種軟體條件發⽣時也可以通過訊號通知程序,例如鬧鐘超時產SIGALRM訊號,向讀端已關閉的管道寫資料時產⽣SIGPIPE訊號。 如果不想按預設動作處理訊號,⽤戶程式可以調⽤sigaction(2)函式告訴核心如何處理某種訊號.
《4》軟體條件產生
2)終端產生訊號
SIGINT的預設處理動作是終⽌程序,SIGQUIT的預設處理動作是終⽌程序並且Core Dump,現在我們來驗證⼀下。
Core Dump
⾸先解釋什麼是Core Dump。當⼀個程序要異常終⽌時,可以選擇把程序的⽤戶空間記憶體資料全部 儲存到磁碟
上,⽂件名通常是core,這叫做Core Dump。程序異常終⽌通常是因為有Bug,⽐如⾮法記憶體訪問導致段錯誤,事後可以⽤偵錯程式檢查core⽂件以查清錯誤原因,這叫做Post-mortemDebug(事後除錯
)。
⼀個程序允許產⽣多⼤的core⽂件取決於程序的Resource Limit
(這個資訊儲存 在PCB中)。預設是不允許產⽣core⽂件的,因為core⽂件中可能包含⽤戶密碼等敏感資訊,不安全。在開發除錯階段可以⽤ulimit命令改變這個限制,允許產⽣core⽂件。
$ ulimit -c 1024//修改core檔案允許產生最大1024k
$ ulimit -a //檢視資源
事後除錯:
$ ulimit -c 1024
- 寫一個死迴圈程式,使用ctrl + \將它終止掉,ls發現會多一個core.檔案。
- 使用gdb除錯,core-file core.檔案。
- core檔案直接幫我們定位出來錯誤的地方。
3)呼叫系統函式向程序發訊號
《1》kill命令
kill命令是調⽤kill函式實現的。kill函式可以給⼀個指定的程序傳送指定的訊號。
#include<signal.h>
int kill(pid_t pid,int signo);//kill命令的呼叫kill函式實現的
返回值:成功返回0,失敗返回-1
⾸先在後臺執⾏死迴圈程式
然後⽤kill命令給它發SIGSEGV訊號
之所以要再次回⻋才顯⽰ Segmentation fault ,是因為在13972程序終⽌
掉 之前已經回到了Shell提⽰符等待⽤戶輸⼊下⼀條命令,Shell不希望Segmentation fault資訊和⽤戶的輸⼊交錯在⼀起,所以等⽤戶輸⼊命令之後才顯⽰。
kill -SIGSEGV 程序PID與`kill -11 程序PID(11是SIGSEGV的編號)` 相同。
以往遇 到的段錯誤都是由⾮法記憶體訪問產⽣的,⽽這個程式本⾝沒錯,給它發SIGSEGV也能產⽣段錯誤
《2》raise函式
給當前程序傳送指定訊號(自己給自己發)
#include<signal.h>
int raise(int signo);
返回值:成功返回0,失敗返回-1
《3》abort函式
使當前程序接收到訊號⽽異常終⽌
#include <stdlib.h>
void abort(void);
就像exit函式⼀樣,abort函式總是會成功的,所以沒有返回值。
4)軟體條件產生訊號
SIGPIPE是⼀種由軟體條件產⽣的訊號,當讀端將自己的檔案描述符關閉,OS會向寫端傳送一個SIGPIPE訊號來關閉寫端。
《1》alarm函式
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
功能:設定一個鬧鐘,告訴核心在seconds秒後給當前程序傳送SIGALRM訊號,該訊號的預設動作是終止程序
這個函式的返回值是0或者是以前設定的鬧鐘時間還餘下的秒數。
eg:下面這個程式作用是2秒鐘之內不停的數數,2秒鐘到了就被SIGALRM訊號終⽌.
#include<stdio.h>
#include<unistd.h>
int main()
{
int count = 0;
alarm(2);
for(count = 0;2;count++){
printf("count = %d\n",count);
}
return 0;
}
《2》signal函式
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//signum是訊號編號,
//handler是一個函式指標,這個函式指標指向的是對預設處理替換的方法函式
3.阻塞訊號
1)訊號其他相關常見概念
- 實際執行訊號的處理動作稱為訊號遞達(Delivery)(要遞達絕對沒有阻塞)
- 訊號從產生到遞達之間的狀態,稱為訊號未決(Pending)
pending->block->阻塞
pending->未block->合適時遞達
- 程序可以選擇阻塞(Block)某個訊號。(阻塞也叫做遮蔽)沒處理除非解除阻塞。
- 被阻塞的訊號產生時將保持在未決狀態,直到程序解除對此洗腦的阻塞,才執行遞達的動作。
- 阻塞與忽略是不同的,只要訊號被阻塞就不會遞達,而忽略是在遞達之後,可選的一種處理動作。(忽略表明處理了)
2)在核心中的表達
訊號在核心中的表示示意圖:(點陣圖儲存)
由上圖知
block的0/1表明是否被遮蔽。
pending0/1表明訊號是否存在。訊號產生時,核心在程序控制塊中的未決標誌直到訊號遞達才清除該標誌。圖中的2號訊號還沒有被處理。
handler表示處理動作:函式指標陣列,先編號找到位置,再將函式指標放入。
如果在程序解除對某訊號的阻塞之前這種訊號產⽣過多次,將如何處理?POSIX.1允許系統遞送該訊號⼀次或多次。Linux是這樣實現的:常規訊號在遞達之前產⽣多次只計⼀
次,⽽實時訊號在遞達之前產⽣多次可以依次放在⼀個佇列⾥。
3)sigset_t
點陣圖,包含block表和pending表。
每個訊號只有⼀個bit的未決標誌,⾮0即1,不記錄該訊號產⽣了多少次,阻塞標誌也是這樣表⽰的。 因此,未決和阻塞標誌可以⽤相同的資料型別sigsett來儲存,sigsett稱為訊號集,這個型別可以表⽰每個訊號的“有效”或“⽆效”狀態,在阻塞訊號集中“有效”和“⽆效”的含義是該訊號是否被阻塞,⽽在未決信
號集中“有效”和“⽆效”的含義是該訊號是否處於未決狀態。 阻塞訊號集也叫做當前程序的訊號遮蔽字(Signal Mask),這⾥的“遮蔽”應該理解為阻塞⽽不是忽略。
4)訊號集操作函式
sigsett型別對於每種訊號⽤⼀個bit表⽰“有效”或“⽆效”狀態,⾄於這個型別內部如何儲存這些bit則依賴於系統實現,從使⽤者的⾓度是不必關⼼的,使⽤者只能調⽤以下函式來操作sigset t變數,⽽不應該對它的內部資料做任何解釋,⽐如⽤printf直接列印sigset_t變數是沒有意義的。
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
- 函式sigemptyset初始化set所指向的訊號集,使其中所有訊號的對應bit清零,表⽰該訊號集不包含 任何有效訊號。
- 函式sigfillset初始化set所指向的訊號集,使其中所有訊號的對應bit置位,表⽰ 該訊號集的有效訊號包括系統⽀持的所有訊號。
- 注意,在使⽤sigset_ t型別的變數之前,⼀定要調 ⽤sigemptyset或sigfillset做初始化,使訊號集處於確定的狀態。初始化sigset_t變數之後就可以在調⽤sigaddset和sigdelset在該訊號集中新增或刪除某種有效訊號。
這四個函式都是成功返回0,出錯返回-1。sigismember是⼀個布林函式,⽤於判斷⼀個訊號集的有效訊號中是否包含某種 訊號,若包含則返回1,不包含則返回0,出錯返回-1。
5)sigprocmask函式
可以讀取或更改程序的訊號遮蔽字(阻塞訊號集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功則為0,若出錯則為-1
如果oset是⾮空指標,則讀取程序的當前訊號遮蔽字通過oset引數傳出。
如果set是⾮空指標,則 更改程序的訊號遮蔽字,引數how指⽰如何更改。
如果oset和set都是⾮空指標,則先將原來的訊號 遮蔽字備份到oset⾥,然後根據set和how引數更改訊號遮蔽字。
假設當前的訊號遮蔽字為mask,下表說明了how引數的可選值。
如果呼叫sigprocmask解除了對當前若干個未決訊號的阻塞,則在函式返回之前,至少將其中一個訊號遞達。
6)sigpending函式
讀取當前程序的未決訊號集,通過set引數傳出。呼叫成功則返回0,出錯則返回-1.
#include<signal.h>
int sigpending(sigset_t *set);
下面我們使用下剛才學習的函式:
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void printsigset(sigset_t *set)
{
int i = 0;
for(;i<32;i++)
{
if(sigismember(set,i))
{
putchar('1');
}
else{
putchar('0');
}
}
puts("");
}
int main()
{
sigset_t s,p;//定義訊號集物件,並清空初始化
sigemptyset(&s);
sigaddset(&s,SIGQUIT);
sigprocmask(SIG_BLOCK,&s,NULL);//設定阻塞訊號集,阻塞SIGQUIT訊號
while(1){
sigpending(&p);//獲取未決訊號集
printsigset(&p);
sleep(1);
}
}
執行該程式,每秒鐘把各訊號的未決狀態列印一遍,由於我們阻塞了SIGQUIT訊號,按Ctrl+\會使SIGQUIT處於未決狀態,如下圖所示。
使用Ctrl+C仍然可以終止程式,因為SIGINT訊號沒有阻塞。
4.捕捉訊號
1)訊號的捕捉
《1》首先我們先了解下核心態和使用者態的基本概念
-
使用者態:使用者執行自身程式碼
-
核心態:執行核心程式碼的狀態
使用系統呼叫介面時可以由使用者態變為核心態,返回時再又核心態轉為使用者態,系統呼叫介面的存在就是為了保護OS,普通使用者不能訪問OS的程式碼資料。切入核心的方式除了系統呼叫還有中斷,異常,缺陷等方式
《2》核心如何實現訊號的捕捉
如果訊號的處理動作是使用者自定義函式,在訊號遞達時就呼叫這個函式,這就成為捕捉訊號。由於訊號處理函式的程式碼是在使用者空間的,處理過程比較複雜,舉例如下:使用者程式註冊了SIGQUIT 訊號的處理函式sighandler.當前正在執行main函式,這時發生中斷或者異常切換到核心態。在中斷處理完畢要返回使用者態的main函式之前檢查到有訊號SIGQUIT遞達。核心決定返回使用者態後不是恢復main函式的上下文繼續執行,而是執行sighandler函式,sighandler和main函式使用不同的堆疊空間,它們之間不存在呼叫和被呼叫的關係,如果沒有新的訊號要遞達,這次再返回使用者態就是恢復main函式的上下文繼續執行了。
訊號處理在核心態切換至使用者態的時候,執行緒間的切換,程序間的切換也發生在核心態切換使用者態的期間。
《3》sigaction函式
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
//const sigaction *act是指處理新的訊號
//struct sigaction *oact是將舊的返回(輸出型引數)
struct sigaction
{
void(*) (int) sa_handler ;
sigset_t sa_mask ;
int sa_flags ;
void(*) (int, siginfo_t *, void *) sa_sigaction ;
}
sigaction函式可以讀取和修改與指定訊號相關聯的處理動作。調⽤成功則返回0,出錯則返回- 1。signo是指定訊號的編號。若act指標⾮空,則根據act修改該訊號的處理動作。若oact指標⾮ 空,則通過oact傳出該訊號原來的處理動作。act和oact指向sigaction結構體:將sahandler賦值為常數SIGIGN傳給sigaction表⽰忽略訊號,賦值為常數SIG_DFL表⽰執⾏系統預設動作,賦值為⼀個函式指標表⽰⽤⾃定義函式捕捉訊號,或者說向核心註冊了⼀個訊號處理函 數,該函式返回值為void,可以帶⼀個int引數,通過引數可以得知當前訊號的編號,這樣就可以⽤同⼀個函式處理多種訊號。顯然,這也是⼀個回撥函式,不是被main函式調⽤,⽽是被系統所呼叫的。
當某個訊號的處理函式被調⽤時,核心⾃動將當前訊號加⼊程序的訊號遮蔽字,當訊號處理函式返回時⾃動恢復原來的訊號遮蔽字,這樣就保證了在處理某個訊號時,如果這種訊號再次產⽣,那麼 它會被阻塞到當前處理結束
為⽌。 如果在調⽤訊號處理函式時,除了當前訊號被⾃動遮蔽之外,還希望⾃動遮蔽另外⼀些訊號,則⽤samask欄位說明這些需要額外遮蔽的訊號,當訊號處理函式返回時⾃動恢復原來的訊號遮蔽字。
saflags欄位包含⼀些選項,saflags設為0 ,sasigaction是實時訊號的處理函式。
《4》pause
#include <unistd.h>
int pause(void);
pause函式使調⽤程序掛起直到有訊號遞達。如果訊號的處理動作是終⽌程序,則程序終 ⽌,pause函式沒有機會返回;如果訊號的處理動作是忽略,則程序繼續處於掛起狀態,pause不返回;如果訊號的處理動作是捕捉,則調
⽤了訊號處理函式之後pause返回-1,errno設定為EINTR, 所以pause只有錯的返回值(只有出錯返回值含有exec函式系列,程序函式替換)。錯誤碼EINTR表 ⽰“被訊號中斷”。
pause要有返回值則:1:收到訊號。2:自定義捕捉訊號。
下面我們用alarm和pause實現mysleep函式
思路
- main 函式呼叫mysleep函式,然後呼叫sigaction註冊SIGALRM訊號的處理函式sig_alrm.
- 呼叫alarm(nsecs)設為鬧鐘
- 呼叫pause等待,核心切換到別的程序執行
- nsecs秒後,鬧鐘超時,核心發SIGAKRM給這個程序
- 從核心態返回這個程序的使用者態之前處理未決訊號,發現有SIGALRMB訊號被自動遮蔽,從從sig_alrm函式返回時SIGALRM訊號⾃動解除遮蔽。然後⾃動執⾏系統調⽤sigreturn再次進⼊ 核心,再返回⽤戶態繼續執⾏程序的主控制流程(main函式調⽤的mysleep函式)。
- pause函式返回-1,然後調⽤alarm(0)取消鬧鐘,調⽤sigaction恢復SIGALRM訊號以前的處理動作。
實現程式碼:
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void sig_alrm(int signo)
{
}
unsigned int mysleep(unsigned int nsecs)
{
struct sigaction new,old;
unsigned int unslept = 0;
new.sa_handler = sig_alrm;
sigemptyset(&new.sa_mask);
new.sa_flags = 0;
sigaction(SIGALRM,&new,&old);//註冊訊號處理函式
alarm(nsecs);//設定鬧鐘
pause();
unslept = alarm(0);//清空鬧鐘
sigaction(SIGALRM,&old,NULL);//恢復訊號預設動作
return unslept;
}
int main()
{
while(1){
mysleep(1);
printf("1 seconds passed\n");
}
return 0;
}
2)可重入函式
《1》引入
我們以單鏈表的頭插為例:
main函式呼叫insert函式向一個連結串列head中插入結點node1,插入操作為兩步,剛做完第一步的時候,因為硬體中斷使程序切換到核心,再次回用戶態之前檢查到有訊號待處理,於是切換到sighandler函式,sighandler也呼叫insert函式向同⼀個連結串列head中插⼊節點node2,插⼊操作的 兩步都做完之後從sighandler返回核心態,再次回到⽤戶態就從main函式調⽤的insert函式中繼續 往下執⾏,先前做第⼀步之後被打斷,現在繼續做完第⼆步。結果是,main函式和sighandler先後 向連結串列中插⼊兩個節點,⽽最後只有⼀個節點真正插⼊連結串列中了。導致node2記憶體洩漏
。
《2》重入
上例中的insert函式被不同的控制流程呼叫,有可能在第一次呼叫還沒有返回時就再次進入該函式,這稱為重入。
《3》不可重入函式
上例中insert函式訪問一個全域性連結串列,有可能因為重入而造成錯亂,像這樣的函式稱為不可重入函式
不可重入函式的條件:
- 呼叫了malloc或free,因為malloc也是用全域性連結串列來管理堆的
- 呼叫了標準I/O庫函式。標準I/O庫的很多實現都以不可重入的方式使用全域性資料結構
《4》可重入函式
如果一個函式只訪問自己的區域性變數或引數則稱為可重入函式。
《5》多執行緒或多CPU程式設計要使用volatile關鍵字
對於程式中存在多個執⾏流程訪問同⼀全域性變數的情況,volatile限定符是必要的,此外,雖然程 序只有單⼀的執⾏流程,但是變數屬於以下情況之⼀的,也需要volatile限定:
1>變數的記憶體單元中的資料不需要寫操作就可以⾃⼰發⽣變化,每次讀上來的值都可能不⼀樣。
2>即使多次向變數的記憶體單元中寫資料,只寫不讀,也並不是在做⽆⽤功,⽽是有特殊意義的 。
什麼樣的記憶體單元會具有這樣的特性呢?肯定不是普通的記憶體,⽽是對映到記憶體地址空
間的硬體暫存器,例如串⼝的接收暫存器屬於上述第⼀種情況,⽽傳送暫存器屬於上述第⼆種情況。
3>.sig_ atomic_ t型別的變數應該總是加上volatile限定符,因為要使⽤sigatomict型別的理由也正 是要加volatile限定符的理由。
5.競態條件與sigsuspend函式
其實我們之前寫的mysleep函式還是有缺陷的,設想這樣的時序
問題:
- 註冊SIGALRM訊號的處理函式。
- 調⽤alarm(nsecs)設定鬧鐘。
- 核心排程優先順序更⾼的程序取代當前程序執⾏,並且優先順序更⾼的程序有很多個,每個都要 執⾏很⻓時間
- nsecs秒鐘之後鬧鐘超時了,核心傳送SIGALRM訊號給這個程序,處於未決狀態。
- 優先順序更⾼的程序執⾏完了,核心要排程回這個程序執⾏。SIGALRM訊號遞達,執⾏處理函 數sig_alrm之後再次進⼊核心。
- 返回這個程序的主控制流程,alarm(nsecs)返回,調⽤pause()掛起等待。
- 可是SIGALRM訊號已經處理完了,還等待什麼呢?
出現這個問題的根本原因是系統運⾏的時序(Timing)並不像我們寫程式時所設想的那樣。雖然alarm(nsecs)緊接著的下⼀⾏就是pause(),但是⽆法保證pause()⼀定會在調⽤alarm(nsecs)之 後的nsecs秒之內被調⽤。由
於非同步事件在任何時候都有可能發⽣(這⾥的非同步事件指出現更⾼優 先級的程序),如果我們寫程式時考慮不周密,就可能由於時序問題⽽導致錯誤,這叫做競態條件
解決方式:
1)
1. 遮蔽SIGALRM訊號
2. alarm(nsecs)
3. 解除對SIGALRM訊號的遮蔽
4. pause();
上述過程相對靠譜,但是第三步在解除訊號遮蔽的時候,也可能立刻對遞達。
2)
1. 遮蔽SIGALRM訊號;
2. alarm(nsecs);
3. pause();
4. 解除對SIGALRM訊號的遮蔽
這樣更不⾏了,還沒有解除遮蔽就調⽤pause,pause根本不可能等到SIGALRM訊號。要是“解除訊號遮蔽”和“掛起等待訊號”這兩步能合併成⼀個原⼦操作就好了,這正是sigsuspend函式
的功 能。sigsuspend包含了pause的掛起等待功能,同時解決了競態條件的問題,在對時序要求嚴格的場合下都應該調⽤sigsuspend⽽不是pause。
sigsuspend函式
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
和pause⼀樣,sigsuspend沒有成功返回值,只有執⾏了⼀個訊號處理函式之後sigsuspend才返回,返回值為-1,errno設定為EINTR。 調⽤sigsuspend時,程序的訊號遮蔽字由sigmask引數指定,可以通過指定sigmask來臨時解除對某 個訊號的遮蔽,然後掛起等待,當sigsuspend返回時,程序的訊號遮蔽字恢復為原來的值,如果原來對該訊號是遮蔽的從sigsuspend返回後仍然是遮蔽的。
接下來用sigsupend重新實現mysleep函式:
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void sig_alrm(int signo)
{
}
unsigned int mysleep(unsigned int nsecs)
{
struct sigaction new,old;
sigset_t newmask,oldmask,suspmask;
unsigned int unslept = 0;
new.sa_handler = sig_alrm;
sigemptyset(&new.sa_mask);
new.sa_flags = 0;
sigaction(SIGALRM,&new,&old);//註冊訊號處理函式
sigemptyset(&newmask);
sigaddset(&newmask,SIGALRM);
sigprocmask(SIG_BLOCK,&newmask,&oldmask);
alarm(nsecs);//設定鬧鐘
suspmask = oldmask ;
sigdelset(&suspmask,SIGALRM);
sigsuspend(&suspmask);
unslept = alarm(0);//清空鬧鐘
sigaction(SIGALRM,&old,NULL);//恢復訊號預設動作
sigprocmask(SIG_SETMASK,&oldmask,NULL);
return unslept;
}
int main()
{
while(1){
mysleep(1);
printf("1 seconds passed\n");
}
return 0;
}
如果在調⽤mysleep函式時SIGALRM訊號沒有遮蔽:
調⽤sigprocmask(SIG_ BLOCK, &newmask, &oldmask),遮蔽SIGALRM。
調⽤sigsuspend(&suspmask);解除對SIGALRM的遮蔽,然後掛起等待待。
SIGALRM遞達後suspend返回,⾃動恢復原來的遮蔽字,也就是再次遮蔽SIGALRM。
調⽤sigprocmask(SIG_ SETMASK, &oldmask, NULL);再次解除對SIGALRM的遮蔽。
6.SIGCHLD訊號
⽤wait和waitpid函式清理僵⼫程序,⽗程序可以阻塞等待⼦程序結束,也可以⾮阻 塞地查詢是否有⼦程序結束等待清理(也就是輪詢的⽅式)。採⽤第⼀種⽅式,⽗程序阻塞了就不 能處理⾃⼰的⼯作了;採⽤第⼆種⽅式,⽗程序在處理⾃⼰的⼯作的同時還要記得時不時地輪詢⼀ 下,程式實現複雜。
其實,⼦程序在終⽌時會給⽗程序發SIGCHLD訊號,該訊號的預設處理動作是忽略,⽗程序可以⾃ 定義SIGCHLD訊號的處理函式,這樣⽗程序只需專⼼處理⾃⼰的⼯作,不必關⼼⼦程序了,⼦程序 終⽌時會通知⽗程序,⽗程序在訊號處理函式中調⽤wait清理⼦程序即可。
請編寫⼀個程式完成以下功能:⽗程序fork出⼦程序,⼦程序調⽤exit(2)終⽌,⽗程序⾃定 義SIGCHLD訊號的處理函式,在其中調⽤wait獲得⼦程序的退出狀態並列印。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
pid_t id;
while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
printf("wait child success: %d\n", id);
}
printf("child is quit! %d\n", getpid());
}
int main()
{
signal(SIGCHLD, handler);
pid_t cid;
if((cid = fork()) == 0){//child
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while(1){
printf("father proc is doing some thing!\n");
sleep(1);
}
return 0;
}