本文主要介紹Linux信號(hào)系統(tǒng)和如何使用POSIX API來響應(yīng)信號(hào)。本文中的示例適用于Linux系統(tǒng)和大部分POSIX兼容系統(tǒng)。
Linux系統(tǒng)中的信號(hào)
在下列情況下,我們的應(yīng)用進(jìn)程可能會(huì)收到系統(tǒng)信號(hào):
用戶空間的其他進(jìn)程調(diào)用了類似kill(2)函數(shù)
進(jìn)程自身調(diào)用了類似about(3)函數(shù)
當(dāng)子進(jìn)程退出時(shí),內(nèi)核會(huì)向父進(jìn)程發(fā)送SIGCHLD信號(hào)
當(dāng)父進(jìn)程退出時(shí),所有子進(jìn)程會(huì)收到SIGHUP信號(hào)
當(dāng)用戶通過鍵盤終端進(jìn)程(ctrl+c)時(shí),進(jìn)程會(huì)收到SIGINT信號(hào)
當(dāng)進(jìn)程運(yùn)行出現(xiàn)問題時(shí),可能會(huì)收到SIGILL、SIGFPE、SIGSEGV等信號(hào)
當(dāng)進(jìn)程在調(diào)用mmap(2)的時(shí)候失敗(可能是因?yàn)橛成涞奈募黄渌M(jìn)程截短),會(huì)收到SIGBUS信號(hào)
當(dāng)使用性能調(diào)優(yōu)工具時(shí),進(jìn)程可能會(huì)收到SIGPROF。這一般是程序未能正確處理中斷系統(tǒng)函數(shù)(如read(2))。
當(dāng)使用write(2)或類似數(shù)據(jù)發(fā)送函數(shù)時(shí),如果對方已經(jīng)斷開連接,進(jìn)程會(huì)收到SIGPIPE信號(hào)。
如需了解所有系統(tǒng)信號(hào),參見signal(7)手冊。
信號(hào)的默認(rèn)行為
每個(gè)信號(hào)都關(guān)聯(lián)一個(gè)默認(rèn)的行為,當(dāng)進(jìn)程沒有捕獲并處理信號(hào)時(shí),進(jìn)程會(huì)按照默認(rèn)的行為處理信號(hào)。
這些默認(rèn)行為包括:
結(jié)束進(jìn)程。這是最通用默認(rèn)行為,包括SIGTERM、SIGQUIT、SIGPIPE、SIGUSR1、SIGUSR2等信號(hào)。
結(jié)束并執(zhí)行核心轉(zhuǎn)儲(chǔ)。包括SIGSEGV、SIGILL、SIGABRT等信號(hào),這一般都是因?yàn)榇a中存在錯(cuò)誤。
一些信號(hào)默認(rèn)會(huì)被忽略,例如SIGCHLD。
掛起進(jìn)程。SIGSTOP信號(hào)會(huì)引起進(jìn)程掛起,而SIGCOND能夠?qū)炱鸬倪M(jìn)程繼續(xù)運(yùn)行。該過程常見于在控制臺(tái)使用ctrl+z組合鍵。
信號(hào)處理
最傳統(tǒng)的信號(hào)處理方式是使用signal(2)函數(shù)裝載一個(gè)信號(hào)處理函數(shù)。但是這種方式已經(jīng)被廢棄,主要原因是在UNIX實(shí)現(xiàn)中,收到信號(hào)之后,會(huì)重置回默認(rèn)的信號(hào)處理行為。同時(shí),該行為是不跨平臺(tái)的。因此,建議的信號(hào)處理方式是使用sigaction(2)函數(shù)。
sigaction(2)函數(shù)的原型為:
int sigaction (int signum, const struct sigaction *act, struct sigaction *oldact);
值得注意的是,sigaction(2)函數(shù)不直接接受信號(hào)處理函數(shù),而需要使用struct sigaction結(jié)構(gòu)體,其定義為:
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void);};
其中一些關(guān)鍵字段:
sa_handler:信號(hào)處理函數(shù)的函數(shù)指針,其函數(shù)原型和signal(2)接受的信號(hào)處理函數(shù)相同。
sa_sigaction:另一種信號(hào)處理函數(shù)指針,它能在處理信號(hào)時(shí)獲取更多信號(hào)相關(guān)的信息。
sa_mask:允許設(shè)置信號(hào)處理函數(shù)執(zhí)行時(shí)需要阻塞的信號(hào)。
sa_flags:修改信號(hào)處理函數(shù)執(zhí)行時(shí)的默認(rèn)行為,具體可選值請參照手冊。
sigaction使用示例:
#include #include #include #include static void hdl (int sig, siginfo_t *siginfo, void *context){ printf (“Sending PID: %ld, UID: %ld\n”, (long)siginfo-》si_pid, (long)siginfo-》si_uid);}int main (int argc, char *argv[]){ struct sigaction act; memset (&act, ‘\0’, sizeof(act)); /* 這里使用sa_sigaction字段,因?yàn)樵撟侄翁峁┝藘蓚€(gè)額外的參數(shù), 可以獲取關(guān)于接收信號(hào)的更多信息。 */ act.sa_sigaction = &hdl; /* SA_SIGINFO標(biāo)識(shí)告訴sigaction函數(shù)使用sa_sigaction字段,而非sa_handler字段*/ act.sa_flags = SA_SIGINFO; if (sigaction(SIGTERM, &act, NULL) 《 0) { perror (“sigaction”); return 1; } while (1) sleep (10); return 0;}
該示例中使用了三個(gè)參數(shù)版本的信號(hào)處理函數(shù)來響應(yīng)SIGTERM信號(hào),編譯(假設(shè)源文件名為sig.c)并執(zhí)行程序,可以有以下輸出:
gcc -o sig sig.c./sig &kill $!
Sending PID: 16200, UID: 1000
注意,使用三參數(shù)版本信號(hào)處理函數(shù)時(shí),必須將sa_flags字段設(shè)置為SA_SIGINFO,否則信號(hào)處理函數(shù)將無法獲取到正確的siginfo_t對象。
對于siginfo_t結(jié)構(gòu)體,sigaction(2)的手冊中有詳細(xì)介紹,其中的幾個(gè)字段非常有用:
si_code:用于標(biāo)識(shí)信號(hào)的來源,例如kill(2)、raise(3)等通過程序調(diào)用產(chǎn)生的信號(hào),該值為SI_USER;而由內(nèi)核發(fā)送的信號(hào),該值為SI_KERNEL。
對于SIGCHLD信號(hào),可以從si_status字段(進(jìn)程退出碼)、si_utime字段(進(jìn)程消耗的用戶態(tài)時(shí)間)和si_stime字段(進(jìn)程消耗的內(nèi)核態(tài)時(shí)間)獲取更多信息。
對于SIGILL、SIGFPE、SIGSEGV、SIGBUS等信號(hào),可以從si_addr字段獲取發(fā)生錯(cuò)誤的內(nèi)存地址。
常見問題
由于信號(hào)處理函數(shù)是異步執(zhí)行且無法預(yù)知執(zhí)行時(shí)間,因此編碼時(shí)需要特別注意異步執(zhí)行產(chǎn)生的問題,尤其是主函數(shù)和信號(hào)處理函數(shù)之間共享的數(shù)據(jù)。
首先是編譯器優(yōu)化。如果一個(gè)變量在主函數(shù)中循環(huán)讀取,信號(hào)處理函數(shù)中修改(例如一個(gè)退出標(biāo)識(shí)),這時(shí)編譯器優(yōu)化可能導(dǎo)致信號(hào)處理函數(shù)中的修改無法讓主函數(shù)感知到。例如如下代碼:
#include #include #include #include static int exit_flag = 0;static void hdl (int sig){ exit_flag = 1;}int main (int argc, char *argv[]){ struct sigaction act; memset (&act, ‘\0’, sizeof(act)); act.sa_handler = &hdl; if (sigaction(SIGTERM, &act, NULL) 《 0) { perror (“sigaction”); return 1; } while (!exit_flag) ; return 0;}
如果使用gcc O2級(jí)別的優(yōu)化,該程序會(huì)按照預(yù)期,在接收到SIGTERM信號(hào)時(shí)退出。但是,如果優(yōu)化級(jí)別調(diào)整到O3,向進(jìn)程發(fā)送SIGTERM信號(hào)之后,進(jìn)程還會(huì)繼續(xù)運(yùn)行(假設(shè)文件名為test_sig.c):
gcc -o test -O3 test_sig.c./test &killall test
這時(shí)控制臺(tái)不會(huì)提示后臺(tái)進(jìn)程退出,使用jobs命令查看后,test進(jìn)程仍然存在:
jinlingjie@localhost ~/data/Downloads $ 。/test &[1] 2532jinlingjie@localhost ~/data/Downloads $ killall testjinlingjie@localhost ~/data/Downloads $ jobs[1]+ 運(yùn)行中 。/test &
這是因?yàn)樵贠3級(jí)別的優(yōu)化中,編譯器發(fā)現(xiàn)while循環(huán)會(huì)不停讀取exit_flag變量,為了加快讀取速度,編譯器會(huì)把該變量值直接加載到寄存器中,而不再每次從內(nèi)存讀取。此時(shí)信號(hào)處理函數(shù)再修改exit_flag變量,不會(huì)被更新到寄存器中,因此進(jìn)程無法退出。對于這種場景,需要給共享變量增加volatile關(guān)鍵字,以確保進(jìn)程每次讀取變量時(shí),都去內(nèi)存重新獲取最新的值。
上面的示例中的場景,還需要考慮對共享變量修改的原子性。在一些平臺(tái)上int類型的讀取或者寫入可能不是原子的。信號(hào)系統(tǒng)提供sig_atomic_t對象,以確保原子的讀寫。
除此以外,編寫信號(hào)處理函數(shù)還需要注意信號(hào)安全。因?yàn)樾盘?hào)處理函數(shù)調(diào)用的其他函數(shù)也有可能被信號(hào)中斷,signal(7)手冊的Async-signal-safe functions(異步信號(hào)安全函數(shù))章節(jié)詳細(xì)列舉了所有在信號(hào)處理函數(shù)中可以安全調(diào)用的函數(shù)。
特殊信號(hào)處理
SIGCHLD信號(hào)
如果父進(jìn)程不需要獲取子進(jìn)程的退出狀態(tài)碼,也不需要等待子進(jìn)程的退出,唯一的目的是清理僵尸進(jìn)程。那么,父進(jìn)程只需要處理SIGCHLD信號(hào),并進(jìn)行清理即可:
static void sigchld_hdl (int sig){ /* 等待所有已經(jīng)退出的子進(jìn)程。 * 這里使用非阻塞的調(diào)用以防止子進(jìn)程在代碼其他地方被清理。 */ while (waitpid(-1, NULL, WNOHANG) 》 0) { }}
這是一個(gè)簡單的信號(hào)處理函數(shù),如果需要做更多的工作,請?zhí)貏e注意不要使用非異步信號(hào)安全的函數(shù)。
SIGBUS信號(hào)
前面提到過SIGBUS信號(hào)通常是訪問被映射(mmap(2))的內(nèi)存時(shí),無法映射到對應(yīng)文件(通常是文件被截?cái)嗔耍_@種非正常情況下,進(jìn)程的一般行為是直接退出,但是如果一定要處理SIGBUS信號(hào)還是可行的。這時(shí)可以通過sigsetjmp(3)和siglongjmp(3)來跳過發(fā)生錯(cuò)誤的地方,從而讓程序繼續(xù)運(yùn)行。
需要特別注意的是,信號(hào)處理函數(shù)執(zhí)行了siglongjmp(3)調(diào)用之后,代碼沒有繼續(xù)運(yùn)行下去,而是直接跳轉(zhuǎn)到sigsetjmp(3)位置重新開始執(zhí)行。如果此時(shí)代碼仍然持有鎖等資源,將不會(huì)釋放,如果后續(xù)代碼繼續(xù)去競爭鎖,可能會(huì)導(dǎo)致死鎖的發(fā)生。
SIGSEGV信號(hào)
處理SIGSEGV(段錯(cuò)誤)信號(hào)是可能的,但這一般是沒有意義的,因?yàn)榧词勾a重新運(yùn)行了,運(yùn)行到同樣的地方仍然可能發(fā)生段錯(cuò)誤。其中一種重啟程序有效的情況是通過mmap(2)獲取到的內(nèi)存有寫保護(hù),由此產(chǎn)生的SIGSEGV信號(hào)(可以通過信號(hào)處理函數(shù)中的siginfo_t參數(shù)獲取發(fā)生原因),可能可以通過mprotect(2)函數(shù)來去除寫保護(hù)。
如果段錯(cuò)誤是因?yàn)闂?臻g不足導(dǎo)致的,那么這時(shí)將無法通過信號(hào)處理函數(shù)來處理SIGSEGV信號(hào)。因?yàn)樾盘?hào)處理函數(shù)同樣需要分配棧空間來執(zhí)行。這種情況下,可以通過sigaltstack(2)函數(shù)為信號(hào)處理函數(shù)定義獨(dú)立的棧空間。
SIGABRT信號(hào)
試圖處理SIGABRT信號(hào)時(shí),需要了解abort(3)函數(shù)的運(yùn)行原理:該函數(shù)會(huì)先發(fā)送SIGABRT信號(hào),如果該信號(hào)被忽略,或者對應(yīng)的信號(hào)處理函數(shù)正常返回(沒有通過longjmp(3)跳轉(zhuǎn)),它會(huì)將信號(hào)處理函數(shù)重置為默認(rèn)方式,并且重新發(fā)送SIGABRT信號(hào)信號(hào),這將導(dǎo)致進(jìn)程退出。因此,處理SIGABRT信號(hào)的作用可能是在進(jìn)程結(jié)束前做一些最后的操作,或者使用longjmp(3)從新的地方開始執(zhí)行。
信號(hào)和fork()
當(dāng)父進(jìn)程調(diào)用fork(2)函數(shù)創(chuàng)建子進(jìn)程時(shí),子進(jìn)程不會(huì)復(fù)制父進(jìn)程的信號(hào)隊(duì)列,即使此時(shí)父進(jìn)程的信號(hào)隊(duì)列非空,也會(huì)單獨(dú)創(chuàng)建一個(gè)空的信號(hào)隊(duì)列。但是,子進(jìn)程會(huì)繼承父進(jìn)程的所有信號(hào)處理函數(shù)和信號(hào)阻塞狀態(tài)。因此如果父進(jìn)程已經(jīng)完成對信號(hào)的設(shè)置,沒有特殊情況子進(jìn)程無須重新設(shè)置。
信號(hào)和線程
由于POSIX規(guī)范中,所有的一個(gè)進(jìn)程的所有線程都有相同的進(jìn)程ID(PID),向多線程進(jìn)程發(fā)送信號(hào)有兩種情況:
向進(jìn)程發(fā)送信號(hào)(使用類似kill(2)這樣的函數(shù)直接向進(jìn)程發(fā)送信號(hào)):線程可以通過pthread_sigmask(2)單獨(dú)設(shè)置需要阻塞的信號(hào)。因此如果有線程沒有阻塞當(dāng)前發(fā)送的信號(hào),進(jìn)程中的一個(gè)線程會(huì)收到該信號(hào)(但是沒有特殊說明具體哪個(gè)線程會(huì)收到);如果所有的線程都阻塞了當(dāng)前發(fā)送的信號(hào),該信號(hào)會(huì)被加入進(jìn)程的信號(hào)隊(duì)列;如果進(jìn)程沒有設(shè)置當(dāng)前信號(hào)的信號(hào)處理函數(shù),并且該信號(hào)的默認(rèn)行為是終止進(jìn)程,那么整個(gè)進(jìn)程都將被終止。
向特性線程發(fā)送信號(hào)(使用pthread_kill(2)):線程可以通過pthread_kill(2)向進(jìn)程中的其他線程(或者自身)發(fā)送信號(hào),此時(shí)信號(hào)會(huì)發(fā)送到對應(yīng)線程的信號(hào)隊(duì)列中。同時(shí)操作系統(tǒng)也可能會(huì)向特性線程發(fā)送諸如SIGSEGV信號(hào)。如果接收信號(hào)的線程沒有處理對應(yīng)的信號(hào),且該信號(hào)的默認(rèn)行為是終止進(jìn)程,那么該線程所在的進(jìn)程都將被終止。
信號(hào)發(fā)送
向進(jìn)程發(fā)送信號(hào)的方式可以有:
通過鍵盤交互:一些鍵盤的組合鍵,可以向控制臺(tái)正在執(zhí)行的進(jìn)程發(fā)送信號(hào)。
CTRL+C:發(fā)送SIGINT信號(hào),該信號(hào)默認(rèn)行為是終止進(jìn)程。
CTRL+\:發(fā)送SIGQUIT信號(hào),該信好默認(rèn)行為是終止進(jìn)程并核心轉(zhuǎn)儲(chǔ)。
CTRL+Z:發(fā)送SIGSTOP信號(hào),該信號(hào)默認(rèn)行為是掛起進(jìn)程。
kill(2):kill(2)函數(shù)接受兩個(gè)參數(shù),一個(gè)是信號(hào)發(fā)送的進(jìn)程ID,一個(gè)是需要發(fā)送的信號(hào)。其中的進(jìn)程ID有一些特殊的約定。
0:如果PID為0,信號(hào)發(fā)送的目標(biāo)是當(dāng)前進(jìn)程組的所有進(jìn)程。
-1:如果PID為-1,信號(hào)發(fā)送的目標(biāo)是所有(有權(quán)限發(fā)送信號(hào))的進(jìn)程。
《 -1:如果PID小于-1,信號(hào)發(fā)送的目標(biāo)是進(jìn)程ID為-PID的進(jìn)程組。
向進(jìn)程自身發(fā)送信號(hào):進(jìn)程可以通過調(diào)用raise(3)、abort(3)等函數(shù)向自身發(fā)送信號(hào)。
raise(3):可以向進(jìn)程發(fā)送指定信號(hào),需要注意的是,在多線程環(huán)境中,只會(huì)向當(dāng)前線程發(fā)送信號(hào)。
abort(3):向當(dāng)前進(jìn)程發(fā)送SIGABRT信號(hào),前文已經(jīng)提到過,該函數(shù)會(huì)重置信號(hào)處理函數(shù),因此無需關(guān)心進(jìn)程是否已經(jīng)處理了SIGABRT信號(hào)。
sigqueue(2):該函數(shù)和kill(2)函數(shù)類似,但是多了一個(gè)sigval參數(shù)。因此調(diào)用者可以向信號(hào)處理函數(shù)傳遞一個(gè)整數(shù)或者一個(gè)指針。信號(hào)處理函數(shù)可以通過siginfo_t參數(shù)獲取該參數(shù)。
信號(hào)阻塞
有些時(shí)候,我們需要阻塞信號(hào),防止信號(hào)打斷當(dāng)前程序的執(zhí)行,而不是捕獲和處理信號(hào)。傳統(tǒng)的 signal(2)函數(shù)可以通過將信號(hào)處理函數(shù)設(shè)置為SIG_IGN來實(shí)現(xiàn)阻塞的功能。但是該方式已經(jīng)廢棄,建議使用sigprocmask(2)函數(shù)來實(shí)現(xiàn)信號(hào)阻塞功能,因?yàn)樗峁┝烁嗟膮?shù),可以適用于復(fù)雜場景。
一個(gè)簡單的示例:
#include #include #include #include static int got_signal = 0;static void hdl (int sig){ got_signal = 1;}int main (int argc, char *argv[]){ sigset_t mask; sigset_t orig_mask; struct sigaction act; memset (&act, 0, sizeof(act)); act.sa_handler = hdl; if (sigaction(SIGTERM, &act, 0)) { perror (“sigaction”); return 1; } sigemptyset (&mask); sigaddset (&mask, SIGTERM); if (sigprocmask(SIG_BLOCK, &mask, &orig_mask) 《 0) { perror (“sigprocmask”); return 1; } sleep (10); if (sigprocmask(SIG_SETMASK, &orig_mask, NULL) 《 0) { perror (“sigprocmask”); return 1; } sleep (1); if (got_signal) puts (“Got signal”); return 0;}
上述示例展示了通過sigprocmask(2)函數(shù)來阻塞SIGTERM信號(hào)10秒,此時(shí)如果進(jìn)程接收到了SIGTERM信號(hào),會(huì)被加入到進(jìn)程的信號(hào)隊(duì)列中。解除對SIGTERM信號(hào)的阻塞,此時(shí)如果之前的信號(hào)隊(duì)列中有SIGTERM信號(hào),或者新收到了SIGTERM信號(hào),就會(huì)執(zhí)行對應(yīng)的信號(hào)處理函數(shù)。
阻塞信號(hào)使用的一個(gè)場景就是防止信號(hào)的競爭。一些函數(shù)(如select(2)、poll(2))會(huì)阻塞當(dāng)前函數(shù)執(zhí)行,這時(shí)在異常的情況下,這些函數(shù)會(huì)期望通過信號(hào)來中斷當(dāng)前的阻塞操作。但是,如果此時(shí)程序還設(shè)置了其他信號(hào)處理函數(shù),這時(shí)信號(hào)可能會(huì)被設(shè)置的信號(hào)處理函數(shù)消費(fèi),導(dǎo)致阻塞操作的函數(shù)仍然執(zhí)行,無法中斷。
遇到這種情況,就需要使用sigprocmask(2)配合支持重置sigmask的阻塞函數(shù)(如pselect(2)poll(2)),大致的示例代碼片段如下:
sigemptyset (&mask);sigaddset (&mask, SIGTERM);if (sigprocmask(SIG_BLOCK, &mask, &orig_mask) 《 0) { perror (“sigprocmask”); return 1;}while (!exit_request) { /* 如果在這里接收到信號(hào),信號(hào)會(huì)被阻塞, * 直到取消阻塞(下面pselect實(shí)現(xiàn)) */ FD_ZERO (&fds); FD_SET (lfd, &fds); res = pselect (lfd + 1, &fds, NULL, NULL, NULL, &orig_mask); /* 下面繼續(xù)文件描述符操作 */}
后記
本文對Linux/UNIX信號(hào)系統(tǒng)、信號(hào)的處理、發(fā)送、阻塞等做了簡單的介紹。但是整個(gè)信號(hào)系統(tǒng)非常復(fù)雜,還有很多沒有提到的內(nèi)容,期待和大家繼續(xù)交流。
評(píng)論
查看更多