电竞比分网-中国电竞赛事及体育赛事平台

分享

探索Linux信號機制:有效管理進程間通信

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

在 Linux 的世界里,進程就像生活在城市中的人,它們需要相互溝通來協(xié)調(diào)行動。而信號機制呢,就像是一種神奇的 “信號彈”,用于進程之間的交流。當一個進程有重要消息要傳達給另一個進程時,就會發(fā)射出這樣的 “信號彈”。這就是 Linux 信號機制,它是管理進程間通信的一把 “金鑰匙”,讓我們一起深入了解它是如何發(fā)揮作用的吧。

一、概述

Linux 的信號機制作為進程間通信的重要方式,發(fā)揮著關鍵作用。它本質(zhì)上是一種軟件中斷,能夠異步地通知進程發(fā)生了特定事件。信號的全稱為軟中斷信號,簡稱軟中斷,在頭文件<signal.h>中定義了 64 種信號,這些信號的名字都以SIG開頭,且都被定義為正整數(shù),稱為信號編號。可以用 “kill -l” 命令查看信號的具體名稱。

其中,編號為 1~31 的信號為早期 Linux 所支持的信號,是不可靠信號(非實時的),編號為 34~63 的信號時后來擴充的,稱為可靠信號(實時信號)。不可靠信號與可靠信號的區(qū)別在于前者不支持排隊,可能會造成信號丟失,而后者的注冊機制是每收到一個可靠信號就會去注冊這個信號,不會丟失。

信號機制可以類比為硬件中斷,當某個事件發(fā)生時,就像硬件中斷一樣,能夠打斷進程的正常執(zhí)行流,迫使進程去處理特定的事件。例如,當用戶在終端按下Ctrl+C時,會產(chǎn)生SIGINT信號,表示進程應被終止;當控制終端被關閉時,會發(fā)送SIGHUP信號,常用于通知守護進程重新讀取配置。信號機制為進程間的通信和交互提供了一種靈活且有效的方式,使得不同進程能夠在特定事件發(fā)生時做出相應的反應。

二、信號基本原理

信號機制是UNIX系統(tǒng)最古老的機制之一,它不僅是內(nèi)核處理程序在運行時發(fā)生錯誤的方式,還是終端管理進程的方式,并且還是一種進程間通信機制。信號機制由三部分構成,首先是信號是怎么產(chǎn)生的,或者說是誰發(fā)送的,然后是信號是怎么投遞到進程或者線程的,最后是信號是怎么處理的。下面我們先看一張圖:

從圖中我們可以看到信號的產(chǎn)生方式也就是發(fā)送方有三種。首先是終端發(fā)送,比如我們在終端里輸入Ctrl+C快捷鍵時,終端會給當前進程發(fā)送SIGINT信號。其次是內(nèi)核發(fā)送,這里的內(nèi)核發(fā)送是指內(nèi)核里的異常處理的信號發(fā)送,比如進程非法訪問內(nèi)存,在異常處理中就會給當前線程發(fā)送SIGSEGV信號。最后是進程發(fā)送,也就是一個進程給另一個進程發(fā)送或者是進程自己給自己發(fā)送。這里有很多接口函數(shù)可以選擇,有的可以發(fā)給線程,有的可以發(fā)給進程,有的可以發(fā)給進程組甚至會話組。

下一個過程就是信號是如何從發(fā)送方發(fā)送到目標進程或者線程的信號隊列里的,這個過程叫做投遞。不同的發(fā)送方,其發(fā)送方式和投遞過程是不同的,這個后面會展開講。

最后是信號的處理過程,這個最復雜牽涉問題最多。信號發(fā)送可以發(fā)送給進程或者線程,但是信號的處理是在線程中進行的,因為線程是代碼執(zhí)行的單元。線程首先處理自己隊列里的信號,自己的處理完了再去處理進程隊列里的信號。處理的時候要考慮信號掩碼(mask),被掩碼阻塞的信號暫時不處理,還放回原隊列中去。信號處理方式有三種,如果程序什么也沒設置的話,走默認處理(default)方式。默認處理有五種情況,不同的信號,其默認處理方式不同。這五種情況分別是ignore(忽略)、term(終結進程也就是殺死進程)、core(coredump內(nèi)存轉(zhuǎn)儲并殺死進程)、stop(暫停進程)、cont(continue恢復執(zhí)行進程)。還有兩種方式是進程提前通過接口函數(shù)signal或者sigaction設置了處理方式,設置IGN來忽略信號,或者設置一個信號處理函數(shù)handler來處理信號。大家注意,默認處理中的忽略和進程主動設置的忽略,兩者的邏輯是不同的,一個是默認處理是忽略,一個是進程主動要求要忽略。你想要忽略一個默認處理不是忽略的信號,就必須要主動設置忽略。

三、信號的分類與產(chǎn)生

我們明白了信號的基本原理之后,就要進一步追問,系統(tǒng)都有哪些信號呢,這些信號有什么不同呢?剛開始的時候,UNIX系統(tǒng)只有1-31總共31個信號,這些信號每個都有特殊的含義和特定的用法。這些信號的實現(xiàn)有一個特點,它們是用bit flag實現(xiàn)的。這就會導致當一個信號還在待決的時候,又來了一個同樣的信號,再次設置bit位是沒有意義的,所以就會丟失一次信號。為了解決這個問題,后來POSIX規(guī)定增加32-64這33個信號作為實時信號,并規(guī)定實時信號不能丟失,要用隊列來實現(xiàn)。我們把之前的信號1-31叫做標準信號,由于標準信號會丟失,所以標準信號也叫做不可靠信號,由于標準信號是用bit flag實現(xiàn)的,所以標準信號也叫做標記信號(flag signal)。由于實時信號不會丟失,所以實時信號也叫作可靠信號,由于實時信號是用隊列實現(xiàn)的,所以實時信號也叫做排隊信號(queue signal)。我們平常遇到的SIGSEGV、SIGABRT等信都是標準信號。

3.1信號的分類

可靠信號與不可靠信號、實時信號與非實時信號在很多方面存在區(qū)別。

可靠信號與不可靠信號:不可靠信號主要來自早期的 Unix 系統(tǒng),其存在一些問題。例如,進程每次處理完信號后,系統(tǒng)會自動將該信號的處理方式恢復為默認操作,這就需要在信號處理函數(shù)的末尾再次調(diào)用signal()函數(shù)重新綁定處理函數(shù),增加了編程復雜性。而且,不可靠信號可能會丟失,當進程正在處理一個信號時,如果相同類型的另一個信號到達,第二個信號可能會被直接丟棄。而可靠信號支持排隊,即使進程在處理某個信號時有新的信號到達,這些信號也不會丟失,而是被加入隊列,待當前信號處理完成后再依次處理。Linux 引入了新的信號發(fā)送函數(shù)sigqueue()和信號綁定函數(shù)sigaction()來增強信號處理的靈活性和可靠性。

實時信號與非實時信號:非實時信號一般指編號在 1 到 31 之間的信號,不支持排隊,處理時沒有嚴格的順序保證,且如果在處理某個信號時有相同類型的新信號到達,后者可能會被忽略或丟失,所以也被稱為不可靠信號。實時信號是編號在 34 到 64 之間的信號,支持排隊,即使在處理某個信號期間有新的相同類型的信號到達,這些信號也不會被丟棄,而是按照到達的順序依次處理,因此被稱為可靠信號。

信號是單線程時代的產(chǎn)物。在單線程時代,一個進程就只有一個線程(就是主線程),所以進程就是線程,線程就是進程。信號所有的屬性既是進程全局的又是線程私有的,因為這兩者沒有區(qū)別。但是到了多線程時代,這兩者就有區(qū)別了,進程是資源分配與管理的單元,線程是程序執(zhí)行的單元。一個進程往往有多個線程,那么信號的這些屬性究竟應該是進程全局的還是線程私有的呢?這還真不好處理的。經(jīng)過一番慎重的分析與思考,UNIX系統(tǒng)做出了如下的決定。

信號的發(fā)送既可以發(fā)送給進程,也可以發(fā)送給線程,但是同步信號(也就是和當前線程執(zhí)行相關而產(chǎn)生的信號)應當發(fā)送給當前線程。進程發(fā)送信號可以選擇不同的接口函數(shù),有的接口是發(fā)給進程的,有的接口是發(fā)給線程的。線程信號隊列中的信號只能由線程自己處理,進程信號隊列中的信號由進程中的線程處理,具體是由哪個線程處理是不確定的。

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

  • 信號處理方式的設置是進程全局的,后面線程設置的方式會覆蓋前面線程的設置。

  • 信號處理的效果是進程全局的。

我們先說默認處理的幾種情況:忽略一個信號是指整個進程忽略這個信號,而不是說某個線程忽略了其它線程還可以去處理。終結是終結的整個進程,而不只終結一個線程。內(nèi)存轉(zhuǎn)儲是整個進程進行內(nèi)存轉(zhuǎn)儲并終結整個進程。Stop是暫停整個進程而不是只暫停一個線程。Cont是恢復執(zhí)行整個進程而不是只恢復執(zhí)行一個線程。

非默認處理有兩種情況:如果進程設置了忽略某個信號,則是整個進程都忽略這個信號,而不是某個線程忽略這個信號。如果進程設置了信號處理函數(shù)handler,則handler的執(zhí)行效果是進程全局的。這點怎么理解呢?可以從兩方面來理解,一是如果信號是發(fā)送給進程的,則每個線程都有可能來執(zhí)行這個handler;二是handler雖然是在某個線程中執(zhí)行的,但是對于線程來說,只有線程棧是線程私有的,其它內(nèi)存是整個進程共享的,handler對線程棧的影響是線程私有的,handler返回之后它的棧幀就銷毀了,handler只有對全局內(nèi)存的影響才會留下來,所以它的影響是進程全局的。

我們再來總結一下:信號可以發(fā)送給進程也可以發(fā)送給線程。發(fā)送給線程的信號只能由線程處理,如果線程阻塞了信號則信號會一直pending,直到線程解除阻塞然后就會去處理該信號。發(fā)送給進程的信號可以由該進程中的任意一個未阻塞該信號的線程來處理,具體哪個線程是不確定的,如果所有線程都阻塞該信號,則該信號一直pending,直到任一線程解除阻塞。信號無論是怎么發(fā)送和處理的,信號的處理效果都是進程全局的。

3.2信號類型詳解

⑴標準信號與實時信號的區(qū)別

我們知道信號分為標準信號和實時信號,它們之間最大的區(qū)別就是在信號處于待決的狀態(tài)下又來了同樣的信號會怎么處理。除此之外,它們還有以下三點不同。

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

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

  3. 標準信號都是預定義信號,每個信號都有特定的含義,而實時信號則沒有預定義的含義。

根據(jù)特點3,兩個進程可以使用實時信號來達到進程間通信的目的。因為實時信號沒有特定的含義,所以系統(tǒng)不會使用實時信號,進程之間可以自行約定某個信號的含義。而且不同的進程之間可以約定不同的含義而不會相互影響。不過glibc的pthread實現(xiàn)使用了32、33這兩個實時信號,所以大家不要用這兩個實時信號。

⑵信號的屬性特征

可阻塞:我們可以通過某些接口來阻塞(暫時屏蔽)一個信號。但是有的信號可以阻塞,有的信號無法阻塞。有的信號雖然可以成功設置阻塞,但是其信號會被強制發(fā)送,所以最終還是阻塞不了。比如內(nèi)核在異常處理時會強制發(fā)送信號,所以是阻塞不了的。但是同樣的信號你用kill來發(fā),阻塞還是生效的,因為kill不是強制發(fā)送。信號阻塞,有很多地方會叫做信號屏蔽,兩者都是一樣的。但是屏蔽容易被人和忽略理解混了,所以本文里用阻塞。阻塞,含義明確,就是阻塞住了,后面不阻塞了信號還是會到來的。

可忽略:有些信號默認處理就是忽略的,但是有些信號默認處理不是忽略。如果我們想忽略這些信號的話,可以通過一些接口設置來忽略它。有些信號是可以設置忽略的,但是有些接口無法設置忽略。有的信號雖然可以設置忽略成功,但是內(nèi)核在異常處理時會強制發(fā)送信號,這時忽略是無效的。不過同樣的信號用kill來發(fā),忽略就是有效的,因為kill不是強制發(fā)送。大家注意忽略和阻塞不同,阻塞是暫時不處理,而忽略其實也是一種處理,相當于是空處理。

可捕獲:我們可以通過一些接口來設置信號處理函數(shù)handler來處理信號,這個行為叫做捕獲。有些信號是能捕獲的,有些信號是不能捕獲的。與可阻塞和可忽略不同的是,強制發(fā)送的信號也是可捕獲的。但是可捕獲存在一個特殊情況,有些時候是不能二次捕獲的。有兩個信號SIGSEGV、SIGABRT是不能二次捕獲的,后面會進行講解。

默認處理:默認處理是當我們沒有設置忽略和捕獲函數(shù)時,內(nèi)核對信號的默認處理方式。前面已經(jīng)介紹過有五種處理方式,這里就不再贅述了。由于大部分的信號處理是terminate或者coredump,都是會導致進程死亡的,所以信號發(fā)送命令叫做kill。其實kill并不會殺死進程,它只是給進程送了個信號而已。

發(fā)送者:這里指的是信號在一般情況是從哪里發(fā)送的,表明了信號使用的場景。

發(fā)給:這里是指信號一般情況下是發(fā)給進程還是線程,表明了信號是和整個進程相關還是和某個線程相關。一般由某個線程自己觸發(fā)的信號會發(fā)送給這個線程自己,讓它自己來處理,但是這個信號的含義如果是進程全局的就會發(fā)送給進程來處理,進程里的任何一個線程都有可能會被選擇來處理。無論是發(fā)送給進程還是線程,信號的處理效果都是進程全局的。

含義:這個信號的含義,代表什么時候該使用它,如果收到了它就意味著遇到了什么情況。

⑶標準信號詳解

下面讓我們通過一張圖來看看所有信號的相關信息:

我們先來解釋一下信號0,其實0不算是一個信號,但是也可以算作是半個信號。因為發(fā)送信號0給一個進程或者線程,它會走發(fā)送檢測過程,但是并不會真的投遞給進程或者線程。檢測流程會檢測發(fā)送者是否有權限發(fā)送、進程是否存在,如果遇到問題就返回錯誤值。所以發(fā)送信號0可以用作檢測進程是否存在的方法。

我們再來看一下實時信號,因為實時信號沒有特定的含義,所以比較簡單。實時信號的默認處理是終結進程,相關屬性是可阻塞,可忽略,可捕獲。它的一般使用方法都是進程發(fā)給其它進程或者線程來作為進程間通信的方法。其中32-33被glibc的pthread使用了。

標準信號一共有1-31共31個,我們按照它們的特點不同分類進行講解:

首先說一下SIGKILL和一些暫停、繼續(xù)相關的信號。其中SIGKILL和SIGSTOP是POSIX標準規(guī)定的不可阻塞、不可忽略、不可捕獲的信號,它們的語義一定會得到執(zhí)行。SIGCONT信號官方?jīng)]有特別規(guī)定,它的實現(xiàn)上是不可阻塞、不可忽略的,雖然能捕獲,但是相當于沒捕獲。因為捕獲的意思是執(zhí)行其信號處理函數(shù)就不再執(zhí)行其默認處理了,但是SIGCONT的默認語義一定會得到執(zhí)行。其它三個暫停信號SIGTSTP、SIGTTIN、SIGTTOU是不能阻塞的,但是可以忽略可以捕獲,忽略或者捕獲之后,它們的默認語義暫停程序就不會得到執(zhí)行。

SIGSTOP、SIGCONT,進程在想要暫停、恢復執(zhí)行其它進程的時候可以發(fā)送這兩個信號,內(nèi)核里面再需要暫停、恢復執(zhí)行進程的時候也會發(fā)送這兩個信號。SIGTSTP是當在終端輸入Ctrl+Z快捷鍵時,終端驅(qū)動會給當前進程發(fā)送這個信號。SIGTTIN是當后臺進程讀取終端的時候,終端會向進程發(fā)送的。SIGTTOU是在后臺進程想要向終端輸出的時候,終端會向進程發(fā)送的。這幾個信號都是直接發(fā)送給進程的,因為它們的語義就是要操作整個進程。

下面我們再來看6個標記紫色的信號,這幾個信號都是和當前線程正在執(zhí)行時發(fā)生異常有關。內(nèi)核里單獨把這6個信號放在一起成為同步信號。因為它們都是強制發(fā)送的,會忽略阻塞和忽略設置,所以圖中把它們都看做是不可忽略不可阻塞的。但是它們是可以捕獲的,讓它們可以捕獲的原因是因為這樣可以讓進程知道自己出錯的原因,讓進程可以在臨死之前可以做一些記錄工作,為程序員解BUG多提供一些信息。捕獲了之后,原先默認的語義就不會執(zhí)行,所以信號函數(shù)執(zhí)行完之后它們還會繼續(xù)執(zhí)行。

但是一般情況下這么做是沒有意義的,所以一般都會在信號函數(shù)里退出進程。SIGSEGV的可捕獲前面加了個[不],代表的是不能二次捕獲,也就是說如果在信號處理函數(shù)里面又發(fā)生了SIGSEGV,則這個SIGSEGV就不可捕獲了,會走默認語義發(fā)生coredump并殺死進程。這些信號的發(fā)送方都是內(nèi)核里異常處理相關的代碼,信號都會發(fā)送給線程,因為是這些線程引起的這些問題,放到原線程里去處理比較好。

我們再接著看SIGABRT信號,這個信號比較特殊。它的目的是給庫程序來用的。當庫程序發(fā)現(xiàn)程序出現(xiàn)了不可挽回的錯誤,就會調(diào)用函數(shù)abort,這個函數(shù)會給當前線程發(fā)送信號SIGABRT。SIGABRT信號本身沒什么特殊的,但是abort函數(shù)比較特殊。POSIX規(guī)范要求abort函數(shù)執(zhí)行完成之后,進程一定要被殺死。于是abort函數(shù)的實現(xiàn)就是這樣的,先取消阻塞SIGABRT信號,然后給當前線程發(fā)信號SIGABRT。無論SIGABRT信號是被忽略還是被捕獲了,最后還是要返回到abort函數(shù)里面,然后abort函數(shù)就把SIGABRT信號的處理方式設置為默認,然后再發(fā)一個SIGABRT,這下進程就一定會死了。

也就是說你可以捕獲SIGABRT信號,但是進程最后還是一定會死。所以上圖里說SIGABRT是不可阻塞、不可忽略、不可二次捕獲的([不]可捕獲代表的是不可二次捕獲)。SIGABRT的不可二次捕獲和SIGSEGV的不可二次捕獲情形不太一樣。如果是手工發(fā)送的SIGABRT信號,它就是一個普通的信號,沒有前面說的邏輯。不過手工發(fā)送SIGABRT信號沒有意義,一般都是使用abort函數(shù)來發(fā)送。其實遇到abort函數(shù)的SIGABRT信號也不是必死,有一種不規(guī)范的做法可以避免一死,那就是在信號處理函數(shù)中使用longjmp。但是這種做法沒有意義,因為程序現(xiàn)在已經(jīng)處于不一致狀態(tài)了,coredump之后結束進程,然后好好地解bug才是最好的選擇。

下面我們再看一下與終端相關的4個信號,SIGINT、SIGHUP、SIGQUIT、SIGTERM。你在終端上輸入Ctrl+C,終端驅(qū)動就會給當前進程發(fā)送SIGINT,默認處理是殺死進程。你用kill命令給一個進程發(fā)信號,默認發(fā)的就是SIGTERM信號,默認處理也是殺死進程。當終端脫離進程的時候會給進程發(fā)SIGHUP,默認處理也是殺死進程。脫離終端有三種情況:一是物理終端與大型機斷開了連接,現(xiàn)在已經(jīng)沒有物理終端了,所以這種情況不會有了;二是終端模擬器(也就是命令行窗口)被關閉了;三是我們通過ssh等工具連接到了網(wǎng)絡終端,如果此時網(wǎng)絡斷了或者客戶端程序死了。這三種情況終端驅(qū)動都會給關聯(lián)的進程發(fā)送SIGHUP信號。最后一個信號是SIGTERM,當你在終端輸入Ctrl+\的時候,終端驅(qū)動就會給當前進程發(fā)送SIGTERM信號,默認處理是coredump并殺死進程。

3.3信號的產(chǎn)生來源

⑴硬件來源

比如我們按下Ctrl+C,會產(chǎn)生SIGINT信號。當用戶在終端按下某些鍵時,終端驅(qū)動程序會發(fā)送信號給前臺進程。這是一種常見的硬件來源產(chǎn)生信號的方式。

硬件故障也可能產(chǎn)生信號,例如內(nèi)存訪問錯誤等情況可能會產(chǎn)生相應的信號,如SIGBUS(非法地址,包括內(nèi)存地址對齊出錯)、SIGSEGV(試圖訪問未分配給自己的內(nèi)存,或試圖往沒有寫權限的內(nèi)存地址寫數(shù)據(jù))等信號。

⑵軟件來源

調(diào)用系統(tǒng)函數(shù)

kill函數(shù)可以給一個指定的進程發(fā)送指定的信號。例如,kill(pid_t pid, int sig),其中pid為進程的 pid,你要向哪個進程發(fā)送信號,就寫哪個進程的 pid;sig就是你要發(fā)送的信號的編號。成功返回 0,失敗返回 -1。

raise函數(shù)可以給當前進程發(fā)送指定的信號(自己給自己發(fā)信號)。

abort函數(shù)使當前進程接收到信號而異常終止。

用戶命令:通過命令向進程發(fā)送信號。例如在一個終端下,可以使用kill -9 <進程的 PID>向指定的進程發(fā)送信號 9(SIGKILL),這個信號的默認功能是停止進程。

軟件條件:主要介紹alarm函數(shù)和SIGALRM信號。調(diào)用alarm(unsigned int seconds)函數(shù)可以設定一個鬧鐘,也就是告訴內(nèi)核在seconds秒后給當前進程發(fā)送SIGALRM信號,該信號的默認處理動作是終止當前進程。這個函數(shù)的返回值是 0 或者是以前設定的鬧鐘時間還余下的秒數(shù)。

四、信號的發(fā)送

現(xiàn)在我們來看一下信號發(fā)送,主要是看發(fā)送場景。具體的發(fā)送過程在下一章信號的投遞里面講解。信號發(fā)送場景比較典型的有三種,一是終端發(fā)送,也就是我們在命令行運行程序時會遇到的情況;二是內(nèi)核發(fā)送,內(nèi)核也很龐大,里面的情況也很多,我們這里主要講的是異常處理發(fā)送信號;三是進程發(fā)送,就是一個進程給另一個進程發(fā)。

4.1 終端發(fā)送

我們看一下偽終端是如何發(fā)送信號的: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ū)動發(fā)送信號的幾個場景,代碼就不具體分析了。

4.2 內(nèi)核發(fā)送

我們最常遇到的信號SIGSEGV,一般都是在缺頁異常里,如果我們訪問的虛擬內(nèi)存是未分配的虛擬內(nèi)存,則會發(fā)生SIGSEGV。下面我們看一下代碼。

X86的缺頁異常的代碼如下: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();
}

處理用戶空間缺頁異常的函數(shù)是do_user_addr_fault,在這個函數(shù)里面會檢測各種錯誤情況并最終調(diào)用函數(shù)__bad_area_nosemaphore給當前線程發(fā)送信號SIGSEGV。

4.3 進程發(fā)送

進程如果想要向另外一個進程\線程或發(fā)送信號的話,可以使用系統(tǒng)提供的一些接口函數(shù)。如下所示:

我們最常用的接口函數(shù)就是kill,它有兩個參數(shù),一個是進程標識符pid,一個是信號的值sig,就是把信號sig發(fā)給進程pid。raise函數(shù)給自己也就是當前線程發(fā)信號,它只有一個參數(shù)sig。killpg是給整個進程組發(fā)信號,在實現(xiàn)上是給進程組的每個進程都發(fā)信號。pthread_kill是給同一個進程中的某個線程發(fā)信號。tgkill可以給其它進程中的某個線程發(fā)信號。sigqueue是用來發(fā)實時信號的,實時信號可以多帶一個附加數(shù)據(jù),當然可以用來發(fā)普通信號,但是這樣附加數(shù)據(jù)就會被忽略。

五、信號的投遞

5.1 信號待決隊列

每個進程都有一個信號隊列,每個線程也有一個信號隊列。信號隊列的數(shù)據(jù)結構如下所示:linux-src/include/linux/signal_types.h

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

可以看到信號隊列非常簡單,sigset是個bit flag,代表當前隊列里有哪些信號,list是信號列表的頭指針。下面我們來看一下信號隊列里的條目。

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

每發(fā)送一次信號都會生成一個sigqueue,sigqueue里面包含了很多和信號相關的信息。

在Linux里面,每個task_struct都代表一個線程,里面包含了一個sigpending 。Linux里面沒有直接代表進程的結構體,但是一個進程的所有線程都共享同一個signal_struct。signal_struct里面也包含了一個sigpending,這個sigpending代表進程的信號隊列。

5.2 信號投遞流程

我們前面說了很多發(fā)送信號的方法,總體上可以分為兩類,普通發(fā)送和強制發(fā)送。異常處理發(fā)送信號都是用的強制發(fā)送,其它的基本上都是用的普通發(fā)送,但也有一些其它情況用的是強制發(fā)送。這兩類方法方法最終都會調(diào)用同一個函數(shù)來發(fā)送信號,我們來看一下: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做了一些簡單的處理,然后直接調(diào)用__send_signal。__send_signal先調(diào)用prepare_signal,prepare_signal對暫?;謴皖惖男盘栂茸隽艘幌骂A處理,然后查看信號是否被忽略。然后根據(jù)PID類型決定是把信號放到進程隊列里還是線程隊列里。然后會判斷信號是不是傳統(tǒng)信號(也就是標準信號),對于傳統(tǒng)信號,如果信號隊列里已經(jīng)有一個了,就不再接收了,這么做是為了兼容過去。然后調(diào)用__sigqueue_alloc分配一個信號條目sigqueue,分配好之后填充各種數(shù)據(jù),然后把它加入到隊列中去。最后調(diào)用complete_signal,此函數(shù)會選擇一個合適的線程來喚醒,一般會喚醒當前線程。喚醒的線程很可能醒來就去進行信號處理。

①強制發(fā)送:強制發(fā)送的入口函數(shù)是force_sig_info_to_task,它會先把信號的阻塞和忽略取消掉,然后再調(diào)用函數(shù)send_signal進行發(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)核又封裝了幾個函數(shù)來輔助強制發(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先對send_signal進行了一次封裝,然后do_send_specific、group_send_sig_info又分別對其進行了封裝。do_send_specific代表發(fā)送到線程,group_send_sig_info代表發(fā)送到進程。給線程發(fā)信號的接口函數(shù)最終都是調(diào)用的do_send_specific。給進程發(fā)信號的接口函數(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.
*/
}
}

下面再來看一下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);
}

六、信號的儲存與處理

6.1信號的存儲方式

在 Linux 內(nèi)核中,信號的存儲主要通過三張表來實現(xiàn):pending 表、block 表和 handler 表。

Pending 表是通過位圖來儲存的,一共有 31 位,每個比特位代表信號編號,比特位的內(nèi)容代表信號是否收到。當進程收到信號但未遞達時,對應編號的比特位就會由 0 改為 1。

Block 表也是通過位圖來儲存,其結構與 Pending 表類似。每個比特位代表信號編號,比特位的內(nèi)容代表信號是否阻塞。如果某個信號被阻塞,那么阻塞位圖結構中對應的比特位(信號編號)就會置為 1,在此信號阻塞未被解除之前,會一直處于信號未決狀態(tài)。

Handler 表是一個函數(shù)指針數(shù)組。數(shù)組的下標是對應的信號編號,數(shù)組下標中的內(nèi)容就是對應信號的處理方法(函數(shù)指針)。當調(diào)用signal(signo,handler)時,就會把信號對應的處理方法設置為自定義方法,內(nèi)核中就是將數(shù)組下標(信號編號)中的內(nèi)容(處理方法)設置為自定義方法的函數(shù)指針,從而在遞達后執(zhí)行處理方法。

sigset_t類型是 Linux 給用戶提供的一個用戶級的數(shù)據(jù)類型,禁止用戶直接修改位圖。每個信號只有一個 bit 的未決標志,非 0 即 1,不記錄該信號產(chǎn)生了多少次,阻塞標志也是這樣表示的。因此,未決和阻塞標志可以用相同的數(shù)據(jù)類型sigset_t來存儲,sigset_t稱為信號集,這個類型可以表示每個信號的 “有效” 或 “無效” 狀態(tài),在阻塞信號集中 “有效” 和 “無效” 的含義是該信號是否被阻塞,而在未決信號集中 “有效” 和 “無效” 的含義是該信號是否處于未決狀態(tài)。阻塞信號集也叫做當前進程的信號屏蔽字,這里的 “屏蔽” 應該理解為阻塞而不是忽略。

6.2信號的阻塞與未決狀態(tài)

信號的阻塞、未決和遞達是理解 Linux 信號機制的重要概念。執(zhí)行信號的處理動作稱為信號遞達(Delivery),信號從產(chǎn)生到遞達之間的狀態(tài),稱為信號未決(Pending)。進程可以選擇阻塞(Block)某個信號。被阻塞的信號產(chǎn)生時將保持在未決狀態(tài),直到進程解除對此信號的阻塞,才執(zhí)行遞達的動作。注意,阻塞和忽略是不同,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作。

信號在內(nèi)核中的表示可以看作是這樣的:在 PCB 進程控制塊中有信號屏蔽狀態(tài)字(block)、信號未決狀態(tài)字(pending)以及是否忽略標志(或是信號處理函數(shù))。block 狀態(tài)字和 pending 狀態(tài)字都是 64bit。信號屏蔽狀態(tài)字(block)中,1 代表阻塞、0 代表不阻塞;信號未決狀態(tài)字(pending)的 1 代表未決,0 代表信號可以抵達了。它們都是每一個 bit 代表一個信號,比如,bit0 代表信號 SIGHUP。

可以使用信號集操作函數(shù)來操作信號集。例如:

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

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

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

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

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

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

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

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

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

還有一個函數(shù)可以讀取未決狀態(tài)字(pending)信息:int sigpending(sigset_t *set);。它讀取當前進程的未決信號集,通過 set 參數(shù)傳出。調(diào)用成功則返回 0,出錯則返回 -1。

6.3信號的捕捉與阻塞

在 Linux 中,可以使用 signal 和 sigaction 系統(tǒng)調(diào)用來自定義信號處理函數(shù),實現(xiàn)對特定信號的捕捉和處理。

signal 函數(shù)較為簡單,其函數(shù)原型為 typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);。它主要用于處理前 32 種非實時信號,不支持信號的傳遞信息。例如,當使用 signal(SIGINT, my_func) 函數(shù)調(diào)用時,其中 my_func 是自定義函數(shù)。應用進程收到 SIGINT 信號時,會跳轉(zhuǎn)到自定義處理信號函數(shù) my_func 處執(zhí)行。在 Linux 系統(tǒng)中,signal 函數(shù)已被改寫,由 sigaction 函數(shù)封裝實現(xiàn)。

sigaction 函數(shù)則更加強大,它可以讀取和修改與指定信號相關聯(lián)的處理動作。函數(shù)原型為 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)。其中,signum 代表指定信號的編號;若 act 指針非空,則根據(jù) act 修改該信號的處理動作;若 oldact 指針非空,則通過 oldact 傳出該信號原來的處理動作。struct sigaction 結構體成員解釋如下:

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

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

  • sa_flags:包含一些選項,通常設置為 0,表示使用默認屬性。

例如,以下代碼用 sigaction 函數(shù)對 2 號信號進行了捕捉,將 2 號信號的處理動作改為了自定義的打印動作,并在執(zhí)行一次自定義動作后將 2 號信號的處理動作恢復為原來默認的處理動作:

#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() {
// 先把兩個結構體變量的成員都初始化為 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異步信號安全

我們可以通過設置信號處理函數(shù)來捕獲信號,那信號處理函數(shù)能像普通函數(shù)一樣什么接口函數(shù)都能調(diào)用嗎?不能,我們只能調(diào)用異步信號安全的函數(shù)。很多常用的函數(shù)都不是信號安全函數(shù),不能在信號處理函數(shù)里面調(diào)用,比如printf。那要是想在信號處理函數(shù)里面輸出數(shù)據(jù)該咋辦呢?可以使用write接口函數(shù),這個函數(shù)是異步信號安全的。

6.5信號處理流程

信號處理是在線程從內(nèi)核空間返回用戶空間的時候處理的。而從內(nèi)核空間返回用戶空間是和架構相關的,所以這一部分的代碼是在架構代碼里面的。下面我們以x86為例講解一下(代碼進行了刪減)。

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();
}

可以看出線程在返回到用戶空間之前不斷地檢查有沒有信號要處理。如果有的話就使用函數(shù)get_signal取出一個信號,然后在函數(shù)handle_signal里面去執(zhí)行。get_signal的代碼我們就不貼出來了,在這里講一下它的大概邏輯。

get_signal會先看有沒有STOP相關的信號,如果有的話執(zhí)行處理。然后去取一個信號出來,先取同步信號,同步信號只從當前線程的信號隊列里去取,這里的同步信號是指前面講的異常處理的6個信號。

如果沒有同步信號的話就去取其它信號,其它信號先從線程的信號隊列里面去取,如果沒有的話就再去進程的信號里面去取。如果取到的信號的處理設置是忽略,或者是默認處理但默認處理方式也是忽略,則繼續(xù)取下一個信號。

如果取到的信號沒有設置信號處理函數(shù),則在這里執(zhí)行其默認處理,終結進程或者coredump之后再終結進程。如果沒有取到信號則get_signal返回值為0,如果取到了信號,且信號設置了信號處理函數(shù)則返回值為1,且輸出參數(shù)ksig會包含相應信號的相關的信息。然后把ksig傳遞給函數(shù)handle_signal來處理。下面我們看一下handle_signal函數(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);
}

這段代碼雖然看起來不太復雜,但是實際上卻非常難以理解。setup_rt_frame為了使線程返回用戶空間后能執(zhí)行信號處理函數(shù)便開始偽造用戶線程棧幀。棧幀首先保存一些線程當前的狀態(tài)到棧上,然后再偽造出仿佛是一個蹦床函數(shù)調(diào)用了信號處理函數(shù)一樣。然后再偽造出仿佛是信號處理函數(shù)通過系統(tǒng)調(diào)用進入了內(nèi)核一樣。

然后線程從內(nèi)核返回用戶空間就會執(zhí)行信號處理函數(shù),信號處理函數(shù)執(zhí)行完返回的時候時候會返回到蹦床函數(shù)。蹦床函數(shù)會調(diào)用sigreturn系統(tǒng)調(diào)用進入內(nèi)核,sigreturn會讀取蹦床函數(shù)的棧幀,因為這上面保持的是之前的線程執(zhí)行信息。然后把這些信息進行恢復,這樣線程再回到用戶空間的時候就又回到了線程之前執(zhí)行的地方。

七、信號處理的同步化

對于異步信號來說,有很多的問題,比如你不確定你正在干啥的時候它來了,還有就是在異步信號的處理函數(shù)里面有很多的函數(shù)不能調(diào)用。為此我們可以把異步信號轉(zhuǎn)化為同步信號。我們前面說過,同步信號、異步信號是指信號的發(fā)送是同步的還是異步的,那異步信號肯定不可能轉(zhuǎn)化為同步信號啊。我們此處所說的轉(zhuǎn)化是指把信號的處理從異步轉(zhuǎn)化為同步。

轉(zhuǎn)化的方法就是用一個函數(shù)來等信號,這樣信號和線程執(zhí)行的相對性就是固定的了,就相當于是同步信號了。等的方式有兩種,一種是等待信號被處理,信號還是走前面所說的處理流程,另一種是等待信號并截獲信號,信號被我們偷走了,不會再走前面所說的信號處理流程了。

7.1 信號等待

信號等待的接口函數(shù)有兩個pause和sigsuspend,它們的接口是:

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

7.2 信號截獲

除了等待信號被處理之外,我們還可以等待并截獲信號,信號就不會走正常的處理流程,我們可以對截獲到的信號進行相應的處理。信號截獲一共有四個接口函數(shù),我們先來講三個。

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有兩個參數(shù),第一個參數(shù)是要等待的信號集,第二個參數(shù)是輸出參數(shù),是等待并截獲到的信號。函數(shù)返回之后,我們就可以根據(jù)sig的值進行相應的處理。接口函數(shù)sigwaitinfo也有兩個參數(shù),第一個參數(shù)和前面的是一樣的,第二個參數(shù)是輸出參數(shù),類型是siginfo_t,能獲得更多信號相關的信息。接口函數(shù)sigtimedwait和sigwaitinfo差不多,只是多個了時間參數(shù),如果等了這么長時間之后還沒有等來信號就會直接返回。

還有一個接口函數(shù),它把要等待的信號信息轉(zhuǎn)化為了fd,等信號直接變成了read fd的操作。其接口如下:

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

第二個參數(shù)代表要等待的信號集。第一個參數(shù)如果是-1,代表要創(chuàng)建一個新的fd,如果是一個已有的signalfd,代表修改已經(jīng)fd的信號集。然后我們就可以對這個fd進行read操作了,read的緩存區(qū)至少要有 sizeof(struct signalfd_siginfo)個字節(jié)。Read每次返回都會讀取若干個struct signalfd_siginfo結構體。最關鍵的是我們還可以對這個fd進行select、poll操作。

八、應用場景與總結

8.1應用場景舉例

⑴使用 “ctrl+c” 中止程序

當用戶在終端運行程序時,按下 “Ctrl+C” 會產(chǎn)生SIGINT信號。這個信號通常會被發(fā)送給前臺進程,以請求終止進程。例如,在一個長時間運行的計算任務中,如果用戶發(fā)現(xiàn)結果不符合預期或者想要提前終止程序,就可以通過按下 “Ctrl+C” 來發(fā)送SIGINT信號。當進程接收到這個信號后,會根據(jù)其對SIGINT信號的處理方式來做出響應。如果進程沒有自定義信號處理函數(shù),那么通常會采用默認的處理動作,即終止進程。

⑵kill 命令殺進程

在 Linux 系統(tǒng)中,kill命令是一個常用的工具,用于向進程發(fā)送信號以終止它們。例如,kill -9 <進程的 PID>會向指定的進程發(fā)送SIGKILL信號。SIGKILL信號是一種強制終止信號,無法被捕捉、忽略或阻塞。當進程接收到SIGKILL信號時,會立即終止。這種方式通常用于終止那些無法正常退出的進程,或者在系統(tǒng)出現(xiàn)問題時強制關閉某些進程以恢復系統(tǒng)的穩(wěn)定性。

除了終止進程,信號機制還可以用于進程間的通信。例如,一個進程可以向另一個進程發(fā)送特定的信號,以通知它某個事件的發(fā)生。這種通信方式雖然比較簡單,但在某些情況下非常有用。

8.2總結信號機制的重要性

Linux 信號機制在編寫健壯程序中具有至關重要的意義。首先,它提供了一種靈活的方式來處理異步事件。在復雜的多進程或多線程環(huán)境中,程序可能會面臨各種不可預測的情況,如硬件故障、用戶輸入、系統(tǒng)資源變化等。通過信號機制,程序可以及時響應這些事件,采取適當?shù)拇胧?,避免出現(xiàn)不可預料的錯誤或崩潰。

其次,信號機制使得進程間的通信更加多樣化。相比于傳統(tǒng)的管道、共享內(nèi)存等通信方式,信號通信更加輕量級和高效。它可以用于簡單的事件通知,讓不同的進程之間能夠協(xié)調(diào)工作,提高系統(tǒng)的整體性能和穩(wěn)定性。

深入理解信號機制還可以幫助程序員更好地調(diào)試和優(yōu)化程序。當程序出現(xiàn)異常情況時,通過分析信號的產(chǎn)生和處理過程,可以快速定位問題所在。同時,合理地利用信號機制可以優(yōu)化程序的資源管理,例如在程序退出時及時清理資源,避免資源泄漏。

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

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多