## 5.3.?旗標和互斥體
讓我們看看我們如何給 scull 加鎖. 我們的目標是使我們對 scull 數據結構的操作原子化, 就是在有其他執行線程的情況下這個操作一次發生. 對于我們的內存泄漏例子, 我們需要保證, 如果一個線程發現必須分配一個特殊的內存塊, 它有機會進行這個分配在其他線程可做測試之前. 為此, 我們必須建立臨界區: 在任何給定時間只有一個線程可以執行的代碼.
不是所有的臨界區是同樣的, 因此內核提供了不同的原語適用不同的需求. 在這個例子中, 每個對 scull 數據結構的存取都發生在由一個直接用戶請求所產生的進程上下文中; 沒有從中斷處理或者其他異步上下文中的存取. 沒有特別的周期(響應時間)要求; 應用程序程序員理解 I/O 請求常常不是馬上就滿足的. 進一步講, scull 沒有持有任何其他關鍵系統資源, 在它存取它自己的數據結構時. 所有這些意味著如果 scull 驅動在等待輪到它存取數據結構時進入睡眠, 沒人介意.
"去睡眠" 在這個上下文中是一個明確定義的術語. 當一個 Linux 進程到了一個它無法做進一步處理的地方時, 它去睡眠(或者 "阻塞"), 讓出處理器給別人直到以后某個時間它能夠再做事情. 進程常常在等待 I/O 完成時睡眠. 隨著我們深入內核, 我們會遇到很多情況我們不能睡眠. 然而 scull 中的 write 方法不是其中一個情況. 因此我們可使用一個加鎖機制使進程在等待存取臨界區時睡眠.
正如重要地, 我們將進行一個可能會睡眠的操作( 使用 kmalloc 分配內存 ) -- 因此睡眠是一個在任何情況下的可能性. 如果我們的臨界區要正確工作, 我們必須使用一個加鎖原語在一個擁有鎖的進程睡眠時起作用. 不是所有的加鎖機制都能夠在可能睡眠的地方使用( 我們在本章后面會看到幾個不可以的 ). 然而, 對我們現在的需要, 最適合的機制時一個旗標.
旗標在計算機科學中是一個被很好理解的概念. 在它的核心, 一個旗標是一個單個整型值, 結合有一對函數, 典型地稱為 P 和 V. 一個想進入臨界區的進程將在相關旗標上調用 P; 如果旗標的值大于零, 這個值遞減 1 并且進程繼續. 相反, 如果旗標的值是 0 ( 或更小 ), 進程必須等待直到別人釋放旗標. 解鎖一個旗標通過調用 V 完成; 這個函數遞增旗標的值, 并且, 如果需要, 喚醒等待的進程.
當旗標用作互斥 -- 阻止多個進程同時在同一個臨界區內運行 -- 它們的值將初始化為 1. 這樣的旗標在任何給定時間只能由一個單個進程或者線程持有. 以這種模式使用的旗標有時稱為一個互斥鎖, 就是, 當然, "互斥"的縮寫. 幾乎所有在 Linux 內核中發現的旗標都是用作互斥.
### 5.3.1.?Linux 旗標實現
Linux 內核提供了一個遵守上面語義的旗標實現, 盡管術語有些不同. 為使用旗標, 內核代碼必須包含 <asm/semaphore.h>. 相關的類型是 struct semaphore; 實際旗標可以用幾種方法來聲明和初始化. 一種是直接創建一個旗標, 接著使用 sema_init 來設定它:
~~~
void sema_init(struct semaphore *sem, int val);
~~~
這里 val 是安排給旗標的初始值.
然而, 通常旗標以互斥鎖的模式使用. 為使這個通用的例子更容易些, 內核提供了一套幫助函數和宏定義. 因此, 一個互斥鎖可以聲明和初始化, 使用下面的一種:
~~~
DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);
~~~
這里, 結果是一個旗標變量( 稱為 name ), 初始化為 1 ( 使用 DECLARE_MUTEX ) 或者 0 (使用 DECLARE_MUTEX_LOCKED ). 在后一種情況, 互斥鎖開始于上鎖的狀態; 在允許任何線程存取之前將不得不顯式解鎖它.
如果互斥鎖必須在運行時間初始化( 這是如果動態分配它的情況, 舉例來說), 使用下列中的一個:
~~~
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);
~~~
在 Linux 世界中, P 函數稱為 down -- 或者這個名子的某個變體. 這里, "down" 指的是這樣的事實, 這個函數遞減旗標的值, 并且, 也許在使調用者睡眠一會兒來等待旗標變可用之后, 給予對被保護資源的存取. 有 3 個版本的 down:
~~~
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
~~~
down 遞減旗標值并且等待需要的時間. down_interruptible 同樣, 但是操作是可中斷的. 這個可中斷的版本幾乎一直是你要的那個; 它允許一個在等待一個旗標的用戶空間進程被用戶中斷. 作為一個通用的規則, 你不想使用不可中斷的操作, 除非實在是沒有選擇. 不可中斷操作是一個創建不可殺死的進程( 在 ps 中見到的可怕的 "D 狀態" )和惹惱你的用戶的好方法, 使用 down_interruptible 需要一些格外的小心, 但是, 如果操作是可中斷的, 函數返回一個非零值, 并且調用者不持有旗標. 正確的使用 down_interruptible 需要一直檢查返回值并且針對性地響應.
最后的版本 ( down_trylock ) 從不睡眠; 如果旗標在調用時不可用, down_trylock 立刻返回一個非零值.
一旦一個線程已經成功調用 down 各個版本中的一個, 就說它持有著旗標(或者已經"取得"或者"獲得"旗標). 這個線程現在有權力存取這個旗標保護的臨界區. 當這個需要互斥的操作完成時, 旗標必須被返回. V 的 Linux 對應物是 up:
~~~
void up(struct semaphore *sem);
~~~
一旦 up 被調用, 調用者就不再擁有旗標.
如你所愿, 要求獲取一個旗標的任何線程, 使用一個(且只能一個)對 up 的調用釋放它. 在錯誤路徑中常常需要特別的小心; 如果在持有一個旗標時遇到一個錯誤, 旗標必須在返回錯誤狀態給調用者之前釋放旗標. 沒有釋放旗標是容易犯的一個錯誤; 這個結果( 進程掛在看來無關的地方 )可能是難于重現和跟蹤的.
### 5.3.2.?在 scull 中使用旗標
旗標機制給予 scull 一個工具, 可以在存取 scull_dev 數據結構時用來避免競爭情況. 但是正確使用這個工具是我們的責任. 正確使用加鎖原語的關鍵是嚴密地指定要保護哪個資源并且確認每個對這些資源的存取都使用了正確的加鎖方法. 在我們的例子驅動中, 感興趣的所有東西都包含在 scull_dev 結構里面, 因此它是我們的加鎖體制的邏輯范圍.
讓我們在看看這個結構:
~~~
struct scull_dev {
struct scull_qset *data; /* Pointer to first quantum set */
int quantum; /* the current quantum size */
int qset; /* the current array size */
unsigned long size; /* amount of data stored here */
unsigned int access_key; /* used by sculluid and scullpriv */
struct semaphore sem; /* mutual exclusion semaphore */
struct cdev cdev; /* Char device structure */
};
~~~
到結構的底部是一個稱為 sem 的成員, 當然, 它是我們的旗標. 我們已經選擇為每個虛擬 scull 設備使用單獨的旗標. 使用一個單個的全局的旗標也可能會是同樣正確. 通常各種 scull 設備不共享資源, 然而, 并且沒有理由使一個進程等待, 而另一個進程在使用不同 scull 設備. 不同設備使用單獨的旗標允許并行進行對不同設備的操作, 因此, 提高了性能.
旗標在使用前必須初始化. scull 在加載時進行這個初始化, 在這個循環中:
~~~
for (i = 0; i < scull_nr_devs; i++) {
scull_devices[i].quantum = scull_quantum;
scull_devices[i].qset = scull_qset;
init_MUTEX(&scull_devices[i].sem);
scull_setup_cdev(&scull_devices[i], i);
}
~~~
注意, 旗標必須在 scull 設備對系統其他部分可用前初始化. 因此, init_MUTEX 在 scull_setup_cdev 前被調用. 以相反的次序進行這個操作可能產生一個競爭情況, 旗標可能在它準備好之前被存取.
下一步, 我們必須瀏覽代碼, 并且確認在沒有持有旗標時沒有對 scull_dev 數據結構的存取. 因此, 例如, scull_write 以這個代碼開始:
~~~
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
~~~
注意對 down_interruptible 返回值的檢查; 如果它返回非零, 操作被打斷了. 在這個情況下通常要做的是返回 -ERESTARTSYS. 看到這個返回值后, 內核的高層要么從頭重啟這個調用要么返回這個錯誤給用戶. 如果你返回 -ERESTARTSYS, 你必須首先恢復任何用戶可見的已經做了的改變, 以保證當重試系統調用時正確的事情發生. 如果你不能以這個方式恢復, 你應當替之返回 -EINTR.
scull_write 必須釋放旗標, 不管它是否能夠成功進行它的其他任務. 如果事事都順利, 執行落到這個函數的最后幾行:
~~~
out:
up(&dev->sem);
return retval;
~~~
這個代碼釋放旗標并且返回任何需要的狀態. 在 scull_write 中有幾個地方可能會出錯; 這些地方包括內存分配失敗或者在試圖從用戶空間拷貝數據時出錯. 在這些情況中, 代碼進行了一個 goto out, 以確保進行正確的清理.
### 5.3.3.?讀者/寫者旗標
旗標為所有調用者進行互斥, 不管每個線程可能想做什么. 然而, 很多任務分為 2 種清楚的類型: 只需要讀取被保護的數據結構的類型, 和必須做改變的類型. 允許多個并發讀者常常是可能的, 只要沒有人試圖做任何改變. 這樣做能夠顯著提高性能; 只讀的任務可以并行進行它們的工作而不必等待其他讀者退出臨界區.
Linux 內核為這種情況提供一個特殊的旗標類型稱為 rwsem (或者" reader/writer semaphore"). rwsem 在驅動中的使用相對較少, 但是有時它們有用.
使用 rwsem 的代碼必須包含 <linux/rwsem.h>. 讀者寫者旗標 的相關數據類型是 struct rw_semaphore; 一個 rwsem 必須在運行時顯式初始化:
~~~
void init_rwsem(struct rw_semaphore *sem);
~~~
一個新初始化的 rwsem 對出現的下一個任務( 讀者或者寫者 )是可用的. 對需要只讀存取的代碼的接口是:
~~~
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
~~~
對 down_read 的調用提供了對被保護資源的只讀存取, 與其他讀者可能地并發地存取. 注意 down_read 可能將調用進程置為不可中斷的睡眠. down_read_trylock 如果讀存取是不可用時不會等待; 如果被準予存取它返回非零, 否則是 0. 注意 down_read_trylock 的慣例不同于大部分的內核函數, 返回值 0 指示成功. 一個使用 down_read 獲取的 rwsem 必須最終使用 up_read 釋放.
讀者的接口類似:
~~~
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);
~~~
down_write, down_write_trylock, 和 up_write 全部就像它們的讀者對應部分, 除了, 當然, 它們提供寫存取. 如果你處于這樣的情況, 需要一個寫者鎖來做一個快速改變, 接著一個長時間的只讀存取, 你可以使用 downgrade_write 在一旦你已完成改變后允許其他讀者進入.
一個 rwsem 允許一個讀者或者不限數目的讀者來持有旗標. 寫者有優先權; 當一個寫者試圖進入臨界區, 就不會允許讀者進入直到所有的寫者完成了它們的工作. 這個實現可能導致讀者饑餓 -- 讀者被長時間拒絕存取 -- 如果你有大量的寫者來競爭旗標. 由于這個原因, 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. 快速參考