信號是 Unix/Linux 進程間異步通訊的基礎機制,類似軟體中斷。本文從 signal() 歷史介面進階到現代 sigaction(),結合 kill() 發送、fork() 父子進程協作,完整演示信號發送、捕捉、處理流程,並分析 signal() 的可重入性與競態條件缺陷。
#include
int kill(pid_t pid, int signo);
pid > 0:發送給指定 PID 進程pid == 0:發送給同進程組所有進程pid < -1:發送給進程組 ID 為 -pid 的所有進程signo:信號編號 (如 SIGUSR1, SIGALRM, SIGTERM)errno)void (*signal(int signo, void (*func)(int)))(int);
func 為 signo 的處理函數struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
sa_handler:處理函數 (同 signal())sa_sigaction:擴展處理函數 (啟用 SA_SIGINFO 旗標),可獲取 siginfo_t 詳細資訊sa_mask:處理函數執行期間**額外阻塞**的信號集sa_flags:SA_RESTART (自動重啟被中斷系統呼叫)、SA_SIGINFO、SA_NOCLDSTOP 等#include
#include
#include
#include
#include
void sig_handler(int signo) {
printf("子進程收到信號 %d\n", signo);
}
int main(void) {
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid > 0) { // 父進程
printf("父進程 %d 發送 SIGALRM 給子進程 %d\n", getpid(), pid);
sleep(1); // 確保子進程已註冊處理函數
kill(pid, SIGALRM);
wait(NULL);
} else { // 子進程
signal(SIGALRM, sig_handler); // 註冊處理函數
printf("子進程 %d 等待信號...\n", getpid());
pause(); // 暫停等待信號
}
return 0;
}
#include
#include
#include
#include
#include
#include
void sig_handler(int signo, siginfo_t *info, void *context) {
printf("子進程 %d 收到信號 %d (發送者 PID: %d)\n", getpid(), signo, info->si_pid);
}
int main(void) {
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_sigaction = sig_handler;
act.sa_flags = SA_SIGINFO; // 使用擴展處理函數
sigemptyset(&act.sa_mask);
if (sigaction(SIGALRM, &act, NULL) == -1) {
perror("sigaction");
return 1;
}
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid > 0) {
printf("父進程 %d 發送 SIGALRM 給子進程 %d\n", getpid(), pid);
sleep(1);
kill(pid, SIGALRM);
wait(NULL);
} else {
printf("子進程 %d 等待信號 (sigaction 版)...\n", getpid());
pause();
}
return 0;
}
# signal() 版本 (可能因競態條件漏收信號)
父進程 3208 發送 SIGALRM 給子進程 3209
子進程 3209 等待信號...
子進程收到信號 14
# sigaction() 版本 (可靠、可獲取發送者資訊)
父進程 2611 發送 SIGALRM 給子進程 2612
子進程 2612 等待信號 (sigaction 版)...
子進程 2612 收到信號 14 (發送者 PID: 2611)
若子進程註冊處理函數 **晚於** 父進程發送信號:
// 子進程調換順序
printf("子進程 %d 等待信號...\n", getpid()); // 先印訊息
signal(SIGALRM, sig_handler); // 再註冊 (延遲)
pause();
結果:子進程直接退出,**未執行處理函數**,父進程 wait() 正常返回。
原因:父進程 kill() 發送信號時,子進程尚未完成 signal() 註冊,信號依預設動作處理 (SIGALRM 預設為終止進程)。
sigaction() 同樣存在此問題,解決方案:
sleep() / usleep() 延遲發送 (不可靠)signalfd() (Linux 專用) 將信號轉為檔案描述符讀取**只能呼叫 Async-Signal-Safe 函數** (man 7 signal-safety):
| 安全函數 | 不安全函數 (禁用) |
|---|---|
| write, read, open, close | printf, malloc, free, fopen |
| sigaction, sigprocmask, kill | pthread_mutex_lock, pthread_cond_wait |
| _exit, abort | std::cout, std::string, 任何 C++ STL |
| getpid, getuid, time | 非重入函數、靜態緩衝區函數 |
最佳實踐:處理函數僅設定 volatile sig_atomic_t 旗標,主迴輪檢查旗標執行複雜邏輯。
| 信號 | 編號 | 預設動作 | 典型用途 |
|---|---|---|---|
| SIGHUP | 1 | 終止 | 終端掛起、守護進程重載配置 |
| SIGINT | 2 | 終止 | Ctrl+C 中斷 |
| SIGQUIT | 3 | 核心傾印 | Ctrl+\ 退出並生成 core |
| SIGKILL | 9 | 終止 | **不可捕捉/阻塞**,強制殺死 |
| SIGUSR1/2 | 10/12 | 終止 | **用戶自定義**,進程間協作首選 |
| SIGALRM | 14 | 終止 | alarm() 定時器 |
| SIGTERM | 15 | 終止 | 優雅關閉請求 (kill 預設) |
| SIGCHLD | 17 | 忽略 | 子進程狀態變化,父進程 wait() |
| SIGPIPE | 13 | 終止 | 管道/Socket 讀取端關閉寫入 |