5.6. 鎖陷阱

2018-02-24 15:49 更新

5.6.?鎖陷阱

多年使用鎖的經(jīng)驗 -- 早于 Linux 的經(jīng)驗 -- 已經(jīng)表明加鎖可能是非常難于正確的. 管理并發(fā)是一個固有的技巧性的事情, 有很多出錯的方式. 在這一節(jié), 我們快速看一下可能出錯的東西.

5.6.1.?模糊的規(guī)則

如同上面已經(jīng)說過的, 一個正確的加鎖機(jī)制需要清晰和明確的規(guī)則. 當(dāng)你創(chuàng)建一個可以被并發(fā)存取的資源時, 你應(yīng)當(dāng)定義哪個鎖將控制存取. 加鎖應(yīng)當(dāng)真正在開始處進(jìn)行; 事后更改會是難的事情. 開始時花費的時間常常在調(diào)試時獲得回報.

當(dāng)你編寫你的代碼, 你會毫無疑問遇到幾個函數(shù)需要存取通過一個特定鎖保護(hù)的結(jié)構(gòu). 在此, 你必須小心: 如果一個函數(shù)需要一個鎖并且接著調(diào)用另一個函數(shù)也試圖請求這個鎖, 你的代碼死鎖. 不論旗標(biāo)還是自旋鎖都不允許一個持鎖者第 2 次請求鎖; 如果你試圖這樣做, 事情就簡單地完了.

為使的加鎖正確工作, 你不得不編寫一些函數(shù), 假定它們的調(diào)用者已經(jīng)獲取了相關(guān)的鎖. 常常地, 只有你的內(nèi)部的, 靜態(tài)函數(shù)能夠這樣編寫; 從外部調(diào)用的函數(shù)必須明確處理加鎖. 當(dāng)你編寫內(nèi)部函數(shù)對加鎖做了假設(shè), 方便自己(和其他使用你的代碼的人)并且明確記錄這些假設(shè). 在幾個月后可能很難回來并記起是否你需要持有一個鎖來調(diào)用一個特殊函數(shù).

在 sucll 的例子里, 采用的設(shè)計決定是要求所有的函數(shù)直接從系統(tǒng)調(diào)用里調(diào)用, 來請求應(yīng)用到被存取的設(shè)備結(jié)構(gòu)上的旗標(biāo). 所有的內(nèi)部函數(shù), 那些只是從其他 scull 函數(shù)里調(diào)用的, 可以因此假設(shè)旗標(biāo)已經(jīng)正確獲得.

5.6.2.?加鎖順序規(guī)則

在有大量鎖的系統(tǒng)中(并且內(nèi)核在成為這樣一個系統(tǒng)), 一次需要持有多于一個鎖, 對代碼是不尋常的. 如果某類計算必須使用 2 個不同的資源進(jìn)行, 每個有它自己的鎖, 常常沒有選擇只能獲取 2 個鎖.

獲得多個鎖可能是危險的, 然而. 如果你有 2 個鎖, 稱為 Lock1 和 Lock2, 代碼需要同時都獲取, 你有一個潛在的死鎖. 僅僅想象一個線程鎖住 Lock1 而另一個同時獲得 Lock2. 接著每個線程試圖得到它沒有的那個. 2 個線程都會死鎖.

這個問題的解決方法常常是簡單的: 當(dāng)多個鎖必須獲得時, 它們應(yīng)當(dāng)一直以同樣順序獲得. 只要遵照這個慣例, 象上面描述的簡單死鎖能夠避免. 然而, 遵照加鎖順序規(guī)則是做比說難. 非常少見這樣的規(guī)則真正在任何地方被寫下. 常常你能做的最好的是看看別的代碼如何做的.

一些經(jīng)驗規(guī)則能幫上忙. 如果你必須獲得一個對你的代碼來說的本地鎖(假如, 一個設(shè)備鎖), 以及一個屬于內(nèi)核更中心部分的鎖, 先獲取你的. 如果你有一個旗標(biāo)和自旋鎖的組合, 你必須, 當(dāng)然, 先獲得旗標(biāo); 調(diào)用 down (可能睡眠) 在持有一個自旋鎖時是一個嚴(yán)重的錯誤. 但是最重要的, 盡力避免需要多于一個鎖的情況.

5.6.3.?細(xì) -粗- 粒度加鎖

第一個支持多處理器系統(tǒng)的 Linux 內(nèi)核是 2.0; 它只含有一個自旋鎖. 這個大內(nèi)核鎖將整個內(nèi)核變?yōu)橐粋€大的臨界區(qū); 在任何時候只有一個 CPU 能夠執(zhí)行內(nèi)核代碼. 這個鎖足夠好地解決了并發(fā)問題以允許內(nèi)核開發(fā)者從事所有其他的開發(fā) SMP 所包含的問題. 但是它不是擴(kuò)充地很好. 甚至一個 2 個處理器的系統(tǒng)可能花費可觀數(shù)量的時間只是等待這個大內(nèi)核鎖. 一個 4 個處理器的系統(tǒng)的性能甚至不接近 4 個獨立的機(jī)器的性能.

因此, 后續(xù)的內(nèi)核發(fā)布已經(jīng)包含了更細(xì)粒度的加鎖. 在 2.2 中, 一個自旋鎖控制對塊 I/O 子系統(tǒng)的存取; 另一個為網(wǎng)絡(luò)而工作, 等等. 一個現(xiàn)代的內(nèi)核能包含幾千個鎖, 每個保護(hù)一個小的資源. 這種細(xì)粒度的加鎖可能對伸縮性是好的; 它允許每個處理器在它自己特定的任務(wù)上工作而不必競爭其他處理器使用的鎖. 很少人忘記大內(nèi)核鎖.[19]

但是, 細(xì)粒度加鎖帶有開銷. 在有幾千個鎖的內(nèi)核中, 很難知道你需要那個鎖 -- 以及你應(yīng)當(dāng)以什么順序獲取它們 -- 來進(jìn)行一個特定的操作. 記住加鎖錯誤可能非常難發(fā)現(xiàn); 更多的鎖提供了更多的機(jī)會使真正有害的加鎖 bug 鉆進(jìn)內(nèi)核中. 細(xì)粒度加鎖能帶來一定水平的復(fù)雜性, 長期來, 對內(nèi)核的可維護(hù)性有一個大的, 不利的效果.

在一個設(shè)備驅(qū)動中加鎖常常是相對直接的; 你可以用一個鎖來涵蓋你做的所有東西, 或者你可以給你管理的每個設(shè)備創(chuàng)建一個鎖. 作為一個通用的規(guī)則, 你應(yīng)當(dāng)從相對粗的加鎖開始, 除非你有確實的理由相信競爭可能是一個問題. 忍住慫恿去過早地優(yōu)化; 真實地性能約束常常表現(xiàn)在想不到的地方.

如果你確實懷疑鎖競爭在損壞性能, 你可能發(fā)現(xiàn) lockmeter 工具有用. 這個補丁(從 http://oss.sgi.com/projects/lockmeter/ 可得到) 裝備內(nèi)核來測量在鎖等待花費的時間. 通過看這個報告, 你能夠很快知道是否鎖競爭真的是問題.

[19] 這個鎖仍然存在于 2.6, 幾個它現(xiàn)在覆蓋內(nèi)核非常小的部分. 如果你偶然發(fā)現(xiàn)一個 lock_kernel 調(diào)用, 你已找到了這個大內(nèi)核鎖. 但是, 想都不要想在任何新代碼中使用它.

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號