首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C 語言進程間通訊:信號機制實作詳解 (kill/signal/sigaction)

     編輯:關於C語言

C 語言進程間通訊:信號機制實作詳解

信號是 Unix/Linux 進程間異步通訊的基礎機制,類似軟體中斷。本文從 signal() 歷史介面進階到現代 sigaction(),結合 kill() 發送、fork() 父子進程協作,完整演示信號發送、捕捉、處理流程,並分析 signal() 的可重入性與競態條件缺陷。

核心系統呼叫

1. kill() - 發送信號

#include 
int kill(pid_t pid, int signo);
  • pid > 0:發送給指定 PID 進程
  • pid == 0:發送給同進程組所有進程
  • pid < -1:發送給進程組 ID 為 -pid 的所有進程
  • signo:信號編號 (如 SIGUSR1, SIGALRM, SIGTERM)
  • 成功返回 0,失敗返回 -1 (設定 errno)

2. signal() - 舊式信號處理註冊 (不推薦新代碼使用)

void (*signal(int signo, void (*func)(int)))(int);
  • 註冊 func 為 signo 的處理函數
  • 回傳前一個處理函數指標
  • 缺陷:處理函數執行時系統可能重置為預設動作 (Unix V7 行為)、不可移植、不支援信號遮罩、存在競態條件

3. sigaction() - 現代標準介面 (POSIX.1 推薦)

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 等

實作範例:父進程發送 SIGALRM,子進程捕捉處理

版本 A:使用 signal() (教學示範,含缺陷)

#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;
}

版本 B:使用 sigaction() (生產環境標準)

#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)

signal() 競態條件實測與分析

若子進程註冊處理函數 **晚於** 父進程發送信號:

// 子進程調換順序
printf("子進程 %d 等待信號...\n", getpid());  // 先印訊息
signal(SIGALRM, sig_handler);                 // 再註冊 (延遲)
pause();

結果:子進程直接退出,**未執行處理函數**,父進程 wait() 正常返回。

原因:父進程 kill() 發送信號時,子進程尚未完成 signal() 註冊,信號依預設動作處理 (SIGALRM 預設為終止進程)。

sigaction() 同樣存在此問題,解決方案:

  1. 父進程 sleep() / usleep() 延遲發送 (不可靠)
  2. 雙向管道/Eventfd 同步:子進程註冊完成後通知父進程
  3. 使用 signalfd() (Linux 專用) 將信號轉為檔案描述符讀取

信號處理函數編寫鐵律 (Async-Signal-Safe)

**只能呼叫 Async-Signal-Safe 函數** (man 7 signal-safety):

安全函數不安全函數 (禁用)
write, read, open, closeprintf, malloc, free, fopen
sigaction, sigprocmask, killpthread_mutex_lock, pthread_cond_wait
_exit, abortstd::cout, std::string, 任何 C++ STL
getpid, getuid, time非重入函數、靜態緩衝區函數

最佳實踐:處理函數僅設定 volatile sig_atomic_t 旗標,主迴輪檢查旗標執行複雜邏輯。

常用信號一覽表

總結

  • 新代碼**務必使用 sigaction()**,避免 signal() 可移植性與可靠性問題
  • 信號處理函數**極簡原則**:只修改 volatile sig_atomic_t 變數
  • 父子進程同步**不可依賴 sleep()**,使用管道/Eventfd 明確同步
  • 複雜 IPC 需求優先考慮 **Socket/共享記憶體/訊息佇列**,信號適合簡單通知
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
信號編號預設動作典型用途
SIGHUP1終止終端掛起、守護進程重載配置
SIGINT2終止Ctrl+C 中斷
SIGQUIT3核心傾印Ctrl+\ 退出並生成 core
SIGKILL9終止**不可捕捉/阻塞**,強制殺死
SIGUSR1/210/12終止**用戶自定義**,進程間協作首選
SIGALRM14終止alarm() 定時器
SIGTERM15終止優雅關閉請求 (kill 預設)
SIGCHLD17忽略子進程狀態變化,父進程 wait()
SIGPIPE13終止管道/Socket 讀取端關閉寫入