README ABOUT SIGNAL
信號(hào)是什么?
- 信號(hào)是一種很古老的IPC(進(jìn)程間通信)方式。在早期的類Unix操作系統(tǒng)中,信號(hào)只能傳遞很簡(jiǎn)單的信息,即一個(gè)int(信號(hào)的編號(hào))值。
- 信號(hào)是一種異步的消息傳遞方式或者說(shuō)通知機(jī)制。
舉一個(gè)不恰當(dāng)?shù)睦樱阏诤芘d奮地解決一個(gè)BUG,突然手機(jī)響了,女朋友說(shuō)要分手。你立馬放下手上的活,去處理女朋友那件事。手機(jī)隨時(shí)都會(huì)響,隨時(shí)都會(huì)中斷你當(dāng)下的事情。所以稱之為異步事件。這個(gè)例子中的信號(hào)就體現(xiàn)在手機(jī)鈴聲響了。你去處理女朋友分手就是對(duì)信號(hào)做出的響應(yīng)。 - 雖然信號(hào)是一種低級(jí)的IPC方式,但同時(shí)它保持了很簡(jiǎn)單的特性。在一些大型服務(wù)端程序中,很多時(shí)候也要考慮信號(hào)造成的影響。還是值得一學(xué)的。
從上面的例子可以看出,當(dāng)產(chǎn)生一個(gè)信號(hào)(如:手機(jī)鈴聲響),之后就要去響應(yīng)這個(gè)信號(hào),如何響應(yīng)這個(gè)信號(hào)則需要編程人員來(lái)寫一個(gè)函數(shù)。當(dāng)產(chǎn)生信號(hào)時(shí),程序去執(zhí)行之這個(gè)函數(shù)(如:處理女朋友分手)。
Linux下信號(hào)分兩種.
- 第一種: 用于內(nèi)核向進(jìn)程通知事件.即傳統(tǒng)或者標(biāo)準(zhǔn)信號(hào).(信號(hào)編號(hào)由1~31)
- 第二種: 實(shí)時(shí)信號(hào)(信號(hào)編號(hào)由34~64)
- 0號(hào)信號(hào)用來(lái)測(cè)試對(duì)應(yīng)進(jìn)程是否存在或者是否由權(quán)限給其發(fā)送信號(hào)
一般程序收到信號(hào)時(shí)進(jìn)程的現(xiàn)象:
- 忽略信號(hào)
- 終止(殺死)進(jìn)程
- 產(chǎn)生核心轉(zhuǎn)儲(chǔ)文件,同時(shí)進(jìn)程終止(核心轉(zhuǎn)儲(chǔ)文件即保存了程序跪掉的現(xiàn)場(chǎng)信息的文件,用來(lái)分析程序?yàn)楹喂虻?。
- 停止進(jìn)程
- 于之前暫停后再度恢復(fù)執(zhí)行
編程可以做到的
- 設(shè)置信號(hào)到來(lái)時(shí)采取默認(rèn)的行為.
- 忽略掉該信號(hào).
- 執(zhí)行信號(hào)處理器程序.
常用信號(hào)類型即默認(rèn)行為
信號(hào)名稱 | 產(chǎn)生的效果 | 對(duì)進(jìn)程默認(rèn)的效果 |
---|---|---|
SIGINT | Ctrl-C終端下產(chǎn)生 | 終止當(dāng)前進(jìn)程 |
SIGABRT | 產(chǎn)生SIGABRT信號(hào) | 默認(rèn)終止進(jìn)程,并產(chǎn)生core(核心轉(zhuǎn)儲(chǔ))文件 |
SIGALRM | 由定時(shí)器如alarm(),setitimer()等產(chǎn)生的定時(shí)器超時(shí)觸發(fā)的信號(hào) | 終止當(dāng)前進(jìn)程 |
SIGCHLD | 子進(jìn)程結(jié)束后向父進(jìn)程發(fā)送 | 忽略 |
SIGBUS | 總線錯(cuò)誤,即發(fā)生了某種內(nèi)存訪問(wèn)錯(cuò)誤 | 終止當(dāng)前進(jìn)程并產(chǎn)生核心轉(zhuǎn)儲(chǔ)文件 |
SIGKILL | 必殺信號(hào),收到信號(hào)的進(jìn)程一定結(jié)束,不能捕獲 | 終止進(jìn)程 |
SIGPIPE | 管道斷裂,向已關(guān)閉的管道寫操作 | 進(jìn)程終止 |
SIGIO | 使用fcntl注冊(cè)I/O事件,當(dāng)管道或者socket上由I/O時(shí)產(chǎn)生此信號(hào) | 終止當(dāng)前進(jìn)程 |
SIGQUIT | 在終端下Ctrl-\產(chǎn)生 | 終止當(dāng)前進(jìn)程,并產(chǎn)生core文件 |
SIGSEGV | 對(duì)內(nèi)存無(wú)效的訪問(wèn)導(dǎo)致即常見的“段錯(cuò)誤” | 終止當(dāng)前進(jìn)程,并產(chǎn)生core文件 |
SIGSTOP | 必停信號(hào),不能被阻塞,不能被捕捉 | 停止當(dāng)前進(jìn)程 |
SIGTERM | 終止進(jìn)程的標(biāo)準(zhǔn)信號(hào) | 終止當(dāng)前進(jìn)程 |
0號(hào)信號(hào)用來(lái)測(cè)試對(duì)應(yīng)進(jìn)程是否存在或者是否有權(quán)限向?qū)?yīng)進(jìn)程發(fā)送該信號(hào) (后面就理解了)
如何設(shè)計(jì)信號(hào)處理函數(shù)?
- 一般而言,信號(hào)處理函數(shù)設(shè)計(jì)的越簡(jiǎn)單越好,因?yàn)楫?dāng)前代碼的執(zhí)行邏輯被打斷,最好盡快恢復(fù)到剛才被打斷之前的狀態(tài)。從而避免競(jìng)爭(zhēng)條件的產(chǎn)生。
- 在信號(hào)處理函數(shù)中,建議不要調(diào)用printf等與I/O相關(guān)的函數(shù)。以及一些慢設(shè)備操作。這樣會(huì)使得信號(hào)處理函數(shù)的執(zhí)行時(shí)間變長(zhǎng),可能,操作系統(tǒng)就會(huì)切換其它程序去在CPU上執(zhí)行。但如果有特殊需要,則也可以使用。
- 在信號(hào)處理函數(shù)中,不要使用任何不可重入的函數(shù)后面會(huì)說(shuō)到。保證信號(hào)處理函數(shù)可以安全地執(zhí)行完。并不會(huì)影響主邏輯執(zhí)行。
信號(hào)的發(fā)送與處理
來(lái)簡(jiǎn)單看一個(gè)實(shí)例,看看信號(hào)最簡(jiǎn)單的使用方式(當(dāng)鍵入Ctrl-C時(shí)候即程序收到SIGINT信號(hào)時(shí)進(jìn)行處理。):
// signal 的函數(shù)原型
// void (*signal(int sig , void (*func)(int)))(int);
// 至于這個(gè)怎么理解,這里就不再贅述了,請(qǐng)參考 《C Traps and Pitfalls》2.1節(jié)即理解函數(shù)聲明。
// filename : simple_signal.cpp
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>
#define MSG "Catch signal SIGINT processing \n"
#define MSG_END "Finished process SIGINT return \n"
void do_too_heavy_work () {
long long s = 0 ;
for (long long i = 0 ; i < 500000000L ; i++ ) {
s += i ;
}
}
void sig_handler (int signuum ) {
// 本程序只是為了來(lái)進(jìn)行演示,
// 在信號(hào)處理程序中,盡量不要調(diào)用與標(biāo)準(zhǔn)IO相關(guān)的和不可重入的函數(shù)。
write ( STDOUT_FILENO , MSG , strlen (MSG) ) ;
do_too_heavy_work();
write ( STDOUT_FILENO , MSG_END , strlen (MSG_END) ) ;
}
int main() {
// 注冊(cè)信號(hào)處理函數(shù)
if ( SIG_ERR == signal ( SIGINT , sig_handler ) ) {
fprintf (stderr , "signal error ") , perror ("") ;
exit (1) ;
}
// 讓主程序不退出,掛起,等待信號(hào)產(chǎn)生
while (1) {
pause () ;
}
return EXIT_SUCCESS ;
}
程序會(huì)一直停著等待用戶行為。當(dāng)我們鍵入Ctrl-C時(shí)程序打印相關(guān)信息,之后程序自己退出。那么程序的執(zhí)行流程就類似這樣:
[tutu@localhost Linux-Book]$ gcc simple_signal.cpp
[tutu@localhost Linux-Book]$ ./a.out
^CCatch signal SIGINT processing
Finished process SIGINT return
^CCatch signal SIGINT processing
Finished process SIGINT return
^CCatch signal SIGINT processing
Finished process SIGINT return
這是一種古老的注冊(cè)信號(hào)并設(shè)置信號(hào)處理函數(shù)的方式。現(xiàn)在我們使用新的信號(hào)注冊(cè)函數(shù)即sigaction函數(shù)。它提供了更多的控制字段(舊的signal已經(jīng)使用sigaction進(jìn)行了實(shí)現(xiàn)。祥見glibc源碼,我自己的Fedora 22 glibc 版本2.21可以在glibc官網(wǎng)Glibc下載Glibc源碼,對(duì)應(yīng)路徑下glibc/glibc-2.21/sysdeps/posix/signal.c看到)包括屏蔽信號(hào)集,設(shè)置高級(jí)信號(hào)處理函數(shù)等(稍后會(huì)詳細(xì)講述,別擔(dān)心).
為什么不是用signal來(lái)進(jìn)行信號(hào)注冊(cè)了?
- signal 無(wú)法設(shè)置在執(zhí)行信號(hào)處理程序時(shí)要屏蔽哪些信號(hào)的產(chǎn)生。
- signal 函數(shù)注冊(cè)的信號(hào)處理函數(shù)只能攜帶很少的信息(也不常用),在信號(hào)處理函數(shù)進(jìn)行信號(hào)處理時(shí)。
- signal 無(wú)法設(shè)置一些標(biāo)志位來(lái)執(zhí)行一些動(dòng)作(后面再講)。
- signal 只能設(shè)置所給信號(hào)的處理方式但sigaction還可以獲取之前這個(gè)信號(hào)的處理方式
廢話這么多,大家都嫌棄了,來(lái)一個(gè)真實(shí)的例子吧,和上面的程序功能一樣,但是使用sigaction進(jìn)行處理。
// filename : simple_sigaction.cpp
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>
#define MSG "Catch signal SIGINT processing \n"
#define MSG_END "Finished process SIGINT return \n"
void do_too_heavy_work () {
long long s = 0 ;
for (long long i = 0 ; i < 500000000L ; i++ ) {
s += i ;
}
}
void sig_handler (int signuum ) {
// 本程序只是為了來(lái)進(jìn)行演示,
// 在信號(hào)處理程序中,盡量不要調(diào)用與標(biāo)準(zhǔn)IO相關(guān)的,不可重入的函數(shù)。
write ( STDOUT_FILENO , MSG , strlen (MSG) ) ;
do_too_heavy_work();
write ( STDOUT_FILENO , MSG_END , strlen (MSG_END) ) ;
}
int main() {
// 注冊(cè)信號(hào)處理函數(shù)
struct sigaction newact ;
// 將信號(hào)處理函數(shù)執(zhí)行期間掩碼設(shè)置為空
sigemptyset (&newact.sa_mask ) ;
// 將標(biāo)志設(shè)置為0即默認(rèn)
newact.sa_flags = 0 ;
// 注冊(cè)信號(hào)處理函數(shù)
newact.sa_handler = sig_handler ;
if ( 0 > sigaction ( SIGINT , &newact , NULL ) ) {
fprintf (stderr , "sigaction error ") , perror ("") ;
exit (1) ;
}
// 讓主程序不退出,掛起,等待信號(hào)產(chǎn)生
while (1) {
pause () ;
}
return EXIT_SUCCESS ;
}
執(zhí)行效果和剛才的一樣,在這里就不再貼過(guò)來(lái)了。
sigaction是怎么做到上述功能
首先sigaction比signal多了參數(shù):
int sigaction ( int sig , const struct sigaction * restrict act ,\
struct sigaction * restrict oact ) ;
而結(jié)構(gòu)體 struct sigaction act 里有什么呢? 我們來(lái)看看:
man 3 sigaction 可以看到對(duì)應(yīng)的sigaction結(jié)構(gòu)體中的內(nèi)容。
Member Tyep | Member Name | Description |
---|---|---|
void (*)(int) | sa_handler | 指向一個(gè)信號(hào)處理函數(shù),或者使用SIG_IGN,SIG_DFL |
sigset_t | sa_mask | 在執(zhí)行信號(hào)處理函數(shù)時(shí),屏蔽sa_mask中的信號(hào)的產(chǎn)生 |
int | sa_flags | 特殊的標(biāo)志位,影響信號(hào)的處理行為 |
void () ( int , siginfo_t * ,void) | sa_sigaction | 指向一個(gè)信號(hào)處理函數(shù)(當(dāng)設(shè)置了SA_SIGINFO標(biāo)志位時(shí)) |
以下幾個(gè)函數(shù)也與上述內(nèi)容相關(guān):
// 設(shè)置信號(hào)集為全空
int sigemptyset (sigset_t * set ) ;
// 設(shè)置信號(hào)集為全滿
int sigfillset ( sigset_t * set ) ;
// 將信號(hào)signum添加到信號(hào)集中
int sigaddset ( sigset_t * set , int signum) ;
// 去除信號(hào)集中signum信號(hào)
int sigdelset ( sigset_t * set , int signum ) ;
// 判斷signum信號(hào)是否在信號(hào)集set中
int sigismember ( const sigset_t * set , int signum ) ;
flags相關(guān)的選項(xiàng)理解:
- SA_SIGINFO: 設(shè)置此標(biāo)志位表示可以使用sa_sigaction字段進(jìn)行注冊(cè)信號(hào)處理函數(shù)
- SA_RESTART: 被中斷的系統(tǒng)調(diào)用自動(dòng)重啟
- SA_NODEFER: 在執(zhí)行信號(hào)處理函數(shù)期間不屏蔽引起信號(hào)處理函數(shù)執(zhí)行的信號(hào)
更多的標(biāo)志位信息還請(qǐng)各位man 3 sigaction 這里只說(shuō)常用的。
實(shí)際上判斷某個(gè)信號(hào)是否在對(duì)應(yīng)的信號(hào)集中,即判斷對(duì)應(yīng)信號(hào)在信號(hào)集中的那一個(gè)位是否被置1。去除信號(hào)集中的signum信號(hào)只需要對(duì)應(yīng)位置0即可。
以上函數(shù)可以操作與sigset_t類型相關(guān)的變量。
當(dāng)然除了在執(zhí)行信號(hào)處理函數(shù)時(shí)候可以用sigaction設(shè)置要屏蔽的信號(hào)時(shí),在程序中也可以給進(jìn)程設(shè)置要屏蔽的信號(hào)。需要使用sigprocmask這個(gè)系統(tǒng)調(diào)用。
int sigprocmask ( int how , const sigset_t *set , sigset_t * oldset ) ;
how字段的含義,具體使用大家man手冊(cè)一下喵~
- SIG_SETMASK : 將set中的信號(hào)集設(shè)置為當(dāng)前進(jìn)程的阻塞信號(hào)集。
- SIG_BLOCK:將set中的信號(hào)集加到當(dāng)前進(jìn)程的阻塞信號(hào)集中。
- SIG_UNBLOCK:將set中的信號(hào)集從當(dāng)前阻塞信號(hào)集中去除。
那么問(wèn)題來(lái)了,為什么要設(shè)置屏蔽信號(hào)集?簡(jiǎn)單理解,就和你設(shè)置接電話的黑名單一樣,你不想讓這些電話中斷你正在干的事情(什么原因我就不知道了),同樣的你不想讓這些信號(hào)中斷你程序正常運(yùn)行(SIGSTOP和SIGKILL不能設(shè)置忽略或者捕捉,由于內(nèi)核必須保證能夠殺死(終止)一個(gè)進(jìn)程,所以這兩個(gè)信號(hào)就是這個(gè)作用)。通常情況下我們是要對(duì)SIGINT(終端下Ctrl-C),SIGTERM(kill 的默認(rèn)信號(hào)),SIGQUIT(終端下Ctrl+),SIGHUP(終端掉線網(wǎng)絡(luò)斷開收到)做一些處理(通常做一些IO同步如將還沒有寫到數(shù)據(jù)庫(kù)中的要同步到數(shù)據(jù)庫(kù),將還在緩沖的東西同步到文件等或者是做一些清理銷毀對(duì)象等工作),假如你要寫大型程序的時(shí)候。
說(shuō)了這么多關(guān)于,該說(shuō)說(shuō)如何如何發(fā)送信號(hào)了,各位久等了
- 在終端下可以用kill/killall命令發(fā)送(缺省為SIGTERM信號(hào)),如下:
[tutu@localhost Linux-Book]$ ps
PID TTY TIME CMD
2711 pts/0 00:00:00 bash
3990 pts/0 00:00:00 a.sh
3991 pts/0 00:00:00 sleep
3992 pts/0 00:00:00 ps
// kill 可以給一個(gè)進(jìn)程組發(fā)送信號(hào) 詳細(xì)的大家 man kill
[tutu@localhost Linux-Book]$ kill -SIGKILL 3990
[tutu@localhost Linux-Book]$ ps
PID TTY TIME CMD
2711 pts/0 00:00:00 bash
3991 pts/0 00:00:00 sleep
3993 pts/0 00:00:00 ps
[1]+ 已殺死 ./a.sh
[tutu@localhost Linux-Book]$
- 可以在終端下使用Ctrl+C(SIGINT信號(hào)),Ctrl+(SIGQUIT信號(hào)).Ctrl+z(SIGSTOP信號(hào))...
- 在程序中可以使用:
// 給指定pid的進(jìn)程發(fā)送信號(hào)
int kill(pid_t pid , int sig) ; 詳細(xì)使用,大家man 2 kill
// 給當(dāng)前進(jìn)程發(fā)信號(hào),也就是給自己發(fā)信號(hào)
int raise (int signo) ;
// 給進(jìn)程號(hào)為pid的進(jìn)程發(fā)送sig信號(hào),并攜帶參數(shù)value(需要設(shè)置sigaction的flags的SA_SIGINFO標(biāo)志才能使用)
int sigqueue ( pid_t pid , int sig , const union sigval value ) ;
// 雖然功能很好,但自己平時(shí)使用的比較少,或者基本不用
typedef union sigval {
int sival_int;
// sival_ptr 字段使用的較少,或者說(shuō)幾乎不用
void __user *sival_ptr;
} sigval_t;
// 對(duì)應(yīng)的結(jié)構(gòu)定義如上。在sigaction函數(shù)中,對(duì)應(yīng)struct sigaction中的信號(hào)處理函數(shù)的使用
4.與信號(hào)相關(guān)的函數(shù)
// 一下詳細(xì)內(nèi)容均可以 man 手冊(cè)!!!!!!
// 古老的定時(shí)函數(shù),用來(lái)測(cè)試程序,粗略的定時(shí)函數(shù)
unsigned int alarm (unsigned int seconds) ;
// 新的定時(shí)函數(shù),用于定時(shí),當(dāng)然也是簡(jiǎn)單的定時(shí)實(shí)現(xiàn)
int setitimer ( int which , const struct itimerval *new_value , \
struct itimerval *old_value ) ;
// 產(chǎn)生SIGABRT信號(hào),一般用于程序出現(xiàn)異常的情況下調(diào)用,如malloc分配內(nèi)存失敗。
void abort (void) ;
// 掛起進(jìn)程,等待設(shè)置在set中的信號(hào)產(chǎn)生,通過(guò)sig返回發(fā)生的并在set中設(shè)置的信號(hào)。不屏蔽其它信號(hào)
int sigwait (const sigset_t * set , int *sig) ;
// 掛起進(jìn)程等待設(shè)置在set中的信號(hào)產(chǎn)生,并接受攜帶的額外信息,返回產(chǎn)生的信號(hào)。不屏蔽其它信號(hào)
int sigwaitinfo (const sigset_t *set , siginfo_t *info ) ;
// 掛起進(jìn)程等待信號(hào)產(chǎn)生,mask中的信號(hào)集在等待信號(hào)產(chǎn)生期間進(jìn)行屏蔽
int sigsuspend (const sigset_t * mask ) ;
// 以上內(nèi)容不能逐一舉例,還望大家多加操練操練~喵~
說(shuō)了這么多廢話,來(lái)看一個(gè)實(shí)例吧 (例子比較簡(jiǎn)單)
[tutu@localhost Linux-Book]$ cat sigqueue_post_signal.cpp
// 信號(hào)發(fā)送端:
// filename : sigqueue_post_signal.cpp
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <errno.h>
#include <unistd.h>
int main (int argc , char * argv[]) {
if ( 2 != argc ) {
fprintf (stderr , "Bad argument!\nUsage ./post_signal pid\n") ;
exit (1) ;
}
pid_t pid = atoi ( argv[argc-1] ) ;
printf ("Sending signal to %d , by using sigqueue\n" , pid) ;
sigval_t sigval ;
sigval.sival_int = 8888 ;
int errcode = 0 ;
if ( 0 > ( errcode = sigqueue ( pid , SIGUSR1 , sigval ) )) {
if ( ESRCH == errcode ) {
fprintf (stderr , "No such process!\n") ;
exit (1) ;
} else {
fprintf (stderr , "sigqueue error "),perror ("") ;
exit (1) ;
}
}
printf ("Finished!\n") ;
return 0 ;
}
[tutu@localhost Linux-Book]$ gcc sigqueue_post_signal.cpp -o post_signal
[tutu@localhost Linux-Book]$ cat sigqueue_wait.cpp
// filename sigqueue_wait.cpp
// 信號(hào)接收端
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
// 注冊(cè)SIGUSR1的信號(hào)處理函數(shù)
// man sigaction 顯示,當(dāng)前第三個(gè)參數(shù)無(wú)卵用,第一個(gè)參數(shù)是信號(hào)編號(hào),第二個(gè)是攜帶的信息
void sig_handler (int signo , siginfo_t * info , void * extra ) {
// print signo
printf ("Catch SIGUSR1\n") ;
printf ("signo is %d\n" , signo) ;
// print info -> si_value.sival_ptr
printf ("sigval is %d\n" , info->si_value.sival_int ) ;
}
int main () {
struct sigaction act ;
act.sa_sigaction = sig_handler ;
act.sa_flags = 0 ;
act.sa_flags |= SA_SIGINFO ;
sigemptyset (&act.sa_mask) ;
printf ("My pid is %d\n" , getpid() ) ;
if ( 0 > sigaction (SIGUSR1 , &act , NULL ) ) {
fprintf (stderr , "sigaction error ") , perror ("") ;
exit (1) ;
}
while (1) {
pause () ;
}
return 0 ;
}
[tutu@localhost Linux-Book]$ gcc sigqueue_wait.cpp -o wait_signal
[tutu@localhost Linux-Book]$
首先執(zhí)行wait_signal,wait_signal 會(huì)打印自己的pid,再執(zhí)行post_signal并傳參數(shù)即pid,之后就可以看到效果了。
對(duì)于info->sig_value.sival_ptr字段,在這里暫時(shí)不討論其使用,有興趣的大家可以自行研究,因?yàn)橛玫氖稚佟?/p>
信號(hào)的可靠性概述
上面說(shuō)了,Linux下的信號(hào)分為:
- 不可靠信號(hào): [1~31]均為不可靠信號(hào)
- 可靠信號(hào):[32~63]為可靠信號(hào)或者叫實(shí)時(shí)信號(hào)
可靠信號(hào)是為了彌補(bǔ)Linux的不可靠信號(hào)以及用戶可使用的信號(hào)太少的缺陷。
怎樣理解可靠與不可靠?下面要引進(jìn)幾個(gè)概念:
- 未決信號(hào):已經(jīng)產(chǎn)生,但是沒有給進(jìn)程遞送的信號(hào)(即還未處理)。
- 已被遞送信號(hào):已經(jīng)遞送給進(jìn)程并處理過(guò)。
當(dāng)我們?cè)趫?zhí)行第一個(gè)示例程序的時(shí)候,如果在打印完
Catch signal SIGINT processing
之后,我們很快多次按下Ctrl-C。會(huì)發(fā)現(xiàn)當(dāng)打印完
Finished process SIGINT return
后,僅會(huì)再響應(yīng)一次信號(hào)。這是為什么?為什么我們按下那么多次Ctrl-C卻只響應(yīng)那么一次。原因其時(shí)就在于SIGINT是一個(gè)不可靠信號(hào)。
** 注意,不是說(shuō)用sigaction注冊(cè)的信號(hào)就是可靠的信號(hào)了。**
根本原因在于Linux的SIGINT本身就不是可靠的,也就是說(shuō),在信號(hào)處理函數(shù)處理期間或者信號(hào)被屏蔽期間多次產(chǎn)生該信號(hào),當(dāng)信號(hào)處理函數(shù)結(jié)束或者進(jìn)程解除對(duì)該信號(hào)的屏蔽時(shí)只會(huì)再報(bào)告一次該信號(hào)。因?yàn)閷?duì)于不可靠信號(hào)系統(tǒng)并沒有給他們排隊(duì)。也就多次產(chǎn)生只記錄一次了。
說(shuō)了這么多,測(cè)試一下:
[tutu@localhost Linux-Book]$ gcc simple_signal.cpp
[tutu@localhost Linux-Book]$ ./a.out
^CCatch signal SIGINT processing
^C^C^C^C^C^CFinished process SIGINT return
Catch signal SIGINT processing
Finished process SIGINT return
證實(shí)了SIGINT不是可靠信號(hào),因?yàn)槎啻伟l(fā)生后會(huì)丟失。如何驗(yàn)證信號(hào)被屏蔽后多次發(fā)生,再解除屏蔽后只會(huì)遞送一次?需要用到sigprocmask函數(shù)。再自己寫一個(gè)簡(jiǎn)單的腳本多次發(fā)送信號(hào),就可以了。這里就不多進(jìn)行演示了。
驗(yàn)證可靠信號(hào)(其時(shí)就是將第一個(gè)程序的注冊(cè)信號(hào)改成SIGRTMIN,再改改信號(hào)處理函數(shù)中打印的信息):
// filenam : simple_realiable_signal.cpp
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>
#define MSG "Catch signal SIGRTMIN processing \n"
#define MSG_END "Finished process SIGRTMIN return \n"
void do_too_heavy_work () {
long long s = 0 ;
for (long long i = 0 ; i < 500000000L ; i++ ) {
s += i ;
}
}
void sig_handler (int signuum ) {
// 本程序只是為了來(lái)進(jìn)行演示,
// 在信號(hào)處理程序中,盡量不要調(diào)用與標(biāo)準(zhǔn)IO相關(guān)的,不可重入的函數(shù)。
write ( STDOUT_FILENO , MSG , strlen (MSG) ) ;
do_too_heavy_work();
write ( STDOUT_FILENO , MSG_END , strlen (MSG_END) ) ;
}
int main() {
// 注冊(cè)信號(hào)處理函數(shù)
if ( SIG_ERR == signal ( SIGRTMIN , sig_handler ) ) {
fprintf (stderr , "signal error ") , perror ("") ;
exit (1) ;
}
// 讓主程序不退出,掛起,等待信號(hào)產(chǎn)生
while (1) {
pause () ;
}
return EXIT_SUCCESS ;
}
還有一個(gè)腳本,用來(lái)發(fā)送信號(hào):
#!/bin/bash
# 過(guò)濾出realiable_signal的pid
pid=$(ps aux|grep realiable_signal | grep -v grep | awk '{print $2}')
# 循環(huán)發(fā)送5次信號(hào)
for((i=0;i<5;i++))
do
kill -34 $pid
done
執(zhí)行一下看看,首先編譯realiable_signal
[tutu@localhost Linux-Book]$ gcc simple_realiable_signal.cpp -o realiable_signal
[tutu@localhost Linux-Book]$ ./realiable_signal
再起一個(gè)終端運(yùn)行腳本:
[tutu@localhost Linux-Book]$ ./kill_SIGRTMIN.sh
[tutu@localhost Linux-Book]$
看看運(yùn)行結(jié)果:
[tutu@localhost Linux-Book]$ gcc simple_realiable_signal.cpp -o realiable_signal
[tutu@localhost Linux-Book]$ ./realiable_signal
Catch signal SIGRTMIN processing
Finished process SIGRTMIN return
Catch signal SIGRTMIN processing
Finished process SIGRTMIN return
Catch signal SIGRTMIN processing
Finished process SIGRTMIN return
Catch signal SIGRTMIN processing
Finished process SIGRTMIN return
Catch signal SIGRTMIN processing
Finished process SIGRTMIN return
講講本質(zhì)上的原因,或許現(xiàn)在不能很快理解,想詳細(xì)了解本質(zhì)上的原因,大家可以去看內(nèi)核源碼的東西,這個(gè)是本質(zhì)上的東西。我們看到的都是表象,內(nèi)核里寫的才是真實(shí)/原理的東西。
每個(gè)進(jìn)程都會(huì)有一個(gè)結(jié)構(gòu)體,來(lái)記錄未決信號(hào)集,用戶可以通過(guò)
int sigpending (sigset_t *set ) ;
來(lái)獲取進(jìn)程的未決信號(hào)集(就是收到信號(hào)但是還沒有處理的信號(hào)集),對(duì)于不可靠信號(hào)集,當(dāng)被屏蔽或者在信號(hào)處理函數(shù)正在執(zhí)行的時(shí)候多次發(fā)生,只會(huì)將對(duì)應(yīng)信號(hào)位置1。多次將一位置1的效果和只置一次1的效果相同,更核心的原因是,不可靠信號(hào)內(nèi)核不負(fù)責(zé)給它排隊(duì)記錄。這就造成了不可靠。
對(duì)于可靠信號(hào),存在一個(gè)雙向鏈表,內(nèi)核判斷是否是可靠信號(hào),如果是,那么即使是屏蔽了該可靠信號(hào)或者正在處理可靠信號(hào)的信號(hào)處理函數(shù),在這期間多次發(fā)生,也會(huì)將其記錄下來(lái),鏈接到一個(gè)雙向鏈表上,當(dāng)進(jìn)程解除了該信號(hào)的屏蔽或者處理完信號(hào)處理函數(shù)之后,將其再次遞送給進(jìn)程,直到鏈表上的信號(hào)全部遞送完。這個(gè)在struct sigpending 結(jié)構(gòu)體中(現(xiàn)在別看哈,直到這個(gè)概念就好,過(guò)早陷入內(nèi)核,有點(diǎn)不知頭緒,先打好數(shù)據(jù)結(jié)構(gòu),基礎(chǔ)算法,C語(yǔ)言,一定哦o)。
被中斷的系統(tǒng)調(diào)用
當(dāng)系統(tǒng)調(diào)用正在被執(zhí)行的時(shí)候,發(fā)生了信號(hào)腫么辦?一般情況下我們會(huì)這樣做:
ssize_t ret ;
while ( len != 0 && ( ret = read ( fd , buf , len ) != 0 ) {
if ( -1 == ret ) {
// 通過(guò)判斷errno即錯(cuò)誤類型來(lái)片判斷read出錯(cuò)是由于什么具體原因造成的
if ( errno == EINTR ) {
continue ;
}
perror ("read error") ;
break ;
}
len -= ret ;
buf += ret ;
}
ssize_t ret ;
while ( 0 != len && ( ret = write ( fd , buf , len)) != 0 ) {
if ( -1 == ret ) {
if ( errno == EINTR ) {
continue ;
}
perror ( "write error ") ;
break ;
}
len -= ret ;
buf += ret ;
}
但在這里想說(shuō)的是:如read,write這樣的系統(tǒng)調(diào)用內(nèi)核已經(jīng)支持其自動(dòng)重啟了,不需要放太多注意力在這里。當(dāng)然有些系統(tǒng)調(diào)用或者說(shuō)庫(kù)函數(shù),還是會(huì)出現(xiàn)這樣的情況,相關(guān)牽扯到的還有sigaction 的flags的SA_RESTART標(biāo)志位,詳細(xì)的man 7 signal 介紹的很詳細(xì)很詳細(xì)。這里就不抄過(guò)來(lái)了。
幾種常見的信號(hào)處理函數(shù)的設(shè)計(jì)方式:
- 信號(hào)處理函數(shù)設(shè)置全局性標(biāo)志變量并退出。主程序?qū)Υ藰?biāo)志進(jìn)行周期性檢查,一旦標(biāo)志被置位,則進(jìn)行相應(yīng)的處理動(dòng)作(如果主程序監(jiān)聽一個(gè)或者多個(gè)文件描述符的I/O狀態(tài)而無(wú)法進(jìn)行自旋檢測(cè)標(biāo)志是否被設(shè)置,則可以創(chuàng)建一個(gè)管道,通過(guò)對(duì)管道的文件描述符進(jìn)行監(jiān)聽,就可以在信號(hào)處理函數(shù)中向管道fd寫入一字節(jié)內(nèi)容,通過(guò)主程序中對(duì)管道fd事件的處理從而達(dá)到檢測(cè)全局標(biāo)志改變的事件。)。
簡(jiǎn)單看看一個(gè)例子:
/*
這個(gè)文件是用來(lái)實(shí)現(xiàn)通過(guò)設(shè)置標(biāo)志位來(lái)判斷程序是否在規(guī)定事件內(nèi)完成輸入,在超時(shí)事件內(nèi)如果完成輸入則打印輸入內(nèi)容,如果未完成則打印錯(cuò)誤(超時(shí))信息。
*/
//filename : sig_flag.cpp
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <sys/time.h>
#include <string.h>
#define TIMED_OUT 5
#define BUFFER_SIZE 1024
// 這里是一個(gè)全局的標(biāo)志
static volatile sig_atomic_t timedout_flag = 0;
// 原子的將 timedout_flag 置 1
void sig_handler(int signum) {
/* 產(chǎn)生超時(shí)事件*/
timedout_flag = 1;
}
int main() {
struct sigaction old_act, new_act;
new_act.sa_handler = sig_handler ;
new_act.sa_flags = 0 ;
sigemptyset (&new_act.sa_mask) ;
// 注冊(cè)信號(hào)處理函數(shù), 對(duì)應(yīng)定時(shí)器到時(shí)時(shí)調(diào)用sig_handler
if (0 > sigaction(SIGALRM, &new_act, &old_act)) {
fprintf(stderr, "sigaction error "), perror("");
exit(1);
}
struct itimerval new_timer ;
bzero (&new_timer , sizeof new_timer ) ;
new_timer.it_value.tv_sec = TIMED_OUT;
new_timer.it_value.tv_usec = 0;
// 在這里啟動(dòng)定時(shí)器,開始等待是否超時(shí)。
if ( 0 > setitimer (ITIMER_REAL , (const struct itimerval *)&new_timer , NULL ) ) {
fprintf(stderr, "setitimer error , line %d\n" , __LINE__) ;
exit(1);
}
// 在這里用read 來(lái)模擬任何阻塞的系統(tǒng)調(diào)用
char buffer[BUFFER_SIZE] = {0};
int bytes = read(STDIN_FILENO, buffer, BUFFER_SIZE);
if ( 0 < bytes ) {
printf("正常收到鍵盤輸入。\n");
// 設(shè)置全局標(biāo)志為 0
timedout_flag = 0 ;
}
// 判斷是否產(chǎn)生超時(shí)事件 。
if (1 == timedout_flag) {
printf("等待超時(shí)!\n");
// 在這里做一些清理操作,比如 close (fd) 等, free() 空間等。
printf("Quit programm now .\n");
} else {
// 未發(fā)生超時(shí)事件,即正常輸入了數(shù)據(jù)。
// 取消鬧鐘
new_timer.it_value.tv_usec = 0 ;
if (0 > setitimer(ITIMER_REAL, &new_timer, NULL)) {
fprintf(stderr, "setitmer error "), perror("");
exit(1);
}
write(STDOUT_FILENO, buffer, strlen(buffer));
}
return 0;
}
2.信號(hào)處理函數(shù)值某種類型的清理操作。接著終止進(jìn)程或者使用非本地跳轉(zhuǎn)。將控制返回到主程序的預(yù)定位置。
// 這里涉及到非本地跳轉(zhuǎn),即:
int setjmp ( jmp_buf env) ;
int longjmp ( jmp_buf env , int val) ;
int sigsetjmp ( sigjmp_buf env , int savesigs) ;
int siglongjmp ( sigjmp_buf env , int val) ;
首先需要理解上述函數(shù)的含義,兩組函數(shù)的區(qū)別就是是否在跳轉(zhuǎn)后恢復(fù)信號(hào)屏蔽集。前兩個(gè)函數(shù)不保留,即在跳轉(zhuǎn)后不恢復(fù)信號(hào)屏蔽集,后兩個(gè)函數(shù)則恢復(fù)(當(dāng)savesigs為非0詳細(xì)的 man sigsetjmp)。
** 由于只是初學(xué),就不研究這個(gè)例子了 **
設(shè)置信號(hào)處理函數(shù)需要注意到的
- 應(yīng)該了解到,每一個(gè)線程只有一個(gè)errno值,所以信號(hào)處理程序可能會(huì)修改errno的值。因此,作為一個(gè)通用的規(guī)則,應(yīng)當(dāng)在調(diào)用那些可能改變errno的函數(shù)之前保存errno值,之后再進(jìn)行恢復(fù)。
- 在信號(hào)處理函數(shù)中調(diào)用一個(gè)非可重入函數(shù),其結(jié)果未知的,所以盡量不要在信號(hào)處理
函數(shù)中使用非可重入函數(shù) , 判斷是否是可重入函數(shù)有一個(gè)簡(jiǎn)單的原則:
- 函數(shù)內(nèi)部不使用操作靜態(tài)變量。
- 不使用與標(biāo)準(zhǔn)I/O相關(guān)函數(shù)。
- 不使用malloc ,free() 函數(shù)。
- 不調(diào)用其它非可重入函數(shù)。
關(guān)于可重入的概念,大家自行查找一下,或參考APUE,TLPI相關(guān)章節(jié)。簡(jiǎn)單理解就是可以同時(shí)被多個(gè)進(jìn)程/線程執(zhí)行的一段代碼(函數(shù))而不會(huì)出現(xiàn)錯(cuò)誤。
本章完
參考文獻(xiàn)及博客:
《Unix環(huán)境高級(jí)編程 第三版》
《The Linux Programming Interface》
http://blog.chinaunix.net/uid-24774106-id-4061386.html
上述鏈接共4篇
http://www.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html
上述鏈接共2篇
** 初學(xué)者以前兩本書為主,當(dāng)有Linux操作系統(tǒng)知識(shí)后可以詳細(xì)參考后兩個(gè)鏈接中內(nèi)容。十分詳實(shí)。喵~**
.