---
# System prepended metadata

title: 第十四講：多核處理器和 spinlock
tags: [Linux]

---

# 第十四講：多核處理器和 spinlock

> 本筆記僅為個人紀錄，相關教材之 Copyright 為[jserv](http://wiki.csie.ncku.edu.tw/User/jserv)及其他相關作者所有
* 直播：==[Linux 核心設計：多核處理器和 spinlock - 2018/12/27](https://www.youtube.com/watch?v=aYdQ4AtNVro)==
* 詳細共筆：[Linux 核心設計：多核處理器和 spinlock](https://hackmd.io/@sysprog/multicore-locks?type=view)
* 主要參考資料：
    *   [Concurrent Programming](https://www.cs.cmu.edu/afs/cs/academic/class/15213-f19/www/lectures/24-concprog.pdf)
    *   [Symmetric Multi-Processing](https://linux-kernel-labs.github.io/refs/heads/master/lectures/smp.html)
    *   [Synchronization: Basics](https://www.cs.cmu.edu/~213/lectures/24-sync-basic.pdf)
    *   [Synchronization: Advanced](https://www.cs.cmu.edu/~213/lectures/25-sync-advanced.pdf)
    *   [Thread-Level Parallelism](http://www.cs.cmu.edu/~213/lectures/26-parallelism.pdf)
    *   [Linux kernel synchronization](http://www.cs.unc.edu/~porter/courses/cse506/s16/slides/sync.pdf)
    *   [Synchronization primitives in the Linux kernel. Part 1](https://0xax.gitbooks.io/linux-insides/SyncPrim/linux-sync-1.html)
    *   [Kernels and Locking](https://retis.santannapisa.it/luca/KernelProgramming/Slides/kernel_locking.pdf)
    *   [Mutual exclusion, synchronization,communicationin shared memoryOperating systems](https://www.mit.bme.hu/eng/system/files/oktatas/targyak/8699/opre_slides12_mutex_en_1.pdf)

---

## 引言

本課將深入探討 Linux 核心中最基礎也最關鍵的同步機制 Spinlock。從並發程式設計的基本挑戰出發，層層剖析從高階的 Mutex 與 Semaphore 概念，到 Spinlock 在多核處理器 (Multi-core Processor) 架構下的硬體實作細節、效能瓶頸，以及其與中斷處理和核心搶佔 (Kernel Preemption) 的複雜互動。

---

## 自我檢查清單

在開始之前，可以先評估一下自己對以下問題的掌握程度。這些問題涵蓋了本課程的核心議題，在學習完本筆記後，應該能對這些問題有清晰的理解。

- [x] **Atomic Context**：知道 Linux 核心的 atomic context 出現於哪些場合嗎？
- [x] **Busy-Waiting**：教科書常說 Spinlock 是一種 busy-waiting，那它到底在「忙」什麼？對應於現代處理器，它實際做了哪些事？
- [ ] **Disable Interrupt**：Spinlock 的實作可能涉及停用中斷 (disable interrupt)，這對作業系統的 latency 和 scalability 有何影響？
- [ ] **Livelock**：除了 Deadlock，Spinlock 還可能導致 Livelock，你知道為什麼嗎？
- [x] **Kernel Preemption**：為了提升 Kernel Preemption，核心開發者致力於避免停用中斷。在這種情況下，該如何確保共享資料的正確性？(提示：`preempt_count`)
- [ ] **Sleeping Spinlocks**：在 `PREEMPT_RT` 環境下，Spinlock 會變成可以「睡眠」(sleeping) 的鎖，那麼 Critical Section 的概念還適用嗎？
- [ ] **Threaded IRQ**：什麼是 Threaded IRQ？它如何對應到現代處理器的中斷處理機制？
- [ ] **IRQ 分類**：hard IRQ 和 soft IRQ (或稱 Bottom Half, BH) 的關聯為何？`ksoftirqd` 核心執行緒的運作機制是什麼？

> 延伸閱讀：
> *   [第十講：中斷處理和現代架構考量](https://hackmd.io/@Jaychao2099/Linux-kernel-10)
> *   [第十三講：淺談同步機制](https://hackmd.io/@Jaychao2099/Linux-kernel-13)

---

## Symmetric Multi-Processing (SMP) 的衝擊：重新思考 Critical Section 與互斥

並發 (Concurrency) 程式設計的核心挑戰在於管理多個執行單元對共享資源的存取。當多個 thread 或 process 同時執行，其指令序列的交錯 (interleaving) 可能會導致非預期的結果，這種情況稱為 **Race Condition (競爭條件)**。

**SMP (對稱多處理)** 架構的普及，是 Linux 核心同步機制變得複雜的根本原因。在單核時代，同步主要處理的是 **process 與 interrupt 之間** 的 race condition；而在多核時代，還必須處理 **多個 CPU 核心之間** 真正的並行執行，使得舊有的同步方法不再足夠。

### 核心中並行的來源

即便在單核心系統上，並行問題依然存在，主要來自兩種形式的「偽並行」(pseudo-concurrency)：

1.  **中斷搶佔 (Interrupt Preemption)**：一個硬體中斷可以在任何時刻發生，打斷目前正在執行的核心程式碼，轉而執行中斷服務常式 (ISR)。如果 ISR 和被中斷的程式碼存取了相同的共享資料，就會產生 Race Condition。
2.  **行程搶佔 (Process Preemption)**：在啟用核心搶佔 (Kernel Preemption) 的系統中，一個高優先權的 process 可以中斷正在核心態執行的低優先權 process，引發 process 之間的並行問題。

在單核心的世界裡，上述問題有一個相對簡單的解法：**停用本地中斷 (disabling local interrupts)**。透過 `local_irq_disable()` 這類指令，可以確保在 Critical Section 執行期間，不會有任何中斷或排程事件發生，從而達成互斥。

然而，SMP 架構的出現引入了 **真正的並行 (true parallelism)**。多個 CPU 核心可以在 **同一時刻** 獨立執行不同的指令。意味著 **在 CPU 0 上停用中斷，完全無法阻止 CPU 1 存取同一個共享記憶體位址**。舊有的同步方法瞬間失效，核心開發者必須面對一個全新的、更複雜的跨核心同步挑戰。
<center><img src="https://hackmd.io/_uploads/B1AGWEBvll.png" alt="image" width="75%" /></center>

### SMP 如何破壞簡單的同步

一個典型的例子是計數器遞增操作：
```c=
volatile long cnt = 0; // 全域計數器

void *thread(void *vargp) {
    long max = *((long *)vargp);
    for (long i = 0; i < max; i++) cnt++;    // 遞增
    return NULL;
}

int main(int argc, char **argv) {
    long max;
    pthread_t tid1, tid2;

    max = atoi(argv[1]);
    pthread_create(&tid1, NULL, thread, &max);    // thread A
    pthread_create(&tid2, NULL, thread, &max);    // thread B
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    if (cnt != (2 * max)) printf("Wrong! cnt=%ld\n", cnt);
    else                  printf("Correct! cnt=%ld\n", cnt);
    exit(0);
}
```
遞增操作在組合語言層面，至少包含三個獨立操作：
1.  **讀取 (Load)**：從記憶體中讀取 `cnt` 的目前值到處理器的暫存器。
2.  **更新 (Update)**：將暫存器中的值 + 1。
3.  **寫入 (Store)**：將暫存器中的新值寫回記憶體。
```asm=
thread：      # 前置作業    記為 H (head)
        mov     rcx, QWORD PTR [rdi]
        test    rcx, rcx
        jle     .L2
        xor     edx, edx
.L3:
        mov     rax, QWORD PTR cnt[rip]   # 讀取 cnt    記為 L (load)
        add     rax, 1                    # cnt++      記為 U (update)
        mov     QWORD PTR cnt[rip], rax   # 寫入 cnt    記為 S (store)
        
              # 後續作業    記為 T (tail)
        add     rdx, 1    # i++
        cmp     rcx, rdx
        jne     .L3
.L2:
        ...
```
假設兩個執行緒 (Thread A, Thread B) 同時執行 `cnt++`，而 `cnt` 初始值為 0。一種可能的交錯順序如下：
1.  Thread A **讀取** `cnt` (值為 0) 到 **自己的暫存器**。
2.  系統排程切換，Thread B 開始執行。
3.  Thread B **讀取** `cnt` (值仍為 0)，進行 **更新** (暫存器變為 1)，並 **寫回** 記憶體。此時 `cnt` 的值為 1。
4.  系統排程再次切換回 Thread A。
5.  Thread A 基於它 **先前讀取的舊值 0** 進行 **更新** (暫存器變為 1)，並 **寫回** 記憶體。`cnt` 的值最終仍為 1。
![](https://linux-kernel-labs.github.io/refs/heads/master/_images/ditaa-ddd14be50300088958e86912bc5f396797634a3a.png)
雖然兩個 thread 都執行了遞增操作，但最終結果卻是 1 而非預期的 2，這就是 Data Race 導致的錯誤。

實驗也顯示，結果不一定會正確：
```shell
$ ./badcnt 10000
Correct! cnt=20000
$ ./badcnt 10000
Wrong! cnt=10238
```

### Critical Section 關鍵區段

為了防止 Data Race，需要識別出程式中存取共享資源的程式碼區段，並確保其執行的原子性 (atomicity)。這段程式碼被稱為 **Critical Section (CS, 關鍵區段)**。

#### Progress Graph (進度圖) 與不安全區域 (Unsafe Region)
將此概念視覺化。進度圖是一個 N 維空間 (N 為 thread 數量)，每個軸代表一個 thread 的執行進度。圖中的每一個點代表系統的一個狀態，而一條從左下角到右上角的軌跡線 (trajectory) 則代表一種可能的指令交錯順序。

<center><img src="https://hackmd.io/_uploads/S1Jj7XBPll.png" alt="image" width="80%" /></center>

當兩個 thread 的 Critical Section (如 `L`, `U`, `S` 操作) 發生交錯時，軌跡線會進入圖中的「不安全區域」(Unsafe Region)。任何進入此區域的執行路徑都可能導致錯誤的結果。<center><img src="https://i.imgur.com/EU3fibV.png" alt="image" width="80%" /></center>

為了保證程式的正確性，必須採用一種機制，強制所有執行軌跡線都繞開這個不安全區域。這種確保在任何時間點，最多只有一個 thread 能進入 Critical Section 的機制，稱為 **Mutual Exclusion (互斥)**。
![image](https://hackmd.io/_uploads/S1OVsMUDxl.png)

> 延伸閱讀：
> *   [Symmetric Multi-Processing](https://linux-kernel-labs.github.io/refs/heads/master/lectures/smp.html)
> *   [Synchronization: Basics](https://www.cs.cmu.edu/~213/lectures/24-sync-basic.pdf)
> *   [Synchronization: Advanced](https://www.cs.cmu.edu/~213/lectures/25-sync-advanced.pdf)

---

## 高階鎖定機制：Semaphore vs. Mutex

在 Linux 核心中，Semaphore 和 Mutex 是兩種常見用來實現互斥的高階同步工具，但它們在概念和用途上存在根本差異。
*   **Mutex (Mutual Exclusion lock, 互斥鎖)**:
    *   **核心概念**：強調 **「擁有權」(ownership)**。一個 thread 取得了 Mutex，就必須由同一個 thread 來釋放。它像一把鑰匙，誰鎖上了門，就必須由誰來開鎖。
    *   **主要用途**：保護一段程式碼 (Critical Section)，確保只有一個 thread 可以執行它，主要用於解決資源的獨佔存取問題。
    *   **實作考量**：一個設計完善的 Mutex 必須處理 **Priority Inversion (優先權反轉)** 的問題。
*   **Semaphore (號誌)**:
    *   **核心概念**：強調 **「訊號傳遞」 (Signaling)**，是一種更通用的同步機制，本質上是一個計數器，用於控制能同時存取某個資源的 thread 數量。它不強調擁有權，一個 thread 可以發出訊號 (V 操作，增加計數)，而另一個 thread 可以等待訊號 (P 操作，減少計數)。
    *   **主要用途**：常用於解決 **「生產者-消費者」** 問題，或者控制對一組有限資源的存取。例如，一個計數為 N 的 Semaphore 允許最多 N 個 thread 同時存取資源。

> [!Note]**注意**：
> 一個特殊的 Semaphore 稱為 **Binary Semaphore**，其計數值僅為 0 或 1。在某些作業系統的實作中，**Binary Semaphore** 的程式碼可能與 **Mutex** 非常相似，甚至共用。儘管如此，它們在 **概念上** 依然存在「持有者」與「訊號傳遞」的根本區別。

### Mutex 與優先權反轉 (Priority Inversion)

這是即時系統 (Real-time System) 中一個經典且嚴重的問題，尤其與 Mutex 相關。想像以下情境：

1.  系統中有三個任務：高優先權 (H)、中優先權 (M)、低優先權 (L)。
2.  任務 H 和 L 需要存取一個由 Mutex 保護的共享資源。
3.  **時間點 T1**：任務 L 執行並取得了 Mutex。
4.  **時間點 T2**：任務 H 準備就緒，因其優先權高於 L，它試圖搶佔 L。但由於 L 持有 H 所需的 Mutex，H 必須等待 L 釋放鎖，因此 H 進入阻塞狀態。
5.  **時間點 T3**：任務 M (不需該 Mutex) 準備就緒。因為 M 的優先權高於 L，M 搶佔了 L 的執行。

此時，系統出現了**優先權反轉**：高優先權的任務 H 正在等待低優先權的任務 L，而 L 又被中優先權的 M 所搶佔。如果 M 執行很長時間，L 就無法執行並釋放 Mutex，從而導致 H 也一直被阻塞。這使得系統的優先權規則失效，高優先權任務的即時性無法得到保證。

> [!Tip]**經典案例**：
> 1997 年的火星拓荒者號 (Mars Pathfinder) 就因 **Priority Inversion** 問題，導致系統不斷重啟。為了解決此問題，**Mutex** 的實作通常會引入 **Priority Inheritance (優先權繼承)** 或 **Priority Ceiling (優先權天花板)** 等協定。當高優先權任務等待鎖時，暫時將持有鎖的低優先權任務的優先級提升至與等待者相同，使其能盡快完成並釋放資源，從而避免被中優先權任務搶佔。

> 延伸閱讀：[第十三講：淺談同步機制 - Mutex 與 Semaphore 的區別](https://hackmd.io/@Jaychao2099/Linux-kernel-13#Mutex-%E8%88%87-Semaphore-%E7%9A%84%E5%8D%80%E5%88%A5)

---

## Spinlock：核心同步的基石

Semaphore 和 Mutex 屬於高階、可能涉及睡眠和排程器介入的鎖。然而，它們的實作本身依賴於一個更底層、更基礎的鎖定機制，特別是在多核處理器環境下，就是 **Spinlock**。

Spinlock 是一種 **忙碌等待 (busy-waiting)** 的鎖。當一個 CPU 核心嘗試獲取一個已被其他核心持有的 Spinlock 時，它不會進入睡眠狀態，而是會在一個迴圈中不斷地「旋轉」(spin)，檢查鎖是否已被釋放。

### Spinlock 的通用原則：
1.  **適用場景**：僅用於保護非常短的 Critical Section。因為在等待期間，CPU 持續空轉，浪費處理器週期。
2.  **不可睡眠**：持有 Spinlock 的程式碼路徑中，絕對不能發生會導致睡眠或排程的行為 (如呼叫 `kmalloc(GFP_KERNEL)`、等待 I/O 等)，否則可能導致 Deadlock。
3.  **與中斷的關係**：如果 Spinlock 保護的資料可能被中斷處理程序存取，那麼在獲取鎖時必須同時關閉本地中斷。

### 硬體基礎：Atomic Instructions

Spinlock 的實作仰賴於硬體提供的 **Atomic Instructions (原子指令)**。這些指令由處理器保證在執行過程中不會被其他 CPU 或中斷所打斷，是實現所有同步機制的基礎。常見的原子指令包括：
*   **Atomic Increment/Decrement**：原子地對一個整數進行 + 1 或  - 1 操作。
*   **Test-and-Set**：測試一個記憶體位置的值，並將其設定為新值，這整個過程是原子的。
*   **Compare-and-Swap (CAS)**：比較一個記憶體位置的值與一個期望值，若相符，則將其更新為一個新值。

### 效能瓶頸：Cache Coherence

在多核處理器 (SMP, Symmetric Multi-Processing) 系統中，Spinlock 的效能面臨巨大挑戰，其根源在於 **Cache Coherence (快取一致性)**。

現代 CPU 每個核心都有自己的 L1、L2 快取，並共享 L3 快取或主記憶體。為了確保所有核心看到一致的記憶體視圖，必須有一套協定來同步各個快取中的資料。最常見的協定是 **MESI**：

*   **M (Modified)**：Cache Line 內容已被修改，與主記憶體不一致，且僅存在於此核心的快取中。
*   **E (Exclusive)**：Cache Line 內容與主記憶體一致，且僅存在於此核心的快取中。
*   **S (Shared)**：Cache Line 內容與主記憶體一致，且可能存在於多個核心的快取中。
*   **I (Invalid)**：Cache Line 內容無效。

當一個核心對某個記憶體位址進行寫入時，MESI 協定會使其餘所有擁有該資料副本的核心 Cache Line 都變為 `Invalid`。這就是所謂的「寫入-失效」(write-invalidate) 協定。

這個機制對 Spinlock 造成了致命的效能問題，稱為 **Cache Line Bouncing (快取行彈跳)**：
1.  假設 $\text{CPU}_0$ 持有 Spinlock，而 $\text{CPU}_1$、$\text{CPU}_2$ ... $\text{CPU}_n$ 都在 busy-waiting。
2.  一個簡單的 Spinlock 實作會讓等待者不斷嘗試用原子指令 (如 `Test-and-Set`) 去修改鎖變數。
3.  每次 $\text{CPU}_1$ 嘗試寫入，就會觸發快取一致性協定，將 $\text{CPU}_0$、$\text{CPU}_2$ ... $\text{CPU}_n$ 中對應的 Cache Line 設為 `Invalid`。
4.  接著 $\text{CPU}_2$ 嘗試寫入，又會讓其他所有核心的 Cache Line 失效。
5.  導致存放鎖變數的 Cache Line 在不同核心的快取之間瘋狂地「彈跳」，佔用大量的記憶體匯流排頻寬，不僅拖慢了等待者，也嚴重影響了持有鎖的 $\text{CPU}_0$ 存取同一 Cache Line 中其他資料的速度。

> [!Note] **False Sharing 問題**：另一個與 Cache 相關的效能陷阱
> Cache Coherence 的基本單位是 Cache Line (通常是 64 bytes)，而不是單個變數。如果兩個不相關的變數，僅僅因為它們在記憶體中地址相近而恰好位於同一個 Cache Line 上，那麼即使 Thread A 只修改變數 `var_a`，Thread B 只修改變數 `var_b`，它們也會像競爭同一個變數一樣，觸發 Cache Line Bouncing。

為了緩解這個問題，Linux 核心採用了更優化的實作，如 **Test-and-Test-and-Set (TTS)** 鎖：
```c
// 概念性 C 程式碼
while (test_and_set(lock) != 0) {    // 外層迴圈：嘗試寫入
    do {
        // CPU 進入低功耗狀態，等待快取事件
        pause();
    } while (test(lock) <= 0);       // 內層迴圈：僅讀取
}

```
等待者首先在一個 **只讀** 的迴圈 (內層) 中旋轉。在這種模式下，多個核心可以共享 (Shared) 存放鎖變數的 Cache Line，不會引發大量的匯流排流量。只有當它們偵測到鎖可能被釋放時，才會去嘗試執行昂貴的原子 **寫入** 操作 (外層)。

> 延伸閱讀：
> *   [第十三講：淺談同步機制 - Linux spinlock 的實作與 Cache 效應](https://hackmd.io/@Jaychao2099/Linux-kernel-13#Linux-spinlock-%E7%9A%84%E5%AF%A6%E4%BD%9C%E8%88%87-Cache-%E6%95%88%E6%87%89)
> *   [從 CPU cache coherence 談 Linux spinlock 擴展議題](https://hackmd.io/@sysprog/linux-spinlock-scalability) - jserv

---

## Linux Spinlock 的演進與實作

隨著核心數量增加，Linux 的 Spinlock 實作也在不斷演進，以追求更好的 **公平性** 和 **擴展性**。

### Ticket Spinlock

為了解決簡單 Spinlock 可能存在的不公平問題 (即某個核心可能因時序巧合而一直搶不到鎖，導致飢餓)，Linux 引入了 **Ticket Spinlock**。其機制類似於銀行排隊叫號：

*   鎖結構包含兩個部分：`owner` (當前服務號碼) 和 `next` (下一個可用號碼)。
*   **獲取鎖**：核心原子性地取得一個 `next` 號碼並將 `next` + 1。
*   **等待**：該核心在本地旋轉，等待 `owner` 號碼變為自己持有的號碼。
*   **釋放鎖**：持有鎖的核心將 `owner` + 1，從而「叫」下一個號碼。

Ticket Spinlock 確保了先到先服務 (FIFO) 的公平性，但它並未解決 Cache Line Bouncing 的問題，因為所有等待者仍在同一個 `owner` 變數上旋轉。

> 延伸閱讀：[第十三講：淺談同步機制 - spinlock 核心實作](https://hackmd.io/@Jaychao2099/Linux-kernel-13#spinlock-%E6%A0%B8%E5%BF%83%E5%AF%A6%E4%BD%9C)

### Queued Spinlock (qspinlock)

為了徹底解決 Cache Line Bouncing 導致的擴展性問題，新一代的 Linux 核心 (特別是在 x86 架構上) 採用了 **Queued Spinlock** (其思想源於 MCS lock)。

*   **核心思想**：讓每個等待的 CPU 在 **自己的、核心本地的 (per-CPU) 變數** 上旋轉，而不是在共享的鎖變數上。
*   **運作機制**:
    1.  當一個核心嘗試獲取鎖失敗時，它會將自己加入一個隱形的 **等待隊列** 中。
    2.  然後，它開始在 **自己的本地變數上旋轉**。
    3.  當鎖的持有者釋放鎖時，它會 **明確地通知隊列中的下一個等待者**，只修改那個等待者的本地變數，使其停止旋轉並獲得鎖。
<center><img src="https://linux-kernel-labs.github.io/refs/heads/master/_images/ditaa-58545831034f050660727be99cede213bc4a53c7.png" alt="image" width="70%" /></center>

這種方式極大地減少了記憶體匯流排的爭用，因為在等待期間幾乎沒有共享資料的寫入操作，從而顯著提升了 Spinlock 在大規模多核系統上的效能和擴展性。

> 延伸閱讀：[第十三講：淺談同步機制 - Queued/MCS Spinlock](https://hackmd.io/@Jaychao2099/Linux-kernel-13#QueuedMCS-Spinlock)

---

## Spinlock 與中斷處理

在 Linux 核心中，程式碼可能在不同的 context 中執行，最常見的是行程上下文 (process context) 和中斷上下文 (interrupt context)。中斷是異步事件，可能在任何時間點打斷當前的程式碼執行。這為鎖定帶來了新的複雜性。

### 問題
考慮一個場景：
1.  一段在 process context 中執行的程式碼獲取了 Spinlock A。
2.  此時，一個硬體中斷發生在同一個 CPU 核心上。
3.  系統跳轉到該中斷的處理程序 (Interrupt Service Routine, ISR)。
4.  不幸的是，這個 ISR 也需要獲取 Spinlock A。

此時，CPU 陷入了 **Deadlock**。ISR 嘗試獲取一個已經被它自己打斷的程式碼所持有的鎖，但除非 ISR 返回，否則該鎖永遠不會被釋放。而 ISR 不獲取鎖就無法完成並返回。

### 解決辦法
為了處理這種情況，Linux 提供了不同版本的 Spinlock API：

*   **`spin_lock(lock)` / `spin_unlock(lock)`**：
    *   最基本的版本，不處理中斷。
*   **`spin_lock_irqsave(lock, flags)` / `spin_unlock_irqrestore(lock, flags)`**：
    *   在獲取鎖之前，**暫時停用本地 CPU 的所有 maskable interrupt 的中斷請求**，並將當前的中斷狀態保存在 `flags` 中。釋放鎖時，再根據 `flags` 恢復中斷狀態。
    *   最安全、最常用的版本，用於保護可能在 process 和 interrupt context 中共享的資料。
    *   可以處理 **巢狀呼叫** 的狀況，確保程式的強健性：
        *   `_irqsave()`：先把目前的中斷狀態 (開啟 or 被關閉的) 儲存到 flags 變數中，然後才停用中斷。
        *   `_irqrestore()`：**「恢復」到 flags 中儲存的原始狀態**。
        >   [!Note] **巢狀呼叫案例**：
        >   1.   thread A 用 `_irqsave()` 關閉中斷 + 呼叫 thread B。
        >   2.   thread B 也用`_irqsave()` 關閉中斷。
        >   3.   thread B 解鎖時，如果用的是簡單的 `spin_unlock_irq`，它會直接把中斷打開。但這其實是個錯誤，因為外層的 thread A 還期望中斷是關閉的！
        >   4.   有了 `_irqrestore()`，thread B 解鎖時只會將中斷恢復到它進入時的狀態 (關閉狀態)，就不會破壞外層呼叫者的鎖定邏輯。
*   **`spin_lock_irq(lock)` / `spin_unlock_irq(lock)`**：
    *   類似 `irqsave()`，但它只是簡單地停用/啟用中斷，不保存先前狀態。
    *   適用於已知中斷原本是啟用狀態的場景。
*   **`spin_lock_bh(lock)` / `spin_unlock_bh(lock)`**：
    *   一個輕量級的版本，只會停用 Bottom Half 的執行，而不會停用硬體中斷。
    *   當共享資料只在 process context 和 Bottom Half 之間存取時，使用此鎖可獲得比 `spin_lock_irqsave()` 更低的延遲。
    >   [!Tip] 複習：Linux 將中斷處理分為兩部分：
    >   *   **Top Half (上半部)**：即硬體中斷處理程序 (hard IRQ)，要求執行速度極快。
    >   *   **Bottom Half (下半部)**：可延遲執行的部分 (soft IRQ)，由 Top Half 調度，在稍後安全的時間點執行。

> 延伸閱讀：
> *   [Synchronization primitives in the Linux kernel. Part 1](https://0xax.gitbooks.io/linux-insides/SyncPrim/linux-sync-1.html)
> *   [第十講：中斷處理和現代架構考量 - Linux 的中斷處理分層機制](https://hackmd.io/@Jaychao2099/Linux-kernel-10#Linux-%E7%9A%84%E4%B8%AD%E6%96%B7%E8%99%95%E7%90%86%E5%88%86%E5%B1%A4%E6%A9%9F%E5%88%B6)

---

## Kernel Preemption 的影響

**Kernel Preemption (核心搶佔)** 指的是一個在核心模式下執行的任務，可以被另一個更高優先級的任務所中斷 (搶佔) 的能力，是實現低延遲和即時 (Real-time) 系統的關鍵。

### Kernel Preemption 的演進

傳統的 Linux 核心是不可搶佔的，即一旦進入核心模式，除非主動放棄 CPU 或從系統呼叫返回，否則不會被其他 process 搶佔。然而，為了降低系統延遲，滿足即時性需求，Linux 逐步引入了不同等級的 Kernel Preemption：
*   **Voluntary Preemption**：在核心程式碼中插入檢查點，允許主動讓出。
*   **Preemptible Kernel**：允許在核心程式碼的大部分地方 (除了持有 Spinlock 等情況) 進行搶佔。
*   **Full Preemption (`PREEMPT_RT`)**：目標是讓核心幾乎在任何地方都可以被搶佔，以達到硬即時 (hard real-time) 系統的要求。

### **preempt_count** 的角色

核心搶佔允許在高優先權任務就緒時，打斷正在核心中執行的低優先權任務。這個機制與 Spinlock 產生了深刻的互動。如果一個持有 Spinlock 的低優先權任務被搶佔，而高優先權任務又需要同一個 Spinlock，系統就會發生類似 Priority Inversion 的問題。

因此為了管理搶佔，Linux 核心引入了 **`preempt_count`** 這個 per-CPU 計數器，用來追蹤禁止搶佔的原因。

*   當核心進入一個不可搶佔的區域時 (如獲取 spinlock)，`preempt_count` 就會增加。
*   離開該區域時，`preempt_count` 就會減少。
*   只有當 `preempt_count` 為 0 時，核心才允許搶佔。

有趣的是，`preempt_count` 不僅用於搶佔計數，它的不同位元 (bit) 也被用來追蹤是否處於 Hard IRQ 或 Soft IRQ context 中。

### PREEMPT_RT 與 Spinlock

對於硬即時 (hard real-time) 系統，長時間的 busy-waiting 或停用中斷是不可接受的。**`PREEMPT_RT`** 是一個旨在將 Linux 轉變為完全搶佔的即時作業系統的補丁集 (目前已逐漸整合進主線核心)。

在啟用 `PREEMPT_RT` 的核心中，Spinlock 的行為發生了根本性改變：

*   `spin_lock` 不再是 busy-waiting 的鎖，而是被轉換為一個支援 **Priority Inheritance** 的 **`RT-Mutex`**。
*   當一個 thread 嘗試獲取一個已被持有的鎖時 (`RT-Mutex`)，它會進入 **睡眠 (sleep)**，而不是空轉。
*   同時，它會將自己的優先級 "繼承" 給鎖的持有者，幫助持有者盡快完成任務並釋放鎖。

### Raw Spinlock vs. Spinlock

為了實現上述轉換，`PREEMPT_RT` 區分了兩種 Spinlock：
*   **`raw_spinlock_t`**：「原始」的 Spinlock，它的行為始終是 busy-waiting，不受 PREEMPT_RT 配置的影響。
    *   被用於核心中極底層、絕對不能睡眠的地方 (如排程器本身)。
        `include/linux/spinlock_types_raw.h`
        ```c=14
        typedef struct raw_spinlock {
            arch_spinlock_t raw_lock;
            ...    // debug
        } raw_spinlock_t;
        ```
*   **`spinlock_t`**：通用的 Spinlock。
    *   在非 `PREEMPT_RT` 核心中，它就是一個 `raw_spinlock_t`。
        `include/linux/spinlock_types.h`
        ```c=14
        #ifndef CONFIG_PREEMPT_RT

        /* Non PREEMPT_RT kernels map spinlock to raw_spinlock */
        typedef struct spinlock {
            union {
                struct raw_spinlock rlock;
                ...    // debug
            };
        } spinlock_t;
        ```
    *   在 `PREEMPT_RT` 核心中，它會被轉換為 `RT-Mutex`。
        ```c=45
        #else /* !CONFIG_PREEMPT_RT */

        /* PREEMPT_RT kernels map spinlock to rt_mutex */
        #include <linux/rtmutex.h>

        typedef struct spinlock {
            struct rt_mutex_base    lock;
            ...    // debug
        } spinlock_t;
        ...
        #endif /* CONFIG_PREEMPT_RT */
        ```

這種設計讓大部分核心程式碼無需修改，就能在即時和非即時環境之間切換。

> 延伸閱讀：
> *   [第十三講：淺談同步機制 - ARM 架構的 spinlock 的結構體與簡化實作](https://hackmd.io/@Jaychao2099/Linux-kernel-13#ARM-%E6%9E%B6%E6%A7%8B%E7%9A%84-spinlock-%E7%9A%84%E7%B5%90%E6%A7%8B%E9%AB%94%E8%88%87%E7%B0%A1%E5%8C%96%E5%AF%A6%E4%BD%9C%EF%BC%9A)
> *   [第十八講：PREEMPT_RT 作為邁向硬即時作業系統的機制](https://hackmd.io/@Jaychao2099/Linux-kernel-18)

---

## 其他並行相關議題：Deadlock vs. Livelock

### Deadlock

*   **成因**：一組 process 或 thread 處於互相等待對方持有的資源的狀態，導致所有 thread 都無法繼續前進的狀態，是一個 **靜態的等待** 狀態。最常見的原因是 **鎖序不一致 (Inconsistent Lock Ordering)**：如果 Thread 1 獲取了 A，同時 Thread 2 獲取了 B，那麼它們就會相互等待對方釋放鎖，從而造成死鎖。
    *   Thread 1: `lock(A); lock(B);`
    *   Thread 2: `lock(B); lock(A);`
*   **避免方法**：確保所有 thread 都以相同的順序獲取鎖。例如，可以規定總是按照鎖變數的記憶體位址從小到大的順序來獲取。

### Livelock

**Livelock** 是一個比 Deadlock 更微妙的問題。在 Livelock 狀態下，相關的 thread 並沒有被阻塞 (blocked)：它們的狀態在改變，CPU 也在執行指令，但它們卻無法取得任何實質進展，是一個 **動態的徒勞** 狀態。
> 一個經典的比喻是兩個人在狹窄的走廊相遇，都想給對方讓路，於是同時向左，又同時向右，來回往復，雖然都在動，但誰也過不去。

#### Spinlock 如何導致 Livelock

*   **成因**：Spinlock 在某些複雜的場景下可能導致 Livelock。
    1.   一個任務在持有 Spinlock 的同時，如果被中斷，而 ISR (中斷處理常式) 也嘗試獲取同一個鎖，這通常會導致 Deadlock。
    2.   但如果系統中有更複雜的 **重試或退讓機制**，多個核心可能不斷地嘗試獲取鎖、失敗、退讓、再嘗試，雖然 CPU 一直在忙碌，但沒有任何一個核心能成功獲取鎖並進入 Critical Section，這就是一種 Livelock。
*   **避免方法**：關鍵仍然是正確地使用 `spin_lock_irqsave()` 等 API 來協調鎖與中斷的關係。

> 延伸閱讀：
> *   [Kernels and Locking](https://retis.santannapisa.it/luca/KernelProgramming/Slides/kernel_locking.pdf)
> *   [Mutual exclusion, synchronization,communicationin shared memoryOperating systems](https://www.mit.bme.hu/eng/system/files/oktatas/targyak/8699/opre_slides12_mutex_en_1.pdf)
> *   [Linux Locking Mechanisms](https://www.slideshare.net/kerneltlv/linux-locking-mechanisms)

---

## 總結
Spinlock 是 Linux 核心同步機制的基石，它的設計與演進深刻地反映了現代多核處理器架構的挑戰。理解其從硬體原子操作、快取一致性到與中斷和核心搶佔的複雜互動，是掌握 Linux 核心運作的關鍵一步。

---

回[主目錄](https://hackmd.io/@Jaychao2099/Linux-kernel)