?
1.信號是什么?
信號其實就是一個軟件中斷。
例:
輸入命令,在 Shell 下啟動一個前臺進程。
用戶按下 Ctrl-C,鍵盤輸入產生一個硬件中斷。
如果 CPU 當前正在執行這個進程的代碼,則該進程的用戶空間代碼暫停執行, CPU 從用戶態切換到內核態處理硬件中斷。
終端驅動程序將 Ctrl-C 解釋成一個 SIGINT 信號,記在該進程的 PCB 中(也可以說發送了一個 SIGINT 信號給該進程)。
當某個時刻要從內核返回到該進程的用戶空間代碼繼續執行之前,首先處理 PCB 中記錄的信號,發現有一個 SIGINT 信號待處理,而這個信號的默認處理動作是終止進程,所以直接終止進程而不再返回它的用戶空間代碼執行。
在這個例子中,由 ctrl+c 產生的硬件中斷就是一個信號。Ctrl+C 產生的信號只能發送給前臺進程,命令后加 & 就可放到后臺運行。Shell 可同時運行一個前臺進程和任意多個后臺進程,只有前臺進程才能接受到像 CTRL+C 這種控制鍵產生的信號。
2.信號的種類
使用命令查看:
?
kill?-l
?
非可靠信號:1~31 號信號,信號可能會丟失 可靠信號:34~64 號信號,信號不可能丟失
SIGHUP:1 號信號,Hangup detected on controlling terminal or death of controlling process(在控制終端上掛起信號,或讓進程結束),ation:term
SIGINT:2 號信號,Interrupt from keyboard(鍵盤輸入中斷,「ctrl + c」 ),action:term
SIGQUIT:3 號信號,Quit from keyboard(鍵盤輸入退出「ctrl+ |」 ),action:core,產生 core dump 文件
SIGABRT:6 號信號,Abort signal from abort(3)(非正常終止,「double free」),action:core
SIGKILL:9 號信號,Kill signal(殺死進程信號),action:term,該信號不能被阻塞、忽略、自定義處理
SIGSEGV:11 號信號,Invalid memory reference(無效的內存引用,解引用空指針、內存越界訪問),action:core
SIGPIPE:13 號信號,Broken pipe: write to pipe with no readers(管道中止: 寫入無人讀取的管道,會導致管道破裂),action:term
SIGCHLD:17 號信號,Child stopped or terminated(子進程發送給父進程的信號,但該信號為忽略處理的)
SIGSTOP:19 號信號,Stop process(停止進程),action:stop
SIGTSTP:20 號信號,Stop typed at terminal(終端上發出的停止信號,「ctrl + z」),action:stop
具體的信號采取的動作和詳細信息可查看:「man 7 signal」
3.信號的產生
3.1 硬件產生
硬件產生即通過終端按鍵產生的信號:
ctrl + c:SIGINT(2),發送給前臺進程,& 進程放到后臺運行,fg 把剛剛放到后臺的進程,再放到前臺來運行
ctrl + z:SIGTSTP(20),一般不用,除非有特定場景
ctrl + | :SIGQUIT(3),產生 core dump 文件
產生 core dump 文件的條件:
?
當前OS一定不要限制core?dump文件的大小,ulimit?-a 磁盤空間要足夠 如何產生: 3.1?解引用空指針,收到11號信號,產生core?dump文件 3.2?內存訪問越界,程序一旦崩潰,就會收到11號信號,也就會產生core?dump文件 3.3 double free,收到6號信號,并產生core dump。 3.4?free(NULL),不會崩潰
?
3.2 軟件產生
軟件產生即調用系統函數向進程發信號
kill 函數
?
#include?#include? int?kill(pid_t?pid,?int?sig); 參數解釋: pid:進程號 sig:要發送的信號值 返回值:成功返回0,失敗返回-1,并設置錯誤
?
kill 命令:kill -[信號] pid,
abort:void abort(void);,收到 6 號信號,誰調用該函數,誰就收到信號
alarm:unsigned int alarm(unsigned int seconds);,收到 14 號信號,告訴內核在 seconds 秒后給進程發送 SIGALRM 信號,該信號默認處理動作為終止當前進程。
4.信號的注冊
信號注冊又分為可靠信號的注冊和非可靠信號的注冊。信號注冊實際上是一個位圖和一個 sigqueue 隊列。
4.1 非可靠信號的注冊
當進程收到非可靠信號時:
將非可靠信號對應的比特位置為 1
添加 sigqueue 節點到 sigqueue 隊列當中,但是,在添加 sigqueue 節點的時候,隊列當中已然有了該信號的 sigqueue 節點,則不添加
4.2 可靠信號的注冊
當進程所受到可靠信號時:
在 sig 位圖中更改信號對應的比特位為 1 不論之前 sigqueue 隊列中是否存在該信號的 sigqueue 節點,都再次添加 sigqueue 節點到 sigqueue 隊列當中去
5.信號的注銷
5.1 非可靠信號的注銷
信號對應的比特位從 1 置為 0 將該信號的 sigqueue 節點從 sigqueue 隊列當中進行出隊操作
5.2 可靠信號的注銷
將該信號的 sigqueue 節點從 sigqueue 隊列當中進行出隊操作 需要判斷 sigqueue 隊列當中是否還有相同的 sigqueue 節點:①沒有了:信號比特位從 1 置為 0 ②還有:不會更改 sig 位圖中的比特位
6.信號阻塞
6.1 信號是怎樣阻塞的?
?
信號的阻塞,并不會干擾信號的注冊。信號能注冊,但不能被立即處理, 將 block 位圖中對應的信號比特位置為 1,表示阻塞該信號 進程收到該信號,還是一如既往的注冊 當進程進入到內核空間,準備返回用戶空間的時候,調用 do_signal 函數,就不會立即去處理該信號了 當該信號不被阻塞后,就可以進行處理了
?
6.2sigprocmask
函數原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 參數解釋:
?
how,該做什么樣的操作 SIG_BLOCK:設置信號為阻塞 SIG_UNBLOCK:解除信號阻塞 SIG_SETMASK:替換阻塞位圖 set:用來設置阻塞位圖 SIG_BLOCK:設置某個信號為阻塞,block(new)?= block(old)?|?set SIG_UNBLOCK:解除某個信號阻塞,block(new)= block(old)?&?(~set) SIG_SETMASK:替換阻塞位圖,block(new)=?set oldset:原來的阻塞位圖
?
例:下述例子,信號全部被阻塞,采用 kill -9,將該進程結束掉
?
#include?#include? #include? void?signcallback(int?signumber) { ??printf("change?the?signal?%d ",signumber); } int?main() { ??sigset_t?set; ??sigset_t?oldset; ??sigfillset(&set);//所有比特位全置為1,則信號全部會被阻塞 ??sigprocmask(SIG_BLOCK,&set,&oldset); ??while(1) ??{ ????sleep(1); ??} ??return?0; }
?
結果:此時發送信號是不會有作用的,采用 kill -9 強殺掉
7.信號未決
7.1 未決概念
實際執行信號的處理動作稱為信號遞達(Delivery),信號從產生到遞達之間的狀態,稱為信號未決(Pending)。進程可以選擇阻塞(Block)某個信號。被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞,才執行遞達的動作。注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是、在遞達之后可選的一種處理動作。
7.2 sigpending
函數原型:int sigpending(sigset_t *set); 讀取當前進程的未決信號集,通過 set 參數傳出。調用成功返回 0,出錯返回 - 1.
例:
?
#include?#include? #include? void?signalcallback(int?signumber) { ??printf("chang?signumber?%d ",signumber); } void?printsigset(sigset_t?*set) { ??int?i?=?0; ??for(;i?32;i++) ??{ ????if(sigismember(set,i)) ????{ ??????putchar('1'); ????} ????else{ ??????putchar('0'); ????} ??} } int?main() { ??signal(2,signalcallback); ??signal(10,signalcallback); ??sigset_t?set; ??sigset_t?oldset; ??sigset_t?pending; ??sigfillset(&set);//所有比特位全部置為1,則信號會全部被阻塞 ??sigprocmask(SIG_BLOCK,&set,&oldset); ??while(1) ??{ ????sigpending(&pending); ????printsigset(&pending); ????sleep(1); ??} ??return?0; }
?
結果:
8.信號的處理方式
每個信號都有兩個標志位分別表示阻塞和未決,還有一個函數指針表示處理動作。
?
在上述例子中:
SIGHUP 信號未阻塞也未產生過,當它遞達時執行默認處理動作。
SIGINT 信號產生過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒有解除阻塞之前不能忽略這個信號,因為進程仍有機會改變處理動作之后再解除阻塞。
SIGQUIT 信號未產生過,一旦產生 SIGQUIT 信號將被阻塞,它的處理動作是用戶自定義函數 sighandler。
8.1signal 函數
該函數可以更改信號的處理動作。
?
typedef?void?(*sighandler_t)(int); sighandler_t?signal(int?signum,?sighandler_t?handler); 參數解釋: signum:更改的信號值 handler:函數指針,要更改的動作是什么
?
實際上,該函數內部也調用了 sigaction 函數。
8.2sigaction 函數
讀取和修改與指定信號相關聯的處理動作。
?
int?sigaction(int?signum,?const?struct?sigaction?*act,?struct?sigaction?*oldact);
?
參數解釋:
?
signum:待更改的信號值
?
struct sigaction 結構體:
?
void?????(*sa_handler)(int);//函數指針,保存了內核對信號的處理方式 void?????(*sa_sigaction)(int,?siginfo_t?*,?void?*);// sigset_t???sa_mask;//保存的是當進程在處理信號的時候,收到的信號 int????????sa_flags;//SA_SIGINFO,OS在處理信號的時候,調用的就是sa_sigaction函數指針當中 //保存的值0,在處理信號的時候,調用sa_handler保存的函數 void?????(*sa_restorer)(void);
?
例:
?
#include?#include? #include? void?signcallback(int?signumber) { ??printf("change?signumber?%d ",signumber); } int?main() { ??struct?sigaction?act;//act為入參 ??sigemptyset(&act.sa_mask); ??act.sa_flags?=?0; ??act.sa_handler?=?signcallback; ??struct?sigaction?oldact;//oldact為出參 ??sigaction(3,&act,&oldact); ??while(1) ??{ ????sleep(1); ??} ??return?0; }
?
結果:
8.3 自定義信號處理的流程
「task_struct」 結構體中有一個「struct sighand_struct」 結構體。
「struct sighand_struct」 結構體有一個 「struct k_sigaction action[_NSIG]」 結構體數組。
該數組中,其中的 「_sighandler_t sa_handler」 保存的是信號的處理方式,通過改變其指向,可以實現我們對自定義信號的處理。
9.信號的捕捉
9.1 信號捕捉的條件
如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這就稱為信號捕捉。
9.2 信號捕捉流程
內核態返回用戶態會調用 do_signal 函數,兩種情況:
無信號:sys_return 函數,返回用戶態
有信號:先處理信號,信號返回,再調用 do_signal 函數 例:
程序注冊了 SIGQUIT 信號的處理函數 sighandler。
當前正在執行 main 函數,這時發生中斷或異常切換到內核態。
在中斷處理完畢后要返回用戶態的 main 函數之前檢查到有信號 SIGQUIT 遞達。
內核決定返回用戶態后不是恢復 main 函數的上下文繼續執行,而是執行 sighandler 函數, sighandler 和 main 函數使用不同的堆棧空間,它們之間不存在調用和被調用的關系,是兩個獨立的控制流程。
sighandler 函數返回后自動執行特殊的系統調用 sigreturn 再次進入內核態。
如果沒有新的信號要遞達,這次再返回用戶態就是恢復 main 函數的上下文繼續執行了。
10.常用信號集操作函數
?
int?sigemptyset(sigset_t?*set);://將比特位圖全置為0 int?sigfillset(sigset_t?*set);//將比特位圖全置為1 int?sigaddset(sigset_t?*set,?int?signum);//將該set位圖,多少號信號置為1 int?sigdelset(sigset_t?*set,?int?signum);//將該set位圖,多少號信號置為0 int?sigismember(const?sigset_t?*set,?int?signum);//信號signum是否是set位圖中的信號
?
11.SIGCHLD 信號
該信號是子進程在結束是發送給父進程的信號,但是該信號的處理方式是默認處理的。父進程對子進程發送過來的 SIGCHLD 信號進行了忽略處理,就會導致子進程成為僵尸進程。
可以自定義該信號的處理方式:
?
#include?#include? #include? #include? #include? #include? void?signcallback(int?signumber) { ??printf("change?signal?%d ",signumber); ??wait(NULL); } int?main() { ??signal(17,signcallback); ??pid_t?pid?=?fork(); ??if(pid?0) ??{ ????perror("fork"); ????return?-1; ??} ??else?if(pid?==?0) ??{ ????printf("I?am?child "); ????sleep(1); ????exit(12); ??} ??else{ ????while(1) ????{ ??????sleep(1); ????} ??} ??return?0; }
?
指令查看后臺:「ps aux | grep ./fork」
評論
查看更多