如上圖所示一個程序/線程就是一個task_struct結構,該結構包含了屬于這個程序/線程的阻塞信号集、pending的信号等,所有投遞到該程序/線程的信号都會通過雙向連結清單組織在一起,連結清單的元素是sigqueue,所有的信号對應的信号處理函數存放在sighand_struct中的一個類型為k_sigaction數組,每次程式由核心态切換到使用者态時,核心都會發起信号處理,執行信号處理程式的時候為了避免對核心産生影響,是以使用的是使用者棧,還可以自定義信号處理的備用棧。 信号處理函數是每次程式從核心态切換到使用者态的時候,核心才會負責發起信号處理,也就是說信号處理的時機有以下兩種: 程序在目前時間片用完後,獲得了新的時間片時(會發生核心态到使用者态的切換) 系統調用執行完成時(信号的傳遞可能會引起正在阻塞的系統調用過早完成)
通過檢視<code>/proc/PID/status</code>檔案,該檔案中有幾個字端的值,這些值按照十六進制的形式顯示,最低的有效位表示信号1,相鄰的左邊一位代表信号2,依次類推,例如下面這幾個數值:
當信号到達的時候,預設情況下信号有如下幾種處理方式:
忽略信号,核心直接将信号丢棄,不對程序産生任何影響
終止程序,是一種異常的終止方式,和調用exit而發生的終止不同
産生核心存儲檔案,同時程序終止
停止程序,暫停程序的運作
恢複之前暫停的程序
執行使用者自定義的信号處理器
這兩者都可以用來改變信号處置,<code>signal</code>很原始,提供的接口也比較簡單,而<code>sigaction</code> 提供了 <code>signal</code> 所不具備的功能。為了相容,<code>signal</code>系統調用仍然儲存,但是 <code>glibc</code> 是使用sigaction實作了<code>signal</code>的功能。<code>sigaction</code>同時支援兩種形式的信号處理,通過不同的<code>flags</code>區分。
<code>kill</code>系統調用可以用來向指定程序發送信号,如果指定的信号是0的時候,<code>kill</code>僅會進行錯誤的檢查,檢視是否可以想目标程序發送信号,而這一特點恰好可以用來檢測特定程序ID所對應的程序是否存在,如果不存在那麼<code>kill</code>調用失敗,并且<code>errno</code>設定為<code>ESRCH</code>。
注意: 如果kill一個僵屍程序會傳回成功或者權限錯誤的,僵屍程序其程序資料結構依然是存在的。
信号集是一種用來表示一系列信号集合的資料結構,使用<code>sigset_t</code>來表示,它的底層存儲類型其實隻是一個<code>unsigned long</code>類型,如下:
<code>unsigned long</code>一共是八個位元組,總共是64位,每一位表示一個信号的話,最多可以表示64個信号,這個和信号的最大值是吻合的。信号集也提供了一系列用來操作信号集的方法,<code>sigemptyset</code>、<code>sigfillset</code>、<code>sigaddset</code>、<code>sigdelset</code>、<code>sigismember</code>、<code>sigisemptyset</code>等
阻塞信号的實作不難,通過上文中對信号内部實作的分析可知,通過将要阻塞的信号放到<code>task_struct</code>結構中的<code>blocked</code>成員中,那麼在信号的投遞時會先檢視下要投遞的信号是否在阻塞信号集中,如果在就停止投遞,否則就觸發對應的信号處理,通過<code>sigprocmask</code>可以設定目前程序的阻塞信号集,對應到核心的實作如下:
通過<code>sigprocmask</code>設定阻塞的信号集存在一個競态,如果想在設定信号處理函數的同時再設定阻塞的信号集,那麼這需要先調用<code>signal/sigaction</code>,然後再調用<code>sigprocmask</code>,在設定信号處理函數和調用<code>sigprocmask</code>之間存在一個間隙,如果在這個間隙期間後信号投遞,那麼就沒有起到阻塞信号的作用了。為此<code>sigaction</code>的<code>sa_mask</code>成員可以用來設定阻塞信号集,這使得設定信号處理函數的同時就可以設定阻塞信号集。
另外一個問題就是被阻塞的信号在等待解除阻塞後是否會投遞到程序進行處理?信号被阻塞後就會變成待決信号,并通過連結清單連結起來,<code>task_struct</code>結構中的<code>pending</code>成員就是連結清單頭,如果一個信号發送多次,linux是不保證投遞相同次數的,隻會儲存一次,也就是非實時,不對信号排隊。其中<code>SIGKILL</code>和<code>SIGSTOP</code>是不能被阻塞的。
說白了這裡就是去查詢待決信号的連結清單也就是<code>task_struct</code>結構中的<code>pending</code>成員,将裡面的信号放到信号集中傳回即可。對應到核心實作如下:
在使用者态通過<code>sigpending</code>函數就可以查詢目前哪些被阻塞的信号是未決的(也就是已經投遞到程序了,但是因為被屏蔽了還沒有被處理,也就是儲存在程序的<code>pending</code>成員中)
信号處理函數和普通函數是有一些差別的,因為這個函數是異步被執行的,是以需要考慮異步信号安全的問題,在這個函數中沒辦法使用一些非異步信号安全的函數,為此編寫信号處理函數一般要遵從一些設計,兩種常見的設計如下:
信号處理函數設定全局性變量并退出,主程式周期性檢查,一旦置位就立即采取動作,或者信号處理函數通過忘管道中寫 入一個位元組來通知主程式。
信号處理器函數執行某種類型的清理動作,然後終止程序或者執行非本地跳轉,将棧解開并将控制權傳回到主程式的預定位置。
一個信号到達後會觸發信号處理函數,在信号處理函數執行過程中,如果該信号再次産生是不會打斷目前信号處理函數的,但是如果有其他信号進行了投遞這個會打斷目前信号處理函數的。<code>sigaction</code>的<code>sa_flags</code>成員有一個值就是用來控制這個行為的,如果值為<code>SA_NODEFER</code>(參考上文中對<code>sa_flags</code>的解釋)表明在執行信号處理函數時是可以被相同信号打斷的。這很容易造成遞歸死循環。
信号處理函數一般要遵從上文中提到的設計,處理函數中隻對一些全局變量進行處理,然後主程式周期性的檢查,那麼這個全局變量的類型需要考量兩點:
編譯器一般會對變量的讀寫進行緩存,将剛寫入的變量值放在寄存器中,下次讀的時候直接從寄存器中讀取,這個設計适用于gcc可以了解代碼的上下文,但是信号處理函數是任何時候都有可能觸發的,gcc沒辦法知道什麼時候觸發信号處理函數,如果某一時刻主程式對全局變量發生了寫入,但是寫入的值還沒來得及回寫記憶體,然後觸發信号處理函數,編譯器并不知道要從寄存器中讀該全局變量的值(因為沒上下文),是以會直接從記憶體中讀,這樣讀到的值就是一個髒值了。為了避免這個優化,在定義全局變量的時候會加上<code>volatile</code>關鍵字。
全局變量的讀寫可能不止一條機器指令,如果在操作全局變量的中途被打斷,那麼在信号處理函數中再次操作這個全局變量就很有可能造成該全局變量最終值是一個未定義的值。是以<code>sig_atomic_t</code>的類型其實就是一個原子類型,通過閱讀源碼,發現這個資料類型其實就是一個int類型,代碼如下,主要原因是因為在x86_64架構的CPU下,對于8、16、32、64這樣的對齊大小對齊的資料類型,其參考是原子的,是以<code>sig_atomic_t</code>就是一個對int類型的别名。
大多數情況下信号處理函數都是處理完一些事情後就回到了主程式繼續執行,或者是做一些資源的釋放和清理,接着就退出了程式, 除此之外其實還有更多的選擇。
使用_exit終止程序,處理器函數可以事先做一些清理工作,但是這裡注意不能使用exit來終止,因為它不在異步信号安全函數的清單中。
使用kill發送信号來殺掉程序
在信号處理函數中執行非本地跳轉
使用abort終止程序,并産生核心轉儲
對于1、2、4我覺得都是可以了解的,問題不大,重點是第三個,非本地跳轉,跳轉到另外一個地方後,棧會解旋轉,但是有一些點還需要探讨,比如說預設情況下當一個信号開始觸發信号處理函數時,預設會講該信号加入到阻塞的信号集中,這樣信号處理函數就不會被相同信号打斷了,如果使用非本地跳轉的化,帶來的問題就是這個阻塞的信号集需要被恢複,早期的<code>BSD</code>實作時會将阻塞的信号恢複的,但是Linux是遵循<code>System V</code>的實作,是不會将阻塞的信号進行恢複的,鑒于這個行為在不通的平台其實作不同,這将有損于可移植性,<code>POSIX</code>通過定義了一堆新的函數來規範非本地跳轉的行為,<code>sigsetjmp</code>和<code>siglongjmp</code>,其函數原型如下:
除了上面這個<code>HANDLE_EINTR</code>外,<code>GNU C</code>庫還提供了一個非标準的宏,<code>TEMP_FAILURE_RETRY</code>,需要定義特性測試宏<code>_GNU_SOURCE</code>,在<code>unistd</code>頭檔案中還有另外一個宏可以起到相同的作用<code>NO_EINTR</code>,最後一個方法就是使用<code>sigaction</code>中的<code>SA_RESTART</code>标志,通過設定該标志後,但是很不幸的是這個标志并不能處理所有系統調用的自重新開機的問題。
我們都知道程序的棧空間大小是有限制的,如果某一時刻棧空間增長到最大值,然後觸發了信号處理函數,但是棧已經達到了最大值了,無法為信号處理函數建立棧幀,也就沒有辦法調用信号處理函數了,為此可以借助信号備選棧來建立一個額外的堆棧,用于執行信号處理函數,信号備選棧的建立過程如下 :
配置設定一塊被稱為<code>"備選信号棧"</code>的記憶體區域,作為信号處理函數的棧幀
調用<code>sigaltstack</code>,告之核心備選棧的存在(也可以将已建立的備選信号棧的相關資訊傳回)
在建立信号處理函數時指定<code>SA_ONSTACK</code>,也就是通知核心在備選信号棧上為處理器函數建立棧幀。
大多數情況下這個信号備選棧的用途還是比較有限的,隻要重度依賴信号處理函數,對信号處理函數的執行成功與否比較敏感的程式才會考慮使用備選棧,比如說<code>google</code>的<code>breakpad</code>,重度依賴信号處理函數的,它通過注冊新号處理函數的方式将要程式的<code>coredump</code>行為捕獲,然後産生<code>minidump</code>,為了保證信号處理函數成功被執行,<code>breakpad</code>就使用了信号備選棧的方式來執行。下面通過模拟堆棧溢出,然後通過備選棧的方式順利執行信号處理函數,代碼如下:
傳統的信号處理函數隻會傳遞一個信号值,也不能自定義傳遞參數,通過設定<code>sigaction</code>的<code>sa_flags</code>為<code>SA_SIGINFO</code>就可以擷取到信号的一些附加資訊,設定了<code>SA_SIGINFO</code>後,信号處理函數的原型就變成了如下:
使用了新的信号處理函數後,帶來了幾點變化,第一個就是可以傳遞一個<code>siginfo_t</code>的結構,該結構可以攜帶更多的資訊,第二個是一個<code>void*</code> 參數,是一個指向*ucontext_t<code>類型的結構,該結構提供了所謂的上下文資訊,用來描述調用信号處理器函數前的程序上下文(可以用來實作協程,目前在信号處理函數中沒有使用,對應的設定和擷取程序上下文的函數</code>getcontext<code>和</code>setcontext`因為可移植性問題已經從POSIX中廢棄)
一些信号的預設處理方式就是讓程序産生<code>coredump</code>檔案,該檔案就是程序運作時的記憶體鏡像,除了可以通過信号來産生外,還有通過執行<code>gcore</code>指令産生,預設情況下會将全部的記憶體映射區域都寫入到核心存儲檔案中,通過<code>/proc/PID/coredump_filter</code>可以控制對哪些記憶體映射區域寫入,更詳細的内容可以<code>man core</code>來查詢,最後就是核心存儲檔案産生的條件,下面列出了不會産生核心轉儲檔案的情況:
程序對核心轉儲檔案沒有寫權限
存在一個同名、可寫的普通檔案,但是指向該檔案的(硬)連結數超過一個(也就是無法删除)
将要建立的核心轉儲檔案所在目錄并不存在
程序的核心存儲檔案大小限制為0
對程序正在執行的二進制可執行檔案沒有讀權限
磁盤空間滿了、inode資源耗盡了、達到磁盤配額限制、目前目錄是隻讀挂載的檔案系統
<code>Set-user-ID</code>程式在由非檔案屬主(或屬組)執行時,不會産生核心轉儲檔案(通過<code>prctl</code>和<code>PR_SET_DUMPABLE</code>可以控制這個行為,還可以通過<code>/proc/sys/fs/suid_dumpable</code>進行系統級的控制)
産生的core檔案其名稱還可以通過<code>/proc/sys/kernel/core_pattern</code>進行控制。
<code>SIGKILL</code>可以用來終止一個程序,<code>SIGSTOP</code>則是可以停止一個程序,二者的預設行為都是無法被改變的,一個停止的程序通過發送<code>SIGCONT</code>可以使得該信号恢複執行,這兩個信号在大多數情況下都可以立即終止一個程序或者是停止一個程序,但是有一種情況除外,就是核心處于<code>TASK_UNINTERRUPTIBLE</code> 狀态時,也就是睡眠狀态,Linux上有兩類睡眠狀态,一類就是<code>TASK_INTERRUPTIBLE</code>,這個狀态下程序時可以被中斷的,處于這個狀态下的程序一般時等待終端輸入、等待資料寫入目前的空管道等,通過PS查詢的時候,顯示為S。另外一種就是上文說道的<code>TASK_UNINTERRUPTIBLE</code>,不可中斷的睡眠,這類程序一般都是在等待某些特定類型的事件,比如磁盤IO的完成,處于這類狀态的程序時無法被信号終止的,通過PS查詢的時候,顯示為D,極端情況下這類程序可能會因為磁盤故障等原因,永遠無法被終止,這個時候就隻能通過重新開機機器來消滅這類程序了,在<code>inux 2.6.25</code>開始Linux加入了第三種狀态<code>TASK_KILLABLE</code>,這個狀态和<code>TASK_UNINTERRUPTIBLE</code>類似,但是卻可以被緻命信号喚醒。通過使用該狀态可以避免因為程序挂起處于<code>TASK_UNINTERRUPTIBLE</code>狀态而重新開機系統的情況。
當痛過<code>sigprocmask</code>阻塞信号的時候,在此期間産生的信号都會變成待決信号,一旦阻塞信号被恢複,那麼所有的待決信号都會被投遞,而投遞的順序則取決于具體的實作。下面這個例子示範了信号的投遞順序。<code>TODO</code>
實時信号是為了彌補标準信号的投遞順序未定義、信号不排隊會丢失等問題的,相比于标準信号,實時信号具備如下優勢:
實時信号的信号範圍有所擴大,可供應用程式自定義的目的,而标準信号中用于自定義的隻有<code>SIGUSER1</code>和<code>SIGUSER2</code>。
實時信号采取的是隊列化管理,如果某一個信号多次發送給一個程序,那麼該程序會多次收到這個信号,而标準信号才會丢失,隻會接收到一次信号(不過隊列是有大小限制的)。
當發送一個實時信号時,可為信号指定伴随資料,供接收程序的信号處理器使用(标準信号目前也是可以的)。
不同的實時信号的傳遞順序是有保障的,信号的編号越小優先級越高,而标準信号這個行為是未定義的,取決于具體的實作。
對于排隊的信号,是有一個上限的,這個上限值可以通過檢視<code>RLIMIT_SIGPENDING</code>資源限制的值,至于等待某一個程序的實時信号數量,可以從Linux專有檔案夾<code>/proc/PID/status</code>中的<code>SigQ</code>字段讀取 通sigqueue可以給實時信号發送伴随資料
實時信号的編号是從32~63,<code>RTSIG_MAX</code>常量代表了實時信号的數量,<code>SIGRTMIN</code>和<code>SIGRTMAX</code>則表示的是實時信号的最小值和最大值。
我們都知道信号是異步到來的,程式在運作的過程中時刻都有可能被信号打斷,對于一些在運作關鍵任務的程式來說這可能是一個噩夢,通過<code>sigprocmask</code>或者是<code>sigaction</code>的<code>sa_mask</code>可以屏蔽信号。等關鍵任務執行完成後可能需要等待信号到來,然後開始處理信号,對于這樣的場景可以通過<code>sigprocmask</code>解除屏蔽信号後,接着調用<code>pause</code>來等待信号到來。代碼如下:
很顯然<code>sigprocmask</code>解除信号屏蔽和<code>pause</code>等待信号這兩步并不是原子的,是以可能會導緻潛在的<code>bug</code>,信号可能在<code>pause</code>之前達到,導緻<code>pause</code>一緻在等待信号。為了解決這個問題Linux提供了<code>sigsuspend</code>,将解除信号屏蔽和等待信号變成了原子的,代碼如下:
到此為止我介紹了兩種等待信号的方式,一種就是<code>pause</code>,另外一種就是<code>sigsuspend</code>,但是這兩種等待信号的方式都很原始,隻是知道有信号到來了,具體是什麼信号是不知道的,還需要依靠信号處理函數去處理發生的信号。如果把信号比做一種消息的話,我希望可以同步的等待接收這個消息,然後同步的去處理這個消息,而不是靠信号處理函數打斷目前執行流異步的處理。Linux提供了<code>sigwaitinfo</code>相應的還有一個<code>sigtimedwait</code>,前者是永久的等待信号,後者是帶有逾時功能的等待。
通過<code>sigwaitinfo</code>來等待信号已經基本算滿足了需求,但是對于一個網絡程式來說,信号、網絡IO、定時器等都屬于事件,理想情況下應該将這些事件統一來處理,使用fd來管理這些事件這個在Linux下算是一種共識了,網絡IO自然不用說,定時器可以通過<code>timerfd_create</code>來建立一個fd然後和一個定時器關聯即可。而信号的化早期的做法是建立一個管道fd,然後在信号處理函數中往這個fd寫入信号值,這樣所有的事件就都可以使用fd來統一管理了,在Linux 2.6.27的時候提供了一個原生的解決方案就是<code>signalfd</code>,下面是一個簡單的示例代碼: