.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_TW.rst
:Original: :ref:`Documentation/process/4.Coding.rst <development_coding>`
:Translator:
時奎亮 Alex Shi <[email protected]>
:校譯:
吳想成 Wu XiangCheng <[email protected]>
胡皓文 Hu Haowen <[email protected]>
.. _tw_development_coding:
使代碼正確
======================
雖然一個堅實的、面向社區的設計過程有很多值得說道的,但是任何內核開發項目工作
的證明都反映在代碼中。它是將由其他開發人員檢查併合並(或不合並)到主線樹中
的代碼。所以這段代碼的質量決定了項目的最終成功。
本節將檢查編碼過程。我們將從內核開發人員常犯的幾種錯誤開始。然後重點將轉移
到正確的做法和相關有用的工具上。
陷阱
----
代碼風格
********
內核長期以來都有其標準的代碼風格,如
:ref:`Documentation/translations/zh_CN/process/coding-style.rst <tw_codingstyle>`
中所述。在多數時候,該文檔中描述的準則至多被認爲是建議性的。因此,內核中存在
大量不符合代碼風格準則的代碼。這種代碼的存在會給內核開發人員帶來兩方面的危害。
首先,相信內核代碼標準並不重要,也不強制執行。但事實上,如果沒有按照標準
編寫代碼,那麼新代碼將很難加入到內核中;許多開發人員甚至會在審查代碼之前要求
對代碼進行重新格式化。一個像內核這麼大的代碼庫需要一些統一格式的代碼,以使
開發人員能夠快速理解其中的任何部分。所以再也經不起奇怪格式的代碼的折騰了。
內核的代碼風格偶爾會與僱主的強制風格發生衝突。在這種情況下,必須在代碼合併
之前遵從內核代碼風格。將代碼放入內核意味着以多種方式放棄一定程度的控制權——
包括控制代碼樣式。
另一個危害是認爲已經在內核中的代碼迫切需要修復代碼樣式。開發者可能會開始編寫
重新格式化補丁,作爲熟悉開發過程的一種方式,或者作爲將其名字寫入內核變更日誌
的一種方式,或者兩者兼而有之。但是純代碼風格的修復被開發社區視爲噪音,它們往
往受到冷遇。因此,最好避免編寫這種類型的補丁。在由於其他原因處理一段代碼的
同時順帶修復其樣式是很自然的,但是不應該僅爲了更改代碼樣式而更改之。
代碼風格文檔也不應該被視爲絕對不可違反的規則。如果有一個足夠的理由反對這種
樣式(例如爲了80列限制拆分行會導致可讀性大大降低),那麼就這樣做吧。
注意您還可以使用 ``clang-format`` 工具來幫助您處理這些規則,快速自動重新格式
化部分代碼,和審閱完整的文件以發現代碼樣式錯誤、拼寫錯誤和可能的改進。它還
可以方便地排序 ``#includes`` 、對齊變量/宏、重排文本和其他類似任務。有關詳細
信息,請參閱文檔 :ref:`Documentation/dev-tools/clang-format.rst <clangformat>`
抽象層
******
計算機科學教授教學生以靈活性和信息隱藏的名義廣泛使用抽象層。當然,內核廣泛
地使用了抽象;任何涉及數百萬行代碼的項目都必須做到這一點以存續下來。但經驗
表明,過度或過早的抽象可能和過早的優化一樣有害。抽象應用在適當層級,
不要過度。
簡單點,先考慮一個調用時始終只有一個參數且總爲零的函數。我們可以保留這個參數,
以在需要使用它時提供的額外靈活性。不過,在那時實現了這個額外參數的代碼很有
可能以某種從未被注意到的微妙方式被破壞——因爲它從未被使用過。或者當需要額外
的靈活性時,它並未以符合程序員當初期望的方式來實現。內核開發人員通常會提交
補丁來刪除未使用的參數;一般來說,一開始就不應該添加這些參數。
隱藏硬件訪問的抽象層——通常爲了允許大量的驅動程序兼容多個操作系統——尤其不受
歡迎。這樣的層使代碼變得模糊,可能會造成性能損失;它們不屬於Linux內核。
另一方面,如果您發現自己從另一個內核子系統複製了大量的代碼,那麼是時候
瞭解一下:是否需要將這些代碼中的部分提取到單獨的庫中,或者在更高的層次上
實現這些功能。在整個內核中複製相同的代碼沒有價值。
#ifdef 和預處理
***************
C預處理器似乎給一些C程序員帶來了強大的誘惑,他們認爲它是一種將大量靈活性加入
源代碼中的方法。但是預處理器不是C,大量使用它會導致代碼對其他人來說更難閱讀,
對編譯器來說更難檢查正確性。使用了大量預處理器幾乎總是代碼需要一些
清理工作的標誌。
使用#ifdef的條件編譯實際上是一個強大的功能,它在內核中使用。但是很少有人希望
看到代碼被鋪滿#ifdef塊。一般規定,ifdef的使用應儘可能限制在頭文件中。條件
編譯代碼可以限制函數,如果代碼不存在,這些函數就直接變成空的。然後編譯器將
悄悄地優化對空函數的調用。使得代碼更加清晰,更容易理解。
C預處理器宏存在許多危險性,包括可能對具有副作用且沒有類型安全的表達式進行多
重評估。如果您試圖定義宏,請考慮創建一個內聯函數替代。結果相同的代碼,內聯
函數更容易閱讀,不會多次計算其參數,並且允許編譯器對參數和返回值執行類型檢查。
內聯函數
********
不過,內聯函數本身也存在風險。程序員可以傾心於避免函數調用和用內聯函數填充源
文件所固有的效率。然而,這些功能實際上會降低性能。因爲它們的代碼在每個調用站
點都被複制一遍,所以最終會增加編譯內核的大小。此外,這也對處理器的內存緩存
造成壓力,從而大大降低執行速度。通常內聯函數應該非常小,而且相對較少。畢竟
函數調用的成本並不高;大量創建內聯函數是過早優化的典型例子。
一般來說,內核程序員會自冒風險忽略緩存效果。在數據結構課程開頭中的經典
時間/空間權衡通常不適用於當代硬件。空間 *就是* 時間,因爲一個大的程序比一個
更緊湊的程序運行得慢。
較新的編譯器越來越激進地決定一個給定函數是否應該內聯。因此,隨意放置使用
“inline”關鍵字可能不僅僅是過度的,也可能是無用的。
鎖
**
2006年5月,“deviceescape”網絡堆棧在前呼後擁下以GPL發佈,並被納入主線內核。
這是一個受歡迎的消息;Linux中對無線網絡的支持充其量被認爲是不合格的,而
Deviceescape堆棧承諾修復這種情況。然而直到2007年6月(2.6.22),這段代碼才真
正進入主線。發生了什麼?
這段代碼出現了許多閉門造車的跡象。但一個大麻煩是,它並不是爲多處理器系統而
設計。在合併這個網絡堆棧(現在稱爲mac80211)之前,需要對其進行一個鎖方案的
改造。
曾經,Linux內核代碼可以在不考慮多處理器系統所帶來的併發性問題的情況下進行
開發。然而現在,這個文檔就是在雙核筆記本電腦上寫的。即使在單處理器系統上,
爲提高響應能力所做的工作也會提高內核內的併發性水平。編寫內核代碼而不考慮鎖
的日子早已遠去。
可以由多個線程併發訪問的任何資源(數據結構、硬件寄存器等)必須由鎖保護。新
的代碼應該謹記這一要求;事後修改鎖是一項相當困難的任務。內核開發人員應該花
時間充分了解可用的鎖原語,以便爲工作選擇正確的工具。對併發性缺乏關注的代碼
很難進入主線。
迴歸
****
最後一個值得一提的危險是迴歸:它可能會引起導致現有用戶的某些東西中斷的改變
(這也可能會帶來很大的改進)。這種變化被稱爲“迴歸”,迴歸已經成爲主線內核
最不受歡迎的問題。除了少數例外情況,如果迴歸不能及時修正,會導致迴歸的修改
將被取消。最好首先避免迴歸發生。
人們常常爭論,如果迴歸帶來的功能遠超過產生的問題,那麼迴歸是否爲可接受的。
如果它破壞了一個系統卻爲十個系統帶來新的功能,爲何不改改態度呢?2007年7月,
Linus對這個問題給出了最佳答案:
::
所以我們不會通過引入新問題來修復錯誤。這種方式是靠不住的,沒人知道
是否真的有進展。是前進兩步、後退一步,還是前進一步、後退兩步?
(http://lwn.net/Articles/243460/)
特別不受歡迎的一種迴歸類型是用戶空間ABI的任何變化。一旦接口被導出到用戶空間,
就必須無限期地支持它。這一事實使得用戶空間接口的創建特別具有挑戰性:因爲它們
不能以不兼容的方式進行更改,所以必須一次就對。因此,用戶空間接口總是需要大量
的思考、清晰的文檔和廣泛的審查。
代碼檢查工具
------------
至少目前,編寫無錯誤代碼仍然是我們中很少人能達到的理想狀態。不過,我們希望做
的是,在代碼進入主線內核之前,儘可能多地捕獲並修復這些錯誤。爲此,內核開發人
員已經提供了一系列令人印象深刻的工具,可以自動捕獲各種各樣的隱藏問題。計算機
發現的任何問題都是一個以後不會困擾用戶的問題,因此,只要有可能,就應該使用
自動化工具。
第一步是注意編譯器產生的警告。當前版本的GCC可以檢測(並警告)大量潛在錯誤。
通常,這些警告都指向真正的問題。提交以供審閱的代碼一般不會產生任何編譯器警告。
在消除警告時,注意瞭解真正的原因,並儘量避免僅“修復”使警告消失而不解決其原因。
請注意,並非所有編譯器警告都默認啓用。使用“make KCFLAGS=-W”構建內核以
獲得完整集合。
內核提供了幾個配置選項,可以打開調試功能;大多數配置選項位於“kernel hacking”
子菜單中。對於任何用於開發或測試目的的內核,都應該啓用其中幾個選項。特別是,
您應該打開:
- FRAME_WARN 獲取大於給定數量的堆棧幀的警告。
這些警告生成的輸出可能比較冗長,但您不必擔心來自內核其他部分的警告。
- DEBUG_OBJECTS 將添加代碼以跟蹤內核創建的各種對象的生命週期,並在出現問題
時發出警告。如果你要添加創建(和導出)關於其自己的複雜對象的子系統,請
考慮打開對象調試基礎結構的支持。
- DEBUG_SLAB 可以發現各種內存分配和使用錯誤;它應該用於大多數開發內核。
- DEBUG_SPINLOCK, DEBUG_ATOMIC_SLEEP 和 DEBUG_MUTEXES 會發現許多常見的
鎖錯誤。
還有很多其他調試選項,其中一些將在下面討論。其中一些有顯著的性能影響,不應
一直使用。在學習可用選項上花費一些時間,可能會在短期內得到許多回報。
其中一個較重的調試工具是鎖檢查器或“lockdep”。該工具將跟蹤系統中每個鎖
(spinlock或mutex)的獲取和釋放、獲取鎖的相對順序、當前中斷環境等等。然後,
它可以確保總是以相同的順序獲取鎖,相同的中斷假設適用於所有情況等等。換句話
說,lockdep可以找到許多導致系統死鎖的場景。在部署的系統中,這種問題可能會
很痛苦(對於開發人員和用戶而言);LockDep允許提前以自動方式發現問題。具有
任何類型的非普通鎖的代碼在提交合並前應在啓用lockdep的情況下運行測試。
作爲一個勤奮的內核程序員,毫無疑問,您將檢查任何可能失敗的操作(如內存分配)
的返回狀態。然而,事實上,最終的故障復現路徑可能完全沒有經過測試。未測試的
代碼往往會出問題;如果所有這些錯誤處理路徑都被執行了幾次,那麼您可能對代碼
更有信心。
內核提供了一個可以做到這一點的錯誤注入框架,特別是在涉及內存分配的情況下。
啓用故障注入後,內存分配的可配置失敗的百分比;這些失敗可以限定在特定的代碼
範圍內。在啓用了故障注入的情況下運行,程序員可以看到當情況惡化時代碼如何響
應。有關如何使用此工具的詳細信息,請參閱
Documentation/fault-injection/fault-injection.rst。
“sparse”靜態分析工具可以發現其他類型的錯誤。sparse可以警告程序員用戶空間
和內核空間地址之間的混淆、大端序與小端序的混淆、在需要一組位標誌的地方傳遞
整數值等等。sparse必須單獨安裝(如果您的分發服務器沒有將其打包,
可以在 https://sparse.wiki.kernel.org/index.php/Main_page 找到),
然後可以通過在make命令中添加“C=1”在代碼上運行它。
“Coccinelle”工具 :ref:`http://coccinelle.lip6.fr/ <devtools_coccinelle>`
能夠發現各種潛在的編碼問題;它還可以爲這些問題提出修復方案。在
scripts/coccinelle目錄下已經打包了相當多的內核“語義補丁”;運行
“make coccicheck”將運行這些語義補丁並報告發現的任何問題。有關詳細信息,請參閱
:ref:`Documentation/dev-tools/coccinelle.rst <devtools_coccinelle>`
其他類型的可移植性錯誤最好通過爲其他體系結構編譯代碼來發現。如果沒有S/390系統
或Blackfin開發板,您仍然可以執行編譯步驟。可以在以下位置找到一大堆用於x86系統的
交叉編譯器:
https://www.kernel.org/pub/tools/crosstool/
花一些時間安裝和使用這些編譯器將有助於避免以後的尷尬。
文檔
----
文檔通常比內核開發規則更爲例外。即便如此,足夠的文檔將有助於簡化將新代碼合併
到內核中的過程,使其他開發人員的生活更輕鬆,並對您的用戶有所幫助。在許多情況
下,添加文檔已基本上是強制性的。
任何補丁的第一個文檔是其關聯的變更日誌。日誌條目應該描述正在解決的問題、解決
方案的形式、處理補丁的人員、對性能的任何相關影響,以及理解補丁可能需要的任何
其他內容。確保變更日誌說明了*爲什麼*補丁值得應用;大量開發者未能提供這些信息。
任何添加新用戶空間接口的代碼——包括新的sysfs或/proc文件——都應該包含該接口
的文檔,該文檔使用戶空間開發人員能夠知道他們在使用什麼。請參閱
Documentation/ABI/README,瞭解如何此文檔格式以及需要提供哪些信息。
文檔 :ref:`Documentation/admin-guide/kernel-parameters.rst <kernelparameters>`
描述了內核的所有引導時間參數。任何添加新參數的補丁都應該向該文檔添加適當的
條目。
任何新的配置選項都必須附有幫助文本,幫助文本需清楚地解釋這些選項以及用戶可能
希望何時使用它們。
許多子系統的內部API信息通過專門格式化的註釋進行記錄;這些註釋可以通過
“kernel-doc”腳本以多種方式提取和格式化。如果您在具有kerneldoc註釋的子系統中
工作,則應該維護它們,並根據需要爲外部可用的功能添加它們。即使在沒有如此記錄
的領域中,爲將來添加kerneldoc註釋也沒有壞處;實際上,這對於剛開始開發內核的人
來說是一個有用的活動。這些註釋的格式以及如何創建kerneldoc模板的一些信息可以在
:ref:`Documentation/doc-guide/ <doc_guide>` 上找到。
任何閱讀大量現有內核代碼的人都會注意到,註釋的缺失往往是最值得注意的。同時,
對新代碼的要求比過去更高;合併未註釋的代碼將更加困難。這就是說,人們並不期望
詳細註釋的代碼。代碼本身應該是自解釋的,註釋闡釋了更微妙的方面。
某些事情應該總是被註釋。使用內存屏障時,應附上一行文字,解釋爲什麼需要設置內存
屏障。數據結構的鎖規則通常需要在某個地方解釋。一般來說,主要數據結構需要全面
的文檔。應該指出代碼中分立的位之間不明顯的依賴性。任何可能誘使代碼管理人進行
錯誤的“清理”的事情都需要一個註釋來說明爲什麼要這樣做。等等。
內部API更改
-----------
內核提供給用戶空間的二進制接口不能被破壞,除非逼不得已。而內核的內部編程接口
是高度流動的,當需要時可以更改。如果你發現自己不得不處理一個內核API,或者僅
僅因爲它不滿足你的需求導致無法使用特定的功能,這可能是API需要改變的一個標誌。
作爲內核開發人員,您有權進行此類更改。
的確可以進行API更改,但更改必須是合理的。因此任何進行內部API更改的補丁都應該
附帶關於更改內容和必要原因的描述。這種變化也應該拆分成一個單獨的補丁,而不是
埋在一個更大的補丁中。
另一個要點是,更改內部API的開發人員通常要負責修復內核樹中被更改破壞的任何代碼。
對於一個廣泛使用的函數,這個責任可以導致成百上千的變化,其中許多變化可能與其他
開發人員正在做的工作相沖突。不用說,這可能是一項大工程,所以最好確保理由是
可靠的。請注意,coccinelle工具可以幫助進行廣泛的API更改。
在進行不兼容的API更改時,應儘可能確保編譯器捕獲未更新的代碼。這將幫助您確保找
到該接口的樹內用處。它還將警告開發人員樹外代碼存在他們需要響應的更改。支持樹外
代碼不是內核開發人員需要擔心的事情,但是我們也不必使樹外開發人員的生活有不必要
的困難。