Signal主要分兩大部分:html
A. 什麼是Signal,有哪些Signal,都是幹什麼使的。linux
B. 列舉了很是多不正確(不可靠)的處理Signal的方式,以及怎麼樣設計來避免這些錯誤出現。shell
10.2 Signal Concepts安全
1. Signal的實體就是在頭文件中定義的正整數(在我使用的linux系統中在/usr/include/bits/signum.h中),以下:數據結構
2. 列舉了可能會產生Signal的條件:app
(1)終端的user的案件操做:如,Ctrl+c,Ctrl+\;由terminal發出。異步
(2)硬件異常拋出的Signal:如,除0,非法內存引用;由kernel發出。函數
(3)kill(2) kill(1) :向process或process group發送Signal;由process發出。post
(4)軟件執行時產生的Signal :好比後面要提到的SIGALRM,定點到時信號。ui
3. Signal是典型的異步驅動時事件,當Signal出現的時候,能夠設定程序來告知kernal去作什麼事情:
(1)忽略Signal。這裏有兩個Signal不能忽略:分別是SIGKILL和SIGSTOP。不能忽略的緣由是必須讓kernal或root權限能夠強制終止進程。若是遇到了hardware exception狀況,不去處理這類異常的話,進程後續如何執行就不肯定了。
(2)捕獲Signal。這裏的作法是預先告訴kernal「若是出現了這個信號,用哪一個函數去處理」。另,SIGKILL和SIGSTOP是不能被捕獲的。
(3)默認處理策略。系統預先指定了幾個Signal默認的處理策略。在signum.h中:
具體能夠對照Figure10.1中每一個Signal的默認處理策略去查看;有些默認策略是terminal+core類型的,可用於系統down了以後的debug。可是,也不必定保證core必定會產生;若是權限不足的話,極可能沒法產生core。這個也要留意。
4. 這裏插播一個Signal的解釋。SIGTSTP,當執行終端命令交互的程序到時候,按下ctrl+z能夠觸發這個信號,向foreground process group全部的。這個信號名字裏雖然叫stop,可是並非真的給停了。這裏與stop相對的是continue(即SIGCONT),只是給暫停的意思。這個信號若是隻是望文生義,就容易理解誤差。不只僅是這個信號,後面還有更沒法望文生義的名詞解釋。
10.3 signal function
signal這個函數的含義就是告訴系統用什麼函數處理什麼信號。
1. 看一個例子:
1 #include "apue.h" 2 #include <stdio.h> 3 #include <signal.h> 4 5 static void sig_usr(int signo) 6 { 7 if (signo==SIGUSR1) 8 { 9 printf("received SIGUSR1\n"); 10 } 11 else if (signo==SIGUSR2) 12 { 13 printf("received SIGUSR2\n"); 14 } 15 else 16 { 17 } 18 } 19 20 int main(void) 21 { 22 if (signal(SIGUSR1, sig_usr) == SIG_ERR) 23 { 24 err_sys("can't catch SIGUSR1"); 25 } 26 if (signal(SIGUSR2, sig_usr) == SIG_ERR) 27 { 28 err_sys("can't catch SIGUSR2"); 29 } 30 for (;;) 31 { 32 pause(); 33 } 34 }
執行結果以下:
初步分析以下:
(1)main中給SIGUSR1和SIGUSR2都註冊了signal handler
(2)pause()函數的做用是阻塞進程,並一直等着signal到來。
(3)kill跟它叫kill沒有關係,純粹是unix系統的一個misnomer,它的做用就是向process或process group發送信號。所以就是發送SIGUSR1和SIGUSR2信號。
(4)kill默認狀況下發送的信號是SIGTERM,即終止進程。
若是連續兩次發送SIGUSR1信號會怎樣?以下:
經過上面的運行結果可知,用signal給某個信號註冊一次handler,只能管一次。即,在信號發生跳轉到自定的 handler 處理函數執行後, 系統會自動將此處理函數換回原來系統預設的處理方式。那麼有沒有註冊一次,可以處理屢次信號的方法呢?有,改用sigaction函數去註冊signal handler。代碼以下:
1 #include "apue.h" 2 #include <stdio.h> 3 #include <signal.h> 4 5 static void sig_usr(int signo) 6 { 7 if (signo==SIGUSR1) 8 { 9 printf("received SIGUSR1\n"); 10 } 11 else if (signo==SIGUSR2) 12 { 13 printf("received SIGUSR2\n"); 14 } 15 else 16 { 17 } 18 } 19 20 int main(void) 21 { 22 struct sigaction sa1, sa2; 23 sa1.sa_handler = sig_usr; 24 sa2.sa_handler = sig_usr; 25 sigemptyset(&sa1.sa_mask); 26 sigemptyset(&sa2.sa_mask); 27 sigaddset(&sa1.sa_mask, SIGUSR1); 28 sigaddset(&sa2.sa_mask, SIGUSR2); 29 sigaction(SIGUSR1, &sa1, NULL); 30 sigaction(SIGUSR2, &sa2, NULL); 31 /* 32 if (signal(SIGUSR1, sig_usr) == SIG_ERR) 33 { 34 err_sys("can't catch SIGUSR1"); 35 } 36 if (signal(SIGUSR2, sig_usr) == SIG_ERR) 37 { 38 err_sys("can't catch SIGUSR2"); 39 } 40 */ 41 for (;;) pause(); 42 }
運行結果以下:
Program Start-Up :
這裏面提到了一種狀況,若是是由某個程序經過執行exec產生的新的Program,進而替代原來的Process(雖然進程號不變,可是即便是執行原來的程序,程序的地址也變化了);那麼,由原來的Porcess註冊的各類Signal處理相關的內容,在新的Program中是無效的,由於原來的signal-handler中註冊的信號處理函數的地址失效了,不是原來的函數了。
從下面的部分開始,更多的從反面出發,分析signal處理上經歷的各類不靠譜設計,從而理解爲何要設計各類靠譜的機制。
10.4 Unreliable Signals
1. 不靠譜僞代碼(1):
int sig_int(); /*signal處理函數*/ ... signal(SIGINT, sig_int); /*註冊signal handler*/ ... sig_int() { signal(SIGINT, sig_int); /*從新註冊signal handler以便處理下一次信號*/ ... }
瞭解過signal函數以後,能夠知道,上述代碼的意思大概是:每次出現SIGINT信號,都會觸發sig_int()函數來處理信號;因爲signal函數只能管處理一次SIGINT,所以爲了可以連續處理SIGINT信號,在每次進入sig_int()函數時,都從新用signal函數註冊一下sig_int。
上面的代碼看似是沒有問題的,可是卻隱含着比較大的漏洞。
考慮以下的狀況:若是以前已經來了一個SIGINT信號,系統開始調用註冊的sig_int()進行信號處理;若是偏偏在進入sig_int()函數以前,又來了一個SIGINT信號;因爲此時系統已經沒有用於處理SIGINT信號的函數了,則第二個到來的SIGINT信號就被採用默認的信號處理策略(對於SIGINT這個信號來講,就是直接將進程terminates了),每每達不到咱們預期的信號處理效果。這種漏洞的可怕之處還在於,大部分時間程序都是工做正確的,偶爾會出現問題,這種bug是最難排除的。
2. 不靠譜僞代碼(2):
int sig_int(); /*SIGINT信號處理函數*/ int sig_int_flag; /*SIGINT信號出現時候將其賦值爲零*/ main() { signal(SIGINT, sig_int); /*註冊信號處理函數*/ ... while(sig_int_flay == 0) { pause(); /*等着信號到來*/ } } sig_int() { signal(SIGINT, sig_int); /*從新註冊信號處理函數*/ sig_int_flag = 1; /*修改環境變量*/ }
上述的僞代碼的本意是:main函數中的while循環就要等着SIGINT信號的到來,而且處理完sig_int_flag標誌標量,才往下進行。
代碼的本意是好的,可是仍是存在漏洞。
考慮以下的狀況:若是已經註冊完了sig_int函數,首次進入while循環的判斷條件,變量爲0;而偏偏在執行while循環體中的pause()以前,來了一個SIGINT信號;開始執行sig_int的過程當中已經把sig_int_flag設置爲1了;sig_int執行完畢,開始執行pause()函數;若是之後不再來SIGINT信號了,那麼pause()就一直等下去了。仍是跟上一個不靠譜的代碼問題同樣,這樣的代碼大部分時間能夠正常運行,偶爾出現問題,很是難debug。
總結一下上面兩個不靠譜的代碼,其核心問題我認爲是:signal是異步出現的,隨時都能打斷當前執行的程序;而上述代碼的設計思路都是傳統的同步順序執行的,因此會遇到各類細節問題:
(1)第一種狀況是signal「該來的時候來了,不應來的時候也來了」,即信號處理函數失效。
(2)第二種狀況是signal「該來的時候不來,不應來的時候也來了」,即信號丟失。
10.5 Interrupted System Calls
這個部分闡述與System Call相關的signal處理問題。我沒太理解深刻,暫時記錄如下兩點:
(1)早期的unix系統中,若是某個process正在被「a slow system call」給阻塞;那麼這個時候來個信號,原來正在執行的「a slow system call」就被打斷了,返回一個error而且errno被設置爲EINTR。
(2)爲了不這樣的問題,有的系統提供了處理上述問題的system call restart的機制。可能的作法就是每次執行slow system call的時候,都去檢查error的返回值,是不是EINTR,來決定是否啓動restart。
有個僞代碼以下:
again: if ( (n = read(fd, buf, BUFFSIZE)) < 0 ) { if ( error == EINTR ) goto again; /*被interrupted的system call*/ ... /*處理其餘問題*/ }
在後續的14.4節中,select()和poll()函數的時候還會細說interrupted system calls
10.6 Reentrant Functions (可重入函數)
這部分說的是signal處理中安全問題,是否可重入。(能夠查閱這個bloghttp://particle128.com/posts/2014/05/reentrant.html)
所謂的安全問題,是指signal到來與處理,打斷了原來執行的程序;在執行完信號處理函數後,原來正在執行的程序可能就受到影響了,與預想的結果不太同樣,這就是我理解的signal安全。
書上舉了兩個例子,來講明因爲重入某些函數可能帶來的signal不安全狀況:
(1)若是原進程正執行malloc分配內存呢,分配到一半的時候來個signal信號;而後進入到信號處理函數中,又用到malloc函數動態分配內存了。
(2)若是原進程正執行getpwnam函數呢(簡單理解getpwnam往一個static的存數據),正執行到一半忽然來個signal信號;而後進入到signal信號處理函數中,又調用getpwnam;結果就是上次存一半的結果被新的覆蓋了;再次回到原來的進程順序執行的時候,getpwnam得到結果就亂套了。
上述的例子只是一個熱身,系統些來講,判斷一個signal handler函數是否是reentrant的,通常從signal handler function以下的幾個方面進行考慮:
(1)是否用到了static data:若是信號發生時正調用getwpnam,而且在signal handler中也調用了getwpnam,則在handler中新獲取的值就會覆蓋以前進程中獲取的值。
(2)是否調用了free或者malloc函數:若是信號發生時正調用malloc(修改堆上的存儲空間鏈接表),而且信號處理程序又調用malloc,會破壞內核的數據結構。
(3)是否調用了standard IO:由於好多standard I/O函數都使用了全局的數據結構(如,printf中用到的文件偏移量是全局的)
(4)是否用了longjmp這類的函數:信號發生時候程序正在修改一個數據結構,longjmp這種徹底推倒重來的函數在信號處理中一旦出現,就容易出現改一半就沒改完的狀況
類unix系統中,若是對於signal是安全的,有兩種要求:
(1)首先函數必須是reentrant fucntion標準的(上述的4點),即從自身設計上不要出現signal不安全的漏洞
(2)這些函數執行時已經block各種signal,即從外部影響上主動避開signal出現帶來的問題
另外,書上還提醒了一點:即便是調用Figure10.4中的reentrant function,還須要檢驗errno這個變量值;緣由是,可能在信號處理函數中改變了error的實際值。好比,若是信號處理函數中調用了read(參考interrupted system call的內容),就有可能在信號處理完成後改變error的值。
所以,即便在signal handler中調用的是Figure10.4中的reentrant function,也須要在信號處理函數中檢驗errno的值。
例如,在Figure10.4中有fork函數,它的做用是分叉產生一個child process;當child process執行完了以後,就會產生一個SIGCHLD信號,返回給主進程;而這個信號的signal handler通常都有一個wait function,而wait function改變errno值。
總結一下,之後再設計signal處理函數的時候,必定要注意是不是reentrant的,以及信號處理安全問題。
10.7 SIGCLD Semantics
SIGCLD和SIGCHLD都是與child process結束狀態有關的信號。
這重點介紹的是SIGCLD這個信號,有一些類unix系統對於這個SIGCLD信號的處理是比較特殊的。
(1)有時候,咱們不想關心一些child process的運行情況,就會把SIGCLD信號的處理方式設置爲SIG_IGN。這樣作的好處就是,不會產生殭屍進程,「the status of these child processes is discarded」;可是,若是父進程中不當心有wait()函數正等着這個child process,那就會一直阻塞了。
(2)若是註冊signal handler來處理SIGCLD信號,在調用signal函數的時候,kernel會檢測是否已經有child process正在等待被回收處理。這點特性,也帶來了以下的問題。
有問題的示例代碼以下:#include "apue.h#include <sys/wait.hstatic void sig_cld(int);
int main() { pid_t pid; signal(SIGCLD, sig_cld); if ( (pid=fork()) ==0 ) /*child process*/ { sleep(2); _exit(0); } pause(); exit(0); } static void sig_cld(int signo) {
int status; signal(SIGCLD, sig_cld); /*reestablish handler*/
...
pid = wait(&status); }
main()中的sleep(2)是爲了保證parent process先執行pause(),而後child process再執行_exit(0)。sig_cld()函數中在開始就調用signal(SIGCLD, sig_cld)是爲了可以連續處理SIGCLD信號。
不考慮其餘代碼漏洞,上述代碼在正常邏輯順序上有較大的問題:每次調用signal(SIGCLD, XXX)的時候,kernel就會去check是否有child process等待被回收;因爲此時child process尚未被回收呢,所以kernel認爲還有child process等着被處理,所以再調用sig_cld函數;整個代碼陷入了循環。
總結一下,凡是涉及到SIGCLD以及SIGCHLD的信號處理問題,須要參考具體所在系統對於SIGCLD以及SIGCHLD的實現分析。
10.8 10.9 10.10 kill和alarm函數
三個部分放在一塊兒,用一個例子綜合在一塊兒。
1. kill函數:kill(pid_t pid, int signo),向某個進程發送信號。
這裏注意兩點:
(1)調用kill函數的進程是否有權限給另外一個進程發信號
(2)kill(pid, 0) 這種形式能夠用來驗證pid進程號是否存在;可是考慮到類Unix系統,即便是一個進程的資源已經釋放了,進程號pid仍然會被佔用一段時間,所以這種經過檢測pid號的方式來檢測進程是否存在也不必定是徹底準確的,得看具體的系統實現。
2. alarm函數:alarm(unsigned int seconds),啓動一個倒計時器,到達seconds的時間後,會觸發SIGALRM的信號;若是沒有設定信號處理函數,則默認的行爲就是將進程結束。
這裏注意兩點:
(1)一個進程只能同時有一個有效的alarm函數,若是在一個進程中屢次使用alarm函數:後一次的alarm時間會替代上一次的alarm的時間。具體示例以下:
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <sys/time.h> struct timeval start, end; void sig_int() { unsigned int seconds = 0; seconds = alarm(5); printf("left seconds: %u\n", seconds); gettimeofday(&start, NULL); pause(); } void sig_alm() { float time_interval; gettimeofday(&end, NULL); time_interval = 1000000*(end.tv_sec-start.tv_sec)+end.tv_usec-start.tv_usec; time_interval = time_interval/1000000; printf("Pasue time: %f\n", time_interval); } int main() { signal(SIGINT, sig_int); signal(SIGALRM, sig_alm); alarm(100); pause(); }
程序執行結果如圖:
分析以下:main中執行倒計時100秒;大概過了10秒以後在終端ctrl+c觸發interrupt信號,進入SIGINT信號處理函數;在SIGINT信號處理函數中,修改alarm的倒計時爲5秒。最後從執行結果看到,第二次執行的alarm(5)替代了前一次的alarm(100)。
有時候爲了不以前設定的alarm無效了,也能夠檢查相似上述代碼中的seconds的值,來保證等夠100秒。
(2)在某些狀況下,若是產生了資源競爭,調用pause()以前,alarm就執行完了,pasue()就會一直在等着了。爲了不這種狀況,書上給出了以下的sleep函數的設計。我在書上代碼的基礎上稍加修改,爲的就是在代碼中更好的體現資源競爭。
#include "apue.h" #include <signal.h> #include <unistd.h> #include <setjmp.h> static jmp_buf env_alrm; static void sig_alm(int signo) { longjmp(env_alrm, 1); } unsigned int sleep2(unsigned int second) { if (signal(SIGALRM, sig_alm) == SIG_ERR){ return second; } if ( setjmp(env_alrm)==0 ) { alarm(second); kill(getpid(),SIGINT); pause(); } return alarm(0); } static void sig_int(int signo) { int i,j; volatile int k; printf("sig_int starting\n"); for ( i=0; i<300000; i++) { for (j=0; j<40000; j++) k += i*j; } printf("sig_int finished\n"); } int main() { unsigned int unslept; if (signal(SIGINT, sig_int)==SIG_ERR) { err_sys("signal(SIGINT) error"); } unslept = sleep2(5); printf("sleep2 returned : %u\n", unslept); exit(0); }
程序的執行結果以下:
分析以下:
a. 上述代碼體現了setjmp longjmp可以在資源競爭條件下對alarm和pause對進行保護。
上述代碼在main()中調用sleep2()函數。
進入到sleep2()函數以後,先註冊一個SIGALRM的信號處理函數 → 再在setjmp保護下,alarm(second)設定倒計時 → 隨後立刻執行kill命令,向進程自身發送一個SIGINT信號,觸發sig_int函數 → sig_int函數中執行的任務遠超過alarm(second)中second的限制,爲的就是模擬資源競爭時alarm已經倒計時完畢的時候,pause()還沒開始。
因爲sleep2()中保護機制的存在,若是因爲資源競爭致使「alarm已經執行完畢,可是pause還沒開始」,longjmp就直接跳到setjmp的地方了,而且返回的值爲1;這樣就跳過了pause()的語句,天然也就不會無限期的等待了。
b. 上述代碼也體現了setjmp longjmp這種機制的隱患。
加入不是人爲產生資源競爭,而是進程真的在執行某些任務;這個時候因爲alarm到時,強制longjmp結束,頗有可能形成其餘任務還沒處理完成,就直接longjmp結束了。
總結一下,要慎用setjmp和longjmp這樣的長跳起色制。
10.11 10.12 10.13 sigset_t & sigprocmask & sigpending
1. sigset_t
有時候咱們須要設定,一個進程要屏蔽哪些信號、要接受哪些信號、記錄進程原來對信號的設定情況等。這個時候,就須要一種信號集合的數據結構,以及圍繞其的一些周邊函數來完成。其中數據結構就是sigset_t。
2. int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset)
設定信號mask。
how : 修改信號mask的方式,SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK
ret : 信號mask被設定成設麼樣
oret : 保留原來的信號mask
返回值表示函數執行成功或失敗。
3. int sigpending(sigset_t *set)
得到當前進程有哪些信號被pending了。
看一個例子:
#include "apue.h" static void sig_quit(int signo) { printf("caught SIGQUIT\n"); signal(SIGQUIT, SIG_DFL); } int main() { sigset_t newmask, oldmask, pendmask; signal(SIGQUIT, sig_quit); /*捕捉退出信號*/ sigemptyset(&newmask); /*初始化新的信號mask*/ sigaddset(&newmask, SIGQUIT); /*在newmask中添加退出信號*/ sigprocmask(SIG_BLOCK, &newmask, &oldmask); /*阻塞SIGQUIT信號*/ sleep(5); sigpending(&pendmask); /*得到有當前進程被阻塞的信號*/ if (sigismember(&pendmask, SIGQUIT)) { printf("\nSIGQUIT pending\n"); } sigprocmask(SIG_SETMASK, &oldmask, NULL); /*釋放被阻塞的信號*/ printf("SIGQUIT unblocked\n"); sleep(5); exit(0); }
代碼執行結果以下:
分析以下:
(1)上述代碼首先用sigprocmask阻塞了SIGQUIT信號;通過第一個sleep(5)以後,立刻執行sigpending,得到了當前被阻塞的信號,而且獲得了驗證SIGQUIT確實在被阻塞的信號集合pendmask中。
(2)緊接着再用sigprocmask函數釋從新恢復了信號mask的值,即釋放被阻塞的信號;隨後,以前被阻塞的SIGQUIT信號立刻就進來了,而且觸發了sig_quit信號處理函數;注意,在信號處理函數中恢復了對SIGQUIT的默認處理方式。
(3)即便第一個sleep(5)的時候輸入了多個ctrl+\輸入信號,最終被處理的SIGQUIT信號也只有一個(最起碼在我使用的Linux系統上是這樣的)。
在上述代碼基礎上再擴展一下,若是多個不一樣的信號被pending住,當unblock的以後,會有什麼效果呢?將書上的代碼修改以下:
#include "apue.h" static void sig_quit(int signo) { printf("caught SIGQUIT\n"); signal(SIGQUIT, SIG_DFL); } static void sig_int(int signo) { printf("caught SIGINT\n"); signal(SIGINT, SIG_DFL); } int main() { sigset_t newmask, oldmask, pendmask; signal(SIGQUIT, sig_quit); /*捕捉退出信號*/ signal(SIGINT, sig_int); /*捕捉中斷信號*/ sigemptyset(&newmask); /*初始化新的信號mask*/ sigaddset(&newmask, SIGQUIT); /*在newmask中添加退出信號*/ sigaddset(&newmask, SIGINT); /*阻塞interrupt信號*/ sigprocmask(SIG_BLOCK, &newmask, &oldmask); /*阻塞SIGQUIT信號*/ sleep(5); sigpending(&pendmask); /*得到有當前進程被阻塞的信號*/ if (sigismember(&pendmask, SIGQUIT)) { printf("\nSIGQUIT pending\n"); } if (sigismember(&pendmask, SIGINT)) { printf("\nSIGINT pending\n"); } sigprocmask(SIG_SETMASK, &oldmask, NULL); /*釋放被阻塞的信號*/ printf("SIGQUIT unblocked\n"); printf("SIGINT unblocked\n"); sleep(5); exit(0); }
不只阻塞了SIGQUIT信號,並且還阻塞SIGINT信號。再執行代碼以下:
分析以下:
(1)能夠看到,對於同類的信號被阻塞信號,只獲取一個;對於不一樣類的阻塞信號,能夠把不一樣類等待的信號分別處理了。
(2)對比兩次運行程序,雖然輸入信號的順序是不一樣的,可是執行信號處理函數的順序卻沒有改變。
針對以上兩點內容,google了一下相關資料:這個blog的解釋很是好http://galex.cn/【apue】信號/
上述的信號是經典信號,不支持排隊,並且信號的響應順序和信號到來的順序根本沒有關係。恩,暫時這樣理解。
10.14 10.15 sigaction Function & sigsetjmp siglongjmp Function
1. int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact)
本質上用於信號處理函數的註冊,signal(2)的增強改進版。執行成功返回0,出錯返回-1。
signo : 要處理的信號
act : 對信號處理函數的新設定
oact : 以前註冊的signo信號處理函數的信息
其中sigaction是結構體變量,裏面不只存放了信號處理函數,並且還有須要屏蔽掉的信號集合,以及其餘信息。
struct sigaction{
void (*sa_handler) (int); /*信號處理函數*/
sigset_t sa_mask; /*須要屏蔽的信號*/
...
}
這裏的屏蔽信號指的是執行信號處理函數前但願屏蔽掉的信號;當信號處理函數執行完並return的時候,信號mask就會恢復到以前的狀態。
回想以前在signal handler中從新調用signal的例子,應用sigaction函數就能夠作到「Hence, we are guaranteed that whenever we are processing a given signal, another occurrence of that same signal is blocked until we're finished processing the first occurrence」
另外,用sigaction函數註冊的信號處理函數,只要註冊一次就一直生效;除非顯式修改該信號的處理函數。
2. sigsetjmp & siglongjmp
int sigsetjmp(sigjmp_buf env, int savemask)
返回0表明是設置jmp點;返回非零值表明從別處跳回來,具體是什麼值在跳的時候決定。
env : 目前還不詳
savemask : 執行跳轉以前的信號mask值
void siglongjmp(sigjmp_buf env, int val)
跳到sigsetjmp的地方,其中val要求是非零值,即sigsetjmp的返回值。
以前已經有setjmp和longjmp這種終極長跳轉函數對了,爲何還要有sigsetjmp和siglongjmp函數對呢?
考慮下面這種狀況:
a. 假設進入到了信號處理函數中(此時,假設同類信號已經被blocked了),該類信號已經在mask中設置爲blocked了;
b. 若是signal handler被順利執行完,這時信號mask就恢復到執行signal handler以前的狀態了
c. 可是,若是signal handler執行到中間,執行到了longjmp語句了,極可能就把信號mask恢復這個環節就個跳過去了
d. 信號mask沒有恢復的問題就是,原本同類信號能夠再繼續被處理,可是因爲沒有恢復信號mask,就不能被處理了
上面的狀況,也是sigsetjmp和siglongjmp的設計初衷。
下面看一個例子(基於書上Figure 10.20的示例改造的),實際體會一下sigsetjmp和siglongjmp的函數對的做用:
#include "apue.h" #include <setjmp.h> #include <time.h> #include <errno.h> static sigjmp_buf jmpbuf; static volatile sig_atomic_t canjmp; /*得到當前進程是否mask了某種信號*/ void pr_mask(const char *str) { sigset_t sigset; int errno_save; errno_save = errno; sigemptyset(&sigset); if ( sigprocmask(0,NULL,&sigset)<0 ) //得到當前的signal set狀況 { err_ret("sigprocmask error"); errno = errno_save; return; } printf("%s",str); //打印調用信息title if ( sigismember(&sigset, SIGINT) ) printf(" SIGINT"); if ( sigismember(&sigset, SIGQUIT) ) printf(" SIGQUIT"); if ( sigismember(&sigset, SIGUSR1) ) printf(" SIGUSR1"); if ( sigismember(&sigset, SIGALRM) ) printf(" SIGALRM"); printf("\n"); errno = errno_save; } /*SIGUSR1的信號處理函數*/ void sig_usr1(int signo) { time_t starttime; if (canjmp==0) return; pr_mask("starting sig_usr1: "); alarm(3); starttime = time(NULL); for (; ;) /*busy等待5秒*/ if (time(NULL)>starttime+5) break; pr_mask("finishing sig_usr1: "); canjmp = 0; siglongjmp(jmpbuf,1); /*longjmp(jmpbuf,1);*/ } /*SIGALRM信號處理函數*/ void sig_alarm(int signo) { pr_mask("in sig_alrm: "); } int main() { struct sigaction siga1,siga2; siga1.sa_handler = sig_usr1; sigemptyset(&siga1.sa_mask); sigaddset(&siga1.sa_mask, SIGUSR1); siga2.sa_handler = sig_alarm; sigemptyset(&siga2.sa_mask); sigaddset(&siga2.sa_mask, SIGALRM); sigaction(SIGUSR1, &siga1, NULL); sigaction(SIGALRM, &siga2, NULL); pr_mask("staring main: "); if (/*setjmp(jmpbuf)*/sigsetjmp(jmpbuf,1)) { pr_mask("ending main: "); exit(0); } canjmp = 1; for(; ;) pause(); }
程序運行結果以下:
(1)用sigsetjmp和siglongjmp函數對的運行結果:
(2)用setjmp和longjmp函數對的運行結果:
對比以上兩個運行結果能夠看到,sigsetjmp和siglongjmp函數對確實很好地保護了信號mask現場,不會由於jmp的動做就致使某些信號mask位沒有被恢復過來。(另,在我運行的系統上,若是直接用signal還不太行,必須用sigaction函數才能夠得到上述的結果;緣由就是系統的signal沒有實現同類信號屏蔽)
10.18 system Function
這個部分主要說system這個函數與signal相關的內容:POSIX.1標準要求system必須忽略SIGINT和SIGQUIT信號,屏蔽SIGCHLD信號。
下面用正反兩個例子來講明爲何有上述的要求。
例子一
#include <unistd.h> #include <signal.h> #include <stdio.h> #include <errno.h> int system(const char *cmdstring) /* version without signal handling */ { pid_t pid; int status; if (cmdstring == NULL) return(1); /* always a command processor with UNIX */ if ((pid = fork()) < 0) { status = -1; /* probably out of processes */ } else if (pid == 0) { /* child */ execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); _exit(127); /* execl error */ } else { /* parent */ while (waitpid(pid, &status, 0) < 0) { if (errno != EINTR) { status = -1; /* error other than EINTR from waitpid() */ break; } } } return(status); } static void sig_int(int signo) { printf("caught SIGINT\n"); } static void sig_chld(int signo) { printf("caught SIGCHILD\n"); } static void sig_usr1(int signo) { printf("caught SIGUSR1\n"); } int main() { signal(SIGINT, sig_int); signal(SIGCHLD, sig_chld); signal(SIGUSR1, sig_usr1); system("/bin/ed"); }
程序運行結果以下:
分析以下:
-------------插播一段ed的背景知識-------------
首先須要補充一下/bin/ed這個程序有關信號的特色:
(1)ed捕獲SIGINT和SIGQUIT信號
(2)對於SIGINT信號:prints a question mask
(3)對於SIGQUIT信號的處理方式:ignore
爲了驗證ed的特性,我作了以下的試驗:
兩個窗口是tmux開的,左側是調用/bin/ed的窗口;右側的是發送signal用的(另,不知道tmux的,能夠參考個人這篇blog)
能夠看到開啓ed程序後,向其發送SIGINT信號,真的再終端反饋輸出一個?;向其發送SIGQUIT信號,並麼有什麼反應。
------------------------插播結束-------------------------
如今開始切入正題。
先分析運行結果
(參考http://blog.csdn.net/windeal3203/article/details/39049291和http://blog.csdn.net/ctthuangcheng/article/details/9258715):
(1)終端輸入ctrl+c,前臺進程都會受到這個信號;這裏的前臺進程包括ed shell a.out三個進程(其中shell自動忽略SIGINT信號,不討論)
(2)ed在收到SIGINT後,天然會輸出一個?;而a.out中註冊了SIGINT的處理函數,所以也會觸發信號處理函數
(3)輸入q以後,ed退出,發送一個SIGCHLD信號;因爲a.out是ed的父進程(看紅框中的pid和ppid),因此天然收到SIGCHLD信號,並觸發信號處理函數
請注意,上述的system函數是去除了signal設定的閹割版,並無符合POSIX.1的標準。
咱們須要一直記着:這個/bin/ed是由system函數調用的,這樣帶來的問題有兩個:
(1)因爲終端正在運行的是/bin/ed程序,輸入的ctrl+c的想法是給ed的,並非給a.out的,a.out錯誤接收到了發給ed的SIGINT了;這個時候若是a.out還有其餘的任務要執行,而且沒有處理SIGINT的機制,就被強制關閉了。
(2)當ed執行完畢退出的時候,至關於child process結束,所以會給parent process的a.out發送一個SIGCHLD信號;其實這個ed一旦由system函數執行上,就跟a.out沒有太大關係了,a.out錯誤接收了發給ed的SIGCHLD了,認爲是本身的進程執行完畢了。
我我的理解,system function雖說某種程度上方便了啓動某些程序;但經過上述的分析,system Function也是一個挺彆扭的事情。要想知作別扭的緣由,須要參考一下system的實現(不考慮signal版):
int system(const char *cmdstring) /* version without signal handling */ { pid_t pid; int status; if (cmdstring == NULL) return(1); /* always a command processor with UNIX */ if ((pid = fork()) < 0) { status = -1; /* probably out of processes */ } else if (pid == 0) { /* child */ execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); _exit(127); /* execl error */ } else { /* parent */ while (waitpid(pid, &status, 0) < 0) { if (errno != EINTR) { status = -1; /* error other than EINTR from waitpid() */ break; } } } return(status); }
(1)顯然system的實現利用fork和exec兩個重要的函數:fork就是分叉生成一個child process;exec的做用是讓child process用shell運行程序;能夠說system利用了fork和exec實現了極大的代碼便利
(2)可是fork和exec必然會對parent process有影響,好比上面提到的信號問題。
上述兩個方面(1)簡化了工做,(2)複雜了工做,因此說system Function是個彆扭的函數。
例子二
給出另外一個system實現(在原書的代碼上作了一些修改,主要是方便顯示),代碼以下:
#include <sys/wait.h> #include <errno.h> #include <signal.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> static void sig_int(int signo) { printf("caught SIGINT\n"); } static void sig_chld(int signo) { printf("caught SIGCHILD\n"); } int i_system(const char *cmdstring) /* with appropriate signal handling */ { pid_t pid; int status; struct sigaction ignore, saveintr, savequit; sigset_t chldmask, savemask; if (cmdstring == NULL) return(1); /* always a command processor with UNIX */ ignore.sa_handler = SIG_IGN; /* ignore SIGINT and SIGQUIT */ sigemptyset(&ignore.sa_mask); ignore.sa_flags = 0; if (sigaction(SIGINT, &ignore, &saveintr) < 0) return(-1); if (sigaction(SIGQUIT, &ignore, &savequit) < 0) return(-1); sigemptyset(&chldmask); // now block SIGCHLD sigaddset(&chldmask, SIGCHLD); if (sigprocmask(SIG_BLOCK, &chldmask, &savemask) < 0) return(-1); if ((pid = fork()) < 0) { status = -1; /* probably out of processes */ } else if (pid == 0) { /* child */ /* restore previous signal actions & reset signal mask */ sigaction(SIGINT, &saveintr, NULL); sigaction(SIGQUIT, &savequit, NULL); sigprocmask(SIG_SETMASK, &savemask, NULL); execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); _exit(127); /* exec error */ } else { /* parent */ while (waitpid(pid, &status, 0) < 0) if (errno != EINTR) { printf("error\n"); status = -1; /* error other than EINTR from waitpid() */ break; } } printf("ed ends\n"); /* restore previous signal actions & reset signal mask */ if (sigaction(SIGINT, &saveintr, NULL) < 0) return(-1); if (sigaction(SIGQUIT, &savequit, NULL) < 0) return(-1); if (sigprocmask(SIG_SETMASK, &savemask, NULL) < 0) return(-1); printf("return from system\n"); return(status); } int main() { signal(SIGINT, sig_int); signal(SIGCHLD, sig_chld); i_system("/bin/ed"); printf("after ed\n"); }
代碼執行結果:
結果分析:
(1)當終端輸入q以後,ed進程結束,並向父進程發送SIGCHLD信號
(2)可是因爲i_system執行過程當中屏蔽了SIGCHLD信號,所以ed結束後先被主進程waitpid收屍,這個時候父進程a.out是不會收到SIGCHLD信號的
(3)最後,當全部與ed相關的內容都處理完了以後,最後一個sigprocmask放開SIGCHLD信號的阻塞
(4)以前因爲ed進程結束帶來的SIGCHLD信號立刻被處理了,觸發sig_chld信號處理函數
能夠看到,因爲system忽略了SIGINT和SIGQUIT信號,在system的執行過程當中,不會因爲終端發出ctrl+c就會影響父進程a.out的運行;而且因爲屏蔽了SIGCHLD信號,不會使得父進程誤收到SIGCHLD信號;只有當所system中執行的全部內容都結束後,纔會釋放對SIGCHLD的阻塞。
system這個函數考慮的問題太多,實際用到的時候要把與signal相關的東西考慮清楚。