進程間通信之:信號
信號是UNIX中所使用的進程通信的一種最古老的方法。它是在軟件層次上對中斷機制的一種模擬,是一種異步通信方式。信號可以直接進行用戶空間進程和內(nèi)核進程之間的交互,內(nèi)核進程也可以利用它來通知用戶空間進程發(fā)生了哪些系統(tǒng)事件。它可以在任何時候發(fā)給某一進程,而無需知道該進程的狀態(tài)。如果該進程當前并未處于執(zhí)行態(tài),則該信號就由內(nèi)核保存起來,直到該進程恢復(fù)執(zhí)行再傳遞給它為止;如果一個信號被進程設(shè)置為阻塞,則該信號的傳遞被延遲,直到其阻塞被取消時才被傳遞給進程。
在第2章kill命令中曾講解到“−l”選項,這個選項可以列出該系統(tǒng)所支持的所有信號的列表。在筆者的系統(tǒng)中,信號值在32之前的則有不同的名稱,而信號值在32以后的都是用“SIGRTMIN”或“SIGRTMAX”開頭的,這就是兩類典型的信號。前者是從UNIX系統(tǒng)中繼承下來的信號,為不可靠信號(也稱為非實時信號);后者是為了解決前面“不可靠信號”的問題而進行了更改和擴充的信號,稱為“可靠信號”(也稱為實時信號)。那么為什么之前的信號不可靠呢?這里首先要介紹一下信號的生命周期。
一個完整的信號生命周期可以分為3個重要階段,這3個階段由4個重要事件來刻畫的:信號產(chǎn)生、信號在進程中注冊、信號在進程中注銷、執(zhí)行信號處理函數(shù),如圖8.6所示。相鄰兩個事件的時間間隔構(gòu)成信號生命周期的一個階段。要注意這里的信號處理有多種方式,一般是由內(nèi)核完成的,當然也可以由用戶進程來完成,故在此沒有明確畫出。
圖8.6信號生命周期
一個不可靠信號的處理過程是這樣的:如果發(fā)現(xiàn)該信號已經(jīng)在進程中注冊,那么就忽略該信號。因此,若前一個信號還未注銷又產(chǎn)生了相同的信號就會產(chǎn)生信號丟失。而當可靠信號發(fā)送給一個進程時,不管該信號是否已經(jīng)在進程中注冊,都會被再注冊一次,因此信號就不會丟失。所有可靠信號都支持排隊,而所有不可靠信號都不支持排隊。
注意
這里信號的產(chǎn)生、注冊和注銷等是指信號的內(nèi)部實現(xiàn)機制,而不是調(diào)用信號的函數(shù)實現(xiàn)。因此,信號注冊與否,與本節(jié)后面講到的發(fā)送信號函數(shù)(如kill()等)以及信號安裝函數(shù)(如signal()等)無關(guān),只與信號值有關(guān)。
用戶進程對信號的響應(yīng)可以有3種方式。
n 忽略信號,即對信號不做任何處理,但是有兩個信號不能忽略,即SIGKILL及SIGSTOP。
n 捕捉信號,定義信號處理函數(shù),當信號發(fā)生時,執(zhí)行相應(yīng)的自定義處理函數(shù)。
n 執(zhí)行缺省操作,Linux對每種信號都規(guī)定了默認操作。
Linux中的大多數(shù)信號是提供給內(nèi)核的,表8.6列出了Linux中最為常見信號的含義及其默認操作。
表8.6 常見信號的含義及其默認操作
信號名
含義
默認操作
SIGHUP
該信號在用戶終端連接(正?;蚍钦#┙Y(jié)束時發(fā)出,通常是在終端的控制進程結(jié)束時,通知同一會話內(nèi)的各個作業(yè)與控制終端不再關(guān)聯(lián)
終止
SIGINT
該信號在用戶鍵入INTR字符(通常是Ctrl-C)時發(fā)出,終端驅(qū)動程序發(fā)送此信號并送到前臺進程中的每一個進程
終止
SIGQUIT
該信號和SIGINT類似,但由QUIT字符(通常是Ctrl-)來控制
終止
SIGILL
該信號在一個進程企圖執(zhí)行一條非法指令時(可執(zhí)行文件本身出現(xiàn)錯誤,或者試圖執(zhí)行數(shù)據(jù)段、堆棧溢出時)發(fā)出
終止
SIGFPE
該信號在發(fā)生致命的算術(shù)運算錯誤時發(fā)出。這里不僅包括浮點運算錯誤,還包括溢出及除數(shù)為0等其他所有的算術(shù)錯誤
終止
SIGKILL
該信號用來立即結(jié)束程序的運行,并且不能被阻塞、處理或忽略
終止
SIGALRM
該信號當一個定時器到時的時候發(fā)出
終止
SIGSTOP
該信號用于暫停一個進程,且不能被阻塞、處理或忽略
暫停進程
SIGTSTP
該信號用于交互停止進程,用戶鍵入SUSP字符時(通常是Ctrl+Z)發(fā)出這個信號
停止進程
SIGCHLD
子進程改變狀態(tài)時,父進程會收到這個信號
忽略
SIGABORT
進程異常終止時發(fā)出
8.3.2信號發(fā)送與捕捉發(fā)送信號的函數(shù)主要有kill()、raise()、alarm()以及pause(),下面就依次對其進行介紹。
1.kill()和raise()(1)函數(shù)說明。
kill()函數(shù)同讀者熟知的kill系統(tǒng)命令一樣,可以發(fā)送信號給進程或進程組(實際上,kill系統(tǒng)命令只是kill()函數(shù)的一個用戶接口)。這里需要注意的是,它不僅可以中止進程(實際上發(fā)出SIGKILL信號),也可以向進程發(fā)送其他信號。
與kill()函數(shù)所不同的是,raise()函數(shù)允許進程向自身發(fā)送信號。
(2)函數(shù)格式。
表8.7列出了kill()函數(shù)的語法要點。
表8.7 kill()函數(shù)語法要點
所需頭文件
#include<signal.h>
#include<sys/types.h>
函數(shù)原型
intkill(pid_tpid,intsig)
函數(shù)傳入值
pid:
正數(shù):要發(fā)送信號的進程號
0:信號被發(fā)送到所有和當前進程在同一個進程組的進程
-1:信號發(fā)給所有的進程表中的進程(除了進程號最大的進程外)
<-1:信號發(fā)送給進程組號為-pid的每一個進程
sig:信號
函數(shù)返回值
成功:0
出錯:-1
表8.8列出了raise()函數(shù)的語法要點。
表8.8 raise()函數(shù)語法要點
所需頭文件
#include<signal.h>
#include<sys/types.h>
函數(shù)原型
intraise(intsig)
函數(shù)傳入值
sig:信號
函數(shù)返回值
成功:0
出錯:-1
(3)函數(shù)實例。
下面這個示例首先使用fork()創(chuàng)建了一個子進程,接著為了保證子進程不在父進程調(diào)用kill()之前退出,在子進程中使用raise()函數(shù)向自身發(fā)送SIGSTOP信號,使子進程暫停。接下來再在父進程中調(diào)用kill()向子進程發(fā)送信號,在該示例中使用的是SIGKILL,讀者可以使用其他信號進行練習(xí)。
/*kill_raise.c*/
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
#include<sys/types.h>
#include<sys/wait.h>
intmain()
{
pid_tpid;
intret;
/*創(chuàng)建一子進程*/
if((pid=fork())<0)
{
printf("Forkerrorn");
exit(1);
}
if(pid==0)
{
/*在子進程中使用raise()函數(shù)發(fā)出SIGSTOP信號,使子進程暫停*/
printf("Child(pid:%d)iswaitingforanysignaln",getpid());
raise(SIGSTOP);
exit(0);
}
else
{
/*在父進程中收集子進程發(fā)出的信號,并調(diào)用kill()函數(shù)進行相應(yīng)的操作*/
if((waitpid(pid,NULL,WNOHANG))==0)
{
if((ret=kill(pid,SIGKILL))==0)
{
printf("Parentkill%dn",pid);
}
}
waitpid(pid,NULL,0);
exit(0);
}
}
該程序運行結(jié)果如下所示:
$./kill_raise
Child(pid:4877)iswaitingforanysignal
Parentkill4877
2.a(chǎn)larm()和pause()(1)函數(shù)說明。
alarm()也稱為鬧鐘函數(shù),它可以在進程中設(shè)置一個定時器,當定時器指定的時間到時,它就向進程發(fā)送SIGALARM信號。要注意的是,一個進程只能有一個鬧鐘時間,如果在調(diào)用alarm()之前已設(shè)置過鬧鐘時間,則任何以前的鬧鐘時間都被新值所代替。
pause()函數(shù)是用于將調(diào)用進程掛起直至捕捉到信號為止。這個函數(shù)很常用,通常可以用于判斷信號是否已到。
(2)函數(shù)格式。
表8.9列出了alarm()函數(shù)的語法要點。
表8.9 alarm()函數(shù)語法要點
所需頭文件
#include<unistd.h>
函數(shù)原型
unsignedintalarm(unsignedintseconds)
函數(shù)傳入值
seconds:指定秒數(shù),系統(tǒng)經(jīng)過seconds秒之后向該進程發(fā)送SIGALRM信號
函數(shù)返回值
成功:如果調(diào)用此alarm()前,進程中已經(jīng)設(shè)置了鬧鐘時間,則返回上一個鬧鐘時間的剩余時間,否則返回0
出錯:-1
表8.10列出了pause()函數(shù)的語法要點。
表8.10 pause()函數(shù)語法要點
所需頭文件
#include<unistd.h>
函數(shù)原型
intpause(void)
函數(shù)返回值
-1,并且把error值設(shè)為EINTR
(3)函數(shù)實例。
該實例實際上已完成了一個簡單的sleep()函數(shù)的功能,由于SIGALARM默認的系統(tǒng)動作為終止該進程,因此程序在打印信息之前,就會被結(jié)束了。代碼如下所示:
/*alarm_pause.c*/
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
intmain()
{
/*調(diào)用alarm定時器函數(shù)*/
intret=alarm(5);
pause();
printf("Ihavebeenwakenup.n",ret);/*此語句不會被執(zhí)行*/
}
$./alarm_pause
Alarmclock
想一想
用這種形式實現(xiàn)的sleep()功能有什么問題?
8.3.3信號的處理在了解了信號的產(chǎn)生與捕獲之后,接下來就要對信號進行具體的操作了。從前面的信號概述中讀者也可以看到,特定的信號是與一定的進程相聯(lián)系的。也就是說,一個進程可以決定在該進程中需要對哪些信號進行什么樣的處理。例如,一個進程可以選擇忽略某些信號而只處理其他一些信號,另外,一個進程還可以選擇如何處理信號??傊?,這些都是與特定的進程相聯(lián)系的。因此,首先就要建立進程與其信號之間的對應(yīng)關(guān)系,這就是信號的處理。
注意
請讀者注意信號的注冊與信號的處理之間的區(qū)別,前者信號是主動方,而后者進程是主動方。信號的注冊是在進程選擇了特定信號處理之后特定信號的主動行為。
信號處理的主要方法有兩種,一種是使用簡單的signal()函數(shù),另一種是使用信號集函數(shù)組。下面分別介紹這兩種處理方式。
1.信號處理函數(shù)(1)函數(shù)說明。
使用signal()函數(shù)處理時,只需要指出要處理的信號和處理函數(shù)即可。它主要是用于前32種非實時信號的處理,不支持信號傳遞信息,但是由于使用簡單、易于理解,因此也受到很多程序員的歡迎。
Linux還支持一個更健壯、更新的信號處理函數(shù)sigaction(),推薦使用該函數(shù)。
(2)函數(shù)格式。
signal()函數(shù)的語法要點如表8.11所示。
表8.11 signal()函數(shù)語法要點
所需頭文件
#include<signal.h>
函數(shù)原型
void(*signal(intsignum,void(*handler)(int)))(int)
函數(shù)傳入值
signum:指定信號代碼
handler:
SIG_IGN:忽略該信號
SIG_DFL:采用系統(tǒng)默認方式處理信號
自定義的信號處理函數(shù)指針
函數(shù)返回值
成功:以前的信號處理配置
出錯:-1
這里需要對這個函數(shù)原型進行說明。這個函數(shù)原型有點復(fù)雜??上扔萌缦碌膖ypedef進行替換說明:
typedefvoidsign(int);
sign*signal(int,handler*);
可見,首先該函數(shù)原型整體指向一個無返回值并且?guī)б粋€整型參數(shù)的函數(shù)指針,也就是信號的原始配置函數(shù)。接著該原型又帶有兩個參數(shù),其中的第二個參數(shù)可以是用戶自定義的信號處理函數(shù)的函數(shù)指針。
表8.12列舉了sigaction()的語法要點。
表8.12 sigaction()函數(shù)語法要點
所需頭文件
#include<signal.h>
函數(shù)原型
intsigaction(intsignum,conststructsigaction*act,structsigaction*oldact)
函數(shù)傳入值
signum:信號代碼,可以為除SIGKILL及SIGSTOP外的任何一個特定有效的信號
act:指向結(jié)構(gòu)sigaction的一個實例的指針,指定對特定信號的處理
oldact:保存原來對相應(yīng)信號的處理
函數(shù)返回值
成功:0
出錯:-1
這里要說明的是sigaction()函數(shù)中第2個和第3個參數(shù)用到的sigaction結(jié)構(gòu)。這是一個看似非常復(fù)雜的結(jié)構(gòu),希望讀者能夠慢慢閱讀此段內(nèi)容。
首先給出了sigaction的定義,如下所示:
structsigaction
{
void(*sa_handler)(intsigno);
sigset_tsa_mask;
intsa_flags;
void(*sa_restore)(void);
}
sa_handler是一個函數(shù)指針,指定信號處理函數(shù),這里除可以是用戶自定義的處理函數(shù)外,還可以為SIG_DFL(采用缺省的處理方式)或SIG_IGN(忽略信號)。它的處理函數(shù)只有一個參數(shù),即信號值。
sa_mask是一個信號集,它可以指定在信號處理程序執(zhí)行過程中哪些信號應(yīng)當被屏蔽,在調(diào)用信號捕獲函數(shù)之前,該信號集要加入到信號的信號屏蔽字中。
sa_flags中包含了許多標志位,是對信號進行處理的各個選擇項。它的常見可選值如表8.13所示。
表8.13 常見信號的含義及其默認操作
選項
含義
SA_NODEFERSA_NOMASK
當捕捉到此信號時,在執(zhí)行其信號捕捉函數(shù)時,系統(tǒng)不會自動屏蔽此信號
SA_NOCLDSTOP
進程忽略子進程產(chǎn)生的任何SIGSTOP、SIGTSTP、SIGTTIN和SIGTTOU信號
SA_RESTART
令重啟的系統(tǒng)調(diào)用起作用
SA_ONESHOTSA_RESETHAND
自定義信號只執(zhí)行一次,在執(zhí)行完畢后恢復(fù)信號的系統(tǒng)默認動作
(3)使用實例。
第一個實例表明了如何使用signal()函數(shù)捕捉相應(yīng)信號,并做出給定的處理。這里,my_func就是信號處理的函數(shù)指針。讀者還可以將其改為SIG_IGN或SIG_DFL查看運行結(jié)果。第二個實例是用sigaction()函數(shù)實現(xiàn)同樣的功能。
以下是使用signal()函數(shù)的示例:
/*signal.c*/
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
/*自定義信號處理函數(shù)*/
voidmy_func(intsign_no)
{
if(sign_no==SIGINT)
{
printf("IhavegetSIGINTn");
}
elseif(sign_no==SIGQUIT)
{
printf("IhavegetSIGQUITn");
}
}
intmain()
{
printf("WaitingforsignalSIGINTorSIGQUIT...n");
/*發(fā)出相應(yīng)的信號,并跳轉(zhuǎn)到信號處理函數(shù)處*/
signal(SIGINT,my_func);
signal(SIGQUIT,my_func);
pause();
exit(0);
}
運行結(jié)果如下所示。
$./signal
WaitingforsignalSIGINTorSIGQUIT...
IhavegetSIGINT(按ctrl-c組合鍵)
$./signal
WaitingforsignalSIGINTorSIGQUIT...
IhavegetSIGQUIT(按ctrl-組合鍵)
以下是用sigaction()函數(shù)實現(xiàn)同樣的功能,下面只列出更新的main()函數(shù)部分。
/*sigaction.c*/
/*前部分省略*/
intmain()
{
structsigactionaction;
printf("WaitingforsignalSIGINTorSIGQUIT...n");
/*sigaction結(jié)構(gòu)初始化*/
action.sa_handler=my_func;
sigemptyset(&action.sa_mask);
action.sa_flags=0;
/*發(fā)出相應(yīng)的信號,并跳轉(zhuǎn)到信號處理函數(shù)處*/
sigaction(SIGINT,&action,0);
sigaction(SIGQUIT,&action,0);
pause();
exit(0);
}
2.信號集函數(shù)組(1)函數(shù)說明。
使用信號集函數(shù)組處理信號時涉及一系列的函數(shù),這些函數(shù)按照調(diào)用的先后次序可分為以下幾大功能模塊:創(chuàng)建信號集合、注冊信號處理函數(shù)以及檢測信號。
其中,創(chuàng)建信號集合主要用于處理用戶感興趣的一些信號,其函數(shù)包括以下幾個。
n sigemptyset():將信號集合初始化為空。
n sigfillset():將信號集合初始化為包含所有已定義的信號的集合。
n sigaddset():將指定信號加入到信號集合中去。
n sigdelset():將指定信號從信號集合中刪除。
n sigismember():查詢指定信號是否在信號集合之中。
注冊信號處理函數(shù)主要用于決定進程如何處理信號。這里要注意的是,信號集里的信號并不是真正可以處理的信號,只有當信號的狀態(tài)處于非阻塞狀態(tài)時才會真正起作用。因此,首先使用sigprocmask()函數(shù)檢測并更改信號屏蔽字(信號屏蔽字是用來指定當前被阻塞的一組信號,它們不會被進程接收),然后使用sigaction()函數(shù)來定義進程接收到特定信號之后的行為。檢測信號是信號處理的后續(xù)步驟,因為被阻塞的信號不會傳遞給進程,所以這些信號就處于“未處理”狀態(tài)(也就是進程不清楚它的存在)。sigpending()函數(shù)允許進程檢測“未處理”信號,并進一步?jīng)Q定對它們作何處理。
(2)函數(shù)格式。
首先介紹創(chuàng)建信號集合的函數(shù)格式,表8.14列舉了這一組函數(shù)的語法要點。
表8.14 創(chuàng)建信號集合函數(shù)語法要點
所需頭文件
#include<signal.h>
函數(shù)原型
intsigemptyset(sigset_t*set)
intsigfillset(sigset_t*set)
intsigaddset(sigset_t*set,intsignum)
intsigdelset(sigset_t*set,intsignum)
intsigismember(sigset_t*set,intsignum)
函數(shù)傳入值
set:信號集
signum:指定信號代碼
函數(shù)返回值
成功:0(sigismember成功返回1,失敗返回0)
出錯:-1
表8.15列舉了sigprocmask的語法要點。
表8.15 sigprocmask函數(shù)語法要點
所需頭文件
#include<signal.h>
函數(shù)原型
intsigprocmask(inthow,constsigset_t*set,sigset_t*oset)
函數(shù)傳入值
how:決定函數(shù)的操作方式
SIG_BLOCK:增加一個信號集合到當前進程的阻塞集合之中
SIG_UNBLOCK:從當前的阻塞集合之中刪除一個信號集合
SIG_SETMASK:將當前的信號集合設(shè)置為信號阻塞集合
set:指定信號集
oset:信號屏蔽字
函數(shù)返回值
成功:0
出錯:-1
此處,若set是一個非空指針,則參數(shù)how表示函數(shù)的操作方式;若how為空,則表示忽略此操作。
最后,表8.16列舉了sigpending函數(shù)的語法要點。
表8.16 sigpending函數(shù)語法要點
所需頭文件
#include<signal.h>
函數(shù)原型
intsigpending(sigset_t*set)
函數(shù)傳入值
set:要檢測的信號集
函數(shù)返回值
成功:0
出錯:-1
總之,在處理信號時,一般遵循如圖8.7所示的操作流程。
圖8.7一般的信號操作處理流程
(3)使用實例。
該實例首先把SIGQUIT、SIGINT兩個信號加入信號集,然后將該信號集合設(shè)為阻塞狀態(tài),并進入用戶輸入狀態(tài)。用戶只需按任意鍵,就可以立刻將信號集合設(shè)置為非阻塞狀態(tài),再對這兩個信號分別操作,其中SIGQUIT執(zhí)行默認操作,而SIGINT執(zhí)行用戶自定義函數(shù)的操作。源代碼如下所示:
/*sigset.c*/
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
/*自定義的信號處理函數(shù)*/
voidmy_func(intsignum)
{
printf("Ifyouwanttoquit,pleasetrySIGQUITn");
}
intmain()
{
sigset_tset,pendset;
structsigactionaction1,action2;
/*初始化信號集為空*/
if(sigemptyset(&set)<0)
{
perror("sigemptyset");
exit(1);
}
/*將相應(yīng)的信號加入信號集*/
if(sigaddset(&set,SIGQUIT)<0)
{
perror("sigaddset");
exit(1);
}
if(sigaddset(&set,SIGINT)<0)
{
perror("sigaddset");
exit(1);
}
if(sigismember(&set,SIGINT))
{
sigemptyset(&action1.sa_mask);
action1.sa_handler=my_func;
action1.sa_flags=0;
sigaction(SIGINT,&action1,NULL);
}
if(sigismember(&set,SIGQUIT))
{
sigemptyset(&action2.sa_mask);
action2.sa_handler=SIG_DFL;
action2.sa_flags=0;
sigaction(SIGQUIT,&action2,NULL);
}
/*設(shè)置信號集屏蔽字,此時set中的信號不會被傳遞給進程,暫時進入待處理狀態(tài)*/
if(sigprocmask(SIG_BLOCK,&set,NULL)<0)
{
perror("sigprocmask");
exit(1);
}
else
{
printf("Signalsetwasblocked,Pressanykey!");
getchar();
}
/*在信號屏蔽字中刪除set中的信號*/
if(sigprocmask(SIG_UNBLOCK,&set,NULL)<0)
{
perror("sigprocmask");
exit(1);
}
else
{
printf("Signalsetisinunblockstaten");
}
while(1);
exit(0);
}
該程序的運行結(jié)果如下所示,可以看見,在信號處于阻塞狀態(tài)時,所發(fā)出的信號對進程不起作用,并且該信號進入待處理狀態(tài)。讀者輸入任意鍵,并且信號脫離了阻塞狀態(tài)之后,用戶發(fā)出的信號才能正常運行。這里SIGINT已按照用戶自定義的函數(shù)運行,請讀者注意阻塞狀態(tài)下SIGINT的處理和非阻塞狀態(tài)下SIGINT的處理有何不同。
$./sigset
Signalsetwasblocked,Pressanykey!/*此時按任何鍵可以解除阻塞屏蔽字*/
Ifyouwanttoquit,pleasetrySIGQUIT/*阻塞狀態(tài)下SIGINT的處理*/
Signalsetisinunblockstate/*從信號屏蔽字中刪除set中的信號*/
Ifyouwanttoquit,pleasetrySIGQUIT/*非阻塞狀態(tài)下SIGINT的處理*/
Ifyouwanttoquit,pleasetrySIGQUIT
Quit/*非阻塞狀態(tài)下SIGQUIT處理*/