信號是進程間通信(IPC)中最古老、最輕量的機制:異步通知,攜帶極少信息(僅信號編號)。本文演示 fork() 父子進程中,父進程用 kill() 發送 SIGALRM,子進程用 signal() 註冊處理函數接收,並深入剖析 signal() 的競態缺陷:處理函數註冊順序導致信號丟失。
#include// 發送信號 int kill(pid_t pid, int signo); // pid: 目標進程 PID;signo: 信號編號(如 SIGALRM=14) // 註冊處理函數(舊接口,不推薦新代碼使用) void (*signal(int signo, void (*handler)(int)))(int); // 返回前一個處理函數指針
#include#include #include #include void sig_receive(int signo) { printf("Received signal: %d\n", signo); } int main() { pid_t pid, pid_res; int stat_value; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid > 0) { // 父進程:發送 printf("PID: %d -> Father sends signal\n", getpid()); kill(pid, SIGALRM); // 發送 SIGALRM (14) } else { // 子進程:註冊處理並接收 signal(SIGALRM, sig_receive); // 註冊處理函數 printf("PID: %d -> Son waits for signal\n", getpid()); // 子進程這裡可 pause() 等待,或繼續執行 } pid_res = wait(&stat_value); if (pid_res > 0) { printf("Son finished: PID = %d\n", pid_res); } return 0; }
PID: 3208 -> Father sends signal Received signal: 14 PID: 3209 -> Son waits for signal Son finished: PID = 3209
若子進程先打印再註冊:
} else {
printf("PID: %d -> Son waits for signal\n", getpid()); // 先打印
signal(SIGALRM, sig_receive); // 後註冊
}
PID: 2611 -> Father sends signal Son finished: PID = 2612
現象:子進程未打印「Received signal」,直接結束。信號在 printf 到 signal() 間隙到達,默認動作(終止進程)生效,處理函數未來得及註冊。
| 階段 | 說明 | |
|---|---|---|
| 1. 父進程 kill | 信號投遞到子進程內核結構 | |
| 2. 子進程執行 printf | 用戶態運行,信號待處理 | |
| 3. 信號到達 | 內核檢查處理函數:尚未註冊 → 執行默認動作(終止) | |
| 4. 子進程執行 signal() | 已太晚,進程即將退出 |
| 方案 | 代碼 | 優缺點 |
|---|---|---|
| 調整順序:先註冊後業務 | signal(...); printf(...); | 簡單,但不可靠(多線程/複雜邏輯仍有窗口) |
| 用 sigaction + sa_mask | sigaction(SIGALRM, &act, NULL); 並 sigaddset(&act.sa_mask, SIGALRM) | 原子註冊,屏蔽集防嵌套,專業標準 |
| 子進程 pause() 等待 | signal(...); pause(); | 同步等待,但註冊前窗口仍存在 |
| 父進程 sleep 後發送 | sleep(1); kill(...); | 極其不可靠,絕不可用於生產 |
signal() 是教學/兼容用接口,核心缺陷在於註冊與生效非原子,存在「信號丟失/默認動作」競態。實際項目必須使用 sigaction():
sa_mask 防止處理期間同信號遞歸/嵌套。SA_SIGINFO 獲取發送者 PID、用戶數據。本文代碼僅為演示信號 IPC 基本形態,**生產代碼請直接參考《sigaction 替代 signal》篇**。