信號量是 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:標準核心等待佇列,掛載等待進程。static inline void down(struct semaphore *sem); // 獲取,失敗則睡眠
static inline void up(struct semaphore *sem); // 釋放,喚醒等待者
// 使用模式
down(sem);
// 臨界區操作
up(sem);
核心保證同時進入臨界區的進程數 ≤ 初始 count。獲取失敗的進程進入 不可中斷睡眠 (TASK_UNINTERRUPTIBLE),只能由 up() 喚醒。
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。
// __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;
}
sleepers-1 加回 count,若結果非負表示有資源可用,直接獲取;否則重置 sleepers=1 睡眠。TASK_UNINTERRUPTIBLE 確保進程不會被訊號中斷,只能由 up() 喚醒。add_wait_queue_exclusive_locked 標記為獨佔,喚醒時僅喚醒一個進程。wake_up_locked 僅喚醒佇列第一個獨佔進程,而非廣播喚醒所有。spin_lock_irqsave 保護等待佇列操作的原子性。
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) 則無需喚醒。
| 特性 | 自旋鎖 | 信號量 |
|---|---|---|
| 等待方式 | 忙碌等待 (佔用 CPU) | 睡眠等待 (釋放 CPU) |
| 適用場景 | 極短臨界區、中斷上下文 | 較長操作、可能睡眠、程序上下文 |
| 計數器 | 二元 (0/1) | 計數型 (≥1) |
| 中斷安全 | 是 (需 _irqsave 變體) | 否 (不可在中斷上下文使用) |