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更改時,應盡可能確保編譯器捕獲未更新的程式碼。這將幫助您確保找 到該介面的樹內用處。它還將警告開發人員樹外程式碼存在他們需要響應的更改。支援樹外 程式碼不是核心開發人員需要擔心的事情,但是我們也不必使樹外開發人員的生活有不必要 的困難。
朋友會在“發現-看一看”看到你“在看”的內容