## 5.5.?自旋鎖
對于互斥, 旗標是一個有用的工具, 但是它們不是內核提供的唯一這樣的工具. 相反, 大部分加鎖是由一種稱為自旋鎖的機制來實現. 不象旗標, 自旋鎖可用在不能睡眠的代碼中, 例如中斷處理. 當正確地使用了, 通常自旋鎖提供了比旗標更高的性能. 然而, 它們確實帶來對它們用法的一套不同的限制.
自旋鎖概念上簡單. 一個自旋鎖是一個互斥設備, 只能有 2 個值:"上鎖"和"解鎖". 它常常實現為一個整數值中的一個單個位. 想獲取一個特殊鎖的代碼測試相關的位. 如果鎖是可用的, 這個"上鎖"位被置位并且代碼繼續進入臨界區. 相反, 如果這個鎖已經被別人獲得, 代碼進入一個緊湊的循環中反復檢查這個鎖, 直到它變為可用. 這個循環就是自旋鎖的"自旋"部分.
當然, 一個自旋鎖的真實實現比上面描述的復雜一點. 這個"測試并置位"操作必須以原子方式進行, 以便只有一個線程能夠獲得鎖, 就算如果有多個進程在任何給定時間自旋. 必須小心以避免在超線程處理器上死鎖 -- 實現多個虛擬 CPU 以共享一個單個處理器核心和緩存的芯片. 因此實際的自旋鎖實現在每個 Linux 支持的體系上都不同. 核心的概念在所有系統上相同, 然而, 當有對自旋鎖的競爭, 等待的處理器在一個緊湊循環中執行并且不作有用的工作.
它們的特性上, 自旋鎖是打算用在多處理器系統上, 盡管一個運行一個搶占式內核的單處理器工作站的行為如同 SMP, 如果只考慮到并發. 如果一個非搶占的單處理器系統進入一個鎖上的自旋, 它將永遠自旋; 沒有其他的線程再能夠獲得 CPU 來釋放這個鎖. 因此, 自旋鎖在沒有打開搶占的單處理器系統上的操作被優化為什么不作, 除了改變 IRQ 屏蔽狀態的那些. 由于搶占, 甚至如果你從不希望你的代碼在一個 SMP 系統上運行, 你仍然需要實現正確的加鎖.
### 5.5.1.?自旋鎖 API 簡介
自旋鎖原語要求的包含文件是 <linux/spinlock.h>. 一個實際的鎖有類型 spinlock_t. 象任何其他數據結構, 一個 自旋鎖必須初始化. 這個初始化可以在編譯時完成, 如下:
~~~
spinlock_t my_lock = SPIN_LOCK_UNLOCKED;
~~~
或者在運行時使用:
~~~
void spin_lock_init(spinlock_t *lock);
~~~
在進入一個臨界區前, 你的代碼必須獲得需要的 lock , 用:
~~~
void spin_lock(spinlock_t *lock);
~~~
注意所有的自旋鎖等待是, 由于它們的特性, 不可中斷的. 一旦你調用 spin_lock, 你將自旋直到鎖變為可用.
為釋放一個你已獲得的鎖, 傳遞它給:
~~~
void spin_unlock(spinlock_t *lock);
~~~
有很多其他的自旋鎖函數, 我們將很快都看到. 但是沒有一個背離上面列出的函數所展示的核心概念. 除了加鎖和釋放, 沒有什么可對一個鎖所作的. 但是, 有幾個規則關于你必須如何使用自旋鎖. 我們將用一點時間來看這些, 在進入完整的自旋鎖接口之前.
### 5.5.2.?自旋鎖和原子上下文
想象一會兒你的驅動請求一個自旋鎖并且在它的臨界區里做它的事情. 在中間某處, 你的驅動失去了處理器. 或許它已調用了一個函數( copy_from_user, 假設) 使進程進入睡眠. 或者, 也許, 內核搶占發威, 一個更高優先級的進程將你的代碼推到一邊. 你的代碼現在持有一個鎖, 在可見的將來的如何時間不會釋放這個鎖. 如果某個別的線程想獲得同一個鎖, 它會, 在最好的情況下, 等待( 在處理器中自旋 )很長時間. 最壞的情況, 系統可能完全死鎖.
大部分讀者會同意這個場景最好是避免. 因此, 應用到自旋鎖的核心規則是任何代碼必須, 在持有自旋鎖時, 是原子性的. 它不能睡眠; 事實上, 它不能因為任何原因放棄處理器, 除了服務中斷(并且有時即便此時也不行)
內核搶占的情況由自旋鎖代碼自己處理. 內核代碼持有一個自旋鎖的任何時間, 搶占在相關處理器上被禁止. 即便單處理器系統必須以這種方式禁止搶占以避免競爭情況. 這就是為什么需要正確的加鎖, 即便你從不期望你的代碼在多處理器機器上運行.
在持有一個鎖時避免睡眠是更加困難; 很多內核函數可能睡眠, 并且這個行為不是都被明確記錄了. 拷貝數據到或從用戶空間是一個明顯的例子: 請求的用戶空間頁可能需要在拷貝進行前從磁盤上換入, 這個操作顯然需要一個睡眠. 必須分配內存的任何操作都可能睡眠. kmalloc 能夠決定放棄處理器, 并且等待更多內存可用除非它被明確告知不這樣做. 睡眠可能發生在令人驚訝的地方; 編寫會在自旋鎖下執行的代碼需要注意你調用的每個函數.
這有另一個場景: 你的驅動在執行并且已經獲取了一個鎖來控制對它的設備的存取. 當持有這個鎖時, 設備發出一個中斷, 使得你的中斷處理運行. 中斷處理, 在存取設備之前, 必須獲得鎖. 在一個中斷處理中獲取一個自旋鎖是一個要做的合法的事情; 這是自旋鎖操作不能睡眠的其中一個理由. 但是如果中斷處理和起初獲得鎖的代碼在同一個處理器上會發生什么? 當中斷處理在自旋, 非中斷代碼不能運行來釋放鎖. 這個處理器將永遠自旋.
避免這個陷阱需要在持有自旋鎖時禁止中斷( 只在本地 CPU ). 有各種自旋鎖函數會為你禁止中斷( 我們將在下一節見到它們 ). 但是, 一個完整的中斷討論必須等到第 10 章了.
關于自旋鎖使用的最后一個重要規則是自旋鎖必須一直是盡可能短時間的持有. 你持有一個鎖越長, 另一個進程可能不得不自旋等待你釋放它的時間越長, 它不得不完全自旋的機會越大. 長時間持有鎖也阻止了當前處理器調度, 意味著高優先級進程 -- 真正應當能獲得 CPU 的 -- 可能不得不等待. 內核開發者盡了很大努力來減少內核反應時間( 一個進程可能不得不等待調度的時間 )在 2.5 開發系列. 一個寫的很差的驅動會摧毀所有的進程, 僅僅通過持有一個鎖太長時間. 為避免產生這類問題, 重視使你的鎖持有時間短.
### 5.5.3.?自旋鎖函數
我們已經看到 2 個函數, spin_lock 和 spin_unlock, 可以操作自旋鎖. 有其他幾個函數, 然而, 有類似的名子和用途. 我們現在會展示全套. 這個討論將帶我們到一個我們無法在幾章內適當涵蓋的地方; 自旋鎖 API 的完整理解需要對中斷處理和相關概念的理解.
實際上有 4 個函數可以加鎖一個自旋鎖:
~~~
void spin_lock(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_bh(spinlock_t *lock)
~~~
我們已經看到自旋鎖如何工作. spin_loc_irqsave 禁止中斷(只在本地處理器)在獲得自旋鎖之前; 之前的中斷狀態保存在 flags 里. 如果你絕對確定在你的處理器上沒有禁止中斷的(或者, 換句話說, 你確信你應當在你釋放你的自旋鎖時打開中斷), 你可以使用 spin_lock_irq 代替, 并且不必保持跟蹤 flags. 最后, spin_lock_bh 在獲取鎖之前禁止軟件中斷, 但是硬件中斷留作打開的.
如果你有一個可能被在(硬件或軟件)中斷上下文運行的代碼獲得的自旋鎖, 你必須使用一種 spin_lock 形式來禁止中斷. 其他做法可能死鎖系統, 遲早. 如果你不在硬件中斷處理里存取你的鎖, 但是你通過軟件中斷(例如, 在一個 tasklet 運行的代碼, 在第 7 章涉及的主題 ), 你可以使用 spin_lock_bh 來安全地避免死鎖, 而仍然允許硬件中斷被服務.
也有 4 個方法來釋放一個自旋鎖; 你用的那個必須對應你用來獲取鎖的函數.
~~~
void spin_unlock(spinlock_t *lock);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);
~~~
每個 spin_unlock 變體恢復由對應的 spin_lock 函數鎖做的工作. 傳遞給 spin_unlock_irqrestore 的 flags 參數必須是傳遞給 spin_lock_irqsave 的同一個變量. 你必須也調用 spin_lock_irqsave 和 spin_unlock_irqrestore 在同一個函數里. 否則, 你的代碼可能破壞某些體系.
還有一套非阻塞的自旋鎖操作:
~~~
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);
~~~
這些函數成功時返回非零( 獲得了鎖 ), 否則 0. 沒有"try"版本來禁止中斷.
### 5.5.4.?讀者/寫者自旋鎖
內核提供了一個自旋鎖的讀者/寫者形式, 直接模仿我們在本章前面見到的讀者/寫者旗標. 這些鎖允許任何數目的讀者同時進入臨界區, 但是寫者必須是排他的存取. 讀者寫者鎖有一個類型 rwlock_t, 在 <linux/spinlokc.h> 中定義. 它們可以以 2 種方式被聲明和被初始化:
~~~
rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* Static way */
rwlock_t my_rwlock;
rwlock_init(&my_rwlock); /* Dynamic way */
~~~
可用函數的列表現在應當看來相當類似. 對于讀者, 下列函數是可用的:
~~~
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);
~~~
有趣地, 沒有 read_trylock. 對于寫存取的函數是類似的:
~~~
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock);
void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);
~~~
讀者/寫者鎖能夠餓壞讀者, 就像 rwsem 一樣. 這個行為很少是一個問題; 然而, 如果有足夠的鎖競爭來引起饑餓, 性能無論如何都不行.
- Linux設備驅動第三版
- 第 1 章 設備驅動簡介
- 1.1. 驅動程序的角色
- 1.2. 劃分內核
- 1.3. 設備和模塊的分類
- 1.4. 安全問題
- 1.5. 版本編號
- 1.6. 版權條款
- 1.7. 加入內核開發社團
- 1.8. 本書的內容
- 第 2 章 建立和運行模塊
- 2.1. 設置你的測試系統
- 2.2. Hello World 模塊
- 2.3. 內核模塊相比于應用程序
- 2.4. 編譯和加載
- 2.5. 內核符號表
- 2.6. 預備知識
- 2.7. 初始化和關停
- 2.8. 模塊參數
- 2.9. 在用戶空間做
- 2.10. 快速參考
- 第 3 章 字符驅動
- 3.1. scull 的設計
- 3.2. 主次編號
- 3.3. 一些重要數據結構
- 3.4. 字符設備注冊
- 3.5. open 和 release
- 3.6. scull 的內存使用
- 3.7. 讀和寫
- 3.8. 使用新設備
- 3.9. 快速參考
- 第 4 章 調試技術
- 4.1. 內核中的調試支持
- 4.2. 用打印調試
- 4.3. 用查詢來調試
- 4.4. 使用觀察來調試
- 4.5. 調試系統故障
- 4.6. 調試器和相關工具
- 第 5 章 并發和競爭情況
- 5.1. scull 中的缺陷
- 5.2. 并發和它的管理
- 5.3. 旗標和互斥體
- 5.4. Completions 機制
- 5.5. 自旋鎖
- 5.6. 鎖陷阱
- 5.7. 加鎖的各種選擇
- 5.8. 快速參考
- 第 6 章 高級字符驅動操作
- 6.1. ioctl 接口
- 6.2. 阻塞 I/O
- 6.3. poll 和 select
- 6.4. 異步通知
- 6.5. 移位一個設備
- 6.6. 在一個設備文件上的存取控制
- 6.7. 快速參考
- 第 7 章 時間, 延時, 和延后工作
- 7.1. 測量時間流失
- 7.2. 獲知當前時間
- 7.3. 延后執行
- 7.4. 內核定時器
- 7.5. Tasklets 機制
- 7.6. 工作隊列
- 7.7. 快速參考
- 第 8 章 分配內存
- 8.1. kmalloc 的真實故事
- 8.2. 后備緩存
- 8.3. get_free_page 和其友
- 8.4. 每-CPU 的變量
- 8.5. 獲得大量緩沖
- 8.6. 快速參考
- 第 9 章 與硬件通訊
- 9.1. I/O 端口和 I/O 內存
- 9.2. 使用 I/O 端口
- 9.3. 一個 I/O 端口例子
- 9.4. 使用 I/O 內存
- 9.5. 快速參考
- 第 10 章 中斷處理
- 10.1. 準備并口
- 10.2. 安裝一個中斷處理
- 10.3. 前和后半部
- 10.4. 中斷共享
- 10.5. 中斷驅動 I/O
- 10.6. 快速參考
- 第 11 章 內核中的數據類型
- 11.1. 標準 C 類型的使用
- 11.2. 安排一個明確大小給數據項
- 11.3. 接口特定的類型
- 11.4. 其他移植性問題
- 11.5. 鏈表
- 11.6. 快速參考
- 第 12 章 PCI 驅動
- 12.1. PCI 接口
- 12.2. 回顧: ISA
- 12.3. PC/104 和 PC/104+
- 12.4. 其他的 PC 總線
- 12.5. SBus
- 12.6. NuBus 總線
- 12.7. 外部總線
- 12.8. 快速參考
- 第 13 章 USB 驅動
- 13.1. USB 設備基礎知識
- 13.2. USB 和 sysfs
- 13.3. USB 的 Urbs
- 13.4. 編寫一個 USB 驅動
- 13.5. 無 urb 的 USB 傳送
- 13.6. 快速參考
- 第 14 章 Linux 設備模型
- 14.1. Kobjects, Ksets 和 Subsystems
- 14.2. 低級 sysfs 操作
- 14.3. 熱插拔事件產生
- 14.4. 總線, 設備, 和驅動
- 14.5. 類
- 14.6. 集成起來
- 14.7. 熱插拔
- 14.8. 處理固件
- 14.9. 快速參考
- 第 15 章 內存映射和 DMA
- 15.1. Linux 中的內存管理
- 15.2. mmap 設備操作
- 15.3. 進行直接 I/O
- 15.4. 直接內存存取
- 15.5. 快速參考
- 第 16 章 塊驅動
- 16.1. 注冊
- 16.2. 塊設備操作
- 16.3. 請求處理
- 16.4. 一些其他的細節
- 16.5. 快速參考
- 第 17 章 網絡驅動
- 17.1. snull 是如何設計的
- 17.2. 連接到內核
- 17.3. net_device 結構的詳情
- 17.4. 打開與關閉
- 17.5. 報文傳送
- 17.6. 報文接收
- 17.7. 中斷處理
- 17.8. 接收中斷緩解
- 17.9. 連接狀態的改變
- 17.10. Socket 緩存
- 17.11. MAC 地址解析
- 17.12. 定制 ioctl 命令
- 17.13. 統計信息
- 17.14. 多播
- 17.15. 幾個其他細節
- 17.16. 快速參考
- 第 18 章 TTY 驅動
- 18.1. 一個小 TTY 驅動
- 18.2. tty_driver 函數指針
- 18.3. TTY 線路設置
- 18.4. ioctls 函數
- 18.5. TTY 設備的 proc 和 sysfs 處理
- 18.6. tty_driver 結構的細節
- 18.7. tty_operaions 結構的細節
- 18.8. tty_struct 結構的細節
- 18.9. 快速參考