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

Linux 信號編程:sigsuspend 原子性阻塞與信號處理機制深度剖析

     編輯:關於C語言

前言:sigsuspend——信號同步的「核武器」

在信號驅動的並發編程中,sigsuspend 是實現「原子性等待信號」的唯一標準接口。它將「設置臨時信號掩碼」與「掛起進程」合併為**單一原子操作**,完美解決了「檢查條件 → 設置掩碼 → 睡眠」竞態窗口。本文深度解析原型、語義、內核實現、返回值陷阱、經典用法模式與 Stevens 經典教材中的時序細節。

一、 函數原型與核心語義

#include <signal.h>
int sigsuspend(const sigset_t *mask);

原子操作三步曲 (不可分割):

  1. 將進程當前信號掩碼**原子替換**為 mask 指向的集合 (即「開放」mask 中的信號)。
  2. 進程掛起,**等待信號送達**。
  3. 信號處理函數返回後,**自動恢復**原掩碼,sigsuspend 返回。

二、 返回值與錯誤碼:永遠失敗的「成功」

  • **無成功返回**:「成功」意味著被信號喚醒並處理完,此時返回 -1,errno = EINTR。
  • 若信號動作是終止進程 (如 SIGKILL),sigsuspend **永不返回**。
  • 不可被忽略的信號 (SIGKILL/SIGSTOP) 無法被 mask 阻塞,內核強制放行。

三、 經典用法模式:關鍵區保護

sigset_t newmask, oldmask, zeromask;

sigemptyset(&zeromask);
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);  // 要阻塞的信號

// 1. 阻塞 SIGINT,保存舊掩碼
sigprocmask(SIG_BLOCK, &newmask, &oldmask);

// 2. 關鍵區代碼 (SIGINT 不會打斷)
critical_section();

 // 3. 原子性:開放所有信號(zeromask)並等待
//    若 SIGINT 在此前已遞達(pending),會立即喚醒
sigsuspend(&zeromask);  // 返回時 errno=EINTR,掩碼已恢復為 oldmask

// 4. 恢復原始掩碼 (解除 SIGINT 阻塞)
sigprocmask(SIG_SETMASK, &oldmask, NULL);

四、 內核視角:Linux 實現邏輯 (參考 ULK)


// 簡化版 sys_sigsuspend 邏輯
mask &= ~(sigmask(SIGKILL) | sigmask(SIGSTOP)); // 強制開放不可阻塞信號
spin_lock_irq(&current->sigmask_lock);
saveset = current->blocked;           // 保存舊掩碼
siginitset(&current->blocked, mask);  // 設置新掩碼
recalc_sigpending(current);
spin_unlock_irq(&current->sigmask_lock);

regs->eax = -EINTR;  // 預置返回值

while (1) {
    current->state = TASK_INTERRUPTIBLE;
    schedule();                     // 進程睡眠,調度器切換
    if (do_signal(regs, &saveset))  // 喚醒後嘗試投遞信號
        return -EINTR;              // 信號處理函數執行完,返回用戶態
}

關鍵時序:

  1. 掛起前**已恢復掩碼** (釋放 saveset)。
  2. 調度器選中進程 → do_signal 檢測 pending 信號 → 執行處理函數。
  3. 處理函數返回 → do_signal 返回真 → sys_sigsuspend 返回 -EINTR。

這解釋了 Stevens 書籍中「信號處理函數先返回,sigsuspend 再返回」的現象:**處理函數執行在原子操作的「喚醒後、返回前」階段**。

五、 完整可運行演示代碼

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sig_int(int signo) {
    printf("\n在信號處理函數中: SIGINT captured\n");
}

void pr_mask(const char *str) {
    sigset_t set;
    sigprocmask(0, NULL, &set);
    printf("%s", str);
    if (sigismember(&set, SIGINT)) printf(" SIGINT");
    if (sigismember(&set, SIGTSTP)) printf(" SIGTSTP");
    if (sigismember(&set, SIGTERM)) printf(" SIGTERM");
    printf("\n");
}

int main() {
    sigset_t newmask, oldmask, zeromask;
    signal(SIGINT, sig_int);
    sigemptyset(&zeromask);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGINT);
    
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);
    pr_mask("關鍵區內掩碼: ");
    
    sigsuspend(&zeromask);  // 等待 SIGINT
    pr_mask("sigsuspend返回後掩碼: ");
    
    sigprocmask(SIG_SETMASK, &oldmask, NULL);
    pr_mask("最終掩碼: ");
    return 0;
}

運行輸出 (按下 Ctrl+C):

關鍵區內掩碼: SIGINT
在信號處理函數中: SIGINT captured
sigsuspend返回後掩碼: SIGINT
最終掩碼: 

六、 避坑指南

  1. 不要在信號處理函數中調用非 async-signal-safe 函數 (如 printf/malloc/lock),演示代碼僅為教學。
  2. mask 必須包含要等待的信號:若 mask` 阻塞了目標信號,`sigsuspend` 永不被喚醒。
  3. 配合 sigprocmask 成對使用:阻塞 → 關鍵區 → sigsuspend(開放) → 恢復。
  4. 多線程用 pthread_sigmask + sigwait,進程級 sigsuspend 影響全進程。

七、 總結

sigsuspend 是信號編程中「等待條件滿足」的**原語級工具**。理解其「設置掩碼-睡眠-恢復掩碼」原子性、內核掩碼恢復先於信號處理函數執行的時序、EINTR 語義,是編寫正確、健壯信號驅動程序的前提。掌握它,就掌握了 Unix 並發編程的靈魂。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved