歡迎光臨
每天分享高質量文章

linux 內核開髮指南 – 4 讓代碼正確

4. 讓代碼正確

雖然對於一個堅實的、面向社區的設計過程有很多話要說,但是任何內核開發專案的 證明都在生成的代碼中。它是將由其他開發人員檢查併合並(或不合併)到主線樹中 的代碼。所以這段代碼的質量決定了專案的最終成功。

本節將檢查編碼過程。我們將從內核開發人員出錯的幾種方式開始。然後重點將轉移 到正確的事情和可以幫助這個任務的工具上。

4.1. 陷阱

4.1.1. 編碼風格

內核長期以來都有一種標準的編碼風格,如 Documentation/translations/zh_CN/process/coding-style.rst 中所述。在大部分時間里,該檔案中描述的政策被認為至多是建議性的。因此,內核 中存在大量不符合編碼風格準則的代碼。代碼的存在會給內核開發人員帶來兩個獨立 的危害。

首先,要相信內核編碼標準並不重要,也不強制執行。事實上,如果沒有按照標準對代 碼進行編碼,那麼向內核添加新代碼是非常困難的;許多開發人員甚至會在審查代碼之 前要求對代碼進行重新格式化。一個與內核一樣大的代碼庫需要一些統一的代碼,以使 開發人員能夠快速理解其中的任何部分。所以已經沒有空間來存放奇怪的格式化代碼了。

偶爾,內核的編碼風格會與雇主的強制風格發生衝突。在這種情況下,內核的風格必須 在代碼合併之前獲勝。將代碼放入內核意味著以多種方式放棄一定程度的控制權——包括 控制代碼的格式化方式。

另一個陷阱是假定已經在內核中的代碼迫切需要編碼樣式的修複。開發人員可能會開始 生成重新格式化補丁,作為熟悉過程的一種方式,或者作為將其名稱寫入內核變更日誌 的一種方式,或者兩者兼而有之。但是純編碼風格的修複被開發社區視為噪音;它們往 往受到冷遇。因此,最好避免使用這種型別的補丁。由於其他原因,在處理一段代碼的 同時修複它的樣式是很自然的,但是編碼樣式的更改不應該僅為了更改而進行。

編碼風格的文件也不應該被視為絕對的法律,這是永遠不會被違反的。如果有一個很好 的理由反對這種樣式(例如,如果拆分為適合80列限制的行,那麼它的可讀性就會大大 降低),那麼就這樣做。

請註意,您還可以使用 clang-format 工具來幫助您處理這些規則,自動重新格式 化部分代碼,並查看完整的檔案,以發現編碼樣式錯誤、拼寫錯誤和可能的改進。它還 可以方便地進行排序,包括對齊變數/宏、迴流文本和其他類似任務。有關詳細信息,請 參閱檔案 Documentation/process/clang-format.rst

4.1.2. 抽象層

計算機科學教授教學生以靈活性和信息隱藏的名義廣泛使用抽象層。當然,內核廣泛 地使用了抽象;任何涉及數百萬行代碼的專案都不能做到這一點並存活下來。但經驗 表明,過度或過早的抽象可能和過早的優化一樣有害。抽象應用於所需的級別, 不要過度。

在一個簡單的級別上,考慮一個函式的引數,該引數總是由所有呼叫方作為零傳遞。我們可以保留這個論點: 以防有人最終需要使用它提供的額外靈活性。不過,到那時, 實現這個額外引數的代碼很有可能以某種從未被註意到的微妙方式被破壞——因為它從 未被使用過。或者,當需要額外的靈活性時,它不會以符合程式員早期期望的方式來 這樣做。內核開發人員通常會提交補丁來刪除未使用的引數;一般來說,首先不應該 添加這些引數。

隱藏硬體訪問的抽象層——通常允許大量的驅動程式在多個操作系統中使用——尤其不受 歡迎。這樣的層使代碼變得模糊,可能會造成性能損失;它們不屬於Linux內核。

另一方面,如果您發現自己從另一個內核子系統複製了大量的代碼,那麼現在是時候 問一下,事實上,將這些代碼中的一些提取到單獨的庫中,或者在更高的層次上實現 這些功能是否有意義。在整個內核中複製相同的代碼沒有價值。

4.1.3. #ifdef 和預處理

C前處理器似乎給一些C程式員帶來了強大的誘惑,他們認為它是一種有效地將大量靈 活性編碼到源檔案中的方法。但是前處理器不是C,大量使用它會導致代碼對其他人來 說更難讀取,對編譯器來說更難檢查正確性。大量的前處理器幾乎總是代碼需要一些 清理工作的標誌。

使用ifdef的條件編譯實際上是一個強大的功能,它在內核中使用。但是很少有人希望 看到代碼被大量地撒上ifdef塊。作為一般規則,ifdef的使用應盡可能限制在頭檔案 中。有條件編譯的代碼可以限制函式,如果代碼不存在,這些函式就會變成空的。然後 編譯器將悄悄地優化對空函式的呼叫。結果是代碼更加清晰,更容易理解。

C前處理器宏存在許多危險,包括可能對具有副作用且沒有型別安全性的運算式進行多 重評估。如果您試圖定義宏,請考慮創建一個行內函式。結果相同的代碼,但是行內 函式更容易讀取,不會多次計算其引數,並且允許編譯器對引數和傳回值執行型別檢查。

4.1.4. 行內函式

不過,行內函式本身也存在風險。程式員可以傾心於避免函式呼叫和用行內函式填充源 檔案所固有的效率。然而,這些功能實際上會降低性能。因為它們的代碼在每個呼叫站 點都被覆制,所以它們最終會增加編譯內核的大小。反過來,這會對處理器的記憶體快取 造成壓力,從而大大降低執行速度。通常,行內函式應該非常小,而且相對較少。畢竟, 函式呼叫的成本並不高;大量行內函式的創建是過早優化的典型例子。

一般來說,內核程式員會忽略快取效果,這會帶來危險。在開始的資料結構課程中,經 典的時間/空間權衡通常不適用於當代硬體。空間就是時間,因為一個大的程式比一個 更緊湊的程式運行得慢。

最近的編譯器在決定一個給定函式是否應該被行內方面扮演著越來越積極的角色。因此,“inline”關鍵字的自由放置可能不僅僅是過度的,它也可能是無關的。

4.1.5. 鎖

2006年5月,“deviceescape”網絡堆棧在GPL下發佈,並被納入主線內核。這是一個受 歡迎的訊息;對Linux中無線網絡的支持充其量被認為是不合格的,而deviceescape 堆棧提供了修複這種情況的承諾。然而,直到2007年6月(2.6.22),這段代碼才真 正進入主線。發生了什麼?

這段代碼顯示了許多閉門造車的跡象。但一個特別大的問題是,它並不是設計用於多 處理器系統。在合併這個網絡堆棧(現在稱為mac80211)之前,需要對其進行一個鎖 方案的改造。

曾經,Linux內核代碼可以在不考慮多處理器系統所帶來的併發性問題的情況下進行 開發。然而,現在,這個檔案是寫在雙核筆記本電腦上的。即使在單處理器系統上, 為提高響應能力所做的工作也會提高內核內的併發性水平。編寫內核代碼而不考慮鎖 的日子已經過去很長了。

可以由多個執行緒併發訪問的任何資源(資料結構、硬體暫存器等)必須由鎖保護。新 的代碼應該記住這一要求;事後改裝鎖是一項相當困難的任務。內核開發人員應該花 時間充分瞭解可用的鎖原語,以便為作業選擇正確的工具。顯示對併發性缺乏關註的 代碼進入主線將很困難。

4.1.6. 回歸

最後一個值得一提的危險是:它可能會引起改變(這可能會帶來很大的改進),從而 導致現有用戶的某些東西中斷。這種變化被稱為“回歸”,回歸已經成為主線內核最不 受歡迎的。除少數例外情況外,如果回歸不能及時修正,會導致回歸的變化將被取消。最好首先避免回歸。

人們常常爭論,如果回歸讓更多人可以工作,遠超過產生問題,那麼回歸是合理的。如果它破壞的一個系統卻為十個系統帶來新的功能,為什麼不進行更改呢?2007年7月, Linus對這個問題給出了最佳答案:

  • ::
  • 所以我們不會通過引入新問題來修複錯誤。那樣的謊言很瘋狂,沒有人知道 你是否真的有進展。是前進兩步,後退一步,還是向前一步,向後兩步?

(http://lwn.net/articles/243460/)

一種特別不受歡迎的回歸型別是用戶空間ABI的任何變化。一旦接口被匯出到用戶空間, 就必須無限期地支持它。這一事實使得用戶空間接口的創建特別具有挑戰性:因為它們 不能以不兼容的方式進行更改,所以必須第一次正確地進行更改。因此,用戶空間界面 總是需要大量的思考、清晰的文件和廣泛的審查。

4.2. 代碼檢查工具

至少目前,編寫無錯誤代碼仍然是我們中很少人能達到的理想狀態。不過,我們希望做 的是,在代碼進入主線內核之前,盡可能多地捕獲並修複這些錯誤。為此,內核開發人 員已經組裝了一系列令人印象深刻的工具,可以自動捕獲各種各樣的模糊問題。計算機 發現的任何問題都是一個以後不會困擾用戶的問題,因此,只要有可能,就應該使用 自動化工具。

第一步只是註意編譯器產生的警告。當代版本的GCC可以檢測(並警告)大量潛在錯誤。通常,這些警告都指向真正的問題。提交以供審閱的代碼通常不會產生任何編譯器警告。在消除警告時,註意瞭解真正的原因,並儘量避免“修複”,使警告消失而不解決其原因。

請註意,並非所有編譯器警告都預設啟用。使用“make EXTRA_CFLAGS=-W”構建內核以 獲得完整集合。

內核提供了幾個配置選項,可以打開除錯功能;大多數配置選項位於“kernel hacking” 子選單中。對於任何用於開發或測試目的的內核,都應該啟用其中幾個選項。特別是, 您應該打開:

  • 啟用 ENABLE_MUST_CHECK and FRAME_WARN 以獲得一組額外的警告,以解決使用不 推薦使用的接口或忽略函式的重要傳回值等問題。這些警告生成的輸出可能是冗長 的,但您不必擔心來自內核其他部分的警告。
  • DEBUG_OBJECTS 將添加代碼,以跟蹤內核創建的各種物件的生存期,併在出現問題時 發出警告。如果要添加創建(和匯出)自己的複雜物件的子系統,請考慮添加對物件 除錯基礎結構的支持。
  • DEBUG_SLAB 可以發現各種記憶體分配和使用錯誤;它應該用於大多數開發內核。
  • DEBUG_SPINLOCK, DEBUG_ATOMIC_SLEEP and DEBUG_MUTEXES 會發現許多常見的 鎖定錯誤.

還有很多其他除錯選項,其中一些將在下麵討論。其中一些具有顯著的性能影響,不應 一直使用。但是,在學習可用選項上花費的一些時間可能會在短期內得到多次回報。

其中一個較重的除錯工具是鎖定檢查器或“lockdep”。該工具將跟蹤系統中每個鎖 (spinlock或mutex)的獲取和釋放、獲取鎖的相對順序、當前中斷環境等等。然後, 它可以確保總是以相同的順序獲取鎖,相同的中斷假設適用於所有情況,等等。換句話 說,lockdep可以找到許多場景,在這些場景中,系統很少會死鎖。在部署的系統中, 這種問題可能會很痛苦(對於開發人員和用戶而言);LockDep允許提前以自動方式 發現問題。具有任何型別的非普通鎖定的代碼在提交包含前應在啟用lockdep的情況 下運行。

作為一個勤奮的內核程式員,毫無疑問,您將檢查任何可能失敗的操作(如記憶體分配) 的傳回狀態。然而,事實上,最終的故障恢復路徑可能完全沒有經過測試。未測試的 代碼往往會被破壞;如果所有這些錯誤處理路徑都被執行了幾次,那麼您可能對代碼 更有信心。

內核提供了一個可以做到這一點的錯誤註入框架,特別是在涉及記憶體分配的情況下。啟用故障註入後,記憶體分配的可配置百分比將失敗;這些失敗可以限制在特定的代碼 範圍內。在啟用了故障註入的情況下運行,程式員可以看到當情況惡化時代碼如何響 應。有關如何使用此工具的詳細信息,請參閱 Documentation/fault-injection/fault-injection.txt。

使用“sparse”靜態分析工具可以發現其他型別的錯誤。對於sparse,可以警告程式員 用戶空間和內核空間地址之間的混淆、big endian和small endian數量的混合、在需 要一組位標誌的地方傳遞整數值等等。sparse必須單獨安裝(如果您的分發服務器沒 有將其打包,可以在 https://sparse.wiki.kernel.org/index.php/Main_page)找到, 然後可以通過在make命令中添加“C=1”在代碼上運行它。

“Coccinelle”工具 http://coccinelle.lip6.fr/ 能夠發現各種潛在的編碼問題;它還可以為這些問題提出修複方案。在 scripts/coccinelle目錄下已經打包了相當多的內核“語意補丁”;運行 “make coccicheck”將運行這些語意補丁並報告發現的任何問題。有關詳細信息,請參閱 Documentation/dev-tools/coccinelle.rst

其他型別的可移植性錯誤最好通過為其他體系結構編譯代碼來發現。如果沒有S/390系統 或Blackfin開發板,您仍然可以執行編譯步驟。可以在以下位置找到一組用於x86系統的 大型交叉編譯器:

http://www.kernel.org/pub/tools/crosstool/

花一些時間安裝和使用這些編譯器將有助於避免以後的尷尬。

4.3. 文件

文件通常比內核開發規則更為例外。即便如此,足夠的文件將有助於簡化將新代碼合併 到內核中的過程,使其他開發人員的生活更輕鬆,並對您的用戶有所幫助。在許多情況 下,檔案的添加已基本上成為強制性的。

任何補丁的第一個文件是其關聯的變更日誌。日誌條目應該描述正在解決的問題、解決 方案的形式、處理補丁的人員、對性能的任何相關影響,以及理解補丁可能需要的任何 其他內容。確保changelog說明瞭為什麼補丁值得應用;大量開發人員未能提供這些信息。

任何添加新用戶空間界面的代碼(包括新的sysfs或/proc檔案)都應該包含該界面的 文件,該文件使用戶空間開發人員能夠知道他們在使用什麼。請參閱 Documentation/abi/readme,瞭解如何格式化此文件以及需要提供哪些信息。

檔案 Documentation/admin-guide/kernel-parameters.rst 描述了內核的所有引導時間引數。任何添加新引數的補丁都應該向該檔案添加適當的 條目。

任何新的配置選項都必須附有幫助文本,幫助文本清楚地解釋了這些選項以及用戶可能 希望何時選擇它們。

許多子系統的內部API信息通過專門格式化的註釋進行記錄;這些註釋可以通過 “kernel-doc”腳本以多種方式提取和格式化。如果您在具有kerneldoc註釋的子系統中 工作,則應該維護它們,並根據需要為外部可用的功能添加它們。即使在沒有如此記錄 的領域中,為將來添加kerneldoc註釋也沒有壞處;實際上,這對於剛開始開發內核的人 來說是一個有用的活動。這些註釋的格式以及如何創建kerneldoc模板的一些信息可以在 Documentation/doc-guide/上找到。

任何閱讀大量現有內核代碼的人都會註意到,註釋的缺失往往是最值得註意的。再一次, 對新代碼的期望比過去更高;合併未註釋的代碼將更加困難。這就是說,人們幾乎不希望 用語言註釋代碼。代碼本身應該是可讀的,註釋解釋了更微妙的方面。

某些事情應該總是被註釋。使用記憶體屏障時,應附上一行文字,解釋為什麼需要設置記憶體 屏障。資料結構的鎖定規則通常需要在某個地方解釋。一般來說,主要資料結構需要全面 的文件。應該指出單獨代碼位之間不明顯的依賴性。任何可能誘使代碼看門人進行錯誤的 “清理”的事情都需要一個註釋來說明為什麼要這樣做。等等。

4.4. 內部API更改

內核提供給用戶空間的二進制接口不能被破壞,除非在最嚴重的情況下。相反,內核的 內部編程接口是高度流動的,當需要時可以更改。如果你發現自己不得不處理一個內核 API,或者僅僅因為它不滿足你的需求而不使用特定的功能,這可能是API需要改變的一 個標誌。作為內核開發人員,您有權進行此類更改。

當然, 可以進行API更改,但它們必須是合理的。因此,任何進行內部API更改的補丁都 應該附帶一個關於更改內容和必要原因的描述。這種變化也應該分解成一個單獨的補丁, 而不是埋在一個更大的補丁中。

另一個要點是,更改內部API的開發人員通常要負責修複內核樹中被更改破壞的任何代碼。對於一個廣泛使用的函式,這個職責可以導致成百上千的變化,其中許多變化可能與其他 開發人員正在做的工作相衝突。不用說,這可能是一項大工作,所以最好確保理由是 可靠的。請註意,coccinelle工具可以幫助進行廣泛的API更改。

在進行不兼容的API更改時,應盡可能確保編譯器捕獲未更新的代碼。這將幫助您確保找 到該接口的樹內用處。它還將警告開發人員樹外代碼存在他們需要響應的更改。支持樹外 代碼不是內核開發人員需要擔心的事情,但是我們也不必使樹外開發人員的生活有不必要 的困難。


    已同步到看一看
    赞(0)

    分享創造快樂