在 Linux 系統編程中,信號(Signal)是一種非常微妙的機制。初學者往往只會用簡單的 signal() 函數來註冊回調,但在實際的工程開發中,sigaction() 才是真正的“工業級”標準。
為什麼?因為 sigaction 不僅能捕捉信號,還能讓我們精確控制信號處理函數執行期間的屏蔽行為。本文將結合詳細的代碼案例,帶你深入理解 sigaction 的核心機制,特別是自動屏蔽和手動屏蔽特性。
一、 sigaction 的核心機制
sigaction 函數原型如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
其中最關鍵的是 struct sigaction 結構體,通過它我們可以定義信號處理的種種細節:
struct sigaction {
void (*sa_handler)(int); // 信號處理函數
void (*sa_sigaction)(int, siginfo_t *, void *); // 攜帶更多信息的處理函數(不常用)
sigset_t sa_mask; // 【重點】執行期間的臨時屏蔽集
int sa_flags; // 【重點】通常設為0,表示默認行為
void (*sa_restorer)(void); // 已廢棄
};
知識點梳理(必考點)
- 本信號自動屏蔽 (
sa_flags = 0): 默認情況下(sa_flags=0),當進程正在執行某個信號的處理函數時,如果該信號再次產生,它會被自動屏蔽,直到當前處理函數返回。這避免了遞歸調用導致的棧溢出或邏輯混亂。 - 臨時屏蔽集 (
sa_mask): 除了屏蔽本信號,我們還可以通過sa_mask指定在處理函數執行期間額外屏蔽哪些信號。例如:正在處理 Ctrl+C 時,不想被 Ctrl+\ 打斷。 - 不可累積特性: Linux 的常規信號(1-31號)使用位圖記錄。如果在一個信號被屏蔽期間,你連續發送了 5 次該信號,內核的未決位圖只能記錄一次“1”。因此,解除屏蔽後,處理函數只會執行一次,而不是 5 次。
二、 代碼實戰:驗證屏蔽機制
我們將編寫一個程序來驗證上述特性。
實驗目標:
- 捕捉
SIGINT(Ctrl+C)。 - 在處理
SIGINT期間(模擬耗時 5 秒),驗證:
- 再次按下 Ctrl+C(本信號),是否被推遲處理?
- 按下
SIGQUIT(Ctrl+\),是否被我們手動設置的sa_mask屏蔽?
1. 詳細代碼實現
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
// 信號處理函數
void sig_handler(int signum) {
printf("\n>> [Catch] 捕獲信號 %d,開始處理...\n", signum);
// 模擬耗時操作,總共 5 秒
int i;
for (i = 1; i <= 5; i++) {
printf(">> 處理中... %d秒\n", i);
sleep(1);
}
printf(">> [Finish] 信號 %d 處理完畢!\n", signum);
}
int main() {
struct sigaction act, oldact;
// 1. 設置處理函數
act.sa_handler = sig_handler;
// 2. 初始化 sa_mask 並設置要額外屏蔽的信號
// 清空集合
sigemptyset(&act.sa_mask);
// 添加 SIGQUIT (3號信號,對應 Ctrl+\)
// 意味着:在處理 SIGINT 期間,如果收到 SIGQUIT,先暫停不處理,等我忙完!
sigaddset(&act.sa_mask, SIGQUIT);
// 3. 設置 flags
// 0 表示默認屬性:本信號(SIGINT)在處理期間也會自動被屏蔽
act.sa_flags = 0;
// 4. 註冊信號捕捉
// 捕捉 SIGINT (2號信號,對應 Ctrl+C)
int ret = sigaction(SIGINT, &act, &oldact);
if (ret == -1) {
perror("sigaction error");
exit(1);
}
printf("進程運行中 (PID: %d)...\n", getpid());
printf("實驗步驟:\n");
printf("1. 按 Ctrl+C 觸發處理函數。\n");
printf("2. 在5秒處理期間,瘋狂按 Ctrl+C (驗證本信號屏蔽)。\n");
printf("3. 在5秒處理期間,按 Ctrl+\\ (驗證 sa_mask 屏蔽)。\n");
while (1) {
// 保持進程運行
sleep(1);
}
return 0;
}
2. 運行結果與深度解析
編譯並運行程序:
gcc sigaction_test.c -o sigaction_test
./sigaction_test
場景一:驗證“本信號自動屏蔽”與“不可累積”
操作:按下一次 Ctrl+C,然後在它計數的 5 秒內,連續快速按下 3 次 Ctrl+C。
輸出結果:
進程運行中 (PID: 12345)...
...
^C
>> [Catch] 捕獲信號 2,開始處理... <-- 第1次觸發
>> 處理中... 1秒
^C^C^C <-- 處理期間狂按3次 Ctrl+C
>> 處理中... 2秒
>> 處理中... 3秒
>> 處理中... 4秒
>> 處理中... 5秒
>> [Finish] 信號 2 處理完畢!
>> [Catch] 捕獲信號 2,開始處理... <-- 只有第2次處理,沒有第3、4次
>> 處理中... 1秒
...
現象分析:
- 當第一次處理函數正在運行時,後續的 Ctrl+C 信號被阻塞(Blocked)。
- 雖然我們按了 3 次,但內核的未決位圖裏該位只能是 1。
- 當第一次處理結束後,屏蔽解除,未決的信號遞達,觸發第二次處理。
- 這就證明了:常規信號不支持排隊,多次發送只記一次。
場景二:驗證 sa_mask 手動屏蔽
操作:按下一次 Ctrl+C,在處理期間,按下 Ctrl+\ (SIGQUIT)。
輸出結果:
^C
>> [Catch] 捕獲信號 2,開始處理...
>> 處理中... 1秒
>> 處理中... 2秒
^\ <-- 按下 Ctrl+\,屏幕顯示符號但未退出
>> 處理中... 3秒
>> 處理中... 4秒
>> 處理中... 5秒
>> [Finish] 信號 2 處理完畢!
Quit (core dumped) <-- 處理函數一結束,SIGQUIT 立即生效
現象分析:
- 正常情況下,Ctrl+\ 會導致進程直接退出並 Core Dump。
- 但因為我們將
SIGQUIT加入了sa_mask,所以在sig_handler運行期間,SIGQUIT 被“按住”了。 - 一旦
sig_handler打印完 "處理完畢",臨時屏蔽解除,積壓的 SIGQUIT 瞬間遞達,導致進程退出。
三、 知識小結與避坑指南
通過上面的實驗,我們可以總結出 sigaction 編程的幾個核心要點:
- 結構體初始化:在使用
struct sigaction之前,建議先清空或明確設置所有成員。雖然上面的代碼直接賦值,但在複雜項目中,最好先memset或使用sigemptyset初始化sa_mask。 - 屏蔽範圍:
- Global Mask (PCB中):全局有效,需手動
sigprocmask修改。 - sa_mask (結構體中):臨時有效,僅在回調函數執行期間生效,函數返回後自動恢復原來的 Mask。
- 錯誤檢查:永遠不要忽略
sigaction的返回值。如果返回 -1,一定要用perror打印錯誤原因(比如信號編號錯誤)。 - 信號丟失:要時刻記住,Linux 常規信號是不排隊的。如果你的信號處理函數太慢,可能會導致後續信號丟失。因此,信號處理函數越快越好。