天天看點

Unix/Linux程式設計:信号處理函數設計信号處理函數終止信号處理函數的其他方法

設計信号處理函數

一般而言,将信号處理器函數設計得越簡單越好。其中的一個重要原因就在于,這将降低引發競争條件的風險。下面是針對信号處理器函數的兩種常見設計。

  • 信号處理函數設定全局性标志變量并退出。主程式對此标志進行周期性檢查,一旦置位随即采取相應動作。(主程式如果因為監控一個或者多個檔案描述符的IO狀态而無法進行這種周期性檢查時,則可以令信号處理函數向一專用管道寫入一個位元組的資料,同時将該管道的讀取置于主程式所監控的檔案描述符範圍之内)
  • 信号處理函數執行某種類型的清理動作,接着終止程序或者使用非本地跳轉将棧解開并控制傳回到主程式的預訂位置

再論信号的非排隊處理

  • 在執行某信号的處理器函數時會阻塞同類信号的傳遞(除非在調用sigaction()指定了SA_NODEFER标志)。如果在執行處理器函數時(再次)産生同類信号,那麼會将該信号标記為等待狀态并在處理器函數傳回之後再行傳遞。而且不會對信号進行排隊處理,如果在處理器函數執行期間,多次産生同類信号,那麼那麼仍然會将其标記為等待狀态,但稍後隻會傳遞一次。
  • 信号的這種“失蹤”方式無疑将影響對信号處理器函數的設計。首先,無法對信号的産生次數進行可靠計數。其次,在為信号處理器函數編碼時可能需要考慮處理同類信号多次産生的情況

可重入函數

在信号處理器函數中,并非所有系統調用和庫函數均可以安全調用

  • 因為信号處理函數可能會在任一時間異步中斷程式的執行,進而在同一程序中實際形成了兩條(即主程式和信号處理器函數)獨立(雖然不是并發)的執行線程。
  • 如果信号處理器用到了不可重入函數比如printf()、scanf()、crypt()、getpwnam()、gethostbyname()等、或者更新全局變量/靜态資料結構,那麼這種信号處理函數可能會帶來糟糕的後果
可重入函數:函數由兩條或多條線程調用時,即便是交叉執行,其效果也與各線程以未定義順序依次調用時一緻。

全局變量與sig_atomic_t資料類型

盡管存在可重入問題,有時仍需要在主程式和信号處理器函數之間共享全局變量。信号處理器函數可能會随時修改全局變量----隻要主程式能夠正确處理這種可能性,共享全局變量就是安全的。一種常見的設計是:

  • 信号處理器函數隻做一件事,設定全局标志。、
  • 主程式會周期性的檢查這一标志,并采取相應動作來響應信号傳遞(同時清除标志)。
  • 當信号處理器函數以此方式來通路全局變量時,應該總是在聲明變量時使用 volatile 關鍵字,進而防止編譯器将其優化到寄存器中

對全局變量的讀寫可能不止一條機器指令,而信号處理器函數就可能會在這些指令序列之間将主程式中斷(也将此類變量通路稱為非原子操作)。是以,C 語言标準以及 SUSv3 定義了一種整型資料類型 sig_atomic_t,意在保證讀寫操作的原子性。是以,所有在主程式與信号處理器函數之間共享的全局變量都應聲明如下:

注意,C 語言的遞增(++)和遞減(–)操作符并不在 sig_atomic_t 所提供的保障範圍之内。這些操作在某些硬體架構上可能不是原子操作。在使用sig_atomic_t 變量時唯一所能做的就是在信号處理器中進行設定,在主程式中進行檢查(反

之亦可)

C99 和 SUSv3 規定,實作應當(在<stdint.h>中)定義兩個常量SIG_ATOMIC_MIN 和SIG_ATOMIC_MAX,用于規定可賦給 sig_atomic_t 類型的值範圍。标準要求,如果将sig_atomic_t 表示為有符号值,其範圍至少應該在-127~127 之間,如果作為無符号值,則應該在 0~255 之間。在 Linux 中,這兩個常量分别等于有符号 32 位整型數的負、正極限值。

終止信号處理函數的其他方法

目前為止所看到的信号處理函數都是以傳回主程式而終結。不過,有時簡單的從信号處理器函數中傳回并不能滿足需要。

下面是從信号處理器函數中終止的其他方法:

  • 使用_exit()終止函數。處理器函數事先可以做一些清理工作。注意,不要使用exit()來終止信号處理器函數,因為它不是安全的,之是以不安全,是因為該函數會在調用_exit()之前重新整理stdio的緩沖區
  • 使用 kill()發送信号來殺掉程序(即,信号的預設動作是終止程序)。
  • 從信号處理器函數中執行非本地跳轉。
  • 使用 abort()函數終止程序,并産生核心轉儲

在信号處理器函數中執行非本地跳轉

使用 setjmp()和 longjmp()來執行非本地跳轉,以便從一個函數跳轉至該函數

的某個調用者。在信号處理器函數中也可以使用這種技術。這也是因硬體異常而導緻信号傳遞之後的一條恢複途徑,允許将信号捕獲并把控制傳回到程式中某個特定位置。比如,一旦收到 SIGINT 信号(通常由鍵入 Ctrl-C 産生),shell執行一個非本地跳轉,将控制傳回到主輸入循環中(以便讀取下一條指令)。

然而,使用标準longjmp()函數從處理器函數中退出存在一個問題:在進入信号處理器函數時,核心會自動将引發調用的信号以及由act.sa_mask所指定的任意信号添加到程序的信号掩碼中,并在處理器函數正常傳回時再将它們從掩碼中清除。

如果使用longjmp()來退出信号處理器函數,那麼信号掩碼會發生什麼情況呢??這取決于特定 UNIX 實作的血統。在 System V 一脈中,longjmp()不會将信号掩碼恢複,亦即在離開處理器函數時不會對遭阻塞的信号解除阻塞。Linux 遵循 System V 的這一特性。(這通常并非所希望的行為,因為引發對信号處理器調用的信号仍将保持阻塞狀态。)在源于 BSD 一脈的實作中,setjmp()将信号掩碼儲存在其 env 參數中,而信号掩碼的儲存值由 longjmp()恢複。(繼承自BSD 的實作還提供另外兩個擁有 System V 語義的函數:_setjmp()和_longjmp()。)換言之,使用longjmp()來退出信号處理器函數将有損于程式的可移植性

如果編譯程式時定義了_BSD_SOURCE 特性檢測宏,那麼(glibc 的)setjmp()将遵循 BSD語義。

鑒于兩大 UNIX 流派之間的差異,POSIX.1-1990 選擇不對 setjmp()和 longjmp()的信号掩碼處理進行規範,而是定義了一對新函數:sigsetjmp()和 siglongjmp(),針對執行非本地跳轉時的信号掩碼進行顯式控制

SYNOPSIS
       #include <setjmp.h>
	   int sigsetjmp(sigjmp_buf env, int savesigs);

	   void siglongjmp(sigjmp_buf env, int val);
           

函數 sigsetjmp()和 siglongjmp()的操作與 setjmp()和 longjmp()類似。唯一的差別是參數 env的類型不同),并且 sigsetjmp()多出一個參數 savesigs。

  • 如果指定savesigs 為非 0,那麼會将調用 sigsetjmp()時程序的目前信号掩碼儲存于 env 中,之後通過指定相同 env 參數的 siglongjmp()調用進行恢複。
  • 如果 savesigs 為 0,則不會儲存和恢複程序的信号掩碼

異常終止程序

函數abort()終止其調用程序,并生成核心轉儲。

NAME
       abort - cause abnormal process termination

SYNOPSIS
       #include <stdlib.h>

	 void abort(void);
           

函數abort()通過産生SIGABRT信号來終止調用程序。對SIGABRT的預設動作是産生核心轉儲檔案并終止程序。調試器可以利用核心轉儲檔案來檢測調用abort()時的程式狀态

SUSv3要求,無論阻塞還是忽略SIGABRT信号,abort()調用均不受影響。同時規定,除非程序捕獲SIGABRT信号後信号處理函數尚未傳回,否則abort()必須終止程序。

在大多數實作中,終止時可確定發生如下事件:若程序在發出一次SIGABRT 信号後仍未終止(即,處理器捕獲信号并傳回,以便恢複執行 abort()),則 abort()會将對 SIGABRT 信号的處理重置為SIG_DFL,并再度發出 SIGABRT 信号,進而確定将程序殺死。

如果 abort()成功終止了程序,那麼還将重新整理 stdio 流并将其關閉

繼續閱讀