Linux 信號應用之黑匣子程序設計

重要 本文轉載至:http://blog.jobbole.com/101619/css


1. 何爲黑匣子程序及其必要性

飛機上面的黑匣子用於飛機失過後對事故的時候調查,同理,程序的黑匣子用於程序崩潰後對崩潰緣由進程定位。其實Linux提供的core dump機制就是一種黑匣子(core文件就是黑匣子文件)。可是core文件並不是在全部場景都適用,由於core文件是程序崩潰時的內存映像,若是程序使用的內存空間比較大,那產生的core文件也將會很是大,在64bit的操做系統中,該現象更爲顯著。可是,其實咱們定位程序崩潰的緣由通常只須要程序掛掉以前的堆棧信息、內存信息等就足夠了。因此有的時候沒有必要使用系統自帶的core文件機制。linux

2. 黑匣子程序設計

程序異常時,每每會產生某種信號,內核會對該信號進行處理。因此設計黑匣子程序的實質就是咱們定義本身的信號處理函數,來代替內核的默認處理。在咱們的信號處理函數中,咱們能夠將咱們想要的信息保存下來(好比程序崩潰時的堆棧信息),以方便後面問題的定位。git

下面咱們先給出一個我寫的程序,而後邊分析程序邊講具體如何設計一個黑匣子程序:github

2.1 定義一些數據結構

這裏咱們定義了一個sigInfo的數據結構,用來保存信號。利用這個數據結構咱們能夠將信號值與信號名映射起來。你能夠在你的系統中使用 kill –l 命令去查看他們的對應關係。固然,在程序中,若是獲得了信號值,也可使用Linux提供的API函數strsignal來獲取信號的名字,其函數原型以下:web

以後定義了一個全局變量sigCatch來增長咱們想要處理的信號。ubuntu

2.2 sigaction函數

在main函數裏面,除了調用一些函數外,主要是註冊了一下咱們要處理的信號。其實就是將特定的信號與某個信號處理函數關聯起來。這裏咱們所要捕獲的信號的信號處理函數都是同一個blackbox_handler,由於咱們想在這些信號出現時保存堆棧信息,因此使用同一個函數徹底能夠。這裏須要介紹的是sigaction函數,其函數原型以下:數組

使用該函數能夠改變程序默認的信號處理函數。數據結構

第一個參數signum指明咱們想要改變其信號處理函數的信號值。注意,這裏的信號不能是SIGKILL和SIGSTOP。這兩個信號的處理函數不容許用戶重寫,由於它們給超級用戶提供了終止程序的方法( SIGKILL and SIGSTOP cannot be caught, blocked, or ignored)。app

第二個和第三個參數是一個struct sigaction的結構體,該結構體在<signal.h>中定義,用來描述信號處理函數。若是act不爲空,則其指向信號處理函數。若是oldact不爲空,則以前的信號處理函數將保存在該指針中。若是act爲空,則以前的信號處理函數不變。咱們能夠經過將act置空,oldact非空來獲取當前的信號處理函數。dom

咱們來看一下這個重要的結構體:

能夠看到,該結構體共有5個成員:

sa_handler是一個函數指針,指向咱們定義的信號處理函數,該值也能夠是SIG_IGN(忽略信號)或者SIG_DEL(使用默認的信號處理函數)。

sa_mask字段說明了一個信號集,信號處理函數執行期間這一信號集要加到進程的信號屏蔽字中。僅當從信號處理函數返回時再將進程的信號屏蔽字復位爲原先的值。這樣在調用信號處理函數時就能阻塞某些信號。在信號處理函數被調用時,操做系統創建的新信號屏蔽字包括正在被遞送的信號。所以保證了在處理一個給定信號時,若是這種信號再次發生,那麼它會被阻塞到對前一個信號的處理結束爲止。

sa_flags字段指定對信號處理的一些選項,經常使用的選項及其含義說明以下(在 <signal.h>中定義):

選項

含義

SA_INTERRUPT 由此信號中斷的系統調用不會自動重啓
SA_NOCLDSTOP 若signo是SIGCHLD,當子進程中止(做業控制)時,不產生此信號。當子進程終止時,仍產生此信號(參加SA_NOCLDWAIT說明)。若已設置此標誌,則當中止的進程繼續運行時,做爲XSI擴展,不發送SIGCHLD信號。
SA_NOCLDWAIT 若signo是SIGCHLD,則當調用進程的子進程終止時,不建立殭屍進程。若調用進程在後面調用wait,則調用進程阻塞,直到其全部子進程都終止,此時返回-1,並將errno設置爲ECHILD。
SA_NODEFER 當捕捉到此信號時,在執行其信號處理函數時,系統不自動阻塞此信號(除非sa_mask包括了此信號)。
SA_ONSTACK 若用sigaltstack聲明瞭以替換棧,則將此信號遞送給替換棧上的進程。
SA_RESETHAND 在此信號處理函數的入口處,將此信號的處理方式復位爲SIG_DEF,並清除SA_SIGINFO標誌。可是,不能自動復位SIGILL和SIGTRAP這兩個信號的配置。設置此標誌是sigaction的行爲如同SA_NODEFER標誌也設置了同樣。
SA_RESTART 由此信號中斷的系統調用會自動重啓動。
SA_SIGINFO 此選項對信號處理程序提供了附加信息:一個指向siginfo結構的指針以及一個指向進程上下文標識符的指針。

sa_sigaction是一個替代的信號處理函數,當sa_flags字段設置爲SA_SIGINFO時,使用該信號處理函數。須要注意的是,對於sa_sigaction和sa_handler字段,其實現可能使用同一存儲區,因此應用程序只能一次使用這兩個字段中的一個。一般,按以下方式調用信號處理函數:

可是,若是設置了SA_SIGINFO標誌,則按照以下方式調用信號處理函數:

可見第二種方式比第一種方式多了後面兩個參數。其中第二個參數爲一個siginfo_t結構的指針,該結構描述了信號產生的緣由,該結構通常定義以下:

通常siginfo_t結構至少包含si_signo和si_code成員。第三個參數context是一個無類型的指針,它能夠被強制轉換爲ucntext_t結構類型,用於標識信號傳遞時進程的上下文。

2.3 信號集

信號種類數目可能超過一個整型量所包含的位數,因此通常而言,不能用整型量中的一位表明一種信號,也就是不能用一個整型量表示信號集(使用信號集能夠表示多個信號)。POSIX.1定義了數據結構sigset_t以包含一個信號集,而且定義了下面5個處理信號集的函數:

每個進程都有一個信號屏蔽字,它規定了當前要阻塞遞送到該進程的信號集。對於每種可能的信號,該屏蔽字中都有一位與之對應。對於某種信號,若其對應爲已設置,則它當前是被阻塞的。進程能夠調用sigprocmask來檢測和更改當前信號的屏蔽字。

函數sigemptyset初始化由set指向的信號集,清除其中全部的信號。函數sigfillset初始化由set指向的信號集,使其包括全部信號。全部應用程序在使用信號集前,要對該信號集調用sigemptyset或sigfillset一次。這是由於C編譯器把未賦初值的外部和靜態變量都初始化爲0. 一旦已經初始化了一個信號集,之後就能夠在該信號集中增、刪特定的信號。函數sigaddset將一個信號添加到現有集中,sigdelset則從信號集中刪除一個信號。

2.4 kill&&raise&&abort函數

bug_func函數的做用是產生一些異常信號,用於咱們的測試。裏面有兩個注意點:(1)咱們使用微秒數來做爲隨機數種子,這樣產生的僞隨機數分佈會比其餘不少方式更均勻一些。(2)咱們調用了kill函數和abort函數來產生一些信號。其函數原型以下:

kill函數將信號發送給進程或進程組。kill的pid參數有4種不一樣的狀況:

  • pid>0. 將該信號發送給進程ID爲pid的進程。
  • pid==0. 將該信號發送給與發送進程屬於同一進程組的全部進程(這些進程的進程組ID等於發送進程的進程組ID),並且發送進程具備向這些進程發送信號的權限。注意,這裏的「全部進程」不包括實現定義的系統進程集。對於大多數UNIX系統,系統進程集包括內核進程以及init(pid等於1)進程。
  • pid<0. 將該信號發送給其進程組ID等於pid的絕對值,並且發送進程具備向其發送信號的權限。如上所述,「全部進程集」不包括某些系統進程。
  • pid==-1. 將該信號發送給發送進程有權限向它們發送信號的系統上全部的進程。不包括某些系統進程。

raise函數等價於kill(getpid(), signo).

abort函數會先清除對SIGABRT信號阻塞(若是有阻塞的話),而後調用raise函數向調用進程發送信號。注意:若是abort函數使得進程終止了,那終止前會刷新和關閉全部打開的流。

2.5 backtrace&&backtrace_symbols函數

在黑匣子信號處理函數中咱們使用了backtrace和backtrace_symbols函數來獲取進程崩潰時的堆棧信息。這兩個函數的函數原型以下:

backtrace函數會返回進程的調用棧信息,並保存在buffer指向的二維數組中;size指明buffer中能夠保存的最大棧幀數目,若是調用棧信息超過了size的值,則只會保存近期的調用棧信息。返回值是保存的棧幀數。

使用backtrace函數獲得調用棧信息後,咱們就可使用backtrace_symbols函數將調用棧的地址信息翻譯爲用符號描述的信息,保存在返回值裏面。須要注意的是咱們只須要定義返回值的指針,其空間由函數backtrace_symbols本身調用maolloc分配,可是使用完之後的空間由咱們負責釋放。backtrace_symbols_fd沒有返回值,它與backtrace_symbols的不一樣之處在於它會將翻譯的調用棧信息保存在文件裏面。

注意:

  1. 使用backtrace函數時,在編譯選項中須要加上 rdynamic 選項,好比: gcc rdynamic blackbox.c o blackbox 。
  2. backtrace_symbols函數會輸出出錯時的16進制的地址,此時咱們可使用addr2line命令將其轉換爲咱們具體的代碼行數,命令格式爲: addr2line e execute_file  addr ,好比  addr2line e ./a.out 0x400d62 。

在該黑匣子程序中,涉及到了不少Linux信號的知識,以及一些相關的數據結構和API,但願對你們有用。但其實該黑匣子程序在有些極端狀況下仍是有必定的問題,後面咱們會分析並進一步優化。

3. Bug分析

在前文中,咱們實現了一個黑匣子程序——在進程崩潰後,能夠保存進程的調用棧。可是,在文章結尾咱們說程序有bug,那bug是什麼呢?先看下面一個程序:

該程序的執行結果以下:

該程序是一種極端狀況:咱們的程序中使用了無線層次的遞歸函數,致使棧空間被用盡,此時會產生SIGSEGV信號。可是從輸出看,並無走到咱們的信號處理函數裏面。這是由於但因爲棧空間已經被用完,因此咱們的信號處理函數是無法被調用的,這種狀況下,咱們的黑匣子程序是無法捕捉到異常的。

可是該問題也很好解決,咱們能夠爲咱們的信號處理函數在堆裏面分配一塊內存做爲「可替換信號棧」。

4. 使用可替換信號棧&&sigaltstack函數

使用可替換棧優化後的程序以下:

編譯 gcc rdynamic blackbox_overflow.c 後運行,輸出爲:

相關文章
相關標籤/搜索