久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

探索Linux信號(hào)機(jī)制:有效管理進(jìn)程間通信

 深度Linux 2024-11-19 發(fā)布于湖南

在 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è)線程處理是不確定的,。

  • 信號(hào)掩碼(mask)的設(shè)置是線程私有的,每個(gè)線程都可以設(shè)置不同的信號(hào)掩碼,。

  • 信號(hào)處理方式的設(shè)置是進(jìn)程全局的,,后面線程設(shè)置的方式會(huì)覆蓋前面線程的設(shè)置。

  • 信號(hào)處理的效果是進(jìn)程全局的,。

我們先說(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)不同。

  1. 實(shí)時(shí)信號(hào)如果使用接口sigqueue發(fā)送的話,,可以攜帶一個(gè)額外的整數(shù)信息或者指針信息,。

  2. 實(shí)時(shí)信號(hào)有優(yōu)先級(jí),數(shù)值越小優(yōu)先級(jí)越高,,優(yōu)先級(jí)高的優(yōu)先處理,,同等優(yōu)先級(jí)的按照先來(lái)后到的順序處理。

  3. 標(biāo)準(zhǔn)信號(hào)都是預(yù)定義信號(hào),,每個(gè)信號(hào)都有特定的含義,,而實(shí)時(shí)信號(hào)則沒(méi)有預(yù)定義的含義。

根據(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

/* Send a signal to the slave */
static int pty_signal(struct tty_struct *tty, int sig)
{
struct pid *pgrp;

if (sig != SIGINT && sig != SIGQUIT && sig != SIGTSTP)
return -EINVAL;

if (tty->link) {
pgrp = tty_get_pgrp(tty->link);
if (pgrp)
kill_pgrp(pgrp, sig, 1);
put_pid(pgrp);
}
return 0;
}

linux-src/drivers/tty/sysrq.c

static void sysrq_handle_term(int key)
{
send_sig_all(SIGTERM);
console_loglevel = CONSOLE_LOGLEVEL_DEBUG;
}

/*
* Signal sysrq helper function. Sends a signal to all user processes.
*/
static void send_sig_all(int sig)
{
struct task_struct *p;

read_lock(&tasklist_lock);
for_each_process(p) {
if (p->flags & PF_KTHREAD)
continue;
if (is_global_init(p))
continue;

do_send_sig_info(sig, SEND_SIG_PRIV, p, PIDTYPE_MAX);
}
read_unlock(&tasklist_lock);
}

linux-src/drivers/tty/tty_io.c

static void __tty_hangup(struct tty_struct *tty, int exit_session)
{
refs = tty_signal_session_leader(tty, exit_session);
}

linux-src/drivers/tty/tty_jobctrl.c

int tty_signal_session_leader(struct tty_struct *tty, int exit_session)
{
struct task_struct *p;
int refs = 0;
struct pid *tty_pgrp = NULL;

read_lock(&tasklist_lock);
if (tty->ctrl.session) {
do_each_pid_task(tty->ctrl.session, PIDTYPE_SID, p) {
spin_lock_irq(&p->sighand->siglock);
if (p->signal->tty == tty) {
p->signal->tty = NULL;
/*
* We defer the dereferences outside of
* the tasklist lock.
*/
refs++;
}
if (!p->signal->leader) {
spin_unlock_irq(&p->sighand->siglock);
continue;
}
__group_send_sig_info(SIGHUP, SEND_SIG_PRIV, p);
__group_send_sig_info(SIGCONT, SEND_SIG_PRIV, p);
put_pid(p->signal->tty_old_pgrp); /* A noop */
spin_lock(&tty->ctrl.lock);
tty_pgrp = get_pid(tty->ctrl.pgrp);
if (tty->ctrl.pgrp)
p->signal->tty_old_pgrp =
get_pid(tty->ctrl.pgrp);
spin_unlock(&tty->ctrl.lock);
spin_unlock_irq(&p->sighand->siglock);
} while_each_pid_task(tty->ctrl.session, PIDTYPE_SID, p);
}
read_unlock(&tasklist_lock);

if (tty_pgrp) {
if (exit_session)
kill_pgrp(tty_pgrp, SIGHUP, exit_session);
put_pid(tty_pgrp);
}

return refs;
}

這是終端驅(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

DEFINE_IDTENTRY_RAW_ERRORCODE(exc_page_fault)
{
unsigned long address = read_cr2();
irqentry_state_t state;

prefetchw(&current->mm->mmap_lock);

if (kvm_handle_async_pf(regs, (u32)address))
return;

state = irqentry_enter(regs);

instrumentation_begin();
handle_page_fault(regs, error_code, address);
instrumentation_end();

irqentry_exit(regs, state);
}

static __always_inline void
handle_page_fault(struct pt_regs *regs, unsigned long error_code,
unsigned long address)
{
trace_page_fault_entries(regs, error_code, address);

if (unlikely(kmmio_fault(regs, address)))
return;

if (unlikely(fault_in_kernel_space(address))) {
do_kern_addr_fault(regs, error_code, address);
} else {
do_user_addr_fault(regs, error_code, address);
local_irq_disable();
}
}
static inline
void do_user_addr_fault(struct pt_regs *regs,
unsigned long error_code,
unsigned long address)
{
struct vm_area_struct *vma;
struct task_struct *tsk;
struct mm_struct *mm;
vm_fault_t fault;
unsigned int flags = FAULT_FLAG_DEFAULT;

tsk = current;
mm = tsk->mm;

if (unlikely((error_code & (X86_PF_USER | X86_PF_INSTR)) == X86_PF_INSTR)) {
/*
* Whoops, this is kernel mode code trying to execute from
* user memory. Unless this is AMD erratum #93, which
* corrupts RIP such that it looks like a user address,
* this is unrecoverable. Don't even try to look up the
* VMA or look for extable entries.
*/
if (is_errata93(regs, address))
return;

page_fault_oops(regs, error_code, address);
return;
}

/* kprobes don't want to hook the spurious faults: */
if (WARN_ON_ONCE(kprobe_page_fault(regs, X86_TRAP_PF)))
return;

/*
* Reserved bits are never expected to be set on
* entries in the user portion of the page tables.
*/
if (unlikely(error_code & X86_PF_RSVD))
pgtable_bad(regs, error_code, address);

/*
* If SMAP is on, check for invalid kernel (supervisor) access to user
* pages in the user address space. The odd case here is WRUSS,
* which, according to the preliminary documentation, does not respect
* SMAP and will have the USER bit set so, in all cases, SMAP
* enforcement appears to be consistent with the USER bit.
*/
if (unlikely(cpu_feature_enabled(X86_FEATURE_SMAP) &&
!(error_code & X86_PF_USER) &&
!(regs->flags & X86_EFLAGS_AC))) {
/*
* No extable entry here. This was a kernel access to an
* invalid pointer. get_kernel_nofault() will not get here.
*/
page_fault_oops(regs, error_code, address);
return;
}

/*
* If we're in an interrupt, have no user context or are running
* in a region with pagefaults disabled then we must not take the fault
*/
if (unlikely(faulthandler_disabled() || !mm)) {
bad_area_nosemaphore(regs, error_code, address);
return;
}

/*
* It's safe to allow irq's after cr2 has been saved and the
* vmalloc fault has been handled.
*
* User-mode registers count as a user access even for any
* potential system fault or CPU buglet:
*/
if (user_mode(regs)) {
local_irq_enable();
flags |= FAULT_FLAG_USER;
} else {
if (regs->flags & X86_EFLAGS_IF)
local_irq_enable();
}

perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, address);

if (error_code & X86_PF_WRITE)
flags |= FAULT_FLAG_WRITE;
if (error_code & X86_PF_INSTR)
flags |= FAULT_FLAG_INSTRUCTION;

#ifdef CONFIG_X86_64
/*
* Faults in the vsyscall page might need emulation. The
* vsyscall page is at a high address (>PAGE_OFFSET), but is
* considered to be part of the user address space.
*
* The vsyscall page does not have a "real" VMA, so do this
* emulation before we go searching for VMAs.
*
* PKRU never rejects instruction fetches, so we don't need
* to consider the PF_PK bit.
*/
if (is_vsyscall_vaddr(address)) {
if (emulate_vsyscall(error_code, regs, address))
return;
}
#endif

/*
* Kernel-mode access to the user address space should only occur
* on well-defined single instructions listed in the exception
* tables. But, an erroneous kernel fault occurring outside one of
* those areas which also holds mmap_lock might deadlock attempting
* to validate the fault against the address space.
*
* Only do the expensive exception table search when we might be at
* risk of a deadlock. This happens if we
* 1. Failed to acquire mmap_lock, and
* 2. The access did not originate in userspace.
*/
if (unlikely(!mmap_read_trylock(mm))) {
if (!user_mode(regs) && !search_exception_tables(regs->ip)) {
/*
* Fault from code in kernel from
* which we do not expect faults.
*/
bad_area_nosemaphore(regs, error_code, address);
return;
}
retry:
mmap_read_lock(mm);
} else {
/*
* The above down_read_trylock() might have succeeded in
* which case we'll have missed the might_sleep() from
* down_read():
*/
might_sleep();
}

vma = find_vma(mm, address);
if (unlikely(!vma)) {
bad_area(regs, error_code, address);
return;
}
if (likely(vma->vm_start <= address))
goto good_area;
if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
bad_area(regs, error_code, address);
return;
}
if (unlikely(expand_stack(vma, address))) {
bad_area(regs, error_code, address);
return;
}

/*
* Ok, we have a good vm_area for this memory access, so
* we can handle it..
*/
good_area:
if (unlikely(access_error(error_code, vma))) {
bad_area_access_error(regs, error_code, address, vma);
return;
}

/*
* If for any reason at all we couldn't handle the fault,
* make sure we exit gracefully rather than endlessly redo
* the fault. Since we never set FAULT_FLAG_RETRY_NOWAIT, if
* we get VM_FAULT_RETRY back, the mmap_lock has been unlocked.
*
* Note that handle_userfault() may also release and reacquire mmap_lock
* (and not return with VM_FAULT_RETRY), when returning to userland to
* repeat the page fault later with a VM_FAULT_NOPAGE retval
* (potentially after handling any pending signal during the return to
* userland). The return to userland is identified whenever
* FAULT_FLAG_USER|FAULT_FLAG_KILLABLE are both set in flags.
*/
fault = handle_mm_fault(vma, address, flags, regs);

if (fault_signal_pending(fault, regs)) {
/*
* Quick path to respond to signals. The core mm code
* has unlocked the mm for us if we get here.
*/
if (!user_mode(regs))
kernelmode_fixup_or_oops(regs, error_code, address,
SIGBUS, BUS_ADRERR,
ARCH_DEFAULT_PKEY);
return;
}

/*
* If we need to retry the mmap_lock has already been released,
* and if there is a fatal signal pending there is no guarantee
* that we made any progress. Handle this case first.
*/
if (unlikely((fault & VM_FAULT_RETRY) &&
(flags & FAULT_FLAG_ALLOW_RETRY))) {
flags |= FAULT_FLAG_TRIED;
goto retry;
}

mmap_read_unlock(mm);
if (likely(!(fault & VM_FAULT_ERROR)))
return;

if (fatal_signal_pending(current) && !user_mode(regs)) {
kernelmode_fixup_or_oops(regs, error_code, address,
0, 0, ARCH_DEFAULT_PKEY);
return;
}

if (fault & VM_FAULT_OOM) {
/* Kernel mode? Handle exceptions or die: */
if (!user_mode(regs)) {
kernelmode_fixup_or_oops(regs, error_code, address,
SIGSEGV, SEGV_MAPERR,
ARCH_DEFAULT_PKEY);
return;
}

/*
* We ran out of memory, call the OOM killer, and return the
* userspace (which will retry the fault, or kill us if we got
* oom-killed):
*/
pagefault_out_of_memory();
} else {
if (fault & (VM_FAULT_SIGBUS|VM_FAULT_HWPOISON|
VM_FAULT_HWPOISON_LARGE))
do_sigbus(regs, error_code, address, fault);
else if (fault & VM_FAULT_SIGSEGV)
bad_area_nosemaphore(regs, error_code, address);
else
BUG();
}
}
static void
__bad_area_nosemaphore(struct pt_regs *regs, unsigned long error_code,
unsigned long address, u32 pkey, int si_code)
{
struct task_struct *tsk = current;
if (likely(show_unhandled_signals))
show_signal_msg(regs, error_code, address, tsk);

set_signal_archinfo(address, error_code);

if (si_code == SEGV_PKUERR)
force_sig_pkuerr((void __user *)address, pkey);
else
force_sig_fault(SIGSEGV, si_code, (void __user *)address);

local_irq_disable();
}

處理用戶空間缺頁(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

struct sigpending {
struct list_head list;
sigset_t signal;
};

可以看到信號(hào)隊(duì)列非常簡(jiǎn)單,,sigset是個(gè)bit flag,,代表當(dāng)前隊(duì)列里有哪些信號(hào),list是信號(hào)列表的頭指針,。下面我們來(lái)看一下信號(hào)隊(duì)列里的條目,。

struct sigqueue {
struct list_head list;
int flags;
kernel_siginfo_t info;
struct ucounts *ucounts;
};

每發(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

static int send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
enum pid_type type)
{
/* Should SIGKILL or SIGSTOP be received by a pid namespace init? */
bool force = false;

if (info == SEND_SIG_NOINFO) {
/* Force if sent from an ancestor pid namespace */
force = !task_pid_nr_ns(current, task_active_pid_ns(t));
} else if (info == SEND_SIG_PRIV) {
/* Don't ignore kernel generated signals */
force = true;
} else if (has_si_pid_and_uid(info)) {
/* SIGKILL and SIGSTOP is special or has ids */
struct user_namespace *t_user_ns;

rcu_read_lock();
t_user_ns = task_cred_xxx(t, user_ns);
if (current_user_ns() != t_user_ns) {
kuid_t uid = make_kuid(current_user_ns(), info->si_uid);
info->si_uid = from_kuid_munged(t_user_ns, uid);
}
rcu_read_unlock();

/* A kernel generated signal? */
force = (info->si_code == SI_KERNEL);

/* From an ancestor pid namespace? */
if (!task_pid_nr_ns(current, task_active_pid_ns(t))) {
info->si_pid = 0;
force = true;
}
}
return __send_signal(sig, info, t, type, force);
}

static int __send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
enum pid_type type, bool force)
{
struct sigpending *pending;
struct sigqueue *q;
int override_rlimit;
int ret = 0, result;

assert_spin_locked(&t->sighand->siglock);

result = TRACE_SIGNAL_IGNORED;
if (!prepare_signal(sig, t, force))
goto ret;

pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
/*
* Short-circuit ignored signals and support queuing
* exactly one non-rt signal, so that we can get more
* detailed information about the cause of the signal.
*/
result = TRACE_SIGNAL_ALREADY_PENDING;
if (legacy_queue(pending, sig))
goto ret;

result = TRACE_SIGNAL_DELIVERED;
/*
* Skip useless siginfo allocation for SIGKILL and kernel threads.
*/
if ((sig == SIGKILL) || (t->flags & PF_KTHREAD))
goto out_set;

/*
* Real-time signals must be queued if sent by sigqueue, or
* some other real-time mechanism. It is implementation
* defined whether kill() does so. We attempt to do so, on
* the principle of least surprise, but since kill is not
* allowed to fail with EAGAIN when low on memory we just
* make sure at least one signal gets delivered and don't
* pass on the info struct.
*/
if (sig < SIGRTMIN)
override_rlimit = (is_si_special(info) || info->si_code >= 0);
else
override_rlimit = 0;

q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit, 0);

if (q) {
list_add_tail(&q->list, &pending->list);
switch ((unsigned long) info) {
case (unsigned long) SEND_SIG_NOINFO:
clear_siginfo(&q->info);
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_USER;
q->info.si_pid = task_tgid_nr_ns(current,
task_active_pid_ns(t));
rcu_read_lock();
q->info.si_uid =
from_kuid_munged(task_cred_xxx(t, user_ns),
current_uid());
rcu_read_unlock();
break;
case (unsigned long) SEND_SIG_PRIV:
clear_siginfo(&q->info);
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_KERNEL;
q->info.si_pid = 0;
q->info.si_uid = 0;
break;
default:
copy_siginfo(&q->info, info);
break;
}
} else if (!is_si_special(info) &&
sig >= SIGRTMIN && info->si_code != SI_USER) {
/*
* Queue overflow, abort. We may abort if the
* signal was rt and sent by user using something
* other than kill().
*/
result = TRACE_SIGNAL_OVERFLOW_FAIL;
ret = -EAGAIN;
goto ret;
} else {
/*
* This is a silent loss of information. We still
* send the signal, but the *info bits are lost.
*/
result = TRACE_SIGNAL_LOSE_INFO;
}

out_set:
signalfd_notify(t, sig);
sigaddset(&pending->signal, sig);

/* Let multiprocess signals appear after on-going forks */
if (type > PIDTYPE_TGID) {
struct multiprocess_signals *delayed;
hlist_for_each_entry(delayed, &t->signal->multiprocess, node) {
sigset_t *signal = &delayed->signal;
/* Can't queue both a stop and a continue signal */
if (sig == SIGCONT)
sigdelsetmask(signal, SIG_KERNEL_STOP_MASK);
else if (sig_kernel_stop(sig))
sigdelset(signal, SIGCONT);
sigaddset(signal, sig);
}
}

complete_signal(sig, t, type);
ret:
trace_signal_generate(sig, info, t, type != PIDTYPE_PID, result);
return ret;
}

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

static int
force_sig_info_to_task(struct kernel_siginfo *info, struct task_struct *t,
enum sig_handler handler)
{
unsigned long int flags;
int ret, blocked, ignored;
struct k_sigaction *action;
int sig = info->si_signo;

spin_lock_irqsave(&t->sighand->siglock, flags);
action = &t->sighand->action[sig-1];
ignored = action->sa.sa_handler == SIG_IGN;
blocked = sigismember(&t->blocked, sig);
if (blocked || ignored || (handler != HANDLER_CURRENT)) {
action->sa.sa_handler = SIG_DFL;
if (handler == HANDLER_EXIT)
action->sa.sa_flags |= SA_IMMUTABLE;
if (blocked) {
sigdelset(&t->blocked, sig);
recalc_sigpending_and_wake(t);
}
}
/*
* Don't clear SIGNAL_UNKILLABLE for traced tasks, users won't expect
* debugging to leave init killable. But HANDLER_EXIT is always fatal.
*/
if (action->sa.sa_handler == SIG_DFL &&
(!t->ptrace || (handler == HANDLER_EXIT)))
t->signal->flags &= ~SIGNAL_UNKILLABLE;
ret = send_signal(sig, info, t, PIDTYPE_PID);
spin_unlock_irqrestore(&t->sighand->siglock, flags);

return ret;
}

內(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

SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct kernel_siginfo info;

prepare_kill_siginfo(sig, &info);

return kill_something_info(sig, &info, pid);
}

static int kill_something_info(int sig, struct kernel_siginfo *info, pid_t pid)
{
int ret;

if (pid > 0)
return kill_proc_info(sig, info, pid);

/* -INT_MIN is undefined. Exclude this case to avoid a UBSAN warning */
if (pid == INT_MIN)
return -ESRCH;

read_lock(&tasklist_lock);
if (pid != -1) {
ret = __kill_pgrp_info(sig, info,
pid ? find_vpid(-pid) : task_pgrp(current));
} else {
int retval = 0, count = 0;
struct task_struct * p;

for_each_process(p) {
if (task_pid_vnr(p) > 1 &&
!same_thread_group(p, current)) {
int err = group_send_sig_info(sig, info, p,
PIDTYPE_MAX);
++count;
if (err != -EPERM)
retval = err;
}
}
ret = count ? retval : -ESRCH;
}
read_unlock(&tasklist_lock);

return ret;
}

static int kill_proc_info(int sig, struct kernel_siginfo *info, pid_t pid)
{
int error;
rcu_read_lock();
error = kill_pid_info(sig, info, find_vpid(pid));
rcu_read_unlock();
return error;
}

int kill_pid_info(int sig, struct kernel_siginfo *info, struct pid *pid)
{
int error = -ESRCH;
struct task_struct *p;

for (;;) {
rcu_read_lock();
p = pid_task(pid, PIDTYPE_PID);
if (p)
error = group_send_sig_info(sig, info, p, PIDTYPE_TGID);
rcu_read_unlock();
if (likely(!p || error != -ESRCH))
return error;

/*
* The task was unhashed in between, try again. If it
* is dead, pid_task() will return NULL, if we race with
* de_thread() it will find the new leader.
*/
}
}

下面再來(lái)看一下tgkill函數(shù)的流程:linux-src/kernel/signal.c

SYSCALL_DEFINE3(tgkill, pid_t, tgid, pid_t, pid, int, sig)
{
/* This is only valid for single tasks */
if (pid <= 0 || tgid <= 0)
return -EINVAL;

return do_tkill(tgid, pid, sig);
}


static int do_tkill(pid_t tgid, pid_t pid, int sig)
{
struct kernel_siginfo info;

clear_siginfo(&info);
info.si_signo = sig;
info.si_errno = 0;
info.si_code = SI_TKILL;
info.si_pid = task_tgid_vnr(current);
info.si_uid = from_kuid_munged(current_user_ns(), current_uid());

return do_send_specific(tgid, pid, sig, &info);
}

六、信號(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)集,。例如:

  • int sigemptyset(sigset_t *set);:將信號(hào)集清空,共 64bits,。

  • int sigfillset(sigset_t *set);:將信號(hào)集置 1,。

  • int sigaddset(sigset_t *set, int signum);:將 signum 對(duì)應(yīng)的位置為 1。

  • int sigdelset(sigset_t *set, int signum);:將 signum 對(duì)應(yīng)的位置為 0,。

  • int sigismember(const sigset_t *set, int signum);:判斷 signum 是否在該信號(hào)集合中,,如果集合中該位為 1,則返回 1,,表示位于在集合中,。

還有一個(gè)函數(shù)可以讀取更改屏蔽狀態(tài)字的 API 函數(shù) int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);。參數(shù) how 有下面三種取值:

  • SIG_BLOCK:將參數(shù) set 指向的信號(hào)集中設(shè)置的信號(hào)添加到現(xiàn)在的屏蔽狀態(tài)字中,,設(shè)置為阻塞,。

  • SIG_UNBLOCK:將參數(shù) set 指向的信號(hào)集中設(shè)置的信號(hào)添加到現(xiàn)在的屏蔽狀態(tài)字中,,設(shè)置為非阻塞,,也就是解除阻塞。

  • SIG_SETMASK:將參數(shù) set 指向的信號(hào)集直接覆蓋現(xiàn)在的屏蔽狀態(tài)字的值,。如果 oset 是非空指針,,則讀取進(jìn)程的當(dāng)前信號(hào)屏蔽字通過(guò) oset 參數(shù)傳出。若成功則為 0,,若出錯(cuò)則為 -1,。

還有一個(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)體成員解釋如下:

  • sa_handler:如果為 SIG_IGN,,表示忽略信號(hào),;如果為 SIG_DFL,表示執(zhí)行系統(tǒng)默認(rèn)動(dòng)作,;如果為自定義的函數(shù)指針,,表示用自定義函數(shù)捕捉信號(hào),即向內(nèi)核注冊(cè)了一個(gè)信號(hào)處理函數(shù),。所注冊(cè)的信號(hào)處理函數(shù)的返回值為 void,,參數(shù)為 int,通過(guò)參數(shù)可以得知當(dāng)前信號(hào)的編號(hào),,這樣就可以用同一個(gè)函數(shù)處理多種信號(hào),。

  • sa_mask:當(dāng)某個(gè)信號(hào)的處理函數(shù)被調(diào)用,內(nèi)核自動(dòng)將當(dāng)前信號(hào)加入進(jìn)程的信號(hào)屏蔽字,,當(dāng)信號(hào)處理函數(shù)返回時(shí)自動(dòng)恢復(fù)原來(lái)的信號(hào)屏蔽字,。如果在調(diào)用信號(hào)處理函數(shù)時(shí),除了當(dāng)前信號(hào)被自動(dòng)屏蔽之外,,還希望自動(dòng)屏蔽另外一些信號(hào),,則用 sa_mask 字段說(shuō)明這些需要額外屏蔽的信號(hào)。

  • sa_flags:包含一些選項(xiàng),,通常設(shè)置為 0,,表示使用默認(rèn)屬性。

例如,,以下代碼用 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)作:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>

struct sigaction act, oact;

void handler(int signo) {
printf("get a signal:%d\n", signo);
sigaction(2, &oact, NULL);
}

int main() {
// 先把兩個(gè)結(jié)構(gòu)體變量的成員都初始化為 0
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(2, &act, &oact);
while (1) {
printf("I am a process\n");
sleep(1);
}
return 0;
}

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

static unsigned long exit_to_user_mode_loop(struct pt_regs *regs, unsigned long ti_work)
{
while (ti_work & EXIT_TO_USER_MODE_WORK) {
if (ti_work & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
handle_signal_work(regs, ti_work);
}
return ti_work;
}

static void handle_signal_work(struct pt_regs *regs, unsigned long ti_work)
{
if (ti_work & _TIF_NOTIFY_SIGNAL)
tracehook_notify_signal();

arch_do_signal_or_restart(regs, ti_work & _TIF_SIGPENDING);
}

linux-src/arch/x86/kernel/signal.c

void arch_do_signal_or_restart(struct pt_regs *regs, bool has_signal)
{
struct ksignal ksig;

if (has_signal && get_signal(&ksig)) {
handle_signal(&ksig, regs);
return;
}

restore_saved_sigmask();
}

可以看出線程在返回到用戶空間之前不斷地檢查有沒(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

static void
handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
bool stepping, failed;
struct fpu *fpu = &current->thread.fpu;

if (v8086_mode(regs))
save_v86_state((struct kernel_vm86_regs *) regs, VM86_SIGNAL);

/* Are we from a system call? */
if (syscall_get_nr(current, regs) != -1) {
/* If so, check system call restarting.. */
switch (syscall_get_error(current, regs)) {
case -ERESTART_RESTARTBLOCK:
case -ERESTARTNOHAND:
regs->ax = -EINTR;
break;

case -ERESTARTSYS:
if (!(ksig->ka.sa.sa_flags & SA_RESTART)) {
regs->ax = -EINTR;
break;
}
fallthrough;
case -ERESTARTNOINTR:
regs->ax = regs->orig_ax;
regs->ip -= 2;
break;
}
}

/*
* If TF is set due to a debugger (TIF_FORCED_TF), clear TF now
* so that register information in the sigcontext is correct and
* then notify the tracer before entering the signal handler.
*/
stepping = test_thread_flag(TIF_SINGLESTEP);
if (stepping)
user_disable_single_step(current);

failed = (setup_rt_frame(ksig, regs) < 0);
if (!failed) {
/*
* Clear the direction flag as per the ABI for function entry.
*
* Clear RF when entering the signal handler, because
* it might disable possible debug exception from the
* signal handler.
*
* Clear TF for the case when it wasn't set by debugger to
* avoid the recursive send_sigtrap() in SIGTRAP handler.
*/
regs->flags &= ~(X86_EFLAGS_DF|X86_EFLAGS_RF|X86_EFLAGS_TF);
/*
* Ensure the signal handler starts with the new fpu state.
*/
fpu__clear_user_states(fpu);
}
signal_setup_done(failed, ksig, stepping);
}

這段代碼雖然看起來(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,,它們的接口是:

int pause(void);
int sigsuspend(const sigset_t *mask);

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è)。

int sigwait(const sigset_t *restrict set, int *restrict sig);
int sigwaitinfo(const sigset_t *restrict set, siginfo_t *restrict info);
int sigtimedwait(const sigset_t *restrict set, siginfo_t *restrict info, const struct timespec *restrict timeout);

接口函數(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的操作,。其接口如下:

int signalfd(int fd, const sigset_t *mask, int flags);

第二個(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í)清理資源,,避免資源泄漏。

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多