自旋鎖是一種忙等待同步原語,適用於臨界區極短、競爭不激烈、不可睡眠(如中斷上下文)的場景。本文分析一個基於 GCC 内建函數與 x86 内聯彙編的自旋鎖實現,涵蓋可重入支持、CPU 讓出優化、信號處理死鎖風險。
typedef struct {
volatile unsigned long tid; // 持有鎖的線程 ID
volatile unsigned long lock; // 鎖標誌 (0/1)
volatile unsigned long recur; // 重入計數
} spin_lock_t;
#define atomic_test_and_set(a) __sync_lock_test_and_set(a, 1) // 原子交換並返回舊值
#define atomic_clear(a) __sync_lock_release(a) // 釋放語義存儲 0
__sync_lock_test_and_set:完整內存屏障 + 原子交換,獲取鎖語義。__sync_lock_release:釋放語義屏障 + 存儲 0,釋放鎖語義。
// 原子比較交換獲取鎖
inline static unsigned long atomic_test_and_set(volatile unsigned long* a) {
const unsigned long n = 1, old = 0;
unsigned long ret;
__asm__ __volatile__(
"lock; cmpxchgl %3, %1;"
: "=a"(ret)
: "m"(*a), "a"(old), "r"(n)
: "memory", "cc"
);
return ret; // 返回舊值,0=成功獲取,1=已被持有
}
// 原子交換釋放鎖
inline static void atomic_clear(volatile unsigned long* a) {
unsigned long ret, n = 0;
__asm__ __volatile__(
"lock; xchgl %0, %1"
: "=r"(ret)
: "m"(*a), "0"(n)
: "memory"
);
}
lock 前綴:總線鎖定/緩存鎖定,保證多核原子性。cmpxchgl:比較 EAX 與內存,相等則存入新值並置 ZF=1,否則讀入內存值到 EAX。xchgl:原子交換寄存器與內存。
inline static void spin_lock_init(spin_lock_t* lk) {
lk->tid = 0; lk->lock = 0; lk->recur = 0;
}
#define SPIN_YIELD_INTERVAL 63U // 每 64 次自旋調用一次 sched_yield()
inline static int spin_lock_lock(spin_lock_t* lk) {
pid_t now = syscall(SYS_gettid); // 獲取內核線程 ID (TID)
int c = 0;
for (;;) {
if (!atomic_test_and_set(&lk->lock)) { // 成功獲取
lk->tid = now;
lk->recur = 1;
return 0;
}
if (lk->tid == now) // 可重入:同一線程再次加鎖
return ++lk->recur;
if (!(++c & SPIN_YIELD_INTERVAL))
sched_yield(); // 主動讓出 CPU,減少總線爭用
}
}
inline static int spin_lock_unlock(spin_lock_t* lk) {
unsigned long now = (unsigned long)syscall(SYS_gettid);
if (lk->tid != now) return -1; // 非持有者嘗試解鎖
if (--lk->recur == 0) { // 最後一層重入釋放
lk->tid = 0;
atomic_clear(&lk->lock);
}
return 0;
}
tid 與 recur 計數,同一線程多次加鎖只需原子操作一次。SPIN_YIELD_INTERVAL=63 使 sched_yield() 每 64 次循環調用一次,平衡自旋開銷與調度延遲。lock 指令提供全屏障,確保臨界區內存操作不被重排到鎖外。syscall(SYS_gettid) 而非 getpid() 或 pthread_self(),因後兩者在線程組/用戶態庫中可能不唯一或開銷大。
// 註釋中的警告:
// 如果在信號處理函數中使用該鎖,可能會死鎖。
// 場景:
// 1. 主線程持有鎖,進入臨界區
// 2. 信號到達,中斷主線程執行信號處理函數
// 3. 信號處理函數嘗試獲取同一把鎖 → 自旋等待
// 4. 主線程無法繼續釋放鎖 → 永久死鎖
緩解方案:
sig_atomic_t、無鎖環形緩衝區、或將信號轉為管道通知專用線程處理。pthread_sigmask 在臨界區屏蔽信號(但會延遲信號遞送)。| 特性 | 自旋鎖 | pthread_mutex |
|---|---|---|
| 等待方式 | 忙等待 (CPU 佔用) | 阻塞睡眠 (上下文切換) |
| 適用臨界區 | 極短 (< 1µs) | 任意長度 |
| 中斷上下文 | 可用 (無睡眠) | 不可用 |
| 可重入 | 需手動實現 | PTHREAD_MUTEX_RECURSIVE |
| 優先級反轉 | 無保護 | 支持優先級繼承/天花板 |
這個自旋鎖實現展示了原子操作抽象、架構相關優化、可重入邏輯、退讓策略等核心要素。但信號上下文死鎖風險是其致命短板,生產環境優先考慮 pthread_mutex、pthread_rwlock、無鎖數據結構或更高層並發模式(Actor、CSP)。僅在內核模塊、實時系統、極低延遲庫等專用場景謹慎使用,並嚴格審計信號處理路徑。