Linux進(jìn)程間通信--使用信號(hào)
一、什么是信號(hào)
用過Windows的我們都知道,當(dāng)我們無法正常結(jié)束一個(gè)程序時(shí),可以用任務(wù)管理器強(qiáng)制結(jié)束這個(gè)進(jìn)程,但這其實(shí)是怎么實(shí)現(xiàn)的呢?同樣的功能在Linux上是通過生成信號(hào)和捕獲信號(hào)來實(shí)現(xiàn)的,運(yùn)行中的進(jìn)程捕獲到這個(gè)信號(hào)然后作出一定的操作并最終被終止。
信號(hào)是UNIX和Linux系統(tǒng)響應(yīng)某些條件而產(chǎn)生的一個(gè)事件,接收到該信號(hào)的進(jìn)程會(huì)相應(yīng)地采取一些行動(dòng)。通常信號(hào)是由一個(gè)錯(cuò)誤產(chǎn)生的。但它們還可以作為進(jìn)程間通信或修改行為的一種方式,明確地由一個(gè)進(jìn)程發(fā)送給另一個(gè)進(jìn)程。一個(gè)信號(hào)的產(chǎn)生叫生成,接收到一個(gè)信號(hào)叫捕獲。
二、信號(hào)的種類
信號(hào)的名稱是在頭文件signal.h中定義的,信號(hào)都以SIG開頭,常用的信號(hào)并不多,常用的信號(hào)如下:
更多的信號(hào)類型可查看附錄表。
三、信號(hào)的處理——signal函數(shù)
程序可用使用signal函數(shù)來處理指定的信號(hào),主要通過忽略和恢復(fù)其默認(rèn)行為來工作。signal函數(shù)的原型如下:
#include <signal.h> void (*signal(int sig, void (*func)(int)))(int);
這是一個(gè)相當(dāng)復(fù)雜的聲明,耐心點(diǎn)看可以知道signal是一個(gè)帶有sig和func兩個(gè)參數(shù)的函數(shù),func是一個(gè)類型為void (*)(int)的函數(shù)指針。該函數(shù)返回一個(gè)與func相同類型的指針,指向先前指定信號(hào)處理函數(shù)的函數(shù)指針。準(zhǔn)備捕獲的信號(hào)的參數(shù)由sig給出,接收到的指定信號(hào)后要調(diào)用的函數(shù)由參數(shù)func給出。其實(shí)這個(gè)函數(shù)的使用是相當(dāng)簡單的,通過下面的例子就可以知道。注意信號(hào)處理函數(shù)的原型必須為void func(int),或者是下面的特殊值:
SIG_IGN:忽略信號(hào)
SIG_DFL:恢復(fù)信號(hào)的默認(rèn)行為
說了這么多,還是給出一個(gè)例子來說明一下吧,源文件名為signal1.c,代碼如下:
#include <signal.h> #include <stdio.h> #include <unistd.h> void ouch(int sig) { printf("\nOUCH! - I got signal %d\n", sig); //恢復(fù)終端中斷信號(hào)SIGINT的默認(rèn)行為 (void) signal(SIGINT, SIG_DFL); } int main() { //改變終端中斷信號(hào)SIGINT的默認(rèn)行為,使之執(zhí)行ouch函數(shù) //而不是終止程序的執(zhí)行 (void) signal(SIGINT, ouch); while(1) { printf("Hello World!\n"); sleep(1); } return 0; }
運(yùn)行結(jié)果如下:
可以看到,第一次按下終止命令(ctrl+c)時(shí),進(jìn)程并沒有被終止,面是輸出OUCH! - I got signal 2,因?yàn)镾IGINT的默認(rèn)行為被signal函數(shù)改變了,當(dāng)進(jìn)程接受到信號(hào)SIGINT時(shí),它就去調(diào)用函數(shù)ouch去處理,注意ouch函數(shù)把信號(hào)SIGINT的處理方式改變成默認(rèn)的方式,所以當(dāng)你再按一次ctrl+c時(shí),進(jìn)程就像之前那樣被終止了。
四、信號(hào)處理——sigaction函數(shù)
前面我們看到了signal函數(shù)對(duì)信號(hào)的處理,但是一般情況下我們可以使用一個(gè)更加健壯的信號(hào)接口——sigaction函數(shù)。它的原型為:
#include <signal.h> int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
該函數(shù)與signal函數(shù)一樣,用于設(shè)置與信號(hào)sig關(guān)聯(lián)的動(dòng)作,而oact如果不是空指針的話,就用它來保存原先對(duì)該信號(hào)的動(dòng)作的位置,act則用于設(shè)置指定信號(hào)的動(dòng)作。
sigaction結(jié)構(gòu)體定義在signal.h中,但是它至少包括以下成員:
void (*) (int) sa_handler;處理函數(shù)指針,相當(dāng)于signal函數(shù)的func參數(shù)。
sigset_t sa_mask; 指定一個(gè)。信號(hào)集,在調(diào)用sa_handler所指向的信號(hào)處理函數(shù)之前,該信號(hào)集將被加入到進(jìn)程的信號(hào)屏蔽字中。信號(hào)屏蔽字是指當(dāng)前被阻塞的一組信號(hào),它們不能被當(dāng)前進(jìn)程接收到
int sa_flags;信號(hào)處理修改器;
sa_mask的值通常是通過使用信號(hào)集函數(shù)來設(shè)置的,關(guān)于信號(hào)集函數(shù),我將會(huì)在我的下一篇文章——Linux進(jìn)程間通信——信號(hào)集函數(shù),詳細(xì)講述。
sa_flags,通常可以取以下的值:
此外,現(xiàn)在有一個(gè)這樣的問題,我們使用signal或sigaction函數(shù)來指定處理信號(hào)的函數(shù),但是如果這個(gè)信號(hào)處理函數(shù)建立之前就接收到要處理的信號(hào)的話,進(jìn)程會(huì)有怎樣的反應(yīng)呢?它就不會(huì)像我們想像的那樣用我們設(shè)定的處理函數(shù)來處理了。sa_mask就可以解決這樣的問題,sa_mask指定了一個(gè)信號(hào)集,在調(diào)用sa_handler所指向的信號(hào)處理函數(shù)之前,該信號(hào)集將被加入到進(jìn)程的信號(hào)屏蔽字中,設(shè)置信號(hào)屏蔽字可以防止信號(hào)在它的處理函數(shù)還未運(yùn)行結(jié)束時(shí)就被接收到的情況,即使用sa_mask字段可以消除這一競態(tài)條件。
承接上面的例子,下面給出用sigaction函數(shù)重寫的例子代碼,源文件為signal2.c,代碼如下:
#include <unistd.h> #include <stdio.h> #include <signal.h> void ouch(int sig) { printf("\nOUCH! - I got signal %d\n", sig); } int main() { struct sigaction act; act.sa_handler = ouch; //創(chuàng)建空的信號(hào)屏蔽字,即不屏蔽任何信息 sigemptyset(&act.sa_mask); //使sigaction函數(shù)重置為默認(rèn)行為 act.sa_flags = SA_RESETHAND; sigaction(SIGINT, &act, 0); while(1) { printf("Hello World!\n"); sleep(1); } return 0; }
運(yùn)行結(jié)果與前一個(gè)例子中的相同。注意sigaction函數(shù)在默認(rèn)情況下是不被重置的,如果要想它重置,則sa_flags就要為SA_RESETHAND。
五、發(fā)送信號(hào)
上面說到的函數(shù)都是一些進(jìn)程接收到一個(gè)信號(hào)之后怎么對(duì)這個(gè)信號(hào)作出反應(yīng),即信號(hào)的處理的問題,有沒有什么函數(shù)可以向一個(gè)進(jìn)程主動(dòng)地發(fā)出一個(gè)信號(hào)呢?我們可以通過兩個(gè)函數(shù)kill和alarm來發(fā)送一個(gè)信號(hào)。
1、kill函數(shù)
先來看看kill函數(shù),進(jìn)程可以通過kill函數(shù)向包括它本身在內(nèi)的其他進(jìn)程發(fā)送一個(gè)信號(hào),如果程序沒有發(fā)送這個(gè)信號(hào)的權(quán)限,對(duì)kill函數(shù)的調(diào)用就將失敗,而失敗的常見原因是目標(biāo)進(jìn)程由另一個(gè)用戶所擁有。想一想也是容易明白的,你總不能控制別人的程序吧,當(dāng)然超級(jí)用戶root,這種上帝般的存在就除外了。
kill函數(shù)的原型為:
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig);
它的作用把信號(hào)sig發(fā)送給進(jìn)程號(hào)為pid的進(jìn)程,成功時(shí)返回0。
kill調(diào)用失敗返回-1,調(diào)用失敗通常有三大原因:
1、給定的信號(hào)無效(errno = EINVAL)
2、發(fā)送權(quán)限不夠( errno = EPERM )
3、目標(biāo)進(jìn)程不存在( errno = ESRCH )
2、alarm函數(shù)
這個(gè)函數(shù)跟它的名字一樣,給我們提供了一個(gè)鬧鐘的功能,進(jìn)程可以調(diào)用alarm函數(shù)在經(jīng)過預(yù)定時(shí)間后向發(fā)送一個(gè)SIGALRM信號(hào)。
alarm函數(shù)的型如下:
#include <unistd.h> unsigned int alarm(unsigned int seconds);
alarm函數(shù)用來在seconds秒之后安排發(fā)送一個(gè)SIGALRM信號(hào),如果seconds為0,將取消所有已設(shè)置的鬧鐘請求。alarm函數(shù)的返回值是以前設(shè)置的鬧鐘時(shí)間的余留秒數(shù),如果返回失敗返回-1。
馬不停蹄,下面就給合fork、sleep和signal函數(shù),用一個(gè)例子來說明kill函數(shù)的用法吧,源文件為signal3.c,代碼如下:
#include <unistd.h> #include <sys/types.h> #include <stdlib.h> #include <stdio.h> #include <signal.h> static int alarm_fired = 0; void ouch(int sig) { alarm_fired = 1; } int main() { pid_t pid; pid = fork(); switch(pid) { case -1: perror("fork failed\n"); exit(1); case 0: //子進(jìn)程 sleep(5); //向父進(jìn)程發(fā)送信號(hào) kill(getppid(), SIGALRM); exit(0); default:; } //設(shè)置處理函數(shù) signal(SIGALRM, ouch); while(!alarm_fired) { printf("Hello World!\n"); sleep(1); } if(alarm_fired) printf("\nI got a signal %d\n", SIGALRM); exit(0); }
運(yùn)行結(jié)果如下:
在代碼中我們使用fork調(diào)用復(fù)制了一個(gè)新進(jìn)程,在子進(jìn)程中,5秒后向父進(jìn)程中發(fā)送一個(gè)SIGALRM信號(hào),父進(jìn)程中捕獲這個(gè)信號(hào),并用ouch函數(shù)來處理,變改alarm_fired的值,然后退出循環(huán)。從結(jié)果中我們也可以看到輸出了5個(gè)Hello World!之后,程序就收到一個(gè)SIGARLM信號(hào),然后結(jié)束了進(jìn)程。
注:如果父進(jìn)程在子進(jìn)程的信號(hào)到來之前沒有事情可做,我們可以用函數(shù)pause()來掛起父進(jìn)程,直到父進(jìn)程接收到信號(hào)。當(dāng)進(jìn)程接收到一個(gè)信號(hào)時(shí),預(yù)設(shè)好的信號(hào)處理函數(shù)將開始運(yùn)行,程序也將恢復(fù)正常的執(zhí)行。這樣可以節(jié)省CPU的資源,因?yàn)榭梢员苊馐褂靡粋€(gè)循環(huán)來等待。以本例子為例,則可以把while循環(huán)改為一句pause();
下面再以一個(gè)小小的例子來說明alarm函數(shù)和pause函數(shù)的用法吧,源文件名為,signal4.c,代碼如下:
#include <unistd.h> #include <sys/types.h> #include <stdlib.h> #include <stdio.h> #include <signal.h> static int alarm_fired = 0; void ouch(int sig) { alarm_fired = 1; } int main() { //關(guān)聯(lián)信號(hào)處理函數(shù) signal(SIGALRM, ouch); //調(diào)用alarm函數(shù),5秒后發(fā)送信號(hào)SIGALRM alarm(5); //掛起進(jìn)程 pause(); //接收到信號(hào)后,恢復(fù)正常執(zhí)行 if(alarm_fired == 1) printf("Receive a signal %d\n", SIGALRM); exit(0); }
運(yùn)行結(jié)果如下:
進(jìn)程在5秒后接收到一個(gè)SIGALRM,進(jìn)程恢復(fù)運(yùn)行,打印信息并退出。
六、信號(hào)處理函數(shù)的安全問題
試想一個(gè)問題,當(dāng)進(jìn)程接收到一個(gè)信號(hào)時(shí),轉(zhuǎn)到你關(guān)聯(lián)的函數(shù)中執(zhí)行,但是在執(zhí)行的時(shí)候,進(jìn)程又接收到同一個(gè)信號(hào)或另一個(gè)信號(hào),又要執(zhí)行相關(guān)聯(lián)的函數(shù)時(shí),程序會(huì)怎么執(zhí)行?
也就是說,信號(hào)處理函數(shù)可以在其執(zhí)行期間被中斷并被再次調(diào)用。當(dāng)返回到第一次調(diào)用時(shí),它能否繼續(xù)正確操作是很關(guān)鍵的。這不僅僅是遞歸的問題,而是可重入的(即可以完全地進(jìn)入和再次執(zhí)行)的問題。而反觀Linux,其內(nèi)核在同一時(shí)期負(fù)責(zé)處理多個(gè)設(shè)備的中斷服務(wù)例程就需要可重入的,因?yàn)閮?yōu)先級(jí)更高的中斷可能會(huì)在同一段代碼的執(zhí)行期間“插入”進(jìn)來。
簡言之,就是說,我們的信號(hào)處理函數(shù)要是可重入的,即離開后可再次安全地進(jìn)入和再次執(zhí)行,要使信號(hào)處理函數(shù)是可重入的,則在信息處理函數(shù)中不能調(diào)用不可重入的函數(shù)。下面給出可重入的函數(shù)在列表,不在此表中的函數(shù)都是不可重入的,可重入函數(shù)表如下:
七、附錄——信號(hào)表
如果進(jìn)程接收到上面這些信號(hào)中的一個(gè),而事先又沒有安排捕獲它,進(jìn)程就會(huì)終止。
還有其他的一些信號(hào),如下:
以上就是本文的全部內(nèi)容,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來一定的幫助,同時(shí)也希望多多支持本站!
版權(quán)聲明:本站文章來源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請保持原文完整并注明來源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來,僅供學(xué)習(xí)參考,不代表本站立場,如有內(nèi)容涉嫌侵權(quán),請聯(lián)系alex-e#qq.com處理。