久久精品精选,精品九九视频,www久久只有这里有精品,亚洲熟女乱色综合一区
    分享

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

     深度Linux 2024-11-19 發布于湖南

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

    一、概述

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

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

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

    二、信號基本原理

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

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

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

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

    三、信號的分類與產生

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

    3.1信號的分類

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

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

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

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

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

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

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

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

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

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

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

    3.2信號類型詳解

    ⑴標準信號與實時信號的區別

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

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

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

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

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

    ⑵信號的屬性特征

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

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

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

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

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

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

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

    ⑶標準信號詳解

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

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

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

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

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

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

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

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

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

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

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

    3.3信號的產生來源

    ⑴硬件來源

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

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

    ⑵軟件來源

    調用系統函數

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

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

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

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

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

    四、信號的發送

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

    4.1 終端發送

    我們看一下偽終端是如何發送信號的: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;
    }

    這是終端驅動發送信號的幾個場景,代碼就不具體分析了。

    4.2 內核發送

    我們最常遇到的信號SIGSEGV,一般都是在缺頁異常里,如果我們訪問的虛擬內存是未分配的虛擬內存,則會發生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();
    }

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

    4.3 進程發送

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

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

    五、信號的投遞

    5.1 信號待決隊列

    每個進程都有一個信號隊列,每個線程也有一個信號隊列。信號隊列的數據結構如下所示: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;
    };

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

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

    5.2 信號投遞流程

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

    ①強制發送:強制發送的入口函數是force_sig_info_to_task,它會先把信號的阻塞和忽略取消掉,然后再調用函數send_signal進行發送。代碼如下: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;
    }

    內核又封裝了幾個函數來輔助強制發送,分別是force_sig_info、force_sig、force_fatal_sig、force_exit_sig、force_sigsegv、force_sig_fault_to_task、force_sig_fault,它們的代碼就不再具體介紹了。

    ②普通發送:do_send_sig_info先對send_signal進行了一次封裝,然后do_send_specific、group_send_sig_info又分別對其進行了封裝。do_send_specific代表發送到線程,group_send_sig_info代表發送到進程。給線程發信號的接口函數最終都是調用的do_send_specific。給進程發信號的接口函數最終都是調用的group_send_sig_info。下面我們看一下kill和tgkill的調用流程。

    先看kill接口函數的流程: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函數的流程: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 內核中,信號的存儲主要通過三張表來實現:pending 表、block 表和 handler 表。

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

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

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

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

    6.2信號的阻塞與未決狀態

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

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

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

    • 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,表示位于在集合中。

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

    • SIG_BLOCK:將參數 set 指向的信號集中設置的信號添加到現在的屏蔽狀態字中,設置為阻塞。

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

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

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

    6.3信號的捕捉與阻塞

    在 Linux 中,可以使用 signal 和 sigaction 系統調用來自定義信號處理函數,實現對特定信號的捕捉和處理。

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

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

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

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

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

    例如,以下代碼用 sigaction 函數對 2 號信號進行了捕捉,將 2 號信號的處理動作改為了自定義的打印動作,并在執行一次自定義動作后將 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異步信號安全

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

    6.5信號處理流程

    信號處理是在線程從內核空間返回用戶空間的時候處理的。而從內核空間返回用戶空間是和架構相關的,所以這一部分的代碼是在架構代碼里面的。下面我們以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();
    }

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

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

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

    如果取到的信號沒有設置信號處理函數,則在這里執行其默認處理,終結進程或者coredump之后再終結進程。如果沒有取到信號則get_signal返回值為0,如果取到了信號,且信號設置了信號處理函數則返回值為1,且輸出參數ksig會包含相應信號的相關的信息。然后把ksig傳遞給函數handle_signal來處理。下面我們看一下handle_signal函數的實現,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為了使線程返回用戶空間后能執行信號處理函數便開始偽造用戶線程棧幀。棧幀首先保存一些線程當前的狀態到棧上,然后再偽造出仿佛是一個蹦床函數調用了信號處理函數一樣。然后再偽造出仿佛是信號處理函數通過系統調用進入了內核一樣。

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

    七、信號處理的同步化

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

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

    7.1 信號等待

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

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

    7.2 信號截獲

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

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

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

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

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

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

    八、應用場景與總結

    8.1應用場景舉例

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

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

    ⑵kill 命令殺進程

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

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

    8.2總結信號機制的重要性

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

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

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

      轉藏 分享 獻花(0

      0條評論

      發表

      請遵守用戶 評論公約

      類似文章 更多

      主站蜘蛛池模板: 亚洲理论在线A中文字幕| 久久月本道色综合久久| 人妻精品动漫H无码中字| 四虎永久地址WWW成人久久| 夜夜高潮夜夜爽国产伦精品| 国产中文字幕一区二区| 久久99国产精品久久99小说| 噜噜噜噜私人影院| 人妻综合专区第一页| 国产国拍亚洲精品永久软件| 国产美女MM131爽爽爽| 国产综合AV一区二区三区无码| 无码AV无码免费一区二区| 亚洲精品V天堂中文字幕| 377P欧洲日本亚洲大胆| 人妻在卧室被老板疯狂进入| 亚洲人成网站18禁止无码| 体验区试看120秒啪啪免费| 99热精品毛片全部国产无缓冲| 精品久久久久久无码国产| 国产精品日日摸夜夜添夜夜添无码| 无码国产精品一区二区免费式芒果| 精品中文人妻在线不卡| 人人妻人人澡人人爽欧美一区 | 亚洲影院丰满少妇中文字幕无码| 午夜射精日本三级| 18禁美女裸体爆乳无遮挡| 精品一卡2卡三卡4卡乱码精品视频| 欧美又粗又大XXXXBBBB疯狂 | 精品日韩亚洲AV无码| 无码囯产精品一区二区免费| 国产偷国产偷亚洲清高APP| 精品少妇av蜜臀av| 中文乱码人妻系列一区二区| 国产精品午夜剧场免费观看| 玩弄放荡人妻少妇系列| 精品久久久久久无码中文野结衣 | 欧美嫩交一区二区三区 | 亚洲精品国产免费av| 肉大捧一进一出免费视频| 无码一区二区三区AV免费|