kill 不只是「殺掉」#
kill(1) 命令名字會誤導人。它真正的功能是「發送 signal」,殺掉只是其中一種行為(對應 SIGKILL 或 SIGTERM)。kill 也能用來 reload(SIGHUP)、暫停(SIGSTOP)、自訂通知(SIGUSR1)。
kill -l # 列出所有 signal 名稱
kill -TERM 1234 # 對 PID 1234 發 SIGTERM
kill -15 1234 # 同上,數字形式
kill -KILL 1234 # 強制終止
kill -9 1234 # 同上
kill -HUP $(pgrep nginx) # 對 nginx 發 reload
kill -0 1234 # 不發 signal,只檢查 PID 是否存在 / 是否有權限- 預設不指定 signal 時送 SIGTERM
- 負數 PID 表示 process group:
kill -TERM -1234 kill -0是常見的「健康檢查」技巧
kill、killall、pkill 三兄弟#
- kill:對特定 PID 發 signal
- killall:依「執行檔名」批次發送
- pkill:依 pattern 批次發送,與 pgrep 對偶
killall nginx
pkill -TERM -f 'python app.py'
pgrep -f 'python app.py'killall 在不同 OS 行為不一樣。Solaris 上 killall 是「殺掉所有 process」,差點把機器送走。Linux 上是按名字,但仍要小心。
Signal Handler 的標準寫法(C)#
C 程式裝 signal handler 推薦用 sigaction(2),不要用 signal(2)。後者語意不一致,跨平台行為差異大。
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
static volatile sig_atomic_t got_term = 0;
static void on_term(int sig) {
got_term = 1;
}
int main(void) {
struct sigaction sa = {0};
sa.sa_handler = on_term;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGTERM, &sa, NULL) < 0) { perror("sigaction"); return 1; }
if (sigaction(SIGINT, &sa, NULL) < 0) { perror("sigaction"); return 1; }
while (!got_term) {
pause(); /* 等任意 signal */
}
write(1, "graceful exit\n", 14);
return 0;
}幾個關鍵點:
- 用
sig_atomic_t與volatile標示在 handler 與 main 之間共享的旗標 - Handler 內只能呼叫 async-signal-safe 函式(man 7 signal-safety 有完整清單)
SA_RESTART讓被中斷的系統呼叫自動重試,避免到處檢查 EINTR
在 Handler 裡能做什麼、不能做什麼#
Signal handler 是非同步打斷程式碼,限制比一般函式嚴格得多。
- 可:write(2)、_exit(2)、原子變數操作、kill(2) 自己
- 不可:printf、malloc、絕大多數 stdio、pthread_mutex_lock
實務上常見模式:
- Handler 只設一個旗標
- Main loop 檢查旗標,做真正的清理工作
Signal Mask 與 Pending Signal#
Process 可以暫時 block 某些 signal,被 block 的 signal 進入 pending 集合,解除 block 後一次送達(同種 signal 只保留一次,real-time signal 例外)。
- 用 sigprocmask(2) 操作 mask
- 用 sigpending(2) 查 pending 集合
- 多執行緒程式應該用 pthread_sigmask(3)
寫 server 時典型做法:在主執行緒 block 所有 signal,開一條 dedicated thread 用 sigwait(3) 同步取出處理。這樣完全避開 async-signal-safety 的限制。
SIGCHLD 與 Reap#
SIGCHLD 是 child 狀態改變時送來的訊號。在 init 或會 fork 的程式中,必須處理。
static void on_sigchld(int sig) {
int saved = errno;
while (waitpid(-1, NULL, WNOHANG) > 0) { /* reap */ }
errno = saved;
}- 用 WNOHANG 才不會卡住
- Loop 是必須的,因為多個 child 同時結束時 SIGCHLD 會被合併成一次
Shell 層級:trap#
不寫 C 也能裝 handler,shell 的 trap 就是 shell 等級的 signal handler。
#!/bin/bash
cleanup() {
echo cleaning up
kill -TERM "$child" 2>/dev/null
wait "$child"
}
trap cleanup TERM INT
./long-running &
child=$!
wait "$child"這個 pattern 對 shell entrypoint 很有用,能把 SIGTERM 從 shell 轉發給真正的工作 process。
延伸閱讀#
- man 1 kill
- man 2 sigaction
- man 7 signal-safety
- man 3 sigwait