Windows XP Windows 7 Windows 2003 Windows Vista Windows 綜合教學 Linux 系統教學
Windows 10 Windows 8 Windows 2008 Windows NT Windows Server 電腦軟體教學

Linux 核心原始碼深度解析:信號量實作機制

編輯:Linux 系統常見問題解答

信號量是 Linux 核心同步機制的核心之一,與自旋鎖互補:自旋鎖適用短暫臨界區且不允許睡眠;信號量適用較長操作、允許進程睡眠等待、支援計數 >1 (多實例資源)。本文基於 Linux 2.6.24 核心原始碼,深度解析 struct semaphore 資料結構、down()/up() 實作邏輯與等待佇列喚醒機制。

一、核心資料結構

struct semaphore {
    atomic_t count;        // 共享計數器 (可用資源數)
    int sleepers;          // 正在等待的睡眠進程數
    wait_queue_head_t wait; // 等待佇列頭
};
  • count:初始化為資源實例數 (1 為互斥鎖)。down() 遞減,up() 遞增。
  • sleepers:追蹤等待佇列中進程數,用於避免驚群效應。
  • wait:標準核心等待佇列,掛載等待進程。

二、API 介面與使用模式

static inline void down(struct semaphore *sem);   // 獲取,失敗則睡眠
static inline void up(struct semaphore *sem);     // 釋放,喚醒等待者

// 使用模式
down(sem);
// 臨界區操作
up(sem);

核心保證同時進入臨界區的進程數 ≤ 初始 count。獲取失敗的進程進入 不可中斷睡眠 (TASK_UNINTERRUPTIBLE),只能由 up() 喚醒。

三、down() 實作深度追蹤

1. 快速路徑 (Inline Assembly)

static inline void down(struct semaphore *sem) {
    might_sleep();
    __asm__ __volatile__(
        "# atomic down operation\n\t"
        LOCK_PREFIX "decl %0\n\t"     // 原子遞減 count (LOCK_PREFIX 鎖總線)
        "jns 2f\n"                    // 結果 ≥ 0 直接跳轉到標號 2 (成功返回)
        "\tlea %0,%%eax\n\t"          // 將 sem->count 位址載入 eax
        "call __down_failed\n"        // 否則呼叫失敗處理
        "2:"
        : "+m" (sem->count)           // 輸出: 記憶體運算元
        : : "memory", "ax"            // 破壞: memory, eax
    );
}

關鍵點:LOCK_PREFIX 確保 decl 原子性;jns (Jump if Not Sign) 檢查遞減後是否 ≥ 0。

2. 失敗處理 __down_failed → __down


// __down_failed 為彈簧板,保存寄存器後呼叫 __down
fastcall void __sched __down(struct semaphore *sem) {
    struct task_struct *tsk = current;
    DECLARE_WAITQUEUE(wait, tsk);
    unsigned long flags;

    tsk->state = TASK_UNINTERRUPTIBLE;      // 設為不可中斷睡眠
    spin_lock_irqsave(&sem->wait.lock, flags); // 取得等待佇列自旋鎖
    add_wait_queue_exclusive_locked(&sem->wait, &wait); // 加入等待佇列 (獨佔)
    sem->sleepers++;

    for (;;) {
        int sleepers = sem->sleepers;
        // 嘗試將 sleepers-1 加回 count (扣除自己)
        if (!atomic_add_negative(sleepers - 1, &sem->count)) {
            sem->sleepers = 0;
            break;  // 獲取成功,跳出迴圈
        }
        sem->sleepers = 1;  // 重置,僅自己在等待

        spin_unlock_irqrestore(&sem->wait.lock, flags);
        schedule();         // 主動讓出 CPU,進入睡眠
        spin_lock_irqsave(&sem->wait.lock, flags);
        tsk->state = TASK_UNINTERRUPTIBLE;
    }

    remove_wait_queue_locked(&sem->wait, &wait);
    wake_up_locked(&sem->wait); // 喚醒下一個等待者 (避免驚群)
    spin_unlock_irqrestore(&sem->wait.lock, flags);
    tsk->state = TASK_RUNNING;
}

關鍵機制解析

  1. 雙重檢查迴圈:先將 sleepers-1 加回 count,若結果非負表示有資源可用,直接獲取;否則重置 sleepers=1 睡眠。
  2. 不可中斷睡眠:TASK_UNINTERRUPTIBLE 確保進程不會被訊號中斷,只能由 up() 喚醒。
  3. 獨佔等待佇列:add_wait_queue_exclusive_locked 標記為獨佔,喚醒時僅喚醒一個進程。
  4. 避免驚群:wake_up_locked 僅喚醒佇列第一個獨佔進程,而非廣播喚醒所有。
  5. 自旋鎖保護佇列操作:spin_lock_irqsave 保護等待佇列操作的原子性。

四、up() 實作 (釋放與喚醒)


static inline void up(struct semaphore *sem) {
    __asm__ __volatile__(
        "# atomic up operation\n\t"
        LOCK_PREFIX "incl %0\n\t"     // 原子遞增 count
        "jns 1f\n"                    // 遞增前 count ≥ 0 (無人等待) 直接返回
        "\tlea %0,%%eax\n\t"
        "call __up_wakeup\n"          // 否則喚醒等待者
        "1:"
        : "+m" (sem->count)
        : : "memory", "ax"
    );
}

void __up_wakeup(struct semaphore *sem) {
    unsigned long flags;
    spin_lock_irqsave(&sem->wait.lock, flags);
    if (sem->sleepers > 0)
        wake_up_locked(&sem->wait);   // 喚醒第一個獨佔等待者
    spin_unlock_irqrestore(&sem->wait.lock, flags);
}

jns 檢查遞增前 count 是否 ≥ 0 (即原本 ≥ -1),若無進程等待 (count ≥ 0) 則無需喚醒。

五、信號量 vs 自旋鎖 選型指南

特性自旋鎖信號量
等待方式忙碌等待 (佔用 CPU)睡眠等待 (釋放 CPU)
適用場景極短臨界區、中斷上下文較長操作、可能睡眠、程序上下文
計數器二元 (0/1)計數型 (≥1)
中斷安全是 (需 _irqsave 變體)否 (不可在中斷上下文使用)
Copyright © Windows教程網 All Rights Reserved