日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

Linux异步之信号(signal)机制分析

發布時間:2024/7/23 linux 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Linux异步之信号(signal)机制分析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

From:http://www.cnblogs.com/hoys/archive/2012/08/19/2646377.html

From:http://kenby.iteye.com/blog/1173862

Linux下的信號詳解及捕捉信號:http://www.jb51.net/article/90695.htm

linux信號詳解:http://blog.csdn.net/fanyun_01/article/details/52704408

Linux環境進程間通信(二) 信號(上):https://www.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html

嵌入式 最全面的linux信號量解析:http://blog.csdn.net/skdkjzz/article/details/38444999



【摘要】本文分析了Linux內核對于信號的實現機制和應用層的相關處理。首先介紹了軟中斷信號的本質及信號的兩種不同分類方法尤其是不可靠信號的原理。接著分析了內核對于信號的處理流程包括信號的觸發/注冊/執行及注銷等。最后介紹了應用層的相關處理,主要包括信號處理函數的安裝、信號的發送、屏蔽阻塞等,最后給了幾個簡單的應用實例。

【關鍵字】軟中斷信號,signal,sigaction,kill,sigqueue,settimer,sigmask,sigprocmask,sigset_t



1. Linux信號介紹


信號機制是進程之間相互傳遞消息的一種方法,信號全稱為軟中斷信號,也有人稱作軟中斷。從它的命名可以看出,它的實質和使用很象中斷。所以,信號可以說是進程控制的一部分

軟中斷(signal,又簡稱為信號)用來通知進程發生了異步事件。進程之間可以互相通過系統調用kill發送軟中斷信號。內核也可以因為內部事件而給進程發送信號,通知進程發生了某個事件。注意,信號只是用來通知某進程發生了什么事件,并不給該進程傳遞任何數據。 在軟件層次上,軟中斷信號 是對中斷機制的一種模擬,一個進程接收到信號之后,有相應的信號的處理程序,而一個進程也可以給另外一個(或一組)進程發送信號。在原理上,一個進程收到一個信號與處理器收到一個中斷請求可以說是一樣的。信號是進程間通信機制中唯一的異步通信機制一個進程不必通過任何操作來等待信號的到達,事實上,進程也不知道信號到底什么時候到達。進程之間可以互相通過系統調用kill發送軟中斷信號。內核也可以因為內部事件而給進程發送信號,通知進程發生了某個事件。信號機制除了基本通知功能外,還可以傳遞附加信息。

收到信號的進程,對各種信號有不同的處理方法。處理方法可以分為三類

(參見:Unix環境高級編程(APUE) 信號 章節。電子書下載地址:http://download.csdn.net/download/freeking101/10012610):

  • 第一種:捕捉信號。類似中斷的處理程序,對于需要處理的信號,進程可以指定處理函數,由該函數來處理。
  • 第二種:忽略。忽略某個信號,對該信號不做任何處理,就象未發生過一樣。
  • 第三種:執行該信號的默認處理動作。對該信號的處理保留系統的默認值,這種缺省操作,對大部分的信號的缺省操作是使得進程終止。
  • 進程通過系統調用signal來指定進程對某個信號的處理行為。

    在進程表的表項中有一個軟中斷信號域,該域中每一位對應一個信號,當有信號發送給進程時,對應位置位。由此可以看出,進程對不同的信號可以同時保留,但對于同一個信號,進程并不知道在處理之前來過多少個。

    如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個自定義函數,這稱為捕捉信號

    進程收到一個信號后不會被立即處理,而是在恰當時機進行處理!即內核態返回用戶態之前?!

    但是由于信號處理函數的代碼在用戶空間,所以這增加了內核處理信號捕捉的復雜度。



    2. 信號的種類


    可以從兩個不同的分類角度對信號進行分類:

  • 可靠性方面:可靠信號與不可靠信號;
  • 與時間的關系上:實時信號與非實時信號。

  • 可靠信號與不可靠信號

    ? ? ? ? Linux信號機制基本上是從Unix系統中繼承過來的。早期Unix系統中的信號機制比較簡單和原始,信號值小于SIGRTMIN的信號都是不可靠信號。這就是"不可靠信號"的來源。它的主要問題是信號可能丟失。

    ? ? ? ? 隨著時間的發展,實踐證明了有必要對信號的原始機制加以改進和擴充。由于原來定義的信號已有許多應用,不好再做改動,最終只好又新增加了一些信號,并在一開始就把它們定義為可靠信號,這些信號支持排隊,不會丟失。

    ? ? ? ? 信號值位于SIGRTMIN和SIGRTMAX之間的信號都是可靠信號,可靠信號克服了信號可能丟失的問題。Linux在支持新版本的信號安裝函數sigation()以及信號發送函數sigqueue()的同時,仍然支持早期的signal()信號安裝函數,支持信號發送函數kill()。

    ? ? ? ? 信號的可靠與不可靠只與信號值有關,與信號的發送及安裝函數無關。目前linux中的signal()是通過sigation()函數實現的,因此,即使通過signal()安裝的信號,在信號處理函數的結尾也不必再調用一次信號安裝函數。同時,由signal()安裝的實時信號支持排隊,同樣不會丟失。

    ? ? ? ? 對于目前linux的兩個信號安裝函數:signal()及sigaction()來說,它們都不能把SIGRTMIN以前的信號變成可靠信號(都不支持排隊,仍有可能丟失,仍然是不可靠信號),而且對SIGRTMIN以后的信號都支持排隊。這兩個函數的最大區別在于,經過sigaction安裝的信號都能傳遞信息給信號處理函數,而經過signal安裝的信號不能向信號處理函數傳遞信息。對于信號發送函數來說也是一樣的。

    可靠信號與不可靠信號的區別:

    • 這里的不可靠主要是不支持信號隊列,就是當多個信號發生在進程中的時候(收到信號的速度超過進程處理的速度的時候),這些沒來的及處理的信號就會被丟掉,僅僅留下一個信號。
    • 可靠信號是多個信號發送到進程的時候(收到信號的速度超過進程處理信號的速度的時候),這些沒來的及處理的信號就會排入進程的隊列。等進程有機會來處理的時候,依次再處理,信號不丟失。

    實時信號與非實時信號

    早期Unix系統只定義了32種信號,前32種信號已經有了預定義值,每個信號有了確定的用途及含義,并且每種信號都有各自的缺省動作。如按鍵盤的CTRL ^C時,會產生SIGINT信號,對該信號的默認反應就是進程終止。后32個信號表示實時信號,等同于前面闡述的可靠信號。這保證了發送的多個實時信號都被接收。

    非實時信號都不支持排隊,都是不可靠信號;實時信號都支持排隊,都是可靠信號。


    使用kill -l可以查看信號列表。可以看到Linux中系統一共支持64種信號。

    1-31是普通信號( 用于內核向進程通知事件。即傳統或者標準信號,也稱為不可靠信號); 34-64是實時信號(可靠信號)。

    0 號信號用來測試對應進程是否存在或者是否由權限給其發送信號


    平時可以接觸的是普通信號,其各個信號的含義如下:

  • ?SIGHUP?:當用戶退出Shell時,由該Shell啟的發所有進程都退接收到這個信號,默認動作為終止進程。
  • ?SIGINT?:用戶按下組合鍵時,用戶端時向正在運行中的由該終端啟動的程序發出此信號。默認動作為終止進程。
  • ?SIGQUIT?:當用戶按下組合鍵時產生該信號,用戶終端向正在運行中的由該終端啟動的程序發出此信號。默認動作為終止進程并產生core文件。
  • ?SIGILL ?:CPU檢測到某進程執行了非法指令。默認動作為終止進程并產生core文件。
  • SIGTRAP:該信號由斷點指令或其他trap指令產生。默認動作為終止進程并產生core文件。
  • ?SIGABRT?:調用abort函數時產生該信號。默認動作為終止進程并產生core文件。
  • SIGBUS:非法訪問內存地址,包括內存地址對齊(alignment)出錯,默認動作為終止進程并產生core文件。
  • SIGFPE:在發生致命的算術錯誤時產生。不僅包括浮點運行錯誤,還包括溢出及除數為0等所有的算術錯誤。默認動作為終止進程并產生core文件。
  • ?SIGKILL?:無條件終止進程。本信號不能被忽略、處理和阻塞。默認動作為終止進程。它向系統管理員提供了一種可以殺死任何進程的方法。
  • SIGUSR1:用戶定義的信號,即程序可以在程序中定義并使用該信號。默認動作為終止進程。
  • SIGSEGV:指示進程進行了無效的內存訪問。默認動作為終止進程并使用該信號。默認動作為終止進程。
  • SIGUSR2:這是另外一個用戶定義信號,程序員可以在程序中定義并使用該信號。默認動作為終止進程。
  • ?SIGPIPE?:Broken pipe:向一個沒有讀端的管道寫數據。默認動作為終止進程。
  • ?SIGALRM?:定時器超時,超時的時間由系統調用alarm設置。默認動作為終止進程。
  • ?SIGTERM?:程序結束(terminate)信號,與SIGKILL不同的是,該信號可以被阻塞和處理。通常用來要求程序正常退出。執行Shell命令kill時,缺少產生這個信號。默認動作為終止進程。
  • ?SIGCHLD?:子程序結束時,父進程會收到這個信號。默認動作為忽略該信號。
  • SIGCONT:讓一個暫停的進程繼續執行。
  • SIGSTOP:停止(stopped)進程的執行。注意它和SIGTERM以及SIGINT的區別:該進程還未結束,只是暫停執行。本信號不能被忽略、處理和阻塞。默認作為暫停進程。
  • SIGTSTP:停止進程的動作,但該信號可以被處理和忽略。按下組合鍵時發出該信號。默認動作為暫停進程。
  • SIGTTIN:當后臺進程要從用戶終端讀數據時,該終端中的所有進程會收到SIGTTIN信號。默認動作為暫停進程。
  • SIGTTOU:該信號類似于SIGTIN,在后臺進程要向終端輸出數據時產生。默認動作為暫停進程。
  • ?SIGURG?:套接字(socket)上有緊急數據時,向當前正在運行的進程發出此信號,報告有緊急數據到達。默認動作為忽略該信號。
  • SIGXCPU:進程執行時間超過了分配給該進程的CPU時間,系統產生該信號并發送給該進程。默認動作為終止進程。
  • SIGXFSZ:超過文件最大長度的限制。默認動作為yl終止進程并產生core文件。
  • SIGVTALRM:虛擬時鐘超時時產生該信號。類似于SIGALRM,但是它只計算該進程占有用的CPU時間。默認動作為終止進程。
  • SIGPROF:類似于SIGVTALRM,它不僅包括該進程占用的CPU時間還抱括執行系統調用的時間。默認動作為終止進程。
  • SIGWINCH:窗口大小改變時發出。默認動作為忽略該信號。
  • SIGIO:此信號向進程指示發出一個異步IO事件。默認動作為忽略。
  • SIGPWR:關機。默認動作為終止進程。
  • 上面標注出來的信號是我們在學習過程中重點關注的信號,在Linux后期的學習中這些信號的身影將經常出現

    信號實質上是軟中斷,中斷有優先級,信號也有優先級。如果一個進程有多個未決信號,則對于同一個未決的實時信號,內核將按照發送的順序來遞送信號。如果存在多個未決信號,則值(或者說編號)越小的越先被遞送。如果即存在不可靠信號,又存在可靠信號(實時信號),雖然POSIX對這一情況沒有明確規定,但Linux系統和大多數遵循POSIX標準的操作系統一樣,將優先遞送不可靠信號。


    Linux支持的信號列表如下。很多信號是與機器的體系結構相關的,首先列出的是POSIX.1中列出的信號:

    信號 值 處理動作 發出信號的原因?
    ---------------------------------------------------------------------?
    SIGHUP 1 A 終端掛起或者控制進程終止?
    SIGINT 2 A 鍵盤中斷(如break鍵被按下)?
    SIGQUIT 3 C 鍵盤的退出鍵被按下?
    SIGILL 4 C 非法指令?
    SIGABRT 6 C 由abort(3)發出的退出指令?
    SIGFPE 8 C 浮點異常?
    SIGKILL 9 AEF Kill信號?
    SIGSEGV 11 C 無效的內存引用?
    SIGPIPE 13 A 管道破裂: 寫一個沒有讀端口的管道?
    SIGALRM 14 A 由alarm(2)發出的信號?
    SIGTERM 15 A 終止信號?
    SIGUSR1 30,10,16 A 用戶自定義信號1?
    SIGUSR2 31,12,17 A 用戶自定義信號2?
    SIGCHLD 20,17,18 B 子進程結束信號?
    SIGCONT 19,18,25 進程繼續(曾被停止的進程)?
    SIGSTOP 17,19,23 DEF 終止進程?
    SIGTSTP 18,20,24 D 控制終端(tty)上按下停止鍵?
    SIGTTIN 21,21,26 D 后臺進程企圖從控制終端讀?
    SIGTTOU 22,22,27 D 后臺進程企圖從控制終端寫?

    下面的信號沒在POSIX.1中列出,而在SUSv2列出?

    信號 值 處理動作 發出信號的原因?
    --------------------------------------------------------------------?
    SIGBUS 10,7,10 C 總線錯誤(錯誤的內存訪問)?
    SIGPOLL A Sys V定義的Pollable事件,與SIGIO同義?
    SIGPROF 27,27,29 A Profiling定時器到?
    SIGSYS 12,-,12 C 無效的系統調用 (SVID)?
    SIGTRAP 5 C 跟蹤/斷點捕獲?
    SIGURG 16,23,21 B Socket出現緊急條件(4.2 BSD)?
    SIGVTALRM 26,26,28 A 實際時間報警時鐘信號(4.2 BSD)?
    SIGXCPU 24,24,30 C 超出設定的CPU時間限制(4.2 BSD)?
    SIGXFSZ 25,25,31 C 超出設定的文件大小限制(4.2 BSD)?

    (對于SIGSYS,SIGXCPU,SIGXFSZ,以及某些機器體系結構下的SIGBUS,Linux缺省的動作是A (terminate),SUSv2 是C (terminate and dump core))。?

    下面是其它的一些信號?

    信號 值 處理動作 發出信號的原因?
    ---------------------------------------------------------------------?
    SIGIOT 6 C IO捕獲指令,與SIGABRT同義?
    SIGEMT 7,-,7?
    SIGSTKFLT -,16,- A 協處理器堆棧錯誤?
    SIGIO 23,29,22 A 某I/O操作現在可以進行了(4.2 BSD)?
    SIGCLD -,-,18 A 與SIGCHLD同義?
    SIGPWR 29,30,19 A 電源故障(System V)?
    SIGINFO 29,-,- A 與SIGPWR同義?
    SIGLOST -,-,- A 文件鎖丟失?
    SIGWINCH 28,28,20 B 窗口大小改變(4.3 BSD, Sun)?
    SIGUNUSED -,31,- A 未使用的信號(will be SIGSYS)?

    (在這里,- 表示信號沒有實現;有三個值給出的含義為,第一個值通常在Alpha和Sparc上有效,中間的值對應i386和ppc以及sh,最后一個值對應mips。信號29在Alpha上為SIGINFO / SIGPWR ,在Sparc上為SIGLOST。)?

    處理動作一項中的字母含義如下?
    A 缺省的動作是終止進程?
    B 缺省的動作是忽略此信號?
    C 缺省的動作是終止進程并進行內核映像轉儲(dump core)?
    D 缺省的動作是停止進程?
    E 信號不能被捕獲?
    F 信號不能被忽略?

    上面介紹的信號是常見系統所支持的。以表格的形式介紹了各種信號的名稱、作用及其在默認情況下的處理動作。各種默認處理動作的含義是: 終止程序是指進程退 出;忽略該信號是將該信號丟棄,不做處理; 停止程序是指程序掛起,進入停止狀況以后還能重新進行下去,一般是在調試的過程中(例如ptrace系統調 用); 內核映像轉儲是指將進程數據在內存的映像和進程在內核結構中存儲的部分內容以一定格式轉儲到文件系統,并且進程退出執行,這樣做的好處是為程序員提供了方便,使得他們可以得到進程當時執行時的數據值,允許他們確定轉儲的原因,并且可以調試他們的程序。?

    注意信號SIGKILL和SIGSTOP既不能被捕捉,也不能被忽略。信號SIGIOT與SIGABRT是一個信號。可以看出,同一個信號在不同的系統中值可能不一樣,所以建議最好使用為信號定義的名字,而不要直接使用信號的值



    3. 信號處理流程


    用戶進程提供的信號處理函數是在用戶態里的,而我們發現信號,找到信號處理函數的時刻處于內核態中,所以我們需要從內核態跑到用戶態去執行信號處理程序,執行完畢后還要返回內核態。這個過程如下圖所示:


    處理信號的整個過程是這樣的進程由于 系統調用或者中斷 進入內核,完成相應任務返回用戶空間的前夕,檢查信號隊列,如果有信號,則根據信號向量表找到信號處理函數,設置好“堆棧”后,跳到用戶態執行信號處理函數。信號處理函數執行完畢后,返回內核態,設置“堆棧”,再返回到用戶態繼續執行程序。


    內核實現信號捕捉的步驟:

    ????? 1、用戶為某信號注冊一個信號處理函數sighandler。

    ????? 2、當前正在執行主程序,這時候因為中斷、異常或系統調用進入內核態。

    ????? 3、在處理完異常要返回用戶態的主程序之前,檢查到有信號未處理,并發現該信號需要按照用戶自定義的函數來處理。

    ????? 4、內核決定返回用戶態執行sighandler函數,而不是恢復main函數的上下文繼續執行!(sighandler和main函數使用的是不同的堆棧空間,它們之間不存在調用和被調用的關系,是兩個獨立的控制流程)

    ????? 5、sighandler函數返回后,執行特殊的系統調用sigreturn從用戶態回到內核態

    ????? 6、檢查是否還有其它信號需要遞達,如果沒有 則返回用戶態并恢復主程序的上下文信息繼續執行。



    進程收到一個信號后不會被立即處理,而是在恰當 時機進行處理!

    什么是適當的時候呢?比如說中斷返回的時候,或者內核態返回用戶態的時候(這個情況出現的比較多)。

    信號不一定會被立即處理,操作系統不會為了處理一個信號而把當前正在運行的進程掛起(切換進程),掛起(進程切換)的話消耗太大了,如果不是緊急信號,是不會立即處理的。操作系統多選擇在內核態切換回用戶態的時候處理信號,這樣就利用兩者的切換來處理了(不用單獨進行進程切換以免浪費時間)。
    總歸是不能避免的,因為很有可能在睡眠的進程就接收到信號,操作系統肯定不愿意切換當前正在運行的進程,于是就得把信號儲存在進程唯一的PCB(task_struct)當中。

    對于一個完整的信號生命周期(從信號發送到相應的處理函數執行完畢)來說,可以分為三個階段

  • 信號誕生
  • 信號在進程中注冊
  • 信號的執行和注銷
  • 信號誕生

    信號事件的發生有兩個來源:硬件來源(比如我們按下了鍵盤或者其它硬件故障);軟件來源,最常用發送信號的系統函數是kill, raise, alarm和setitimer以及sigqueue函數,軟件來源還包括一些非法運算等操作。

    產生信號的條件

    1.用戶在終端按下某些鍵時,終端驅動程序會發送信號給前臺程序。

    ???? 例如:Ctrl-c產生SIGINT信號,Ctrl-\產生SIGQUIT信號,Ctrl-z產生SIGTSTP信號

    2.硬件異常產生信號。

    ???? 這類信號由硬件檢測到并通知內核,然后內核向當前進程發送適當的信號。

    ???? 例如:當前進程執行除以0的指令,CPU的運算單元會產生異常,內核將這個進程解釋為SIGFPE信號發送給當前進程。
    ?????????????? 當前進程訪問了非法內存地址,MMU會產生異常,內核將這個異常解釋為SIGSEGV信號發送給進程。

    3.一個進程調用kill(2)函數可以發送信號給另一個進程。

    ???? 可以用kill(1)命令發送信號給某個進程,kill(1)命令也是調用kill(2)函數實現的,如果不明確指定信號則發送SIGTERM信號,該信號的默認處理動作是終止進程。


    這里按發出信號的原因簡單分類,以了解各種信號:

    (1) 與進程終止相關的信號。當進程退出,或者子進程終止時,發出這類信號。

    (2)?與進程例外事件相關的信號。如進程越界,或企圖寫一個只讀的內存區域(如程序正文區),或執行一個特權指令及其他各種硬件錯誤。

    (3) 與在系統調用期間遇到不可恢復條件相關的信號。如執行系統調用exec時,原有資源已經釋放,而目前系統資源又已經耗盡。

    (4) 與執行系統調用時遇到非預測錯誤條件相關的信號。如執行一個并不存在的系統調用。

    (5)?在用戶態下的進程發出的信號。如進程調用系統調用kill向其他進程發送信號

    (6)?與終端交互相關的信號。如用戶關閉一個終端,或按下break鍵等情況。

    (7) 跟蹤進程執行的信號

    通過終端產生信號示例


    ? ? ? ? 當CPU正在執行這個進程的代碼 , 終端驅動程序發送了一 個?SIGINT?信號給該進程,記錄在該進程的 PCB中,則該進程的用戶空間代碼暫停執行,CPU從用戶態 切換到內核態處理硬件中斷。

    ? ? ? ? 從內核態回到用戶態之前, 會先處理 PCB中記錄的信號 ,發現有一個?SIGINT?信號待處理, 而這個信號的默認處理動作是終止進程,所以直接終止進程而不再返回它的用戶空間代碼執行。


    調用系統函數向進程發信號


    kill命令是調用kill函數實現的。kill函數可以給一個指定的進程發送指定信號

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

    #include<signal.h> int kill(pid_t pid,int signo); int raise(int signo);

    這兩個函數都是成功返回0,錯誤返回-1.

    除此之外,abort函數使當前進程接收到SIGABRT信號而異常終止

    #include<stdlib.h> void abort(void);

    就像?exit函數一樣 ,abort?函數總是會成功的 ,所以沒有返回值

    由軟件條件產生信號


    調用alarm函數可以設定一個鬧鐘,告訴內核在seconds秒之后給當前進程發SIGALRM信號, 該信號的默認處理動作是終止當前進程。

    該程序會在1秒鐘之內不停地數數,并打印計數器,1秒鐘到了就被SIGALRM信號終止。
    由于電腦配置等的不同,每臺電腦一秒鐘之內計數值一般是不同的。

    #include <unistd.h> unsigned int alarm(unsigned int seconds); alarm 函數的返回值是0或上次設置鬧鐘剩余的時間。



    信號在目標進程中注冊

    進程表的表項中有一個軟中斷信號域,該域中每一位對應一個信號。內核給一個進程發送軟中斷信號的方法,是在進程所在的進程表項的信號域設置對應于該信號的位。如果信號發送給一個正在睡眠的進程,如果進程睡眠在可被中斷的優先級上,則喚醒進程;否則僅設置進程表中信號域相應的位,而不喚醒進程。如果發送給一個處于可運行狀態的進程,則只置相應的域即可。

    進程的task_struct結構中有關于本進程中未決信號的數據成員:?struct sigpending pending:

    struct sigpending{struct sigqueue *head, *tail;sigset_t signal; };

    第三個成員是進程中所有未決信號集,第一、第二個成員分別指向一個sigqueue類型的結構鏈(稱之為"未決信號信息鏈")的首尾,信息鏈中的每個sigqueue結構刻畫一個特定信號所攜帶的信息,并指向下一個sigqueue結構:

    struct sigqueue{struct sigqueue *next;siginfo_t info; }

    信號在進程中注冊指的就是信號值加入到進程的未決信號集sigset_t signal(每個信號占用一位)中,并且信號所攜帶的信息被保留到未決信號信息鏈的某個sigqueue結構中。只要信號在進程的未決信號集中,表明進程已經知道這些信號的存在,但還沒來得及處理,或者該信號被進程阻塞。

    當一個實時信號發送給一個進程時,不管該信號是否已經在進程中注冊,都會被再注冊一次,因此,信號不會丟失,因此,實時信號又叫做"可靠信號"。這意味著同一個實時信號可以在同一個進程的未決信號信息鏈中占有多個sigqueue結構(進程每收到一個實時信號,都會為它分配一個結構來登記該信號信息,并把該結構添加在未決信號鏈尾,即所有誕生的實時信號都會在目標進程中注冊)。

    當一個非實時信號發送給一個進程時,如果該信號已經在進程中注冊(通過sigset_t signal指示),則該信號將被丟棄,造成信號丟失。因此,非實時信號又叫做"不可靠信號"。這意味著同一個非實時信號在進程的未決信號信息鏈中,至多占有一個sigqueue結構。

    總之信號注冊與否,與發送信號的函數(如kill()或sigqueue()等)以及信號安裝函數(signal()及sigaction())無關,只與信號值有關(信號值小于SIGRTMIN的信號最多只注冊一次,信號值在SIGRTMIN及SIGRTMAX之間的信號,只要被進程接收到就被注冊)


    信號的執行和注銷

    內核處理一個進程收到的軟中斷信號是在該進程的上下文中,因此,進程必須處于運行狀態。當其由于被信號喚醒或者正常調度重新獲得CPU時,在其從內核空間返回到用戶空間時會檢測是否有信號等待處理。如果存在未決信號等待處理且該信號沒有被進程阻塞,則在運行相應的信號處理函數前,進程會把信號在未決信號鏈中占有的結構卸掉。

    對于非實時信號來說,由于在未決信號信息鏈中最多只占用一個sigqueue結構,因此該結構被釋放后,應該把信號在進程未決信號集中刪除(信號注銷完畢);而對于實時信號來說,可能在未決信號信息鏈中占用多個sigqueue結構,因此應該針對占用sigqueue結構的數目區別對待:如果只占用一個sigqueue結構(進程只收到該信號一次),則執行完相應的處理函數后應該把信號在進程的未決信號集中刪除(信號注銷完畢)。否則待該信號的所有sigqueue處理完畢后再在進程的未決信號集中刪除該信號。

    當所有未被屏蔽的信號都處理完畢后,即可返回用戶空間。對于被屏蔽的信號,當取消屏蔽后,在返回到用戶空間時會再次執行上述檢查處理的一套流程。

    內核處理一個進程收到的信號的時機是在一個進程從內核態返回用戶態時。所以,當一個進程在內核態下運行時,軟中斷信號并不立即起作用,要等到將返回用戶態時才處理。進程只有處理完信號才會返回用戶態,進程在用戶態下不會有未處理完的信號。

    處理信號有三種類型:進程接收到信號后退出;進程忽略該信號;進程收到信號后執行用戶設定用系統調用signal的函數。當進程接收到一個它忽略的信號時,進程丟棄該信號,就象沒有收到該信號似的繼續運行。如果進程收到一個要捕捉的信號,那么進程從內核態返回用戶態時執行用戶定義的函數。而且執行用戶定義的函數的方法很巧妙,內核是在用戶棧上創建一個新的層,該層中將返回地址的值設置成用戶定義的處理函數的地址,這樣進程從內核返回彈出棧頂時就返回到用戶定義的函數處,從函數返回再彈出棧頂時,才返回原先進入內核的地方。這樣做的原因是用戶定義的處理函數不能且不允許在內核態下執行(如果用戶定義的函數在內核態下運行的話,用戶就可以獲得任何權限)。


    4. 信號的安裝


    如果進程要處理某一信號,那么就要在進程中安裝該信號。安裝信號主要用來確定信號值及進程針對該信號值的動作之間的映射關系。
    即進程將要處理哪個信號;該信號被傳遞給進程時,將執行何種操作。

    linux主要有兩個函數實現信號的安裝:signal()、sigaction()。其中signal()只有兩個參數,不支持信號傳遞信息,主要是用于前32種非實時信號的安裝;而sigaction()是較新的函數(由兩個系統調用實現:sys_signal以及sys_rt_sigaction),有三個參數,支持信號傳遞信息,主要用來與 sigqueue() 系統調用配合使用,當然,sigaction()同樣支持非實時信號的安裝。sigaction()優于signal()主要體現在支持信號帶有參數。


    signal()

    #include <signal.h>void (*signal(int signum, void (*handler))(int)))(int);

    如果該函數原型不容易理解的話,可以參考下面的分解方式來理解:

    typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler));第一個參數:signum指定要安裝的信號。 第二個參數:handler指定信號的處理函數。可以忽略該信號(參數設為SIG_IGN);可以采用系統默認方式處理信號(參數設為SIG_DFL);也可以自己實現處理方式(參數指定一個函數地址)。

    如果signal()調用成功,返回最后一次為安裝信號signum而調用signal()時的handler值,如果失敗則返回SIG_ERR。即?該函數的返回值是一個函數指針, 指向上次安裝的handler

    經典安裝方式:

    if (signal(SIGINT, SIG_IGN) != SIG_IGN) {signal(SIGINT, sig_handler); } //先獲得上次的handler, 如果不是忽略信號, 就安裝此信號的handler

    由于信號被交付后, 系統自動的重置handler為默認動作, 為了使信號在handler處理期間, 仍能對后繼信號做出反應, 往往在handler的第一條語句再次調用 signal

    sig_handler(ing signum) {/* 重新安裝信號 */signal(signum, sig_handler);...... }

    傳遞給信號處理例程的整數參數是信號值,這樣可以使得一個信號處理例程處理多個信號。

    #include <signal.h> #include <unistd.h> #include <stdio.h>void sigroutine(int dunno) { /* 信號處理例程,其中dunno將會得到信號的值 */switch (dunno) {case 1:printf("Get a signal -- SIGHUP ");break;case 2:printf("Get a signal -- SIGINT ");break;case 3:printf("Get a signal -- SIGQUIT ");break;}return; }int main() {printf("process id is %d ",getpid());signal(SIGHUP, sigroutine); //* 下面設置三個信號的處理方法signal(SIGINT, sigroutine);signal(SIGQUIT, sigroutine);for (;;) ; }

    其中信號SIGINT由按下Ctrl-C發出,信號SIGQUIT由按下Ctrl-發出。該程序執行的結果如下:

    localhost:~$ ./sig_test process id is 463 Get a signal -SIGINT //按下Ctrl-C得到的結果 Get a signal -SIGQUIT //按下Ctrl-得到的結果 //按下Ctrl-z將進程置于后臺[1]+ Stopped ./sig_test localhost:~$ bg[1]+ ./sig_test & localhost:~$ kill -HUP 463 //向進程發送SIGHUP信號 localhost:~$ Get a signal – SIGHUP kill -9 463 //向進程發送SIGKILL信號,終止進程 localhost:~$

    示例

    #include <unistd.h> #include <stdio.h> #include <signal.h>void handler(int sig) {printf("get a sig,num is %d\n",sig); }int main() {signal(2,handler);while(1){sleep(1);printf("hello\n");}return 0; }

    修改了2號信號(Ctrl-c)的默認處理動作為handler函數的內容,則當該程序在前臺運行時,鍵入Ctrl-c后不會執行它的默認處理動作(終止該進程)

    此時,可以使用 (Ctrl-z)終止程序的運行



    我們知道在程序的任意執行點上, 信號隨時可能發生, 如果信號在sig_handler重新安裝信號之前產生, 這次信號就會執行默認動作, 而不是sig_handler. 這種問題是不可預料的.

    為了克服非可靠信號并同一SVR4和BSD之間的差異, 產生了 POSIX 信號安裝方式, 使用sigaction安裝信號的動作后, 該動作就一直保持, 直到另一次調用 sigaction建立另一個動作為止. 這就克服了古老的 signal 調用存在的問題



    sigaction()

    sigaction函數可以讀取和修改與指定信號相關聯的處理動作

    #include <signal.h>int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

    sigaction函數用于改變進程接收到特定信號后的行為。該函數的第一個參數為信號的值,可以為除SIGKILL及SIGSTOP外的任何一個特定有效的信號(為這兩個信號定義自己的處理函數,將導致信號安裝錯誤)。第二個參數是指向結構sigaction的一個實例的指針,在結構sigaction的實例中,指定了對特定信號的處理,可以為空,進程會以缺省方式對信號處理;第三個參數oldact指向的對象用來保存返回的原來對相應信號的處理,可指定oldact為NULL如果把第二、第三個參數都設為NULL,那么該函數可用于檢查信號的有效性。

    第二個參數最為重要,其中包含了對指定信號的處理、信號所傳遞的信息、信號處理函數執行過程中應屏蔽掉哪些信號等等。

    可以通過?man sigaction 查看?sigaction 相關信息。


    #include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);struct sigaction {void (*sa_handler)(int); //信號處理方式void (*sa_sigaction)(int, siginfo_t *, void *); //實時信號的處理方式 暫不討論sigset_t sa_mask; //額外屏蔽的信號int sa_flags;void (*sa_restorer)(void); };

    signum是指定信號的編號。

    處理方式:

    ???? 1、若act指針非空,則根據act結構體中的信號處理函數來修改該信號的處理動作。

    ???? 2、若oldact指針非 空,則通過oldact傳出該信號原來的處理動作。

    ???? 3、現將原來的處理動作備份到oldact里,然后根據act修改該信號的處理動作。

    (注:后兩個參數都是輸入輸出型參數!)

    sa_handler三種可選方式:

    ???? 1、賦值為常數SIG_IGN傳給sigaction表示忽略信號;

    ???? 2、賦值為常數SIG_DFL表示執行系統默認動作;

    ???? 3、賦值為一個函數指針表示用自定義函數捕捉信號,或者說向內核注冊一個信號處理函 數,該函數返回值為void,可以帶一個int參數,通過參數可以得知當前信號的編號,這樣就可以用同一個函數處理多種信號。

    (注:這是一個回調函數,不是被main函數調用,而是被系統所調用)

      當某個信號的處理函數被調用時,內核自動將當前信號加入進程的信號屏蔽字,當信號處理函數返回時自動恢復原來的信號屏蔽字,這樣就保證了在處理某個信號時,如果這種信號再次產生,那么 它會被阻塞到當前處理結束為止


    1、*sa_handler 以及 *sa_sigaction 指定信號關聯函數,即用戶指定的信號處理函數。除了可以是用戶自定義的處理函數外,還可以為SIG_DFL(采用缺省的處理方式),也可以為SIG_IGN(忽略信號)。

    2、由 *sa_sigaction是指定的信號處理函數帶有三個參數,是為實時信號而設的(當然同樣支持非實時信號),它指定一個3參數信號處理函數。第一個參數為信號值,第三個參數沒有使用,第二個參數是指向siginfo_t 結構的指針,結構中包含信號攜帶的數據值,參數所指向的結構如下:

    siginfo_t {int si_signo; /* 信號值,對所有信號有意義*/int si_errno; /* errno值,對所有信號有意義*/int si_code; /* 信號產生的原因,對所有信號有意義*/union{ /* 聯合數據結構,不同成員適應不同信號 *///確保分配足夠大的存儲空間int _pad[SI_PAD_SIZE];//對SIGKILL有意義的結構struct{...}...... ... //對SIGILL, SIGFPE, SIGSEGV, SIGBUS有意義的結構struct{...}...... ...} }

    前面在討論系統調用sigqueue發送信號時,sigqueue的第三個參數就是sigval聯合數據結構,當調用sigqueue時,該數據結構中的數據就將拷貝到信號處理函數的第二個參數中。這樣,在發送信號同時,就可以讓信號傳遞一些附加信息。信號可以傳遞信息對程序開發是非常有意義的。

    3、sa_mask指定在信號處理程序執行過程中,哪些信號應當被阻塞。缺省情況下當前信號本身被阻塞,防止信號的嵌套發送,除非指定SA_NODEFER或者SA_NOMASK標志位。

    注:請注意sa_mask指定的信號阻塞的前提條件,是在由sigaction()安裝信號的處理函數執行過程中由sa_mask指定的信號才被阻塞。

    4、sa_flags中包含了許多標志位,包括剛剛提到的SA_NODEFER及SA_NOMASK標志位。另一個比較重要的標志位是SA_SIGINFO,當設定了該標志位時,表示信號附帶的參數可以被傳遞到信號處理函數中,因此,應該為sigaction結構中的sa_sigaction指定處理函數,而不應該為sa_handler指定信號處理函數,否則,設置該標志變得毫無意義。即使為sa_sigaction指定了信號處理函數,如果不設置SA_SIGINFO,信號處理函數同樣不能得到信號傳遞過來的數據,在信號處理函數中對這些信息的訪問都將導致段錯誤(Segmentation fault)。


    經典安裝方式:

    struct sigaction action, old_action;/* 設置SIGINT */ action.sa_handler = sig_handler; sigemptyset(&action.sa_mask); sigaddset(&action.sa_mask, SIGTERM); action.sa_flags = 0;/* 獲取上次的handler, 如果不是忽略動作, 則安裝信號 */ sigaction(SIGINT, NULL, &old_action); if (old_action.sa_handler != SIG_IGN) {sigaction(SIGINT, &action, NULL); }

    基于 sigaction 實現的庫函數: signal
    sigaction 自然強大, 但安裝信號很繁瑣, 目前linux中的signal()是通過sigation()函數實現的,因此,即使通過signal()安裝的信號,在信號處理函數的結尾也不必再調用一次信號安裝函數。


    pause函數使調用進程掛起直到有信號遞達!

    #include <unistd.h> int pause(void);


    處理方式: 

    ???? 如果信號的處理動作是終止進程,則進程終止,pause函數沒有機會返回;

    ???? 如果信號的處理動作是忽略,則進程繼續處于掛起狀態,pause不返回;

    ???? 如果信號的處理動作是捕捉,則調用了信號處理函數之后pause返回-1,errno設置為EINTR。

    ???? 所以pause只有出錯的返回值(類似exec函數家族)。錯誤碼EINTR表示“被信號中斷”。

    ?舉個栗子

    ???? 1、定義一個鬧鐘,約定times秒后,內核向該進程發送一個SIGALRM信號;

    ???? 2、調用pause函數將進程掛起,內核切換到別的進程運行;

    ???? 3、times秒后,內核向該進程發送SIGALRM信號,發現其處理動作是一個自定義函數,于是切回用戶態執行該自定義處理函數;

    ???? 4、進入sig_alrm函數時SIGALRM信號被自動屏蔽,從sig_alrm函數返回時SIGALRM信號自動解除屏蔽。然后自動執行特殊的系統調用sigreturn再次進入內核,之后再返回用戶態繼續執行進程的主控制流程(main函數調用的mytest函數)。

    ???? 5、pause函數返回-1,然后調用alarm(0)取消鬧鐘,調用sigaction恢復SIGALRM信號以前的處理 動作。


    #include<stdio.h> #include<signal.h> #include<unistd.h> void sig_alarm(int signum) {printf("I am a custom handler!\n"); } void mysleep(unsigned int times) {//注冊兩個信號處理動作struct sigaction new,old;new.sa_handler=sig_alarm; //信號處理函數sigemptyset(&new.sa_mask);//不屏蔽任何信號屏蔽字new.sa_flags=0;//對SIGALRM 信號的默認處理動作修改為自定義處理動作sigaction(SIGALRM,&new,&old);alarm(times);pause(); //掛起等待alarm(1);sleep(2);alarm(0); //取消鬧鐘//恢復SIGALRM 信號到默認處理動作sigaction(SIGALRM,&old,NULL);alarm(1);sleep(2); } int main() {while(1){mysleep(2);printf("many seconds passed\n");printf("###################\n");}return 0; }


    定義一個鬧鐘并掛起等待,收到信號后執行自定義處理動作,在沒有恢復默認處理動作前,收到SIGALRM信號都會按照其自定義處理函數來處理。恢復自定義處理動作之后收到SIGALRM信號則執行其默認處理動作即終止進程!



    5. 信號的發送


    發送信號的主要函數有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。


    kill()

    #include <sys/types.h> #include <signal.h>int kill(pid_t pid,int signo)

    該系統調用可以用來向任何進程或進程組發送任何信號。參數pid的值為信號的接收進程

    pid>0 進程ID為pid的進程

    pid=0 同一個進程組的進程

    pid<0 pid!=-1 進程組ID為 -pid的所有進程

    pid=-1 除發送進程自身外,所有進程ID大于1的進程

    Sinno是信號值,當為0 (即空信號),實際不發送任何信號,但照常進行錯誤檢查,因此,可用于檢查目標進程是否存在,以及當前進程是否具有向目標發送信號的權限root權限的進程可以向任何進程發送信號,非root權限的進程只能向屬于同一個session或者同一個用戶的進程發送信號)。

    Kill()最常用于pid>0時的信號發送。該調用執行成功時,返回值為0;錯誤時,返回-1,并設置相應的錯誤代碼errno。下面是一些可能返回的錯誤代碼:

    EINVAL:指定的信號sig無效。

    ESRCH:參數pid指定的進程或進程組不存在。注意,在進程表項中存在的進程,可能是一個還沒有被wait收回,但已經終止執行的僵死進程。

    EPERM:?進程沒有權力將這個信號發送到指定接收信號的進程。因為,一個進程被允許將信號發送到進程pid時,必須擁有root權力,或者是發出調用的進程的UID 或EUID與指定接收的進程的UID或保存用戶ID(savedset-user-ID)相同。如果參數pid小于-1,即該信號發送給一個組,則該錯誤表示組中有成員進程不能接收該信號。


    sigqueue()

    #include <sys/types.h> #include <signal.h>int sigqueue(pid_t pid, int sig, const union sigval val)調用成功返回 0;否則,返回 -1。

    sigqueue()是比較新的發送信號系統調用,主要是針對實時信號提出的(當然也支持前32種),支持信號帶有參數,與函數sigaction()配合使用.

    sigqueue的第一個參數是指定接收信號的進程ID,第二個參數確定即將發送的信號,第三個參數是一個聯合數據結構union sigval,指定了信號傳遞的參數,即通常所說的4字節值.

    typedef union sigval {int sival_int;void *sival_ptr; }sigval_t;

    sigqueue()比kill()傳遞了更多的附加信息,但sigqueue()只能向一個進程發送信號,而不能發送信號給一個進程組。如果signo=0,將會執行錯誤檢查,但實際上不發送任何信號,0值信號可用于檢查pid的有效性以及當前進程是否有權限向目標進程發送信號。

    在調用sigqueue時,sigval_t指定的信息會拷貝到對應sig 注冊的3參數信號處理函數的siginfo_t結構中,這樣信號處理函數就可以處理這些信息了。由于sigqueue系統調用支持發送帶參數信號,所以比kill()系統調用的功能要靈活和強大得多。


    alarm()

    #include <unistd.h>unsigned int alarm(unsigned int seconds)系統調用alarm安排內核為調用進程在指定的seconds秒后發出一個SIGALRM的信號。 如果指定的參數seconds為0,則不再發送 SIGALRM信號。后一次設定將取消前一次的設定。 該調用返回值為上次定時調用到發送之間剩余的時間,或者因為沒有前一次定時調用而返回0。

    注意,在使用時,alarm只設定為發送一次信號,如果要多次發送,就要多次使用alarm調用。


    setitimer()

    現在的系統中很多程序不再使用alarm調用,而是使用setitimer調用來設置定時器,用getitimer來得到定時器的狀態,這兩個調用的聲明格式如下:

    nt getitimer(int which, struct itimerval *value);int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);在使用這兩個調用的進程中加入以下頭文件: #include <sys/time.h>

    該系統調用給進程提供了三個定時器,它們各自有其獨有的計時域,當其中任何一個到達,就發送一個相應的信號給進程,并使得計時器重新開始。三個計時器由參數which指定,如下所示:

    TIMER_REAL:按實際時間計時,計時到達將給進程發送SIGALRM信號。

    ITIMER_VIRTUAL:僅當進程執行時才進行計時。計時到達將發送SIGVTALRM信號給進程。

    ITIMER_PROF:當進程執行時和系統為該進程執行動作時都計時。與ITIMER_VIR-TUAL是一對,該定時器經常用來統計進程在用戶態和內核態花費的時間。計時到達將發送SIGPROF信號給進程。

    定時器中的參數value用來指明定時器的時間,其結構如下: struct itimerval {struct timeval it_interval; /* 下一次的取值 */struct timeval it_value; /* 本次的設定值 */ };該結構中timeval結構定義如下: struct timeval {long tv_sec; /* 秒 */long tv_usec; /* 微秒,1秒 = 1000000 微秒*/ };

    在setitimer 調用中,參數ovalue如果不為空,則其中保留的是上次調用設定的值。定時器將it_value遞減到0時,產生一個信號,并將it_value的值設定為it_interval的值,然后重新開始計時,如此往復。當it_value設定為0時,計時器停止,或者當它計時到期,而it_interval 為0時停止。調用成功時,返回0;錯誤時,返回-1,并設置相應的錯誤代碼errno:

    EFAULT:參數value或ovalue是無效的指針。

    EINVAL:參數which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一個。

    下面是關于setitimer調用的一個簡單示范,在該例子中,每隔一秒發出一個SIGALRM,每隔0.5秒發出一個SIGVTALRM信號:

    #include <signal.h> #include <unistd.h> #include <stdio.h> #include <sys/time.h>int sec;void sigroutine(int signo) {switch (signo) {case SIGALRM:printf("Catch a signal -- SIGALRM ");break;case SIGVTALRM:printf("Catch a signal -- SIGVTALRM ");break;}return; }int main() {struct itimerval value,ovalue,value2;sec = 5;printf("process id is %d ",getpid());signal(SIGALRM, sigroutine);signal(SIGVTALRM, sigroutine);value.it_value.tv_sec = 1;value.it_value.tv_usec = 0;value.it_interval.tv_sec = 1;value.it_interval.tv_usec = 0;setitimer(ITIMER_REAL, &value, &ovalue);value2.it_value.tv_sec = 0;value2.it_value.tv_usec = 500000;value2.it_interval.tv_sec = 0;value2.it_interval.tv_usec = 500000;setitimer(ITIMER_VIRTUAL, &value2, &ovalue);for (;;) ; }

    該例子的屏幕拷貝如下:

    localhost:~$ ./timer_test process id is 579 Catch a signal – SIGVTALRM Catch a signal – SIGALRM Catch a signal – SIGVTALRM Catch a signal – SIGVTALRM Catch a signal – SIGALRM Catch a signal –GVTALRM


    abort()

    #include <stdlib.h>void abort(void);

    向進程發送SIGABORT信號,默認情況下進程會異常退出,當然可定義自己的信號處理函數。即使SIGABORT被進程設置為阻塞信號,調用abort()后,SIGABORT仍然能被進程接收。該函數無返回值。


    raise()

    #include <signal.h>int raise(int signo)

    向進程本身發送信號,參數為即將發送的信號值。調用成功返回 0;否則,返回 -1。


    6. 信號集及信號集操作函數


    信號集被定義為一種數據類型:

    typedef struct {unsigned long sig[_NSIG_WORDS]; } sigset_t

    信號集用來描述信號的集合,每個信號占用一位。Linux所支持的所有信號可以全部或部分的出現在信號集中,主要與信號阻塞相關函數配合使用。下面是為信號集操作定義的相關函數。

    #include <signal.h>int sigemptyset(sigset_t *set);int sigfillset(sigset_t *set);int sigaddset(sigset_t *set, int signum)int sigdelset(sigset_t *set, int signum);int sigismember(const sigset_t *set, int signum);sigemptyset(sigset_t *set)初始化由set指定的信號集,信號集里面的所有信號被清空;sigfillset(sigset_t *set)調用該函數后,set指向的信號集中將包含linux支持的64種信號;sigaddset(sigset_t *set, int signum)在set指向的信號集中加入signum信號;sigdelset(sigset_t *set, int signum)在set指向的信號集中刪除signum信號;sigismember(const sigset_t *set, int signum)判定信號signum是否在set指向的信號集中。



    7. 信號阻塞與信號未決


    未決信號:已經產生,但是沒有給進程遞送的信號(即還未處理)

    已被遞送信號:已經遞送給進程并處理過。


    阻塞信號

    ?1.信號在內核中的表示:

    信號遞達delivery:實際執行信號處理信號的動作

    信號未決pending:信號從產生到抵達之間的狀態,信號產生了但是未處理

    忽略:抵達之后的一種 動作

    阻塞block:收到信號不立即處理???? 被阻塞的信號將保持未決狀態,直到進程解除對此信號的阻塞,才執行抵達動作

    信號產生和阻塞沒有直接關系 抵達和解除阻塞沒有直接關系!

    進程收到一個信號后,不會立即處理,它會在恰當的時機被處理。

    每個信號都由兩個標志位分別表示阻塞和未決,以及一個函數指針表示信號的處理動作。

    在上圖的例子中,

      1.?SIGHUP信號未阻塞也未產生過,當它遞達時執行默認處理動作。

      2.?SIGINT信號產生過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒 有解除阻塞之前不能忽略這個信號,因為進程仍有機會改變處理動作之后再解除阻塞。

      3.?SIGQUIT信號未產生過,一旦產生SIGQUIT信號將被阻塞,它的處理動作是用戶自定義函數sighandler。阻塞信號集也叫作信號屏蔽字。

    信號產生但是不立即處理,前提條件是要把它保存在pending表中,表明信號已經產生。


    信號集操作函數

    #include <signal.h> int sigemptyset(sigset_t *set); //初始化set所指向的信號集,使所有信號的對應位清0 int sigfillset(sigset_t *set); //初始化set所指向的信號集,表示該信號集的有效信號包括系統支持的所有信號 int sigaddset(sigset_t *set, int signo); //在該信號集中添加有效信號 int sigdelset(sigset_t *set, int signo); //在該信號集中刪除有效信號 int sigismember(const sigset_t *set, int signo); //用于判斷一個信號集的有效信號中是否包含某種信號

    參數解析:

    sigset_t結構體的參數表示信號集,信號操作的時候都是以信號集合的方式進行操作,需要事先創建一個該結構體的對象,然后把想要操作的信號添加到信號集合對象當中去。signo就是信號的標號了。


    調用函數sigprocmask可以讀取或更改進程的信號屏蔽字(阻塞信號集)

    #include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

    一個進程的信號屏蔽字規定了當前阻塞而不能遞送給該進程的信號集。調用函數sigprocmask可以檢測或更改(或兩者)進程的信號屏蔽字。如果調用sigprocmask解除了對當前若干個未決信號的阻塞,則在sigprocmask返回前,至少將其中 一個信號遞達

    參數解析:

    how,有三個宏

    ?????SIG_BLOCK????? 添加到block表當中去

    ?????SIG_UNBLOCK??從block表中刪除

    ?????SIG_SETMASK??設置block表 設置當前信號屏蔽字為set所指向的值

    ?set表示新設置的信號屏蔽字,oset表示當前信號屏蔽字

    處理方式:

    ??????set?非空,?oset?為NULL :按照how指示的方法更改set指向信號集的信號屏蔽字。

    ??????set?為NULL,oset?非空:讀取oset指向信號集的信號屏蔽字,通過oset參數傳出。

    ??????set?和?oset?都非空 :現將原來的信號屏蔽字備份到oset里,然后根據sethow參數更改信號屏蔽字。

    sigpending讀取當前進程的未決信號集,通過set參數傳出

    #include <signal.h> int sigpending(sigset_t *set); 這是一個輸出型參數,會把當前進程的 pending 表打印到傳入的set集中

    實例驗證上面幾個函數:



    一開始沒有任何信號,所以pending表中全是0,我通過Ctrl+C傳入2號信號,看到pending表中有2號被置位了,經過10秒取消阻塞,2號信號被處理(經過我自定義的函數)



    每個進程都有一個用來描述哪些信號遞送到進程時將被阻塞的信號集,該信號集中的所有信號在遞送到進程后都將被阻塞。下面是與信號阻塞相關的幾個函數。

    #include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)); int sigpending(sigset_t *set)); int sigsuspend(const sigset_t *mask));

    sigprocmask()函數能夠根據參數how來實現對信號集的操作,操作主要有三種:

    ? ? ? ? SIG_BLOCK 在進程當前阻塞信號集中添加set指向信號集中的信號
    ? ? ? ? SIG_UNBLOCK 如果進程阻塞信號集中包含set指向信號集中的信號,則解除對該信號的阻塞
    ? ? ? ? SIG_SETMASK 更新進程阻塞信號集為set指向的信號集

    sigpending(sigset_t *set))獲得當前已遞送到進程,卻被阻塞的所有信號,在set指向的信號集中返回結果。

    sigsuspend(const sigset_t *mask))用于在接收到某個信號之前, 臨時用mask替換進程的信號掩碼, 并暫停進程執行,直到收到信號為止。sigsuspend 返回后將恢復調用之前的信號掩碼。信號處理函數完成后,進程將繼續執行。該系統調用始終返回-1,并將errno設置為EINTR。


    如何屏蔽信號

    所謂屏蔽, 并不是禁止遞送信號, 而是暫時阻塞信號的遞送, 解除屏蔽后, 信號將被遞送, 不會丟失. 相關API為

    int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset(sigset_t *set, int signum); int sigdelset(sigset_t *set, int signum); int sigismember(const sigset_t *set, int signum); int sigsuspend(const sigset_t *mask); int sigpending(sigset_t *set); ----------------------------------------------------------------- int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)); sigprocmask()函數能夠根據參數how來實現對信號集的操作,操作主要有三種: * SIG_BLOCK 在進程當前阻塞信號集中添加set指向信號集中的信號 * SIG_UNBLOCK 如果進程阻塞信號集中包含set指向信號集中的信號,則解除對該信號的阻塞 * SIG_SETMASK 更新進程阻塞信號集為set指向的信號集

    屏蔽整個進程的信號:

    #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <error.h> #include <string.h> void sig_handler(int signum) { printf("catch SIGINT\n"); } int main(int argc, char **argv) { sigset_t block; struct sigaction action, old_action; /* 安裝信號 */ action.sa_handler = sig_handler; sigemptyset(&action.sa_mask); action.sa_flags = 0; sigaction(SIGINT, NULL, &old_action); if (old_action.sa_handler != SIG_IGN) { sigaction(SIGINT, &action, NULL); } /* 屏蔽信號 */ sigemptyset(&block); sigaddset(&block, SIGINT); printf("block SIGINT\n"); sigprocmask(SIG_BLOCK, &block, NULL); printf("--> send SIGINT -->\n"); kill(getpid(), SIGINT); printf("--> send SIGINT -->\n"); kill(getpid(), SIGINT); sleep(1); /* 解除信號后, 之前觸發的信號將被遞送, * 但SIGINT是非可靠信號, 只會遞送一次 */ printf("unblock SIGINT\n"); sigprocmask(SIG_UNBLOCK, &block, NULL); sleep(2); return 0; }

    運行結果:

    work> ./a.out block SIGINT --> send SIGINT --> --> send SIGINT --> unblock SIGINT catch SIGINT

    這里發送了兩次SIGINT信號 可以看到, 屏蔽掉SIGINT后,信號無法遞送, 解除屏蔽后, 才遞送信號, 但只被遞送一次,因為SIGINT是非可靠信號, 不支持排隊。

    只在信號處理期間, 屏蔽其它信號在信號的handler執行期間, 系統將自動屏蔽此信號, 但如果還想屏蔽其它信號怎么辦? 可以利用 struct sigaction 結構體的 sa_mask 屬性.

    #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <error.h> #include <string.h> void sig_handler(int signum) { printf("in handle, SIGTERM is blocked\n"); /* 在此handler內將屏蔽掉SIGTERM, 直到此handler返回 */ printf("--> send SIGTERM -->\n"); kill(getpid(), SIGTERM); sleep(5); printf("handle done\n"); } void handle_term(int signum) { printf("catch sigterm and exit..\n"); exit(0); } int main(int argc, char **argv) { struct sigaction action, old_action; /* 設置SIGINT */ action.sa_handler = sig_handler; sigemptyset(&action.sa_mask); /* 安裝handler的時候, 設置在handler * 執行期間, 屏蔽掉SIGTERM信號 */ sigaddset(&action.sa_mask, SIGTERM); action.sa_flags = 0; sigaction(SIGINT, NULL, &old_action); if (old_action.sa_handler != SIG_IGN) { sigaction(SIGINT, &action, NULL); } /* 設置SIGTERM */ action.sa_handler = handle_term; sigemptyset(&action.sa_mask); action.sa_flags = 0; sigaction(SIGTERM, NULL, &old_action); if (old_action.sa_handler != SIG_IGN) { sigaction(SIGTERM, &action, NULL); } printf("--> send SIGINT -->\n"); kill(getpid(), SIGINT); while (1) { sleep(1); } return 0; }

    運行結果:

    work> ./a.out --> send SIGINT --> in handle, SIGTERM is blocked --> send SIGTERM --> handle done catch sigterm and exit..

    收到SIGINT后, 進入sig_handler,此時發送SIGTERM信號將被屏蔽。等sig_handler返回后, 才收到SIGTERM信號, 然后退出程序


    另一個示例

    //忽略,屏蔽信號 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <signal.h>int main(int arg, char *args[]) {pid_t pid=fork();if(pid==-1){printf("fork() failed! error message:%s\n",strerror(errno));return -1;}//注冊信號,屏蔽SIGCHLD信號,子進程退出,將不會給父進程發送信號,因此也不會出現僵尸進程signal(SIGCHLD,SIG_IGN);if(pid>0){printf("father is runing !\n");sleep(10);}if(pid==0){printf("i am child!\n");exit(0);}printf("game over!\n");return 0; }

    恢復信號

    //恢復信號 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <signal.h>void catch_signal(int sign) {switch (sign){case SIGINT:printf("ctrl + C 被執行了!\n");//exit(0);break;} }int main(int arg, char *args[]) {//注冊終端中斷信號signal(SIGINT, catch_signal);char tempc = 0;while ((tempc = getchar()) != 'a'){printf("tempc=%d\n", tempc);//sleep()}//恢復信號signal(SIGINT, SIG_DFL);while (1){pause();}printf("game over!\n");return 0; }

    返回值

    //signal()函數的返回值 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <signal.h>void catch_signal(int sign) {switch (sign){case SIGINT:printf("ctrl + C 被執行了!\n");//exit(0);break;} }int main(int arg, char *args[]) {/** signal()函數的返回值是signal()函數上一次的行為* */typedef void (*sighandler_t)(int);//因為第一次注冊信號SIGINT,所以上一次的行為就是默認行為sighandler_t old=signal(SIGINT, catch_signal);if(old==SIG_ERR){//注冊信號失敗perror("signal error");}/*正規寫法*/if(signal(SIGQUIT,catch_signal)==SIG_ERR){//注冊新號失敗perror("signal error");}char tempc = 0;while ((tempc = getchar()) != 'a'){printf("tempc=%d\n", tempc);//sleep()}//把默認行為重新注冊,不就是恢復默認信號了signal(SIGINT, old);while (1){pause();}printf("game over!\n");return 0; }


    如何獲取未決信號

    所謂未決信號, 是指被阻塞的信號, 等待被遞送的信號.?

    int sigsuspend(const sigset_t *mask)) sigpending(sigset_t *set)) //獲得當前已遞送到進程,卻被阻塞的所有信號,在set指向的信號集中返回結果。

    示例代碼

    #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <error.h> #include <string.h> /* 版本1, 可靠信號將被遞送多次 */ //#define MYSIGNAL SIGRTMIN+5 /* 版本2, 不可靠信號只被遞送一次 */ #define MYSIGNAL SIGTERM void sig_handler(int signum) { psignal(signum, "catch a signal"); } int main(int argc, char **argv) { sigset_t block, pending; int sig, flag; /* 設置信號的handler */ signal(MYSIGNAL, sig_handler); /* 屏蔽此信號 */ sigemptyset(&block); sigaddset(&block, MYSIGNAL); printf("block signal\n"); sigprocmask(SIG_BLOCK, &block, NULL); /* 發兩次信號, 看信號將會被觸發多少次 */ printf("---> send a signal --->\n"); kill(getpid(), MYSIGNAL); printf("---> send a signal --->\n"); kill(getpid(), MYSIGNAL); /* 檢查當前的未決信號 */ flag = 0; sigpending(&pending); for (sig = 1; sig < NSIG; sig++) { if (sigismember(&pending, sig)) { flag = 1; psignal(sig, "this signal is pending"); } } if (flag == 0) { printf("no pending signal\n"); } /* 解除此信號的屏蔽, 未決信號將被遞送 */ printf("unblock signal\n"); sigprocmask(SIG_UNBLOCK, &block, NULL); /* 再次檢查未決信號 */ flag = 0; sigpending(&pending); for (sig = 1; sig < NSIG; sig++) { if (sigismember(&pending, sig)) { flag = 1; psignal(sig, "a pending signal"); } } if (flag == 0) { printf("no pending signal\n"); } return 0; }

    這個程序有兩個版本:
    可靠信號版本, 運行結果:

    work> ./a.out block signal ---> send a signal ---> ---> send a signal ---> this signal is pending: Unknown signal 39 unblock signal catch a signal: Unknown signal 39 catch a signal: Unknown signal 39 no pending signal 發送兩次可靠信號, 最終收到兩次信號

    非可靠信號版本, 運行結果:

    work> ./a.out block signal ---> send a signal ---> ---> send a signal ---> this signal is pending: Terminated unblock signal catch a signal: Terminated no pending signal 發送兩次非可靠信號, 最終只收到一次



    8. 被中斷的系統調用


    一些IO系統調用執行時, 如 read 等待輸入期間, 如果收到一個信號,系統將中斷read, 轉而執行信號處理函數. 當信號處理返回后, 系統遇到了一個問題: 是重新開始這個系統調用, 還是讓系統調用失敗?早期UNIX系統的做法是, 中斷系統調用, 并讓系統調用失敗, 比如read返回 -1, 同時設置 errno 為 EINTR。中斷了的系統調用是沒有完成的調用, 它的失敗是臨時性的, 如果再次調用則可能成功, 這并不是真正的失敗, 所以要對這種情況進行處理, 典型的方式為:

    while (1) {n = read(fd, buf, BUFSIZ);if (n == -1 && errno != EINTR) {printf("read error\n");break;}if (n == 0) {printf("read done\n");break;} }

    這樣做邏輯比較繁瑣, 事實上, 我們可以從信號的角度來解決這個問題, 安裝信號的時候, 設置 SA_RESTART屬性, 那么當信號處理函數返回后, 被該信號中斷的系統
    調用將自動恢復.?

    #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <error.h> #include <string.h> void sig_handler(int signum) { printf("in handler\n"); sleep(1); printf("handler return\n"); } int main(int argc, char **argv) { char buf[100]; int ret; struct sigaction action, old_action; action.sa_handler = sig_handler; sigemptyset(&action.sa_mask); action.sa_flags = 0; /* 版本1:不設置SA_RESTART屬性 * 版本2:設置SA_RESTART屬性 */ //action.sa_flags |= SA_RESTART; sigaction(SIGINT, NULL, &old_action); if (old_action.sa_handler != SIG_IGN) { sigaction(SIGINT, &action, NULL); } bzero(buf, 100); ret = read(0, buf, 100); if (ret == -1) { perror("read"); } printf("read %d bytes:\n", ret); printf("%s\n", buf); return 0; }

    版本1, 不設置 SA_RESTART 屬性 :

    work> gcc signal.c work> ./a.out ^Cin handler handler return read: Interrupted system call read -1 bytes:

    在 read 等待數據期間, 按下ctrl + c, 觸發 SIGINT 信號, handler 返回后, read 被中斷, 返回 -1

    版本2, 設置 SA_RESTART 屬性:

    work> gcc signal.c work> ./a.out ^Cin handler handler return hello, world read 13 bytes: hello, world

    handler 返回后, read 系統調用被恢復執行, 繼續等待數據.



    9. 非局部控制轉移


    int setjmp(jmp_buf env); int sigsetjmp(sigjmp_buf env, int savesigs); void longjmp(jmp_buf env, int val); void siglongjmp(sigjmp_buf env, int val); -------------------------------------------------------- setjmp()會保存目前堆棧環境,然后將目前的地址作一個記號, 而在程序其他地方調用 longjmp 時便會直接跳到這個記號位置, 然后還原堆棧,繼續程序好執行。setjmp調用有點fork的味道, setjmp()return 0 if returning directly, and non-zero when returning from longjmp using the saved context. if (setjmp(jmpbuf)) {printf("return from jmp\n"); } else {printf("return directly\n"); }setjmp 和 sigsetjmp 的唯一區別是: setjmp 不一定會恢復信號集合, 而sigsetjmp可以保證恢復信號集合 #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <setjmp.h> void sig_alrm(int signum); void sig_usr1(int signum); void print_mask(const char *str); static sigjmp_buf jmpbuf; static volatile sig_atomic_t canjmp; static int sigalrm_appear; int main(int argc, char **argv) { struct sigaction action, old_action; /* 設置SIGUSR1 */ action.sa_handler = sig_usr1; sigemptyset(&action.sa_mask); action.sa_flags = 0; sigaction(SIGUSR1, NULL, &old_action); if (old_action.sa_handler != SIG_IGN) { sigaction(SIGUSR1, &action, NULL); } /* 設置SIGALRM */ action.sa_handler = sig_alrm; sigemptyset(&action.sa_mask); action.sa_flags = 0; sigaction(SIGALRM, NULL, &old_action); if (old_action.sa_handler != SIG_IGN) { sigaction(SIGALRM, &action, NULL); } print_mask("starting main:"); if (sigsetjmp(jmpbuf, 1) != 0) { print_mask("exiting main:"); } else { printf("sigsetjmp return directly\n"); canjmp = 1; while (1) { sleep(1); } } return 0; } void sig_usr1(int signum) { time_t starttime; if (canjmp == 0) { printf("please set jmp first\n"); return; } print_mask("in sig_usr1:"); alarm(1); while (!sigalrm_appear); canjmp = 0; siglongjmp(jmpbuf, 1); } void sig_alrm(int signum) { print_mask("in sig_alrm:"); sigalrm_appear = 1; return; } void print_mask(const char *str) { sigset_t sigset; int i, errno_save, flag = 0; errno_save = errno; if (sigprocmask(0, NULL, &sigset) < 0) { printf("sigprocmask error\n"); exit(0); } printf("%s\n", str); fflush(stdout); for (i = 1; i < NSIG; i++) { if (sigismember(&sigset, i)) { flag = 1; psignal(i, "a blocked signal"); } } if (!flag) { printf("no blocked signal\n"); } printf("\n"); errno = errno_save; } 運行結果:? work> ./a.out & [4] 28483 starting main: no blocked signal sigsetjmp return directly kill -USR1 28483 in sig_usr1: a blocked signal: User defined signal 1 in sig_alrm: a blocked signal: User defined signal 1 a blocked signal: Alarm clock exiting main: no blocked signal



    10. 信號的生命周期


    從信號發送到信號處理函數的執行完畢,對于一個完整的信號生命周期(從信號發送到相應的處理函數執行完畢)來說,可以分為三個重要的階段,這三個階段由四個重要事件來刻畫:信號誕生;信號在進程中注冊完畢;信號在進程中的注銷完畢;信號處理函數執行完畢。


    下面闡述四個事件的實際意義:

    信號"誕生"

    信號的誕生指的是觸發信號的事件發生(如檢測到硬件異常、定時器超時以及調用信號發送函數kill()或sigqueue()等)。信號在目標進程中"注冊";
    進程的task_struct結構中有關于本進程中未決信號的數據成員:

    struct sigpending pending: struct sigpending{struct sigqueue *head, **tail;sigset_t signal; };第三個成員是進程中所有未決信號集,第一、第二個成員分別指向一個sigqueue類型的結構鏈(稱之為"未決信號鏈表")的首尾,鏈表中的每個sigqueue結構刻畫一個特定信號所攜帶的信息,并指向下一個sigqueue結構:
    struct sigqueue{struct sigqueue *next;siginfo_t info; }


    信號的注冊

    信號在進程中注冊指的就是信號值加入到進程的未決信號集中(sigpending結構的第二個成員sigset_t signal),并且加入未決信號鏈表的末尾。 只要信號在進程的未決信號集中,表明進程已經知道這些信號的存在,但還沒來得及處理,或者該信號被進程阻塞。當一個實時信號發送給一個進程時,不管該信號是否已經在進程中注冊,都會被再注冊一次,因此,信號不會丟失,因此,實時信號又叫做"可靠信號"。這意味著同一個實時信號可以在同一個進程的未決信號鏈表中添加多次. 當一個非實時信號發送給一個進程時,如果該信號已經在進程中注冊,則該信號將被丟棄,造成信號丟失。因此,非實時信號又叫做"不可靠信號"。這意味著同一個非實時信號在進程的未決信號鏈表中,至多占有一個sigqueue結構.

    一個非實時信號誕生后,
    (1)、如果發現相同的信號已經在目標結構中注冊,則不再注冊,對于進程來說,相當于不知道本次信號發生,信號丟失.
    (2)、如果進程的未決信號中沒有相同信號,則在進程中注冊自己。

    信號的注銷
    在進程執行過程中,會檢測是否有信號等待處理(每次從系統空間返回到用戶空間時都做這樣的檢查)。如果存在未決信號等待處理且該信號沒有被進程阻塞,則在運行相應的信號處理函數前,進程會把信號在未決信號鏈中占有的結構卸掉。是否將信號從進程未決信號集中刪除對于實時與非實時信號是不同的。對于非實時信號來說,由于在未決信號信息鏈中最多只占用一個sigqueue結構,因此該結構被釋放后,應該把信號在進程未決信號集中刪除(信號注銷完畢);而對于實時信號來說,可能在未決信號信息鏈中占用多個sigqueue結構,因此應該針對占用sigqueue結構的數目區別對待:如果只占用一個sigqueue結構(進程只收到該信號一次),則應該把信號在進程的未決信號集中刪除(信號注銷完畢)。否則,不應該在進程的未決信號集中刪除該信號(信號注銷完畢)。 進程在執行信號相應處理函數之前,首先要把信號在進程中注銷。

    信號生命終止

    進程注銷信號后,立即執行相應的信號處理函數,執行完畢后,信號的本次發送對進程的影響徹底結束。



    11.?關于可重入函數


    在信號處理函數中應使用可重入函數。信號處理程序中應當使用可重入函數(注:所謂可重入函數是指一個可以被多個任務調用的過程,任務在調用時不必擔心數據是否會出錯)。因為進程在收到信號后,就將跳轉到信號處理函數去接著執行。如果信號處理函數中使用了不可重入函數,那么信號處理函數可能會修改原來進程中不應該被修改的數據,這樣進程從信號處理函數中返回接著執行時,可能會出現不可預料的后果。不可再入函數在信號處理函數中被視為不安全函數。

    滿足下列條件的函數多數是不可再入的:

  • 使用靜態的數據結構,如getlogin(),gmtime(),getgrgid(),getgrnam(),getpwuid()以及getpwnam()等等;
  • 函數實現時,調用了malloc()或者free()函數;
  • 實現時使用了標準I/O函數的。The Open Group視下列函數為可再入的:
    _exit()、access()、alarm()、cfgetispeed()、cfgetospeed()、
    cfsetispeed()、cfsetospeed()、chdir()、chmod()、chown() 、
    close()、creat()、dup()、dup2()、execle()、execve()、
    fcntl()、fork()、fpathconf()、fstat()、fsync()、getegid()、
    geteuid()、getgid()、getgroups()、getpgrp()、getpid()、
    getppid()、getuid()、kill()、link()、lseek()、mkdir()、
    mkfifo()、 open()、pathconf()、pause()、pipe()、raise()、
    read()、rename()、rmdir()、setgid()、setpgid()、setsid()、
    setuid()、 sigaction()、sigaddset()、sigdelset()、sigemptyset()、
    sigfillset()、sigismember()、signal()、sigpending()、
    sigprocmask()、sigsuspend()、sleep()、stat()、sysconf()、
    tcdrain()、tcflow()、tcflush()、tcgetattr()、tcgetpgrp()、
    tcsendbreak()、tcsetattr()、tcsetpgrp()、time()、times()、
    umask()、uname()、unlink()、utime()、wait()、waitpid()、
    write()。
  • 即使信號處理函數使用的都是"安全函數",同樣要注意進入處理函數時,首先要保存errno的值,結束時,再恢復原值。因為,信號處理過程中,errno值隨時可能被改變。另外,longjmp()以及siglongjmp()沒有被列為可重入函數,因為不能保證緊接著兩個函數的其它調用是安全的。



    12.?如何設計信號處理函數


    信號處理函數最好遵從下面原則

  • 一般而言,信號處理函數設計的越簡單越好,因為當前代碼的執行邏輯被打斷,最好盡快恢復到剛才被打斷之前的狀態。從而避免競爭條件的產生。
  • 在信號處理函數中,建議不要調用printf等與I/O相關的函數。以及一些慢設備操作。這樣會使得信號處理函數的執行時間變長,可能,操作系統就會切換其它程序去在CPU上執行。但如果有特殊需要,則也可以使用
  • 在信號處理函數中,不要使用任何不可重入的函數后面會說到。保證信號處理函數可以安全地執行完。并不會影響主邏輯執行

  • 一個簡單例子:

    當鍵入Ctrl-C時候即程序收到SIGINT信號時進行處理。

    // signal 的函數原型 // void (*signal(int sig , void (*func)(int)))(int); // 至于這個怎么理解,這里就不再贅述了,請參考 《C Traps and Pitfalls》2.1節即理解函數聲明。 // filename : simple_signal.cpp #include <unistd.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <errno.h> #include <fcntl.h>#define MSG "Catch signal SIGINT processing \n" #define MSG_END "Finished process SIGINT return \n"void do_too_heavy_work () {long long s = 0 ;for (long long i = 0 ; i < 500000000L ; i++ ) {s += i ; } }void sig_handler (int signuum ) {// 本程序只是為了來進行演示,// 在信號處理程序中,盡量不要調用與標準IO相關的和不可重入的函數。write ( STDOUT_FILENO , MSG , strlen (MSG) ) ;do_too_heavy_work();write ( STDOUT_FILENO , MSG_END , strlen (MSG_END) ) ; }int main() {// 注冊信號處理函數if ( SIG_ERR == signal ( SIGINT , sig_handler ) ) {fprintf (stderr , "signal error ") , perror ("") ;exit (1) ;}// 讓主程序不退出,掛起,等待信號產生while (1) {pause () ;}return EXIT_SUCCESS ; }

    程序會一直停著等待用戶行為。當我們鍵入Ctrl-C時程序打印相關信息,之后程序自己退出。那么程序的執行流程就類似這樣:

    [tutu@localhost Linux-Book]$ gcc simple_signal.cpp [tutu@localhost Linux-Book]$ ./a.out ^CCatch signal SIGINT processing Finished process SIGINT return ^CCatch signal SIGINT processing Finished process SIGINT return ^CCatch signal SIGINT processing Finished process SIGINT return

    這是一種古老的注冊信號并設置信號處理函數的方式。現在我們使用新的信號注冊函數即sigaction函數。它提供了更多的控制字段(舊的signal已經使用sigaction進行了實現。祥見glibc源碼


    為什么不用signal來進行信號注冊

  • signal 無法設置在執行信號處理程序時要屏蔽哪些信號的產生。
  • signal 函數注冊的信號處理函數只能攜帶很少的信息(也不常用),在信號處理函數進行信號處理時。
  • signal 無法設置一些標志位來執行一些動作(后面再講)。
  • signal 只能設置所給信號的處理方式但sigaction還可以獲取之前這個信號的處理方式

  • 另一個例子,和上面的程序功能一樣,但是使用sigaction進行處理。

    // filename : simple_sigaction.cpp #include <unistd.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <errno.h> #include <fcntl.h>#define MSG "Catch signal SIGINT processing \n" #define MSG_END "Finished process SIGINT return \n"void do_too_heavy_work () {long long s = 0 ;for (long long i = 0 ; i < 500000000L ; i++ ) {s += i ; } } void sig_handler (int signuum ) {// 本程序只是為了來進行演示,// 在信號處理程序中,盡量不要調用與標準IO相關的,不可重入的函數。write ( STDOUT_FILENO , MSG , strlen (MSG) ) ;do_too_heavy_work();write ( STDOUT_FILENO , MSG_END , strlen (MSG_END) ) ; } int main() {// 注冊信號處理函數struct sigaction newact ;// 將信號處理函數執行期間掩碼設置為空sigemptyset (&newact.sa_mask ) ;// 將標志設置為0即默認newact.sa_flags = 0 ;// 注冊信號處理函數newact.sa_handler = sig_handler ;if ( 0 > sigaction ( SIGINT , &newact , NULL ) ) {fprintf (stderr , "sigaction error ") , perror ("") ;exit (1) ;}// 讓主程序不退出,掛起,等待信號產生while (1) {pause () ;}return EXIT_SUCCESS ; }

    執行效果和剛才的一樣


    示例

    [tutu@localhost Linux-Book]$ cat sigqueue_post_signal.cpp // 信號發送端: // filename : sigqueue_post_signal.cpp #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <string.h> #include <strings.h> #include <sys/types.h> #include <errno.h> #include <unistd.h>int main (int argc , char * argv[]) {if ( 2 != argc ) {fprintf (stderr , "Bad argument!\nUsage ./post_signal pid\n") ;exit (1) ;}pid_t pid = atoi ( argv[argc-1] ) ;printf ("Sending signal to %d , by using sigqueue\n" , pid) ;sigval_t sigval ;sigval.sival_int = 8888 ;int errcode = 0 ;if ( 0 > ( errcode = sigqueue ( pid , SIGUSR1 , sigval ) )) {if ( ESRCH == errcode ) {fprintf (stderr , "No such process!\n") ;exit (1) ;} else {fprintf (stderr , "sigqueue error "),perror ("") ;exit (1) ;}}printf ("Finished!\n") ;return 0 ; } [tutu@localhost Linux-Book]$ gcc sigqueue_post_signal.cpp -o post_signal [tutu@localhost Linux-Book]$ cat sigqueue_wait.cpp // filename sigqueue_wait.cpp // 信號接收端 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <signal.h> #include <unistd.h> #include <sys/types.h>// 注冊SIGUSR1的信號處理函數 // man sigaction 顯示,當前第三個參數無卵用,第一個參數是信號編號,第二個是攜帶的信息 void sig_handler (int signo , siginfo_t * info , void * extra ) {// print signoprintf ("Catch SIGUSR1\n") ;printf ("signo is %d\n" , signo) ; // print info -> si_value.sival_ptrprintf ("sigval is %d\n" , info->si_value.sival_int ) ; }int main () {struct sigaction act ;act.sa_sigaction = sig_handler ;act.sa_flags = 0 ;act.sa_flags |= SA_SIGINFO ;sigemptyset (&act.sa_mask) ;printf ("My pid is %d\n" , getpid() ) ;if ( 0 > sigaction (SIGUSR1 , &act , NULL ) ) {fprintf (stderr , "sigaction error ") , perror ("") ;exit (1) ;}while (1) {pause () ;}return 0 ; }

    首先執行wait_signal,wait_signal 會打印自己的pid,再執行post_signal并傳參數即pid,之后就可以看到效果了






    13. 信號應用實例


    linux下的信號應用并沒有想象的那么恐怖,程序員所要做的最多只有三件事情:

  • 安裝信號(推薦使用sigaction());
  • 實現三參數信號處理函數,handler(int signal,struct siginfo *info, void *);
  • 發送信號,推薦使用sigqueue()。
  • 實際上,對有些信號來說,只要安裝信號就足夠了(信號處理方式采用缺省或忽略)。其他可能要做的無非是與信號集相關的幾種操作。


    實例一:信號發送及處理

    // 實現一個信號接收程序sigreceive(其中信號安裝由sigaction())。#include <signal.h> #include <sys/types.h> #include <unistd.h>void new_op(int,siginfo_t*,void*);int main(int argc,char**argv) {struct sigaction act; int sig;sig=atoi(argv[1]); sigemptyset(&act.sa_mask);act.sa_flags=SA_SIGINFO;act.sa_sigaction=new_op;if(sigaction(sig,&act,NULL) < 0){printf("install sigal error\n");}while(1){sleep(2);printf("wait for the signal\n");} }void new_op(int signum,siginfo_t *info,void *myact) {printf("receive signal %d", signum);sleep(5); }

    說明,命令行參數為信號值,后臺運行sigreceive signo &,可獲得該進程的ID,假設為pid,然后再另一終端上運行kill -s signo pid驗證信號的發送接收及處理。同時,可驗證信號的排隊問題.


    實例二:信號傳遞附加信息

    主要包括兩個實例:

    // 向進程本身發送信號,并傳遞指針參數#include <signal.h> #include <sys/types.h> #include <unistd.h>void new_op(int,siginfo_t*,void*);int main(int argc,char**argv) {struct sigaction act; union sigval mysigval;int i;int sig;pid_t pid; char data[10];memset(data,0,sizeof(data));for(i=0;i < 5;i++)data[i]='2';mysigval.sival_ptr=data; sig=atoi(argv[1]);pid=getpid();sigemptyset(&act.sa_mask);act.sa_sigaction=new_op;//三參數信號處理函數act.sa_flags=SA_SIGINFO;//信息傳遞開關,允許傳說參數信息給new_opif(sigaction(sig,&act,NULL) < 0){printf("install sigal error\n");}while(1){sleep(2);printf("wait for the signal\n");sigqueue(pid,sig,mysigval);//向本進程發送信號,并傳遞附加信息} }void new_op(int signum,siginfo_t *info,void *myact)//三參數信號處理函數的實現 {int i;for(i=0;i<10;i++){printf("%c\n ",(*( (char*)((*info).si_ptr)+i)));}printf("handle signal %d over;",signum); }

    這個例子中,信號實現了附加信息的傳遞,信號究竟如何對這些信息進行處理則取決于具體的應用。


    不同進程間傳遞整型參數:

    把上面例子的信號發送和接收放在兩個程序中,并且在發送過程中傳遞整型參數

    // 信號接收程序:#include <signal.h> #include <sys/types.h> #include <unistd.h>void new_op(int,siginfo_t*,void*);int main(int argc,char**argv) {struct sigaction act;int sig;pid_t pid; pid=getpid();sig=atoi(argv[1]); sigemptyset(&act.sa_mask);act.sa_sigaction=new_op;act.sa_flags=SA_SIGINFO;if(sigaction(sig,&act,NULL)<0){printf("install sigal error\n");}while(1){sleep(2);printf("wait for the signal\n");} }void new_op(int signum,siginfo_t *info,void *myact) {printf("the int value is %d \n",info->si_int); }// 信號發送程序: // 命令行第二個參數為信號值,第三個參數為接收進程ID。#include <signal.h> #include <sys/time.h> #include <unistd.h> #include <sys/types.h>int main(int argc,char**argv) {pid_t pid;int signum;union sigval mysigval;signum=atoi(argv[1]);pid=(pid_t)atoi(argv[2]);mysigval.sival_int=8;//不代表具體含義,只用于說明問題if(sigqueue(pid,signum,mysigval)==-1)printf("send error\n");sleep(2); }

    注:實例2的兩個例子側重點在于用信號來傳遞信息,目前關于在linux下通過信號傳遞信息的實例非常少,倒是Unix下有一些,但傳遞的基本上都是關于傳遞一個整數.


    實例三:信號阻塞及信號集操作

    #include "signal.h" #include "unistd.h"static void my_op(int);int main() {sigset_t new_mask,old_mask,pending_mask;struct sigaction act;sigemptyset(&act.sa_mask);act.sa_flags=SA_SIGINFO;act.sa_sigaction=(void*)my_op;if(sigaction(SIGRTMIN+10,&act,NULL))printf("install signal SIGRTMIN+10 error\n");sigemptyset(&new_mask);sigaddset(&new_mask,SIGRTMIN+10);if(sigprocmask(SIG_BLOCK, &new_mask,&old_mask))printf("block signal SIGRTMIN+10 error\n");sleep(10);printf("now begin to get pending mask and unblock SIGRTMIN+10\n");if(sigpending(&pending_mask)<0)printf("get pending mask error\n");if(sigismember(&pending_mask,SIGRTMIN+10))printf("signal SIGRTMIN+10 is pending\n");if(sigprocmask(SIG_SETMASK,&old_mask,NULL)<0)printf("unblock signal error\n");printf("signal unblocked\n");sleep(10); }static void my_op(int signum) {printf("receive signal %d \n",signum); }

    編譯該程序,并以后臺方式運行。在另一終端向該進程發送信號(運行kill -s 42 pid,SIGRTMIN+10為42),查看結果可以看出幾個關鍵函數的運行機制,信號集相關操作比較簡單。


    其他示例

    #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <signal.h> void do_sig(int a) { printf("Hi, SIGINT, how do you do !\n"); } int main(void) { if (signal(SIGINT, do_sig) == SIG_ERR) { perror("signal"); exit(1); } while (1) { printf("---------------------\n"); sleep(1); } return 0; }

    捕捉信號

    #include <signal.h> #include <stdio.h> #include <errno.h> #include <stdlib.h> #include <unistd.h> typedef void (*sighandler_t) (int); void catchsigint(int signo) { printf("-----------------catch\n"); } int main(void) { sighandler_t handler; handler = signal(SIGINT, catchsigint); if (handler == SIG_ERR) { perror("signal error"); exit(1); } while (1); return 0; }

    捕捉信號

    #include <stdio.h> #include <signal.h>//捕捉函數 void dosig(int num) {printf("num = %d\n", num);printf("dosig function process finish\n"); }int main(void) {int i = 0;struct sigaction act, oldact;/*構造sigaction結構體*/act.sa_handler = dosig; sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGINT, &act, NULL);//linux系統函數 //注冊信號處理函數//signal(SIGINT, dosig); //C標準庫 signal -》sigactionwhile (1) {printf("****************** %d\n", i++);if (i == 5) {oldact.sa_handler = SIG_IGN; //SIG_DFLsigemptyset(&oldact.sa_mask);oldact.sa_flags = 0;sigaction(SIGINT, &oldact, NULL);}//sigaction(SIGINT, &oldact, NULL);sleep(1);}return 0; }

    捕捉信號并傳遞參數

    #include <signal.h> #include <stdio.h> #include <unistd.h>void dosig(int num, siginfo_t *siginfo, void *p) {printf("num = %d\n", num);printf("siginfo->si_int = %d\n", siginfo->si_int); } int main(void) {struct sigaction act;pid_t pid;act.sa_sigaction = dosig;sigemptyset(&act.sa_mask);act.sa_flags = SA_SIGINFO;pid = fork();if (pid == 0) {union sigval s;s.sival_int = 20;sleep(5);sigqueue(getppid(), SIGINT, s);return 0;}sigaction(SIGINT, &act, NULL);while (1) {printf("******\n");sleep(1);}return 0; }

    為某個信號設置捕捉函數 【sigaction1.c】

    #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> /*自定義的信號捕捉函數*/ void sig_int(int signo) { printf("catch signal SIGINT\n");//單次打印 sleep(10); printf("----slept 10 s\n"); } int main(void) { struct sigaction act; act.sa_handler = sig_int; act.sa_flags = 0; sigemptyset(&act.sa_mask); //不屏蔽任何信號 sigaddset(&act.sa_mask, SIGQUIT); sigaction(SIGINT, &act, NULL); printf("------------main slept 10\n"); sleep(10); while(1);//該循環只是為了保證有足夠的時間來測試函數特性 return 0; }

    驗證在信號處理函數執行期間,該信號多次遞送,那么只在處理函數之行結束后,處理一次。 【sigaction2.c】

    /*自動屏蔽本信號,調用完畢后屏蔽自動解除*/ #include <stdio.h> #include <signal.h> #include <unistd.h> /*自定義的信號捕捉函數*/ void sig_int(int signo) { printf("catch signal SIGINT\n"); sleep(10); //模擬信號處理函數執行很長時間 printf("end of handler\n"); } int main(void) { struct sigaction act, old; act.sa_handler = sig_int; sigemptyset(&act.sa_mask); //依然不屏蔽任何信號 act.sa_flags = 0; sigaction(SIGINT, &act, &old); //注冊信號處理函數 while(1); sigaction(SIGINT, &old, NULL); //注冊信號處理函數 return 0; }

    驗證sa_mask在捕捉函數執行期間的屏蔽作用。 【sigaction3.c】

    /*當執行SIGINT信號處理函數期間 *多次收到SIGQUIT信號都將被屏蔽(阻塞) *SIGINT信號處理函數處理完,立刻解除對 *SIGQUIT信號的屏蔽,由于沒有捕捉該信號, *將立刻執行該信號的默認動作,程序退出 */ #include <stdio.h> #include <signal.h> #include <unistd.h> void sig_int(int signo) { printf("catch signal SIGINT\n"); sleep(10); //模擬信號處理函數執行很長時間 printf("end of handler\n"); } int main(void) { struct sigaction act; act.sa_handler = sig_int; sigemptyset(&act.sa_mask); sigaddset(&act.sa_mask, SIGQUIT); /*將SIGQUIT加入信號屏蔽集,這就導致,在調用信號處理函數期間 *不僅不響應SIGINT信號本身,還不響應SIGQUIT*/ act.sa_flags = 0; sigaction(SIGINT, &act, NULL); //注冊信號SIGINT捕捉函數 while(1); return 0; }


    打印未決信號集

    /**************************************************************/ #include <stdio.h> #include <signal.h>void printsigpend(void) {int i;sigset_t sigpend;sigpending(&sigpend);for (i = 1; i < 32; i++) {if (sigismember(&sigpend, i) == 1)printf("1");elseprintf("0");}printf("\n"); } int main(void) {sigset_t set, oldset;sigemptyset(&set);sigaddset(&set, SIGINT);sigaddset(&set, SIGQUIT);sigaddset(&set, 15);sigaddset(&set, 9);sigprocmask(SIG_SETMASK, &set, &oldset);while (1) {printsigpend();sleep(1);}return 0; }


    信號等待

    #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h>void dowait(int num) {while (waitpid(0, NULL, WNOHANG) > 0); } void sys_err(char *str) {perror(str);exit(-1); } int main(void) {int i;pid_t pid;srand(time(NULL));struct sigaction act;act.sa_handler = dowait;sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGCHLD, &act, NULL);for (i = 0; i < 100; i++) {pid = fork();if (pid == 0)break;else if (pid < 0)sys_err("fork");}if (pid == 0) {/*in child*/int n = rand() % 5;while (n--) {printf("I am %u\n", getpid());sleep(1);}return 0;}/* in parent*/while (1) {printf("I am Parent\n");sleep(1);}return 0; }






    總結

    以上是生活随笔為你收集整理的Linux异步之信号(signal)机制分析的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。