在 Linux 的世界里,進(jìn)程就像生活在城市中的人,,它們需要相互溝通來(lái)協(xié)調(diào)行動(dòng),。而信號(hào)機(jī)制呢,就像是一種神奇的 “信號(hào)彈”,,用于進(jìn)程之間的交流,。當(dāng)一個(gè)進(jìn)程有重要消息要傳達(dá)給另一個(gè)進(jìn)程時(shí),就會(huì)發(fā)射出這樣的 “信號(hào)彈”,。這就是 Linux 信號(hào)機(jī)制,,它是管理進(jìn)程間通信的一把 “金鑰匙”,讓我們一起深入了解它是如何發(fā)揮作用的吧,。 一,、概述Linux 的信號(hào)機(jī)制作為進(jìn)程間通信的重要方式,,發(fā)揮著關(guān)鍵作用。它本質(zhì)上是一種軟件中斷,,能夠異步地通知進(jìn)程發(fā)生了特定事件,。信號(hào)的全稱為軟中斷信號(hào),簡(jiǎn)稱軟中斷,,在頭文件<signal.h>中定義了 64 種信號(hào),,這些信號(hào)的名字都以SIG開頭,且都被定義為正整數(shù),,稱為信號(hào)編號(hào),。可以用 “kill -l” 命令查看信號(hào)的具體名稱,。 其中,,編號(hào)為 1~31 的信號(hào)為早期 Linux 所支持的信號(hào),是不可靠信號(hào)(非實(shí)時(shí)的),,編號(hào)為 34~63 的信號(hào)時(shí)后來(lái)擴(kuò)充的,,稱為可靠信號(hào)(實(shí)時(shí)信號(hào))。不可靠信號(hào)與可靠信號(hào)的區(qū)別在于前者不支持排隊(duì),,可能會(huì)造成信號(hào)丟失,,而后者的注冊(cè)機(jī)制是每收到一個(gè)可靠信號(hào)就會(huì)去注冊(cè)這個(gè)信號(hào),不會(huì)丟失。 信號(hào)機(jī)制可以類比為硬件中斷,,當(dāng)某個(gè)事件發(fā)生時(shí),,就像硬件中斷一樣,能夠打斷進(jìn)程的正常執(zhí)行流,,迫使進(jìn)程去處理特定的事件,。例如,當(dāng)用戶在終端按下Ctrl+C時(shí),,會(huì)產(chǎn)生SIGINT信號(hào),,表示進(jìn)程應(yīng)被終止;當(dāng)控制終端被關(guān)閉時(shí),,會(huì)發(fā)送SIGHUP信號(hào),,常用于通知守護(hù)進(jìn)程重新讀取配置。信號(hào)機(jī)制為進(jìn)程間的通信和交互提供了一種靈活且有效的方式,,使得不同進(jìn)程能夠在特定事件發(fā)生時(shí)做出相應(yīng)的反應(yīng),。 二、信號(hào)基本原理信號(hào)機(jī)制是UNIX系統(tǒng)最古老的機(jī)制之一,,它不僅是內(nèi)核處理程序在運(yùn)行時(shí)發(fā)生錯(cuò)誤的方式,,還是終端管理進(jìn)程的方式,并且還是一種進(jìn)程間通信機(jī)制,。信號(hào)機(jī)制由三部分構(gòu)成,,首先是信號(hào)是怎么產(chǎn)生的,或者說(shuō)是誰(shuí)發(fā)送的,,然后是信號(hào)是怎么投遞到進(jìn)程或者線程的,,最后是信號(hào)是怎么處理的。下面我們先看一張圖: 從圖中我們可以看到信號(hào)的產(chǎn)生方式也就是發(fā)送方有三種,。首先是終端發(fā)送,,比如我們?cè)诮K端里輸入Ctrl+C快捷鍵時(shí),終端會(huì)給當(dāng)前進(jìn)程發(fā)送SIGINT信號(hào),。其次是內(nèi)核發(fā)送,,這里的內(nèi)核發(fā)送是指內(nèi)核里的異常處理的信號(hào)發(fā)送,比如進(jìn)程非法訪問(wèn)內(nèi)存,,在異常處理中就會(huì)給當(dāng)前線程發(fā)送SIGSEGV信號(hào),。最后是進(jìn)程發(fā)送,也就是一個(gè)進(jìn)程給另一個(gè)進(jìn)程發(fā)送或者是進(jìn)程自己給自己發(fā)送,。這里有很多接口函數(shù)可以選擇,,有的可以發(fā)給線程,有的可以發(fā)給進(jìn)程,,有的可以發(fā)給進(jìn)程組甚至?xí)捊M,。 下一個(gè)過(guò)程就是信號(hào)是如何從發(fā)送方發(fā)送到目標(biāo)進(jìn)程或者線程的信號(hào)隊(duì)列里的,這個(gè)過(guò)程叫做投遞。不同的發(fā)送方,,其發(fā)送方式和投遞過(guò)程是不同的,,這個(gè)后面會(huì)展開講。 最后是信號(hào)的處理過(guò)程,,這個(gè)最復(fù)雜牽涉問(wèn)題最多,。信號(hào)發(fā)送可以發(fā)送給進(jìn)程或者線程,但是信號(hào)的處理是在線程中進(jìn)行的,,因?yàn)榫€程是代碼執(zhí)行的單元。線程首先處理自己隊(duì)列里的信號(hào),,自己的處理完了再去處理進(jìn)程隊(duì)列里的信號(hào),。處理的時(shí)候要考慮信號(hào)掩碼(mask),被掩碼阻塞的信號(hào)暫時(shí)不處理,,還放回原隊(duì)列中去,。信號(hào)處理方式有三種,如果程序什么也沒(méi)設(shè)置的話,,走默認(rèn)處理(default)方式,。默認(rèn)處理有五種情況,不同的信號(hào),,其默認(rèn)處理方式不同,。這五種情況分別是ignore(忽略)、term(終結(jié)進(jìn)程也就是殺死進(jìn)程),、core(coredump內(nèi)存轉(zhuǎn)儲(chǔ)并殺死進(jìn)程),、stop(暫停進(jìn)程)、cont(continue恢復(fù)執(zhí)行進(jìn)程),。還有兩種方式是進(jìn)程提前通過(guò)接口函數(shù)signal或者sigaction設(shè)置了處理方式,設(shè)置IGN來(lái)忽略信號(hào),或者設(shè)置一個(gè)信號(hào)處理函數(shù)handler來(lái)處理信號(hào),。大家注意,,默認(rèn)處理中的忽略和進(jìn)程主動(dòng)設(shè)置的忽略,兩者的邏輯是不同的,,一個(gè)是默認(rèn)處理是忽略,,一個(gè)是進(jìn)程主動(dòng)要求要忽略。你想要忽略一個(gè)默認(rèn)處理不是忽略的信號(hào),,就必須要主動(dòng)設(shè)置忽略,。 三、信號(hào)的分類與產(chǎn)生我們明白了信號(hào)的基本原理之后,,就要進(jìn)一步追問(wèn),,系統(tǒng)都有哪些信號(hào)呢,這些信號(hào)有什么不同呢?剛開始的時(shí)候,,UNIX系統(tǒng)只有1-31總共31個(gè)信號(hào),,這些信號(hào)每個(gè)都有特殊的含義和特定的用法。這些信號(hào)的實(shí)現(xiàn)有一個(gè)特點(diǎn),,它們是用bit flag實(shí)現(xiàn)的,。這就會(huì)導(dǎo)致當(dāng)一個(gè)信號(hào)還在待決的時(shí)候,又來(lái)了一個(gè)同樣的信號(hào),,再次設(shè)置bit位是沒(méi)有意義的,,所以就會(huì)丟失一次信號(hào)。為了解決這個(gè)問(wèn)題,,后來(lái)POSIX規(guī)定增加32-64這33個(gè)信號(hào)作為實(shí)時(shí)信號(hào),,并規(guī)定實(shí)時(shí)信號(hào)不能丟失,要用隊(duì)列來(lái)實(shí)現(xiàn),。我們把之前的信號(hào)1-31叫做標(biāo)準(zhǔn)信號(hào),,由于標(biāo)準(zhǔn)信號(hào)會(huì)丟失,所以標(biāo)準(zhǔn)信號(hào)也叫做不可靠信號(hào),,由于標(biāo)準(zhǔn)信號(hào)是用bit flag實(shí)現(xiàn)的,,所以標(biāo)準(zhǔn)信號(hào)也叫做標(biāo)記信號(hào)(flag signal)。由于實(shí)時(shí)信號(hào)不會(huì)丟失,,所以實(shí)時(shí)信號(hào)也叫作可靠信號(hào),,由于實(shí)時(shí)信號(hào)是用隊(duì)列實(shí)現(xiàn)的,所以實(shí)時(shí)信號(hào)也叫做排隊(duì)信號(hào)(queue signal),。我們平常遇到的SIGSEGV,、SIGABRT等信都是標(biāo)準(zhǔn)信號(hào)。 3.1信號(hào)的分類可靠信號(hào)與不可靠信號(hào),、實(shí)時(shí)信號(hào)與非實(shí)時(shí)信號(hào)在很多方面存在區(qū)別,。 可靠信號(hào)與不可靠信號(hào):不可靠信號(hào)主要來(lái)自早期的 Unix 系統(tǒng),其存在一些問(wèn)題,。例如,,進(jìn)程每次處理完信號(hào)后,系統(tǒng)會(huì)自動(dòng)將該信號(hào)的處理方式恢復(fù)為默認(rèn)操作,,這就需要在信號(hào)處理函數(shù)的末尾再次調(diào)用signal()函數(shù)重新綁定處理函數(shù),,增加了編程復(fù)雜性。而且,,不可靠信號(hào)可能會(huì)丟失,,當(dāng)進(jìn)程正在處理一個(gè)信號(hào)時(shí),如果相同類型的另一個(gè)信號(hào)到達(dá),,第二個(gè)信號(hào)可能會(huì)被直接丟棄,。而可靠信號(hào)支持排隊(duì),,即使進(jìn)程在處理某個(gè)信號(hào)時(shí)有新的信號(hào)到達(dá),這些信號(hào)也不會(huì)丟失,,而是被加入隊(duì)列,,待當(dāng)前信號(hào)處理完成后再依次處理。Linux 引入了新的信號(hào)發(fā)送函數(shù)sigqueue()和信號(hào)綁定函數(shù)sigaction()來(lái)增強(qiáng)信號(hào)處理的靈活性和可靠性,。 實(shí)時(shí)信號(hào)與非實(shí)時(shí)信號(hào):非實(shí)時(shí)信號(hào)一般指編號(hào)在 1 到 31 之間的信號(hào),,不支持排隊(duì),處理時(shí)沒(méi)有嚴(yán)格的順序保證,,且如果在處理某個(gè)信號(hào)時(shí)有相同類型的新信號(hào)到達(dá),,后者可能會(huì)被忽略或丟失,所以也被稱為不可靠信號(hào),。實(shí)時(shí)信號(hào)是編號(hào)在 34 到 64 之間的信號(hào),,支持排隊(duì),即使在處理某個(gè)信號(hào)期間有新的相同類型的信號(hào)到達(dá),,這些信號(hào)也不會(huì)被丟棄,而是按照到達(dá)的順序依次處理,,因此被稱為可靠信號(hào),。 信號(hào)是單線程時(shí)代的產(chǎn)物。在單線程時(shí)代,,一個(gè)進(jìn)程就只有一個(gè)線程(就是主線程),,所以進(jìn)程就是線程,線程就是進(jìn)程,。信號(hào)所有的屬性既是進(jìn)程全局的又是線程私有的,,因?yàn)檫@兩者沒(méi)有區(qū)別。但是到了多線程時(shí)代,,這兩者就有區(qū)別了,,進(jìn)程是資源分配與管理的單元,線程是程序執(zhí)行的單元,。一個(gè)進(jìn)程往往有多個(gè)線程,,那么信號(hào)的這些屬性究竟應(yīng)該是進(jìn)程全局的還是線程私有的呢?這還真不好處理的,。經(jīng)過(guò)一番慎重的分析與思考,,UNIX系統(tǒng)做出了如下的決定。 信號(hào)的發(fā)送既可以發(fā)送給進(jìn)程,,也可以發(fā)送給線程,,但是同步信號(hào)(也就是和當(dāng)前線程執(zhí)行相關(guān)而產(chǎn)生的信號(hào))應(yīng)當(dāng)發(fā)送給當(dāng)前線程。進(jìn)程發(fā)送信號(hào)可以選擇不同的接口函數(shù),,有的接口是發(fā)給進(jìn)程的,,有的接口是發(fā)給線程的,。線程信號(hào)隊(duì)列中的信號(hào)只能由線程自己處理,進(jìn)程信號(hào)隊(duì)列中的信號(hào)由進(jìn)程中的線程處理,,具體是由哪個(gè)線程處理是不確定的,。
我們先說(shuō)默認(rèn)處理的幾種情況:忽略一個(gè)信號(hào)是指整個(gè)進(jìn)程忽略這個(gè)信號(hào),,而不是說(shuō)某個(gè)線程忽略了其它線程還可以去處理。終結(jié)是終結(jié)的整個(gè)進(jìn)程,,而不只終結(jié)一個(gè)線程,。內(nèi)存轉(zhuǎn)儲(chǔ)是整個(gè)進(jìn)程進(jìn)行內(nèi)存轉(zhuǎn)儲(chǔ)并終結(jié)整個(gè)進(jìn)程。Stop是暫停整個(gè)進(jìn)程而不是只暫停一個(gè)線程,。Cont是恢復(fù)執(zhí)行整個(gè)進(jìn)程而不是只恢復(fù)執(zhí)行一個(gè)線程,。 非默認(rèn)處理有兩種情況:如果進(jìn)程設(shè)置了忽略某個(gè)信號(hào),則是整個(gè)進(jìn)程都忽略這個(gè)信號(hào),,而不是某個(gè)線程忽略這個(gè)信號(hào),。如果進(jìn)程設(shè)置了信號(hào)處理函數(shù)handler,則handler的執(zhí)行效果是進(jìn)程全局的,。這點(diǎn)怎么理解呢,?可以從兩方面來(lái)理解,一是如果信號(hào)是發(fā)送給進(jìn)程的,,則每個(gè)線程都有可能來(lái)執(zhí)行這個(gè)handler,;二是handler雖然是在某個(gè)線程中執(zhí)行的,但是對(duì)于線程來(lái)說(shuō),,只有線程棧是線程私有的,,其它內(nèi)存是整個(gè)進(jìn)程共享的,handler對(duì)線程棧的影響是線程私有的,,handler返回之后它的棧幀就銷毀了,,handler只有對(duì)全局內(nèi)存的影響才會(huì)留下來(lái),所以它的影響是進(jìn)程全局的,。 我們?cè)賮?lái)總結(jié)一下:信號(hào)可以發(fā)送給進(jìn)程也可以發(fā)送給線程,。發(fā)送給線程的信號(hào)只能由線程處理,如果線程阻塞了信號(hào)則信號(hào)會(huì)一直pending,,直到線程解除阻塞然后就會(huì)去處理該信號(hào),。發(fā)送給進(jìn)程的信號(hào)可以由該進(jìn)程中的任意一個(gè)未阻塞該信號(hào)的線程來(lái)處理,具體哪個(gè)線程是不確定的,,如果所有線程都阻塞該信號(hào),,則該信號(hào)一直pending,,直到任一線程解除阻塞。信號(hào)無(wú)論是怎么發(fā)送和處理的,,信號(hào)的處理效果都是進(jìn)程全局的,。 3.2信號(hào)類型詳解⑴標(biāo)準(zhǔn)信號(hào)與實(shí)時(shí)信號(hào)的區(qū)別 我們知道信號(hào)分為標(biāo)準(zhǔn)信號(hào)和實(shí)時(shí)信號(hào),它們之間最大的區(qū)別就是在信號(hào)處于待決的狀態(tài)下又來(lái)了同樣的信號(hào)會(huì)怎么處理,。除此之外,,它們還有以下三點(diǎn)不同。
根據(jù)特點(diǎn)3,,兩個(gè)進(jìn)程可以使用實(shí)時(shí)信號(hào)來(lái)達(dá)到進(jìn)程間通信的目的,。因?yàn)閷?shí)時(shí)信號(hào)沒(méi)有特定的含義,所以系統(tǒng)不會(huì)使用實(shí)時(shí)信號(hào),,進(jìn)程之間可以自行約定某個(gè)信號(hào)的含義。而且不同的進(jìn)程之間可以約定不同的含義而不會(huì)相互影響,。不過(guò)glibc的pthread實(shí)現(xiàn)使用了32,、33這兩個(gè)實(shí)時(shí)信號(hào),所以大家不要用這兩個(gè)實(shí)時(shí)信號(hào),。 ⑵信號(hào)的屬性特征 可阻塞:我們可以通過(guò)某些接口來(lái)阻塞(暫時(shí)屏蔽)一個(gè)信號(hào),。但是有的信號(hào)可以阻塞,有的信號(hào)無(wú)法阻塞,。有的信號(hào)雖然可以成功設(shè)置阻塞,,但是其信號(hào)會(huì)被強(qiáng)制發(fā)送,所以最終還是阻塞不了,。比如內(nèi)核在異常處理時(shí)會(huì)強(qiáng)制發(fā)送信號(hào),,所以是阻塞不了的。但是同樣的信號(hào)你用kill來(lái)發(fā),,阻塞還是生效的,,因?yàn)閗ill不是強(qiáng)制發(fā)送。信號(hào)阻塞,,有很多地方會(huì)叫做信號(hào)屏蔽,,兩者都是一樣的,。但是屏蔽容易被人和忽略理解混了,所以本文里用阻塞,。阻塞,,含義明確,就是阻塞住了,,后面不阻塞了信號(hào)還是會(huì)到來(lái)的,。 可忽略:有些信號(hào)默認(rèn)處理就是忽略的,但是有些信號(hào)默認(rèn)處理不是忽略,。如果我們想忽略這些信號(hào)的話,,可以通過(guò)一些接口設(shè)置來(lái)忽略它。有些信號(hào)是可以設(shè)置忽略的,,但是有些接口無(wú)法設(shè)置忽略,。有的信號(hào)雖然可以設(shè)置忽略成功,但是內(nèi)核在異常處理時(shí)會(huì)強(qiáng)制發(fā)送信號(hào),,這時(shí)忽略是無(wú)效的,。不過(guò)同樣的信號(hào)用kill來(lái)發(fā),忽略就是有效的,,因?yàn)閗ill不是強(qiáng)制發(fā)送,。大家注意忽略和阻塞不同,阻塞是暫時(shí)不處理,,而忽略其實(shí)也是一種處理,,相當(dāng)于是空處理。 可捕獲:我們可以通過(guò)一些接口來(lái)設(shè)置信號(hào)處理函數(shù)handler來(lái)處理信號(hào),,這個(gè)行為叫做捕獲,。有些信號(hào)是能捕獲的,有些信號(hào)是不能捕獲的,。與可阻塞和可忽略不同的是,,強(qiáng)制發(fā)送的信號(hào)也是可捕獲的。但是可捕獲存在一個(gè)特殊情況,,有些時(shí)候是不能二次捕獲的,。有兩個(gè)信號(hào)SIGSEGV、SIGABRT是不能二次捕獲的,,后面會(huì)進(jìn)行講解,。 默認(rèn)處理:默認(rèn)處理是當(dāng)我們沒(méi)有設(shè)置忽略和捕獲函數(shù)時(shí),內(nèi)核對(duì)信號(hào)的默認(rèn)處理方式,。前面已經(jīng)介紹過(guò)有五種處理方式,,這里就不再贅述了。由于大部分的信號(hào)處理是terminate或者coredump,,都是會(huì)導(dǎo)致進(jìn)程死亡的,,所以信號(hào)發(fā)送命令叫做kill,。其實(shí)kill并不會(huì)殺死進(jìn)程,它只是給進(jìn)程送了個(gè)信號(hào)而已,。 發(fā)送者:這里指的是信號(hào)在一般情況是從哪里發(fā)送的,,表明了信號(hào)使用的場(chǎng)景。 發(fā)給:這里是指信號(hào)一般情況下是發(fā)給進(jìn)程還是線程,,表明了信號(hào)是和整個(gè)進(jìn)程相關(guān)還是和某個(gè)線程相關(guān),。一般由某個(gè)線程自己觸發(fā)的信號(hào)會(huì)發(fā)送給這個(gè)線程自己,讓它自己來(lái)處理,,但是這個(gè)信號(hào)的含義如果是進(jìn)程全局的就會(huì)發(fā)送給進(jìn)程來(lái)處理,,進(jìn)程里的任何一個(gè)線程都有可能會(huì)被選擇來(lái)處理。無(wú)論是發(fā)送給進(jìn)程還是線程,,信號(hào)的處理效果都是進(jìn)程全局的,。 含義:這個(gè)信號(hào)的含義,代表什么時(shí)候該使用它,,如果收到了它就意味著遇到了什么情況,。 ⑶標(biāo)準(zhǔn)信號(hào)詳解 下面讓我們通過(guò)一張圖來(lái)看看所有信號(hào)的相關(guān)信息: 我們先來(lái)解釋一下信號(hào)0,其實(shí)0不算是一個(gè)信號(hào),,但是也可以算作是半個(gè)信號(hào),。因?yàn)榘l(fā)送信號(hào)0給一個(gè)進(jìn)程或者線程,它會(huì)走發(fā)送檢測(cè)過(guò)程,,但是并不會(huì)真的投遞給進(jìn)程或者線程,。檢測(cè)流程會(huì)檢測(cè)發(fā)送者是否有權(quán)限發(fā)送、進(jìn)程是否存在,,如果遇到問(wèn)題就返回錯(cuò)誤值,。所以發(fā)送信號(hào)0可以用作檢測(cè)進(jìn)程是否存在的方法。 我們?cè)賮?lái)看一下實(shí)時(shí)信號(hào),,因?yàn)閷?shí)時(shí)信號(hào)沒(méi)有特定的含義,,所以比較簡(jiǎn)單,。實(shí)時(shí)信號(hào)的默認(rèn)處理是終結(jié)進(jìn)程,,相關(guān)屬性是可阻塞,可忽略,,可捕獲,。它的一般使用方法都是進(jìn)程發(fā)給其它進(jìn)程或者線程來(lái)作為進(jìn)程間通信的方法。其中32-33被glibc的pthread使用了,。 標(biāo)準(zhǔn)信號(hào)一共有1-31共31個(gè),,我們按照它們的特點(diǎn)不同分類進(jìn)行講解: 首先說(shuō)一下SIGKILL和一些暫停、繼續(xù)相關(guān)的信號(hào),。其中SIGKILL和SIGSTOP是POSIX標(biāo)準(zhǔn)規(guī)定的不可阻塞,、不可忽略,、不可捕獲的信號(hào),它們的語(yǔ)義一定會(huì)得到執(zhí)行,。SIGCONT信號(hào)官方?jīng)]有特別規(guī)定,,它的實(shí)現(xiàn)上是不可阻塞、不可忽略的,,雖然能捕獲,,但是相當(dāng)于沒(méi)捕獲。因?yàn)椴东@的意思是執(zhí)行其信號(hào)處理函數(shù)就不再執(zhí)行其默認(rèn)處理了,,但是SIGCONT的默認(rèn)語(yǔ)義一定會(huì)得到執(zhí)行,。其它三個(gè)暫停信號(hào)SIGTSTP、SIGTTIN,、SIGTTOU是不能阻塞的,,但是可以忽略可以捕獲,忽略或者捕獲之后,,它們的默認(rèn)語(yǔ)義暫停程序就不會(huì)得到執(zhí)行,。 SIGSTOP、SIGCONT,,進(jìn)程在想要暫停,、恢復(fù)執(zhí)行其它進(jìn)程的時(shí)候可以發(fā)送這兩個(gè)信號(hào),內(nèi)核里面再需要暫停,、恢復(fù)執(zhí)行進(jìn)程的時(shí)候也會(huì)發(fā)送這兩個(gè)信號(hào),。SIGTSTP是當(dāng)在終端輸入Ctrl+Z快捷鍵時(shí),終端驅(qū)動(dòng)會(huì)給當(dāng)前進(jìn)程發(fā)送這個(gè)信號(hào),。SIGTTIN是當(dāng)后臺(tái)進(jìn)程讀取終端的時(shí)候,,終端會(huì)向進(jìn)程發(fā)送的。SIGTTOU是在后臺(tái)進(jìn)程想要向終端輸出的時(shí)候,,終端會(huì)向進(jìn)程發(fā)送的,。這幾個(gè)信號(hào)都是直接發(fā)送給進(jìn)程的,因?yàn)樗鼈兊恼Z(yǔ)義就是要操作整個(gè)進(jìn)程,。 下面我們?cè)賮?lái)看6個(gè)標(biāo)記紫色的信號(hào),,這幾個(gè)信號(hào)都是和當(dāng)前線程正在執(zhí)行時(shí)發(fā)生異常有關(guān)。內(nèi)核里單獨(dú)把這6個(gè)信號(hào)放在一起成為同步信號(hào),。因?yàn)樗鼈兌际菑?qiáng)制發(fā)送的,,會(huì)忽略阻塞和忽略設(shè)置,所以圖中把它們都看做是不可忽略不可阻塞的,。但是它們是可以捕獲的,,讓它們可以捕獲的原因是因?yàn)檫@樣可以讓進(jìn)程知道自己出錯(cuò)的原因,讓進(jìn)程可以在臨死之前可以做一些記錄工作,為程序員解BUG多提供一些信息,。捕獲了之后,,原先默認(rèn)的語(yǔ)義就不會(huì)執(zhí)行,所以信號(hào)函數(shù)執(zhí)行完之后它們還會(huì)繼續(xù)執(zhí)行,。 但是一般情況下這么做是沒(méi)有意義的,,所以一般都會(huì)在信號(hào)函數(shù)里退出進(jìn)程。SIGSEGV的可捕獲前面加了個(gè)[不],,代表的是不能二次捕獲,,也就是說(shuō)如果在信號(hào)處理函數(shù)里面又發(fā)生了SIGSEGV,則這個(gè)SIGSEGV就不可捕獲了,,會(huì)走默認(rèn)語(yǔ)義發(fā)生coredump并殺死進(jìn)程,。這些信號(hào)的發(fā)送方都是內(nèi)核里異常處理相關(guān)的代碼,信號(hào)都會(huì)發(fā)送給線程,,因?yàn)槭沁@些線程引起的這些問(wèn)題,,放到原線程里去處理比較好。 我們?cè)俳又碨IGABRT信號(hào),,這個(gè)信號(hào)比較特殊,。它的目的是給庫(kù)程序來(lái)用的。當(dāng)庫(kù)程序發(fā)現(xiàn)程序出現(xiàn)了不可挽回的錯(cuò)誤,,就會(huì)調(diào)用函數(shù)abort,,這個(gè)函數(shù)會(huì)給當(dāng)前線程發(fā)送信號(hào)SIGABRT。SIGABRT信號(hào)本身沒(méi)什么特殊的,,但是abort函數(shù)比較特殊,。POSIX規(guī)范要求abort函數(shù)執(zhí)行完成之后,進(jìn)程一定要被殺死,。于是abort函數(shù)的實(shí)現(xiàn)就是這樣的,,先取消阻塞SIGABRT信號(hào),然后給當(dāng)前線程發(fā)信號(hào)SIGABRT,。無(wú)論SIGABRT信號(hào)是被忽略還是被捕獲了,,最后還是要返回到abort函數(shù)里面,然后abort函數(shù)就把SIGABRT信號(hào)的處理方式設(shè)置為默認(rèn),,然后再發(fā)一個(gè)SIGABRT,,這下進(jìn)程就一定會(huì)死了。 也就是說(shuō)你可以捕獲SIGABRT信號(hào),,但是進(jìn)程最后還是一定會(huì)死,。所以上圖里說(shuō)SIGABRT是不可阻塞,、不可忽略,、不可二次捕獲的([不]可捕獲代表的是不可二次捕獲)。SIGABRT的不可二次捕獲和SIGSEGV的不可二次捕獲情形不太一樣。如果是手工發(fā)送的SIGABRT信號(hào),,它就是一個(gè)普通的信號(hào),,沒(méi)有前面說(shuō)的邏輯。不過(guò)手工發(fā)送SIGABRT信號(hào)沒(méi)有意義,,一般都是使用abort函數(shù)來(lái)發(fā)送,。其實(shí)遇到abort函數(shù)的SIGABRT信號(hào)也不是必死,有一種不規(guī)范的做法可以避免一死,,那就是在信號(hào)處理函數(shù)中使用longjmp,。但是這種做法沒(méi)有意義,因?yàn)槌绦颥F(xiàn)在已經(jīng)處于不一致?tīng)顟B(tài)了,,coredump之后結(jié)束進(jìn)程,,然后好好地解bug才是最好的選擇。 下面我們?cè)倏匆幌屡c終端相關(guān)的4個(gè)信號(hào),,SIGINT,、SIGHUP、SIGQUIT,、SIGTERM,。你在終端上輸入Ctrl+C,終端驅(qū)動(dòng)就會(huì)給當(dāng)前進(jìn)程發(fā)送SIGINT,,默認(rèn)處理是殺死進(jìn)程,。你用kill命令給一個(gè)進(jìn)程發(fā)信號(hào),默認(rèn)發(fā)的就是SIGTERM信號(hào),,默認(rèn)處理也是殺死進(jìn)程,。當(dāng)終端脫離進(jìn)程的時(shí)候會(huì)給進(jìn)程發(fā)SIGHUP,默認(rèn)處理也是殺死進(jìn)程,。脫離終端有三種情況:一是物理終端與大型機(jī)斷開了連接,,現(xiàn)在已經(jīng)沒(méi)有物理終端了,所以這種情況不會(huì)有了,;二是終端模擬器(也就是命令行窗口)被關(guān)閉了,;三是我們通過(guò)ssh等工具連接到了網(wǎng)絡(luò)終端,如果此時(shí)網(wǎng)絡(luò)斷了或者客戶端程序死了,。這三種情況終端驅(qū)動(dòng)都會(huì)給關(guān)聯(lián)的進(jìn)程發(fā)送SIGHUP信號(hào),。最后一個(gè)信號(hào)是SIGTERM,當(dāng)你在終端輸入Ctrl+\的時(shí)候,,終端驅(qū)動(dòng)就會(huì)給當(dāng)前進(jìn)程發(fā)送SIGTERM信號(hào),,默認(rèn)處理是coredump并殺死進(jìn)程。 3.3信號(hào)的產(chǎn)生來(lái)源⑴硬件來(lái)源 比如我們按下Ctrl+C,,會(huì)產(chǎn)生SIGINT信號(hào),。當(dāng)用戶在終端按下某些鍵時(shí),終端驅(qū)動(dòng)程序會(huì)發(fā)送信號(hào)給前臺(tái)進(jìn)程。這是一種常見(jiàn)的硬件來(lái)源產(chǎn)生信號(hào)的方式,。 硬件故障也可能產(chǎn)生信號(hào),,例如內(nèi)存訪問(wèn)錯(cuò)誤等情況可能會(huì)產(chǎn)生相應(yīng)的信號(hào),如SIGBUS(非法地址,,包括內(nèi)存地址對(duì)齊出錯(cuò)),、SIGSEGV(試圖訪問(wèn)未分配給自己的內(nèi)存,或試圖往沒(méi)有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù))等信號(hào),。 ⑵軟件來(lái)源 調(diào)用系統(tǒng)函數(shù) kill函數(shù)可以給一個(gè)指定的進(jìn)程發(fā)送指定的信號(hào),。例如,kill(pid_t pid, int sig),,其中pid為進(jìn)程的 pid,,你要向哪個(gè)進(jìn)程發(fā)送信號(hào),就寫哪個(gè)進(jìn)程的 pid,;sig就是你要發(fā)送的信號(hào)的編號(hào),。成功返回 0,失敗返回 -1,。 raise函數(shù)可以給當(dāng)前進(jìn)程發(fā)送指定的信號(hào)(自己給自己發(fā)信號(hào)),。 abort函數(shù)使當(dāng)前進(jìn)程接收到信號(hào)而異常終止。 用戶命令:通過(guò)命令向進(jìn)程發(fā)送信號(hào),。例如在一個(gè)終端下,,可以使用kill -9 <進(jìn)程的 PID>向指定的進(jìn)程發(fā)送信號(hào) 9(SIGKILL),這個(gè)信號(hào)的默認(rèn)功能是停止進(jìn)程,。 軟件條件:主要介紹alarm函數(shù)和SIGALRM信號(hào),。調(diào)用alarm(unsigned int seconds)函數(shù)可以設(shè)定一個(gè)鬧鐘,也就是告訴內(nèi)核在seconds秒后給當(dāng)前進(jìn)程發(fā)送SIGALRM信號(hào),,該信號(hào)的默認(rèn)處理動(dòng)作是終止當(dāng)前進(jìn)程,。這個(gè)函數(shù)的返回值是 0 或者是以前設(shè)定的鬧鐘時(shí)間還余下的秒數(shù)。 四,、信號(hào)的發(fā)送現(xiàn)在我們來(lái)看一下信號(hào)發(fā)送,,主要是看發(fā)送場(chǎng)景。具體的發(fā)送過(guò)程在下一章信號(hào)的投遞里面講解,。信號(hào)發(fā)送場(chǎng)景比較典型的有三種,,一是終端發(fā)送,也就是我們?cè)诿钚羞\(yùn)行程序時(shí)會(huì)遇到的情況,;二是內(nèi)核發(fā)送,,內(nèi)核也很龐大,里面的情況也很多,,我們這里主要講的是異常處理發(fā)送信號(hào),;三是進(jìn)程發(fā)送,就是一個(gè)進(jìn)程給另一個(gè)進(jìn)程發(fā),。 4.1 終端發(fā)送我們看一下偽終端是如何發(fā)送信號(hào)的:linux-src/drivers/tty/pty.c
linux-src/drivers/tty/sysrq.c
linux-src/drivers/tty/tty_io.c
linux-src/drivers/tty/tty_jobctrl.c
這是終端驅(qū)動(dòng)發(fā)送信號(hào)的幾個(gè)場(chǎng)景,代碼就不具體分析了,。 4.2 內(nèi)核發(fā)送我們最常遇到的信號(hào)SIGSEGV,,一般都是在缺頁(yè)異常里,如果我們?cè)L問(wèn)的虛擬內(nèi)存是未分配的虛擬內(nèi)存,,則會(huì)發(fā)生SIGSEGV,。下面我們看一下代碼,。 X86的缺頁(yè)異常的代碼如下:linux-src/arch/x86/mm/fault.c
處理用戶空間缺頁(yè)異常的函數(shù)是do_user_addr_fault,在這個(gè)函數(shù)里面會(huì)檢測(cè)各種錯(cuò)誤情況并最終調(diào)用函數(shù)__bad_area_nosemaphore給當(dāng)前線程發(fā)送信號(hào)SIGSEGV,。 4.3 進(jìn)程發(fā)送進(jìn)程如果想要向另外一個(gè)進(jìn)程\線程或發(fā)送信號(hào)的話,可以使用系統(tǒng)提供的一些接口函數(shù),。如下所示: 我們最常用的接口函數(shù)就是kill,,它有兩個(gè)參數(shù),一個(gè)是進(jìn)程標(biāo)識(shí)符pid,,一個(gè)是信號(hào)的值sig,,就是把信號(hào)sig發(fā)給進(jìn)程pid。raise函數(shù)給自己也就是當(dāng)前線程發(fā)信號(hào),,它只有一個(gè)參數(shù)sig,。killpg是給整個(gè)進(jìn)程組發(fā)信號(hào),在實(shí)現(xiàn)上是給進(jìn)程組的每個(gè)進(jìn)程都發(fā)信號(hào),。pthread_kill是給同一個(gè)進(jìn)程中的某個(gè)線程發(fā)信號(hào),。tgkill可以給其它進(jìn)程中的某個(gè)線程發(fā)信號(hào)。sigqueue是用來(lái)發(fā)實(shí)時(shí)信號(hào)的,,實(shí)時(shí)信號(hào)可以多帶一個(gè)附加數(shù)據(jù),,當(dāng)然可以用來(lái)發(fā)普通信號(hào),但是這樣附加數(shù)據(jù)就會(huì)被忽略,。 五,、信號(hào)的投遞5.1 信號(hào)待決隊(duì)列每個(gè)進(jìn)程都有一個(gè)信號(hào)隊(duì)列,每個(gè)線程也有一個(gè)信號(hào)隊(duì)列,。信號(hào)隊(duì)列的數(shù)據(jù)結(jié)構(gòu)如下所示:linux-src/include/linux/signal_types.h
可以看到信號(hào)隊(duì)列非常簡(jiǎn)單,,sigset是個(gè)bit flag,,代表當(dāng)前隊(duì)列里有哪些信號(hào),list是信號(hào)列表的頭指針,。下面我們來(lái)看一下信號(hào)隊(duì)列里的條目,。
每發(fā)送一次信號(hào)都會(huì)生成一個(gè)sigqueue,sigqueue里面包含了很多和信號(hào)相關(guān)的信息,。 在Linux里面,,每個(gè)task_struct都代表一個(gè)線程,里面包含了一個(gè)sigpending ,。Linux里面沒(méi)有直接代表進(jìn)程的結(jié)構(gòu)體,,但是一個(gè)進(jìn)程的所有線程都共享同一個(gè)signal_struct。signal_struct里面也包含了一個(gè)sigpending,,這個(gè)sigpending代表進(jìn)程的信號(hào)隊(duì)列,。 5.2 信號(hào)投遞流程我們前面說(shuō)了很多發(fā)送信號(hào)的方法,總體上可以分為兩類,,普通發(fā)送和強(qiáng)制發(fā)送,。異常處理發(fā)送信號(hào)都是用的強(qiáng)制發(fā)送,其它的基本上都是用的普通發(fā)送,,但也有一些其它情況用的是強(qiáng)制發(fā)送,。這兩類方法方法最終都會(huì)調(diào)用同一個(gè)函數(shù)來(lái)發(fā)送信號(hào),我們來(lái)看一下:linux-src/kernel/signal.c
send_signal做了一些簡(jiǎn)單的處理,,然后直接調(diào)用__send_signal,。__send_signal先調(diào)用prepare_signal,prepare_signal對(duì)暫?;謴?fù)類的信號(hào)先做了一下預(yù)處理,,然后查看信號(hào)是否被忽略。然后根據(jù)PID類型決定是把信號(hào)放到進(jìn)程隊(duì)列里還是線程隊(duì)列里,。然后會(huì)判斷信號(hào)是不是傳統(tǒng)信號(hào)(也就是標(biāo)準(zhǔn)信號(hào)),對(duì)于傳統(tǒng)信號(hào),,如果信號(hào)隊(duì)列里已經(jīng)有一個(gè)了,就不再接收了,,這么做是為了兼容過(guò)去,。然后調(diào)用__sigqueue_alloc分配一個(gè)信號(hào)條目sigqueue,分配好之后填充各種數(shù)據(jù),,然后把它加入到隊(duì)列中去,。最后調(diào)用complete_signal,此函數(shù)會(huì)選擇一個(gè)合適的線程來(lái)喚醒,,一般會(huì)喚醒當(dāng)前線程,。喚醒的線程很可能醒來(lái)就去進(jìn)行信號(hào)處理。 ①?gòu)?qiáng)制發(fā)送:強(qiáng)制發(fā)送的入口函數(shù)是force_sig_info_to_task,,它會(huì)先把信號(hào)的阻塞和忽略取消掉,,然后再調(diào)用函數(shù)send_signal進(jìn)行發(fā)送,。代碼如下:linux-src/kernel/signal.c
內(nèi)核又封裝了幾個(gè)函數(shù)來(lái)輔助強(qiáng)制發(fā)送,分別是force_sig_info,、force_sig,、force_fatal_sig、force_exit_sig,、force_sigsegv,、force_sig_fault_to_task、force_sig_fault,,它們的代碼就不再具體介紹了,。 ②普通發(fā)送:do_send_sig_info先對(duì)send_signal進(jìn)行了一次封裝,,然后do_send_specific,、group_send_sig_info又分別對(duì)其進(jìn)行了封裝。do_send_specific代表發(fā)送到線程,,group_send_sig_info代表發(fā)送到進(jìn)程,。給線程發(fā)信號(hào)的接口函數(shù)最終都是調(diào)用的do_send_specific。給進(jìn)程發(fā)信號(hào)的接口函數(shù)最終都是調(diào)用的group_send_sig_info,。下面我們看一下kill和tgkill的調(diào)用流程,。 先看kill接口函數(shù)的流程:linux-src/kernel/signal.c
下面再來(lái)看一下tgkill函數(shù)的流程:linux-src/kernel/signal.c
六、信號(hào)的儲(chǔ)存與處理6.1信號(hào)的存儲(chǔ)方式在 Linux 內(nèi)核中,,信號(hào)的存儲(chǔ)主要通過(guò)三張表來(lái)實(shí)現(xiàn):pending 表,、block 表和 handler 表。 Pending 表是通過(guò)位圖來(lái)儲(chǔ)存的,,一共有 31 位,,每個(gè)比特位代表信號(hào)編號(hào),比特位的內(nèi)容代表信號(hào)是否收到,。當(dāng)進(jìn)程收到信號(hào)但未遞達(dá)時(shí),,對(duì)應(yīng)編號(hào)的比特位就會(huì)由 0 改為 1。 Block 表也是通過(guò)位圖來(lái)儲(chǔ)存,,其結(jié)構(gòu)與 Pending 表類似,。每個(gè)比特位代表信號(hào)編號(hào),比特位的內(nèi)容代表信號(hào)是否阻塞,。如果某個(gè)信號(hào)被阻塞,,那么阻塞位圖結(jié)構(gòu)中對(duì)應(yīng)的比特位(信號(hào)編號(hào))就會(huì)置為 1,在此信號(hào)阻塞未被解除之前,,會(huì)一直處于信號(hào)未決狀態(tài),。 Handler 表是一個(gè)函數(shù)指針數(shù)組。數(shù)組的下標(biāo)是對(duì)應(yīng)的信號(hào)編號(hào),,數(shù)組下標(biāo)中的內(nèi)容就是對(duì)應(yīng)信號(hào)的處理方法(函數(shù)指針),。當(dāng)調(diào)用signal(signo,,handler)時(shí),就會(huì)把信號(hào)對(duì)應(yīng)的處理方法設(shè)置為自定義方法,,內(nèi)核中就是將數(shù)組下標(biāo)(信號(hào)編號(hào))中的內(nèi)容(處理方法)設(shè)置為自定義方法的函數(shù)指針,,從而在遞達(dá)后執(zhí)行處理方法。 sigset_t類型是 Linux 給用戶提供的一個(gè)用戶級(jí)的數(shù)據(jù)類型,,禁止用戶直接修改位圖,。每個(gè)信號(hào)只有一個(gè) bit 的未決標(biāo)志,非 0 即 1,,不記錄該信號(hào)產(chǎn)生了多少次,,阻塞標(biāo)志也是這樣表示的。因此,,未決和阻塞標(biāo)志可以用相同的數(shù)據(jù)類型sigset_t來(lái)存儲(chǔ),,sigset_t稱為信號(hào)集,這個(gè)類型可以表示每個(gè)信號(hào)的 “有效” 或 “無(wú)效” 狀態(tài),,在阻塞信號(hào)集中 “有效” 和 “無(wú)效” 的含義是該信號(hào)是否被阻塞,,而在未決信號(hào)集中 “有效” 和 “無(wú)效” 的含義是該信號(hào)是否處于未決狀態(tài)。阻塞信號(hào)集也叫做當(dāng)前進(jìn)程的信號(hào)屏蔽字,,這里的 “屏蔽” 應(yīng)該理解為阻塞而不是忽略,。 6.2信號(hào)的阻塞與未決狀態(tài)信號(hào)的阻塞、未決和遞達(dá)是理解 Linux 信號(hào)機(jī)制的重要概念,。執(zhí)行信號(hào)的處理動(dòng)作稱為信號(hào)遞達(dá)(Delivery),,信號(hào)從產(chǎn)生到遞達(dá)之間的狀態(tài),稱為信號(hào)未決(Pending),。進(jìn)程可以選擇阻塞(Block)某個(gè)信號(hào),。被阻塞的信號(hào)產(chǎn)生時(shí)將保持在未決狀態(tài),直到進(jìn)程解除對(duì)此信號(hào)的阻塞,,才執(zhí)行遞達(dá)的動(dòng)作,。注意,阻塞和忽略是不同,,只要信號(hào)被阻塞就不會(huì)遞達(dá),,而忽略是在遞達(dá)之后可選的一種處理動(dòng)作。 信號(hào)在內(nèi)核中的表示可以看作是這樣的:在 PCB 進(jìn)程控制塊中有信號(hào)屏蔽狀態(tài)字(block),、信號(hào)未決狀態(tài)字(pending)以及是否忽略標(biāo)志(或是信號(hào)處理函數(shù)),。block 狀態(tài)字和 pending 狀態(tài)字都是 64bit。信號(hào)屏蔽狀態(tài)字(block)中,,1 代表阻塞,、0 代表不阻塞;信號(hào)未決狀態(tài)字(pending)的 1 代表未決,,0 代表信號(hào)可以抵達(dá)了,。它們都是每一個(gè) bit 代表一個(gè)信號(hào),,比如,bit0 代表信號(hào) SIGHUP,。 可以使用信號(hào)集操作函數(shù)來(lái)操作信號(hào)集,。例如:
還有一個(gè)函數(shù)可以讀取更改屏蔽狀態(tài)字的 API 函數(shù) int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);。參數(shù) how 有下面三種取值:
還有一個(gè)函數(shù)可以讀取未決狀態(tài)字(pending)信息:int sigpending(sigset_t *set);。它讀取當(dāng)前進(jìn)程的未決信號(hào)集,,通過(guò) set 參數(shù)傳出,。調(diào)用成功則返回 0,出錯(cuò)則返回 -1,。 6.3信號(hào)的捕捉與阻塞在 Linux 中,,可以使用 signal 和 sigaction 系統(tǒng)調(diào)用來(lái)自定義信號(hào)處理函數(shù),實(shí)現(xiàn)對(duì)特定信號(hào)的捕捉和處理,。 signal 函數(shù)較為簡(jiǎn)單,,其函數(shù)原型為 typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);。它主要用于處理前 32 種非實(shí)時(shí)信號(hào),,不支持信號(hào)的傳遞信息,。例如,當(dāng)使用 signal(SIGINT, my_func) 函數(shù)調(diào)用時(shí),,其中 my_func 是自定義函數(shù),。應(yīng)用進(jìn)程收到 SIGINT 信號(hào)時(shí),,會(huì)跳轉(zhuǎn)到自定義處理信號(hào)函數(shù) my_func 處執(zhí)行。在 Linux 系統(tǒng)中,,signal 函數(shù)已被改寫,,由 sigaction 函數(shù)封裝實(shí)現(xiàn)。 sigaction 函數(shù)則更加強(qiáng)大,,它可以讀取和修改與指定信號(hào)相關(guān)聯(lián)的處理動(dòng)作,。函數(shù)原型為 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)。其中,,signum 代表指定信號(hào)的編號(hào),;若 act 指針?lè)强眨瑒t根據(jù) act 修改該信號(hào)的處理動(dòng)作,;若 oldact 指針?lè)强?,則通過(guò) oldact 傳出該信號(hào)原來(lái)的處理動(dòng)作。struct sigaction 結(jié)構(gòu)體成員解釋如下:
例如,,以下代碼用 sigaction 函數(shù)對(duì) 2 號(hào)信號(hào)進(jìn)行了捕捉,,將 2 號(hào)信號(hào)的處理動(dòng)作改為了自定義的打印動(dòng)作,并在執(zhí)行一次自定義動(dòng)作后將 2 號(hào)信號(hào)的處理動(dòng)作恢復(fù)為原來(lái)默認(rèn)的處理動(dòng)作:
6.4異步信號(hào)安全我們可以通過(guò)設(shè)置信號(hào)處理函數(shù)來(lái)捕獲信號(hào),那信號(hào)處理函數(shù)能像普通函數(shù)一樣什么接口函數(shù)都能調(diào)用嗎,?不能,,我們只能調(diào)用異步信號(hào)安全的函數(shù)。很多常用的函數(shù)都不是信號(hào)安全函數(shù),,不能在信號(hào)處理函數(shù)里面調(diào)用,,比如printf。那要是想在信號(hào)處理函數(shù)里面輸出數(shù)據(jù)該咋辦呢,?可以使用write接口函數(shù),,這個(gè)函數(shù)是異步信號(hào)安全的。 6.5信號(hào)處理流程信號(hào)處理是在線程從內(nèi)核空間返回用戶空間的時(shí)候處理的,。而從內(nèi)核空間返回用戶空間是和架構(gòu)相關(guān)的,,所以這一部分的代碼是在架構(gòu)代碼里面的。下面我們以x86為例講解一下(代碼進(jìn)行了刪減),。 linux-src/kernel/entry/common.c
linux-src/arch/x86/kernel/signal.c
可以看出線程在返回到用戶空間之前不斷地檢查有沒(méi)有信號(hào)要處理,。如果有的話就使用函數(shù)get_signal取出一個(gè)信號(hào),然后在函數(shù)handle_signal里面去執(zhí)行,。get_signal的代碼我們就不貼出來(lái)了,,在這里講一下它的大概邏輯。 get_signal會(huì)先看有沒(méi)有STOP相關(guān)的信號(hào),,如果有的話執(zhí)行處理,。然后去取一個(gè)信號(hào)出來(lái),先取同步信號(hào),,同步信號(hào)只從當(dāng)前線程的信號(hào)隊(duì)列里去取,,這里的同步信號(hào)是指前面講的異常處理的6個(gè)信號(hào),。 如果沒(méi)有同步信號(hào)的話就去取其它信號(hào),,其它信號(hào)先從線程的信號(hào)隊(duì)列里面去取,如果沒(méi)有的話就再去進(jìn)程的信號(hào)里面去取,。如果取到的信號(hào)的處理設(shè)置是忽略,,或者是默認(rèn)處理但默認(rèn)處理方式也是忽略,則繼續(xù)取下一個(gè)信號(hào),。 如果取到的信號(hào)沒(méi)有設(shè)置信號(hào)處理函數(shù),,則在這里執(zhí)行其默認(rèn)處理,終結(jié)進(jìn)程或者coredump之后再終結(jié)進(jìn)程,。如果沒(méi)有取到信號(hào)則get_signal返回值為0,,如果取到了信號(hào),且信號(hào)設(shè)置了信號(hào)處理函數(shù)則返回值為1,,且輸出參數(shù)ksig會(huì)包含相應(yīng)信號(hào)的相關(guān)的信息,。然后把ksig傳遞給函數(shù)handle_signal來(lái)處理。下面我們看一下handle_signal函數(shù)的實(shí)現(xiàn),linux-src/arch/x86/kernel/signal.c
這段代碼雖然看起來(lái)不太復(fù)雜,,但是實(shí)際上卻非常難以理解,。setup_rt_frame為了使線程返回用戶空間后能執(zhí)行信號(hào)處理函數(shù)便開始偽造用戶線程棧幀。棧幀首先保存一些線程當(dāng)前的狀態(tài)到棧上,,然后再偽造出仿佛是一個(gè)蹦床函數(shù)調(diào)用了信號(hào)處理函數(shù)一樣,。然后再偽造出仿佛是信號(hào)處理函數(shù)通過(guò)系統(tǒng)調(diào)用進(jìn)入了內(nèi)核一樣。 然后線程從內(nèi)核返回用戶空間就會(huì)執(zhí)行信號(hào)處理函數(shù),,信號(hào)處理函數(shù)執(zhí)行完返回的時(shí)候時(shí)候會(huì)返回到蹦床函數(shù),。蹦床函數(shù)會(huì)調(diào)用sigreturn系統(tǒng)調(diào)用進(jìn)入內(nèi)核,sigreturn會(huì)讀取蹦床函數(shù)的棧幀,,因?yàn)檫@上面保持的是之前的線程執(zhí)行信息,。然后把這些信息進(jìn)行恢復(fù),這樣線程再回到用戶空間的時(shí)候就又回到了線程之前執(zhí)行的地方,。 七,、信號(hào)處理的同步化對(duì)于異步信號(hào)來(lái)說(shuō),有很多的問(wèn)題,,比如你不確定你正在干啥的時(shí)候它來(lái)了,,還有就是在異步信號(hào)的處理函數(shù)里面有很多的函數(shù)不能調(diào)用。為此我們可以把異步信號(hào)轉(zhuǎn)化為同步信號(hào),。我們前面說(shuō)過(guò),,同步信號(hào)、異步信號(hào)是指信號(hào)的發(fā)送是同步的還是異步的,,那異步信號(hào)肯定不可能轉(zhuǎn)化為同步信號(hào)啊,。我們此處所說(shuō)的轉(zhuǎn)化是指把信號(hào)的處理從異步轉(zhuǎn)化為同步。 轉(zhuǎn)化的方法就是用一個(gè)函數(shù)來(lái)等信號(hào),,這樣信號(hào)和線程執(zhí)行的相對(duì)性就是固定的了,,就相當(dāng)于是同步信號(hào)了。等的方式有兩種,,一種是等待信號(hào)被處理,,信號(hào)還是走前面所說(shuō)的處理流程,另一種是等待信號(hào)并截獲信號(hào),,信號(hào)被我們偷走了,,不會(huì)再走前面所說(shuō)的信號(hào)處理流程了。 7.1 信號(hào)等待信號(hào)等待的接口函數(shù)有兩個(gè)pause和sigsuspend,,它們的接口是:
7.2 信號(hào)截獲除了等待信號(hào)被處理之外,,我們還可以等待并截獲信號(hào),信號(hào)就不會(huì)走正常的處理流程,,我們可以對(duì)截獲到的信號(hào)進(jìn)行相應(yīng)的處理,。信號(hào)截獲一共有四個(gè)接口函數(shù),,我們先來(lái)講三個(gè)。
接口函數(shù)sigwait有兩個(gè)參數(shù),,第一個(gè)參數(shù)是要等待的信號(hào)集,,第二個(gè)參數(shù)是輸出參數(shù),是等待并截獲到的信號(hào),。函數(shù)返回之后,,我們就可以根據(jù)sig的值進(jìn)行相應(yīng)的處理。接口函數(shù)sigwaitinfo也有兩個(gè)參數(shù),,第一個(gè)參數(shù)和前面的是一樣的,,第二個(gè)參數(shù)是輸出參數(shù),類型是siginfo_t,,能獲得更多信號(hào)相關(guān)的信息,。接口函數(shù)sigtimedwait和sigwaitinfo差不多,只是多個(gè)了時(shí)間參數(shù),,如果等了這么長(zhǎng)時(shí)間之后還沒(méi)有等來(lái)信號(hào)就會(huì)直接返回,。 還有一個(gè)接口函數(shù),它把要等待的信號(hào)信息轉(zhuǎn)化為了fd,,等信號(hào)直接變成了read fd的操作,。其接口如下:
第二個(gè)參數(shù)代表要等待的信號(hào)集。第一個(gè)參數(shù)如果是-1,,代表要?jiǎng)?chuàng)建一個(gè)新的fd,,如果是一個(gè)已有的signalfd,代表修改已經(jīng)fd的信號(hào)集,。然后我們就可以對(duì)這個(gè)fd進(jìn)行read操作了,,read的緩存區(qū)至少要有 sizeof(struct signalfd_siginfo)個(gè)字節(jié)。Read每次返回都會(huì)讀取若干個(gè)struct signalfd_siginfo結(jié)構(gòu)體,。最關(guān)鍵的是我們還可以對(duì)這個(gè)fd進(jìn)行select,、poll操作。 八,、應(yīng)用場(chǎng)景與總結(jié)8.1應(yīng)用場(chǎng)景舉例⑴使用 “ctrl+c” 中止程序 當(dāng)用戶在終端運(yùn)行程序時(shí),,按下 “Ctrl+C” 會(huì)產(chǎn)生SIGINT信號(hào)。這個(gè)信號(hào)通常會(huì)被發(fā)送給前臺(tái)進(jìn)程,,以請(qǐng)求終止進(jìn)程。例如,,在一個(gè)長(zhǎng)時(shí)間運(yùn)行的計(jì)算任務(wù)中,,如果用戶發(fā)現(xiàn)結(jié)果不符合預(yù)期或者想要提前終止程序,就可以通過(guò)按下 “Ctrl+C” 來(lái)發(fā)送SIGINT信號(hào),。當(dāng)進(jìn)程接收到這個(gè)信號(hào)后,,會(huì)根據(jù)其對(duì)SIGINT信號(hào)的處理方式來(lái)做出響應(yīng)。如果進(jìn)程沒(méi)有自定義信號(hào)處理函數(shù),那么通常會(huì)采用默認(rèn)的處理動(dòng)作,,即終止進(jìn)程,。 ⑵kill 命令殺進(jìn)程 在 Linux 系統(tǒng)中,kill命令是一個(gè)常用的工具,,用于向進(jìn)程發(fā)送信號(hào)以終止它們,。例如,kill -9 <進(jìn)程的 PID>會(huì)向指定的進(jìn)程發(fā)送SIGKILL信號(hào),。SIGKILL信號(hào)是一種強(qiáng)制終止信號(hào),,無(wú)法被捕捉、忽略或阻塞,。當(dāng)進(jìn)程接收到SIGKILL信號(hào)時(shí),,會(huì)立即終止。這種方式通常用于終止那些無(wú)法正常退出的進(jìn)程,,或者在系統(tǒng)出現(xiàn)問(wèn)題時(shí)強(qiáng)制關(guān)閉某些進(jìn)程以恢復(fù)系統(tǒng)的穩(wěn)定性,。 除了終止進(jìn)程,信號(hào)機(jī)制還可以用于進(jìn)程間的通信,。例如,,一個(gè)進(jìn)程可以向另一個(gè)進(jìn)程發(fā)送特定的信號(hào),以通知它某個(gè)事件的發(fā)生,。這種通信方式雖然比較簡(jiǎn)單,,但在某些情況下非常有用。 8.2總結(jié)信號(hào)機(jī)制的重要性Linux 信號(hào)機(jī)制在編寫健壯程序中具有至關(guān)重要的意義,。首先,,它提供了一種靈活的方式來(lái)處理異步事件。在復(fù)雜的多進(jìn)程或多線程環(huán)境中,,程序可能會(huì)面臨各種不可預(yù)測(cè)的情況,,如硬件故障、用戶輸入,、系統(tǒng)資源變化等,。通過(guò)信號(hào)機(jī)制,程序可以及時(shí)響應(yīng)這些事件,,采取適當(dāng)?shù)拇胧?,避免出現(xiàn)不可預(yù)料的錯(cuò)誤或崩潰。 其次,,信號(hào)機(jī)制使得進(jìn)程間的通信更加多樣化,。相比于傳統(tǒng)的管道、共享內(nèi)存等通信方式,,信號(hào)通信更加輕量級(jí)和高效,。它可以用于簡(jiǎn)單的事件通知,,讓不同的進(jìn)程之間能夠協(xié)調(diào)工作,提高系統(tǒng)的整體性能和穩(wěn)定性,。 深入理解信號(hào)機(jī)制還可以幫助程序員更好地調(diào)試和優(yōu)化程序,。當(dāng)程序出現(xiàn)異常情況時(shí),通過(guò)分析信號(hào)的產(chǎn)生和處理過(guò)程,,可以快速定位問(wèn)題所在,。同時(shí),合理地利用信號(hào)機(jī)制可以優(yōu)化程序的資源管理,,例如在程序退出時(shí)及時(shí)清理資源,,避免資源泄漏。 |
|