[ APUE ] 第十章 訊號
2. 訊號概念
linux 可支援31種訊號。不存在編號為0的訊號。
不存在編號為0的訊號,kill函式對訊號編號0有特殊應用,這個訊號被稱為空訊號。
產生訊號的條件:
- Ctrl+C 產生中斷訊號(SIGINT)。可以停止程式執行。
- 硬體異常產生訊號:除數為0,無效的記憶體引用等。
- kill函式可以將任意訊號傳送給另一個程序或程序組。但有兩個限制:接收程序和傳送程序的所有者必須相同,或者傳送程序的所有者是超級使用者。
- 檢測到某種軟體條件發生,並應將其通知有關程序時也產生訊號。
某個訊號出現時,可以告訴核心按照以下3種方式之一進行處理。
-
忽略。 大多數訊號都可以忽略,但是SIGKILL和SIGSTOP訊號不可忽略。這兩種訊號不可忽略的原因是:它們向核心和超級使用者提供了使程序終止或停止的可靠方法。如果忽略了某些由硬體產生的異常訊號,則程序產生的行為使未定義的。
-
捕捉。表示 通知核心 在某種訊號發生時,呼叫一個使用者函式。
如鍵盤產生SIGINT訊號,同時希望接收到中斷訊號時程序退出,則呼叫使用者函式終止當前程序。
如捕捉到SIGCHLD訊號,則表示子程序已終止,呼叫waitpid以取得程序終止狀態。
如程序建立了臨時檔案,則可能要為SIGTERM編寫一個訊號捕捉函式來清除臨時檔案。(SIGTERM是kill命令的系統預設訊號)。
注意不能捕捉SIGKILL和SIGSTOP。
-
執行系統預設動作。
對於大多數訊號,系統的預設動作是終止該程序。
終止+core :程序在當前工作目錄的core檔案中複製了該程序的記憶體映像,大多數UNIX系統除錯程式都使用core檔案檢查程序終止時的狀態。
3. signal函式
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
返回值:成功返回以前的訊號處理函式指標,出錯則返回SIG_ERR
handler的值是常量SIG_IGN、SIG_DFL或者訊號處理函式。
SIG_IGN表示忽略該訊號。SIG_DFL表示接到此訊號後的動作是預設動作。
-
程式啟動
當exec執行程式時,程序原先設定的訊號捕捉函式失效,所有的訊號都被設定為預設動作。(因為一個程序原先要捕捉的訊號,當其執行exec函式替換程序映像時,原來的訊號處理函式地址很可能就被覆蓋,無效了)。
-
程序建立
當程序呼叫fork時,其子程序繼承父程序的訊號處理方式。因為子程序在開始時複製了父程序記憶體映像,所以訊號捕捉函式的地址在子程序中是有意義的。
5.中斷的系統呼叫
6. 可重入函式
程序正常執行的指令序列被訊號處理程式臨時中斷,訊號處理程式不能判斷程序原來的指令執行至何處。這就會帶來一系列問題。
譬如,當malloc執行時中斷;當執行修改靜態儲存單元的函式時中斷;
當malloc維護堆記憶體塊連結串列時,中斷可能會破壞剛剛分配的記憶體。
存入靜態儲存單元的資訊可能在訊號處理函式中被覆蓋(破壞)。
SUS說明了可以在訊號處理程式中保證呼叫安全的函式。這些函式是可重入的,並被稱為是非同步訊號安全的。這些函式除了可重入,它還會阻止任何引起不一致的訊號傳送。
(注:可重入函式,即可以被中斷的函式,任何時刻都可被中斷,返回控制時不會引起什麼錯誤。)
沒有列入表中的函式大多數不可重入,它們有以下特徵:
- 使用了靜態資料結構(如果注意了關中斷和互斥手段其實也沒啥)
- 呼叫malloc或者free
- 它們是標準IO函式(標準IO庫的很多實現都是通過不可重入的方式使用全域性資料結構)
即便是對於上表中的函式,每個程序只有一個errno變數,所以訊號處理函式可能會修改原來的值。譬如main函式中設定了errno,之後呼叫上表中函式,之後發生中斷訊號,訊號處理函式中呼叫了read等函式,它可能更改errno。
因此,我們在呼叫上表中函式之前要先儲存errno,之後恢復errno。
(尤其是 SIGCHLD這種常被捕捉的訊號,訊號處理程式中通常要呼叫wait,而wait函式通常會修改errno)。
注意longjmp 和 siglongjmp 也是不可重入的。如果在更新資料結構時產生訊號,且訊號處理程式中呼叫siglongjmp,那麼這次更新可能就半途而廢。所以在更新全域性資料結構,同時某些訊號的訊號處理程式可能會執行siglongjmp的時候,要阻塞這些訊號。
7.SIGCLD語義
linux上等同於SIGCLD
注意SIGCLD的訊號處理函式sig_cld如果先設定signal(SIGCLD,sig_cld); 後wait,則會無限遞迴呼叫sig_cld(在某些系統上,linux似乎解決了這個問題)
8. 可靠訊號術語和語義
訊號的產生和遞送之間的時間間隔內,我們稱訊號是未決的。
程序產生了一個阻塞訊號,且對該訊號的動作為系統預設動作或者捕捉,則該程序將此訊號保持為未決狀態,直到該程序對這個訊號解除了阻塞,或者對此訊號的動作更改為忽略。
* 訊號遞達(Delivery):實際執行訊號的處理動作
* 訊號未決(Pending):訊號從產生到遞達之間的狀態
* 訊號阻塞(Block):程序可以選擇阻塞某個訊號,被阻塞的訊號產生時將保持在未決狀態,直到程序解除對此訊號的阻塞,才執行遞達的動作
*** 阻塞和忽略是不同的,只要訊號被阻塞就不會遞達,而忽略是在遞達之後可選的一種處理動作**
核心在遞送一個被阻塞的訊號給程序時,才決定他的處理方式(而非在產生該訊號時)。於是訊號在這期間仍然可以改變對這個訊號的動作。程序呼叫 sigpending 函式來判斷哪些訊號設定為阻塞並處於未決狀態。
特殊情況:程序解除對某個訊號的阻塞之前,這個訊號發生了多次。
Linux下的解決方式:常規訊號在遞達之前產生多次只計一次,而實時訊號在遞達之前產生多次可以依次放在一個佇列裡。
9. kill 和 raise函式
kill函式將訊號傳送給程序或程序組,raise函式允許程序向自身傳送訊號。
kill返回前該訊號就被傳送給該程序
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
#include <signal.h>
int raise(int sig);
成功返回0,失敗返回-1
呼叫 raise (signo), 等價於呼叫kill(getpid(), signo)
kill 的 pid引數有以下4種不同的情況
- pid > 0, 將訊號傳送給程序ID為PID的程序
- pid == 0, 將訊號傳送給與傳送程序同屬同一程序組的所有程序(不包括系統程序集)
- pid < 0, 將該訊號傳送給其程序組ID等於pid絕對值的所有程序,而且傳送程序具有向這些程序傳送訊號的許可權