吐血整理的萬字Linux內(nèi)核源碼規(guī)范
從編碼風(fēng)格錯誤開始
快速修改編碼風(fēng)格的工具
scripts/checkpatch.pl
scripts/Lindent
astyle
Linux 內(nèi)核代碼風(fēng)格
1 縮進(jìn)
2 把長的行和字符串打散
3 大括號和空格的放置
4 命名
5 Typedef
6 函數(shù)
7 集中的函數(shù)退出途徑
8 注釋
9 你已經(jīng)把事情弄糟了
10 Kconfig 配置文件
11 數(shù)據(jù)結(jié)構(gòu)
12 宏,枚舉和RTL
13 打印內(nèi)核消息
14 分配內(nèi)存
15 內(nèi)聯(lián)弊病
16 函數(shù)返回值及命名
17 不要重新發(fā)明內(nèi)核宏
18 編輯器模式行和其他需要羅嗦的事情
19 內(nèi)聯(lián)匯編
20 條件編譯
從編碼風(fēng)格錯誤開始
曾經(jīng)在開發(fā)Linux內(nèi)核驅(qū)動的時候,創(chuàng)建了一個補(bǔ)丁文件,但是在把補(bǔ)丁打到主分支的時候提示很多編碼風(fēng)格的錯誤問題,后來重做了補(bǔ)丁才解決了問題,這也是沒有嚴(yán)格按照的Linux編碼風(fēng)格從而導(dǎo)致的問題。因為當(dāng)時代碼量不大,所以解決問題的時間相對較少。在代碼量增大的情況下可以借助工具進(jìn)行自動修改。
快速修改編碼風(fēng)格的工具
scripts/checkpatch.pl
這是一個檢查patch是否符合內(nèi)核編碼規(guī)范的腳本。默認(rèn)的調(diào)用也確實如此。如果用來檢查原文件,需要加上-f
的選項。
scripts/Lindent
源碼路徑下的scripts
目錄中的工具Lindent
可以用來自動修改縮進(jìn)問題。不過使用Lindent要求系統(tǒng)安裝indent
這個工具。Ubuntu系統(tǒng)下可以使用apt-get install indent
進(jìn)行「安裝」。
astyle
比較推薦使用這個工具,因為比較相當(dāng)方便,可以一鍵式轉(zhuǎn)換成linux
,java
,gnu
等等風(fēng)格。
下載地址項目地址文檔說明
如何使用,可以參考具體文檔說明,寫的比較詳細(xì)。
總而言之,應(yīng)該顧全大局,在進(jìn)行內(nèi)核開發(fā)和驅(qū)動開發(fā)的時候,嚴(yán)格遵守Linux的編碼規(guī)范,避免由于編碼不規(guī)范帶來的種種問題,可以參考內(nèi)核路徑下Documentation/CodingStyle
文檔,以下轉(zhuǎn)自Linux內(nèi)核文檔,最權(quán)威的文檔路徑,很全很強(qiáng)大,原來在這個網(wǎng)站上已經(jīng)有中文版了,感謝萬分,具體的更新可以跳轉(zhuǎn)到你懂的網(wǎng)址。
「來自 Documentation/process/coding-style.rst 的中文翻譯」
Linux 內(nèi)核代碼風(fēng)格
這是一個簡短的文檔,描述了 linux 內(nèi)核的首選代碼風(fēng)格。代碼風(fēng)格是因人而異的, 而且我不愿意把自己的觀點強(qiáng)加給任何人,但這就像我去做任何事情都必須遵循的原則 那樣,我也希望在絕大多數(shù)事上保持這種的態(tài)度。請 (在寫代碼時) 至少考慮一下這里 的代碼風(fēng)格。
首先,我建議你打印一份 GNU 代碼規(guī)范,然后不要讀。燒了它,這是一個具有重大象征性意義的動作。
不管怎樣,現(xiàn)在我們開始:
1 縮進(jìn)
制表符是 8 個字符,所以縮進(jìn)也是 8 個字符。有些異端運動試圖將縮進(jìn)變?yōu)?4 (甚至 2!) 字符深,這幾乎相當(dāng)于嘗試將圓周率的值定義為 3。
理由:縮進(jìn)的全部意義就在于清楚的定義一個控制塊起止于何處。尤其是當(dāng)你盯著你的 屏幕連續(xù)看了 20 小時之后,你將會發(fā)現(xiàn)大一點的縮進(jìn)會使你更容易分辨縮進(jìn)。
現(xiàn)在,有些人會抱怨 8 個字符的縮進(jìn)會使代碼向右邊移動的太遠(yuǎn),在 80 個字符的終端 屏幕上就很難讀這樣的代碼。這個問題的答案是,如果你需要 3 級以上的縮進(jìn),不管用 何種方式你的代碼已經(jīng)有問題了,應(yīng)該修正你的程序。
簡而言之,8 個字符的縮進(jìn)可以讓代碼更容易閱讀,還有一個好處是當(dāng)你的函數(shù)嵌套太 深的時候可以給你警告。留心這個警告。
在 switch 語句中消除多級縮進(jìn)的首選的方式是讓 switch
和從屬于它的 case
標(biāo)簽對齊于同一列,而不要 兩次縮進(jìn)
case
標(biāo)簽。比如:
switch?(suffix)?{
case?'G':
case?'g':
????????mem?<<=?30;
????????break;
case?'M':
case?'m':
????????mem?<<=?20;
????????break;
case?'K':
case?'k':
????????mem?<<=?10;
????????/*?fall?through?*/
default:
????????break;
}
不要把多個語句放在一行里,除非你有什么東西要隱藏:
if?(condition)?do_this;
??do_something_everytime;
也不要在一行里放多個賦值語句。內(nèi)核代碼風(fēng)格超級簡單。就是避免可能導(dǎo)致別人誤讀 的表達(dá)式。
除了注釋、文檔和 Kconfig 之外,不要使用空格來縮進(jìn),前面的例子是例外,是有意為 之。
選用一個好的編輯器,不要在行尾留空格。
2 把長的行和字符串打散
代碼風(fēng)格的意義就在于使用平常使用的工具來維持代碼的可讀性和可維護(hù)性。
每一行的長度的限制是 80 列,我們強(qiáng)烈建議您遵守這個慣例。
長于 80 列的語句要打散成有意義的片段。除非超過 80 列能顯著增加可讀性,并且不 會隱藏信息。子片段要明顯短于母片段,并明顯靠右。這同樣適用于有著很長參數(shù)列表 的函數(shù)頭。然而,絕對不要打散對用戶可見的字符串,例如 printk 信息,因為這樣就 很難對它們 grep。
3 大括號和空格的放置
C 語言風(fēng)格中另外一個常見問題是大括號的放置。和縮進(jìn)大小不同,選擇或棄用某種放 置策略并沒有多少技術(shù)上的原因,不過首選的方式,就像 Kernighan 和 Ritchie 展示 給我們的,是把起始大括號放在行尾,而把結(jié)束大括號放在行首,所以:
if?(x?is?true)?{
????????we?do?y
}
這適用于所有的非函數(shù)語句塊 (if, switch, for, while, do)。比如:
switch?(action)?{
case?KOBJ_ADD:
????????return?"add";
case?KOBJ_REMOVE:
????????return?"remove";
case?KOBJ_CHANGE:
????????return?"change";
default:
????????return?NULL;
}
不過,有一個例外,那就是函數(shù):函數(shù)的起始大括號放置于下一行的開頭,所以:
int?function(int?x)
{
????????body?of?function
}
全世界的異端可能會抱怨這個不一致性是... 呃... 不一致的,不過所有思維健全的人 都知道 (a) K&R 是 「正確的」 并且 (b) K&R 是正確的。此外,不管怎樣函數(shù)都是特 殊的 (C 函數(shù)是不能嵌套的)。
注意結(jié)束大括號獨自占據(jù)一行,除非它后面跟著同一個語句的剩余部分,也就是 do 語 句中的 “while” 或者 if 語句中的 “else”,像這樣:
do?{
????????body?of?do-loop
}?while?(condition);
和
if?(x?==?y)?{
????????..
}?else?if?(x?>?y)?{
????????...
}?else?{
????????....
}
理由:K&R。
也請注意這種大括號的放置方式也能使空 (或者差不多空的) 行的數(shù)量最小化,同時不 失可讀性。因此,由于你的屏幕上的新行是不可再生資源 (想想 25 行的終端屏幕),你 將會有更多的空行來放置注釋。
當(dāng)只有一個單獨的語句的時候,不用加不必要的大括號。
if?(condition)
????????action();
和
if?(condition)
????????do_this();
else
????????do_that();
這并不適用于只有一個條件分支是單語句的情況;這時所有分支都要使用大括號:
if?(condition)?{
????????do_this();
????????do_that();
}?else?{
????????otherwise();
}
3.1 空格
Linux 內(nèi)核的空格使用方式 (主要) 取決于它是用于函數(shù)還是關(guān)鍵字。(大多數(shù)) 關(guān)鍵字 后要加一個空格。值得注意的例外是 sizeof, typeof, alignof 和 「attribute」,這 些關(guān)鍵字某些程度上看起來更像函數(shù) (它們在 Linux 里也常常伴隨小括號而使用,盡管 在 C 里這樣的小括號不是必需的,就像 struct fileinfo info;
聲明過后的 sizeof info
)。
所以在這些關(guān)鍵字之后放一個空格:
if,?switch,?case,?for,?do,?while
但是不要在 sizeof, typeof, alignof 或者 「attribute」 這些關(guān)鍵字之后放空格。例如,
s?=?sizeof(struct?file);
不要在小括號里的表達(dá)式兩側(cè)加空格。這是一個 「反例」 :
s?=?sizeof(?struct?file?);
當(dāng)聲明指針類型或者返回指針類型的函數(shù)時, *
的首選使用方式是使之靠近變量名 或者函數(shù)名,而不是靠近類型名。例子:
char?*linux_banner;
unsigned?long?long?memparse(char?*ptr,?char?**retptr);
char?*match_strdup(substring_t?*s);
在大多數(shù)二元和三元操作符兩側(cè)使用一個空格,例如下面所有這些操作符:
=??+??-???>??*??/??%??|??&??^??<=??>=??==??!=?????:
但是一元操作符后不要加空格:
&??*??+??-??~??!??sizeof??typeof??alignof??__attribute__??defined
后綴自加和自減一元操作符前不加空格:
++??--
前綴自加和自減一元操作符后不加空格:
++??--
.
和 ->
結(jié)構(gòu)體成員操作符前后不加空格。
不要在行尾留空白。有些可以自動縮進(jìn)的編輯器會在新行的行首加入適量的空白,然后 你就可以直接在那一行輸入代碼。不過假如你最后沒有在那一行輸入代碼,有些編輯器 就不會移除已經(jīng)加入的空白,就像你故意留下一個只有空白的行。包含行尾空白的行就 這樣產(chǎn)生了。
當(dāng) git 發(fā)現(xiàn)補(bǔ)丁包含了行尾空白的時候會警告你,并且可以應(yīng)你的要求去掉行尾空白;不過如果你是正在打一系列補(bǔ)丁,這樣做會導(dǎo)致后面的補(bǔ)丁失敗,因為你改變了補(bǔ)丁的 上下文。
4 命名
C 是一個簡樸的語言,你的命名也應(yīng)該這樣。和 Modula-2 和 Pascal 程序員不同, C 程序員不使用類似 ThisVariableIsATemporaryCounter 這樣華麗的名字。C 程序員會 稱那個變量為 tmp
,這樣寫起來會更容易,而且至少不會令其難于理解。
不過,雖然混用大小寫的名字是不提倡使用的,但是全局變量還是需要一個具描述性的 名字。稱一個全局函數(shù)為 foo
是一個難以饒恕的錯誤。
全局變量 (只有當(dāng)你 「真正」 需要它們的時候再用它) 需要有一個具描述性的名字,就 像全局函數(shù)。如果你有一個可以計算活動用戶數(shù)量的函數(shù),你應(yīng)該叫它count_active_users()
或者類似的名字,你不應(yīng)該叫它 cntuser()
。
在函數(shù)名中包含函數(shù)類型 (所謂的匈牙利命名法) 是腦子出了問題——編譯器知道那些類 型而且能夠檢查那些類型,這樣做只能把程序員弄糊涂了。難怪微軟總是制造出有問題 的程序。
本地變量名應(yīng)該簡短,而且能夠表達(dá)相關(guān)的含義。如果你有一些隨機(jī)的整數(shù)型的循環(huán)計 數(shù)器,它應(yīng)該被稱為 i
。叫它 loop_counter
并無益處,如果它沒有被誤解的 可能的話。類似的, tmp
可以用來稱呼任意類型的臨時變量。
如果你怕混淆了你的本地變量名,你就遇到另一個問題了,叫做函數(shù)增長荷爾蒙失衡綜 合癥。請看第六章 (函數(shù))。
5 Typedef
不要使用類似 vps_t
之類的東西。
對結(jié)構(gòu)體和指針使用 typedef 是一個 「錯誤」 。當(dāng)你在代碼里看到:
vps_t?a;
這代表什么意思呢?
相反,如果是這樣
struct?virtual_container?*a;
你就知道 a
是什么了。
很多人認(rèn)為 typedef 能提高可讀性
。實際不是這樣的。它們只在下列情況下有用:
-
完全不透明的對象 (這種情況下要主動使用 typedef 來 「隱藏」 這個對象實際上 是什么)。
例如:
pte_t
等不透明對象,你只能用合適的訪問函數(shù)來訪問它們。不透明性和 “訪問函數(shù)” 本身是不好的。我們使用 pte_t 等類型的原因在于真 的是完全沒有任何共用的可訪問信息。
-
清楚的整數(shù)類型,如此,這層抽象就可以 「幫助」 消除到底是
int
還是long
的混淆。u8/u16/u32 是完全沒有問題的 typedef,不過它們更符合類別 (d) 而不是這里。
要這樣做,必須事出有因。如果某個變量是
unsigned long
,那么沒有必要typedef unsigned long myflags_t
;不過如果有一個明確的原因,比如它在某種情況下可能會是一個
unsigned int
而在其他情況下可能為unsigned long
,那么就不要猶豫,請務(wù)必使用 typedef。 -
當(dāng)你使用 sparse 按字面的創(chuàng)建一個 「新」 類型來做類型檢查的時候。
-
和標(biāo)準(zhǔn) C99 類型相同的類型,在某些例外的情況下。
雖然讓眼睛和腦筋來適應(yīng)新的標(biāo)準(zhǔn)類型比如
uint32_t
不需要花很多時間,可 是有些人仍然拒絕使用它們。因此,Linux 特有的等同于標(biāo)準(zhǔn)類型的
u8/u16/u32/u64
類型和它們的有符號 類型是被允許的——盡管在你自己的新代碼中,它們不是強(qiáng)制要求要使用的。當(dāng)編輯已經(jīng)使用了某個類型集的已有代碼時,你應(yīng)該遵循那些代碼中已經(jīng)做出的選 擇。
-
可以在用戶空間安全使用的類型。
在某些用戶空間可見的結(jié)構(gòu)體里,我們不能要求 C99 類型而且不能用上面提到的
u32
類型。因此,我們在與用戶空間共享的所有結(jié)構(gòu)體中使用 __u32 和類似 的類型。
可能還有其他的情況,不過基本的規(guī)則是 「永遠(yuǎn)不要」 使用 typedef,除非你可以明 確的應(yīng)用上述某個規(guī)則中的一個。
總的來說,如果一個指針或者一個結(jié)構(gòu)體里的元素可以合理的被直接訪問到,那么它們 就不應(yīng)該是一個 typedef。
6 函數(shù)
函數(shù)應(yīng)該簡短而漂亮,并且只完成一件事情。函數(shù)應(yīng)該可以一屏或者兩屏顯示完 (我們 都知道 ISO/ANSI 屏幕大小是 80x24),只做一件事情,而且把它做好。
一個函數(shù)的最大長度是和該函數(shù)的復(fù)雜度和縮進(jìn)級數(shù)成反比的。所以,如果你有一個理 論上很簡單的只有一個很長 (但是簡單) 的 case 語句的函數(shù),而且你需要在每個 case 里做很多很小的事情,這樣的函數(shù)盡管很長,但也是可以的。
不過,如果你有一個復(fù)雜的函數(shù),而且你懷疑一個天分不是很高的高中一年級學(xué)生可能 甚至搞不清楚這個函數(shù)的目的,你應(yīng)該嚴(yán)格遵守前面提到的長度限制。使用輔助函數(shù), 并為之取個具描述性的名字 (如果你覺得它們的性能很重要的話,可以讓編譯器內(nèi)聯(lián)它 們,這樣的效果往往會比你寫一個復(fù)雜函數(shù)的效果要好。)
函數(shù)的另外一個衡量標(biāo)準(zhǔn)是本地變量的數(shù)量。此數(shù)量不應(yīng)超過 5-10 個,否則你的函數(shù) 就有問題了。重新考慮一下你的函數(shù),把它分拆成更小的函數(shù)。人的大腦一般可以輕松 的同時跟蹤 7 個不同的事物,如果再增多的話,就會糊涂了。即便你聰穎過人,你也可 能會記不清你 2 個星期前做過的事情。
在源文件里,使用空行隔開不同的函數(shù)。如果該函數(shù)需要被導(dǎo)出,它的 「EXPORT」 宏 應(yīng)該緊貼在它的結(jié)束大括號之下。比如:
int?system_is_up(void)
{
????????return?system_state?==?SYSTEM_RUNNING;
}
EXPORT_SYMBOL(system_is_up);
在函數(shù)原型中,包含函數(shù)名和它們的數(shù)據(jù)類型。雖然 C 語言里沒有這樣的要求,在 Linux 里這是提倡的做法,因為這樣可以很簡單的給讀者提供更多的有價值的信息。
7 集中的函數(shù)退出途徑
雖然被某些人聲稱已經(jīng)過時,但是 goto 語句的等價物還是經(jīng)常被編譯器所使用,具體 形式是無條件跳轉(zhuǎn)指令。
當(dāng)一個函數(shù)從多個位置退出,并且需要做一些類似清理的常見操作時,goto 語句就很方 便了。如果并不需要清理操作,那么直接 return 即可。
選擇一個能夠說明 goto 行為或它為何存在的標(biāo)簽名。如果 goto 要釋放 buffer
, 一個不錯的名字可以是 out_free_buffer:
。別去使用像 err1:
和 err2:
這樣的GW_BASIC 名稱,因為一旦你添加或刪除了 (函數(shù)的) 退出路徑,你就必須對它們 重新編號,這樣會難以去檢驗正確性。
使用 goto 的理由是:
-
無條件語句容易理解和跟蹤 -
嵌套程度減小 -
可以避免由于修改時忘記更新個別的退出點而導(dǎo)致錯誤 -
讓編譯器省去刪除冗余代碼的工作 ;)
int?fun(int?a)
{
????????int?result?=?0;
????????char?*buffer;
????????buffer?=?kmalloc(SIZE,?GFP_KERNEL);
????????if?(!buffer)
????????????????return?-ENOMEM;
????????if?(condition1)?{
????????????????while?(loop1)?{
????????????????????????...
????????????????}
????????????????result?=?1;
????????????????goto?out_free_buffer;
????????}
????????...
out_free_buffer:
????????kfree(buffer);
????????return?result;
}
一個需要注意的常見錯誤是 一個 err 錯誤
,就像這樣:
err:
????????kfree(foo->bar);
????????kfree(foo);
????????return?ret;
這段代碼的錯誤是,在某些退出路徑上 foo
是 NULL。通常情況下,通過把它分離 成兩個錯誤標(biāo)簽 err_free_bar:
和 err_free_foo:
來修復(fù)這個錯誤:
err_free_bar:
???????kfree(foo->bar);
err_free_foo:
???????kfree(foo);
???????return?ret;
理想情況下,你應(yīng)該模擬錯誤來測試所有退出路徑。
8 注釋
注釋是好的,不過有過度注釋的危險。永遠(yuǎn)不要在注釋里解釋你的代碼是如何運作的:更好的做法是讓別人一看你的代碼就可以明白,解釋寫的很差的代碼是浪費時間。
一般的,你想要你的注釋告訴別人你的代碼做了什么,而不是怎么做的。也請你不要把 注釋放在一個函數(shù)體內(nèi)部:如果函數(shù)復(fù)雜到你需要獨立的注釋其中的一部分,你很可能 需要回到第六章看一看。你可以做一些小注釋來注明或警告某些很聰明 (或者槽糕) 的 做法,但不要加太多。你應(yīng)該做的,是把注釋放在函數(shù)的頭部,告訴人們它做了什么, 也可以加上它做這些事情的原因。
當(dāng)注釋內(nèi)核 API 函數(shù)時,請使用 kernel-doc 格式。請看 Documentation/doc-guide/ 和 scripts/kernel-doc 以獲得詳細(xì)信息。
長 (多行) 注釋的首選風(fēng)格是:
/*
?*?This?is?the?preferred?style?for?multi-line
?*?comments?in?the?Linux?kernel?source?code.
?*?Please?use?it?consistently.
?*
?*?Description:??A?column?of?asterisks?on?the?left?side,
?*?with?beginning?and?ending?almost-blank?lines.
?*/
對于在 net/ 和 drivers/net/ 的文件,首選的長 (多行) 注釋風(fēng)格有些不同。
/*?The?preferred?comment?style?for?files?in?net/?and?drivers/net
?*?looks?like?this.
?*
?*?It?is?nearly?the?same?as?the?generally?preferred?comment?style,
?*?but?there?is?no?initial?almost-blank?line.
?*/
注釋數(shù)據(jù)也是很重要的,不管是基本類型還是衍生類型。為了方便實現(xiàn)這一點,每一行 應(yīng)只聲明一個數(shù)據(jù) (不要使用逗號來一次聲明多個數(shù)據(jù))。這樣你就有空間來為每個數(shù)據(jù) 寫一段小注釋來解釋它們的用途了。
9 你已經(jīng)把事情弄糟了
這沒什么,我們都是這樣。可能你的使用了很長時間 Unix 的朋友已經(jīng)告訴你 GNU emacs
能自動幫你格式化 C 源代碼,而且你也注意到了,確實是這樣,不過它 所使用的默認(rèn)值和我們想要的相去甚遠(yuǎn) (實際上,甚至比隨機(jī)打的還要差——無數(shù)個猴子 在 GNU emacs 里打字永遠(yuǎn)不會創(chuàng)造出一個好程序) (譯注:Infinite Monkey Theorem)
所以你要么放棄 GNU emacs,要么改變它讓它使用更合理的設(shè)定。要采用后一個方案, 你可以把下面這段粘貼到你的 .emacs 文件里。
(defun?c-lineup-arglist-tabs-only?(ignored)
??"Line?up?argument?lists?by?tabs,?not?spaces"
??(let*?((anchor?(c-langelem-pos?c-syntactic-element))
?????????(column?(c-langelem-2nd-pos?c-syntactic-element))
?????????(offset?(-?(1+?column)?anchor))
?????????(steps?(floor?offset?c-basic-offset)))
????(*?(max?steps?1)
???????c-basic-offset)))
(add-hook?'c-mode-common-hook
??????????(lambda?()
????????????;;?Add?kernel?style
????????????(c-add-style
?????????????"linux-tabs-only"
?????????????'("linux"?(c-offsets-alist
????????????????????????(arglist-cont-nonempty
?????????????????????????c-lineup-gcc-asm-reg
?????????????????????????c-lineup-arglist-tabs-only))))))
(add-hook?'c-mode-hook
??????????(lambda?()
????????????(let?((filename?(buffer-file-name)))
??????????????;;?Enable?kernel?mode?for?the?appropriate?files
??????????????(when?(and?filename
?????????????????????????(string-match?(expand-file-name?"~/src/linux-trees")
???????????????????????????????????????filename))
????????????????(setq?indent-tabs-mode?t)
????????????????(setq?show-trailing-whitespace?t)
????????????????(c-set-style?"linux-tabs-only")))))
這會讓 emacs 在 ~/src/linux-trees
下的 C 源文件獲得更好的內(nèi)核代碼風(fēng)格。
不過就算你嘗試讓 emacs 正確的格式化代碼失敗了,也并不意味著你失去了一切:還可 以用 indent
。
不過,GNU indent 也有和 GNU emacs 一樣有問題的設(shè)定,所以你需要給它一些命令選 項。不過,這還不算太糟糕,因為就算是 GNU indent 的作者也認(rèn)同 K&R 的權(quán)威性 (GNU 的人并不是壞人,他們只是在這個問題上被嚴(yán)重的誤導(dǎo)了),所以你只要給 indent 指定選項 -kr -i8
(代表 K&R,8 字符縮進(jìn)
),或使用 scripts/Lindent
這樣就可以以最時髦的方式縮進(jìn)源代碼。
indent
有很多選項,特別是重新格式化注釋的時候,你可能需要看一下它的手冊。不過記?。?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">indent 不能修正壞的編程習(xí)慣。
10 Kconfig 配置文件
對于遍布源碼樹的所有 Kconfig* 配置文件來說,它們縮進(jìn)方式有所不同。緊挨著 config
定義的行,用一個制表符縮進(jìn),然而 help 信息的縮進(jìn)則額外增加 2 個空 格。舉個例子:
config?AUDIT
??????bool?"Auditing?support"
??????depends?on?NET
??????help
????????Enable?auditing?infrastructure?that?can?be?used?with?another
????????kernel?subsystem,?such?as?SELinux?(which?requires?this?for
????????logging?of?avc?messages?output).??Does?not?do?system-call
????????auditing?without?CONFIG_AUDITSYSCALL.
而那些危險的功能 (比如某些文件系統(tǒng)的寫支持) 應(yīng)該在它們的提示字符串里顯著的聲 明這一點:
config?ADFS_FS_RW
??????bool?"ADFS?write?support?(DANGEROUS)"
??????depends?on?ADFS_FS
??????...
要查看配置文件的完整文檔,請看 Documentation/kbuild/kconfig-language.txt。
11 數(shù)據(jù)結(jié)構(gòu)
如果一個數(shù)據(jù)結(jié)構(gòu),在創(chuàng)建和銷毀它的單線執(zhí)行環(huán)境之外可見,那么它必須要有一個引 用計數(shù)器。內(nèi)核里沒有垃圾收集 (并且內(nèi)核之外的垃圾收集慢且效率低下),這意味著你 絕對需要記錄你對這種數(shù)據(jù)結(jié)構(gòu)的使用情況。
引用計數(shù)意味著你能夠避免上鎖,并且允許多個用戶并行訪問這個數(shù)據(jù)結(jié)構(gòu)——而不需要 擔(dān)心這個數(shù)據(jù)結(jié)構(gòu)僅僅因為暫時不被使用就消失了,那些用戶可能不過是沉睡了一陣或 者做了一些其他事情而已。
注意上鎖 「不能」 取代引用計數(shù)。上鎖是為了保持?jǐn)?shù)據(jù)結(jié)構(gòu)的一致性,而引用計數(shù)是一 個內(nèi)存管理技巧。通常二者都需要,不要把兩個搞混了。
很多數(shù)據(jù)結(jié)構(gòu)實際上有 2 級引用計數(shù),它們通常有不同 類
的用戶。子類計數(shù)器統(tǒng) 計子類用戶的數(shù)量,每當(dāng)子類計數(shù)器減至零時,全局計數(shù)器減一。
這種 多級引用計數(shù)
的例子可以在內(nèi)存管理 (struct mm_struct
: mm_users 和 mm_count),和文件系統(tǒng) (struct super_block
: s_count 和 s_active) 中找到。
記?。喝绻硪粋€執(zhí)行線索可以找到你的數(shù)據(jù)結(jié)構(gòu),但這個數(shù)據(jù)結(jié)構(gòu)沒有引用計數(shù)器, 這里幾乎肯定是一個 bug。
12 宏,枚舉和RTL
用于定義常量的宏的名字及枚舉里的標(biāo)簽需要大寫。
#define?CONSTANT?0x12345
在定義幾個相關(guān)的常量時,最好用枚舉。
宏的名字請用大寫字母,不過形如函數(shù)的宏的名字可以用小寫字母。
一般的,如果能寫成內(nèi)聯(lián)函數(shù)就不要寫成像函數(shù)的宏。
含有多個語句的宏應(yīng)該被包含在一個 do-while 代碼塊里:
#define?macrofun(a,?b,?c)???????????????????????\
????????do?{????????????????????????????????????\
????????????????if?(a?==?5)?????????????????????\
????????????????????????do_this(b,?c);??????????\
????????}?while?(0)
使用宏的時候應(yīng)避免的事情:
-
影響控制流程的宏:
#define?FOO(x)??????????????????????????????????\
????????do?{????????????????????????????????????\
????????????????if?(blah(x)?0)????????????????\
????????????????????????return?-EBUGGERED;??????\
????????}?while?(0)
「非?!?/strong> 不好。它看起來像一個函數(shù),不過卻能導(dǎo)致 調(diào)用
它的函數(shù)退出;不要打 亂讀者大腦里的語法分析器。
-
依賴于一個固定名字的本地變量的宏:
#define?FOO(val)?bar(index,?val)
可能看起來像是個不錯的東西,不過它非常容易把讀代碼的人搞糊涂,而且容易導(dǎo)致看起 來不相關(guān)的改動帶來錯誤。
-
作為左值的帶參數(shù)的宏:FOO(x) = y;如果有人把 FOO 變成一個內(nèi)聯(lián)函數(shù)的話,這 種用法就會出錯了。 -
忘記了優(yōu)先級:使用表達(dá)式定義常量的宏必須將表達(dá)式置于一對小括號之內(nèi)。帶參數(shù) 的宏也要注意此類問題。
#define?CONSTANT?0x4000
#define?CONSTEXP?(CONSTANT?|?3)
-
在宏里定義類似函數(shù)的本地變量時命名沖突:
#define?FOO(x)??????????????????????????\
({??????????????????????????????????????\
????????typeof(x)?ret;??????????????????\
????????ret?=?calc_ret(x);??????????????\
????????(ret);??????????????????????????\
})
ret 是本地變量的通用名字 - __foo_ret 更不容易與一個已存在的變量沖突。
cpp 手冊對宏的講解很詳細(xì)。gcc internals 手冊也詳細(xì)講解了 RTL,內(nèi)核里的匯編語 言經(jīng)常用到它。
13 打印內(nèi)核消息
內(nèi)核開發(fā)者應(yīng)該是受過良好教育的。請一定注意內(nèi)核信息的拼寫,以給人以好的印象。不要用不規(guī)范的單詞比如 dont
,而要用 do not
或者 don't
。保證這些信 息簡單明了,無歧義。
內(nèi)核信息不必以英文句號結(jié)束。
在小括號里打印數(shù)字 (%d) 沒有任何價值,應(yīng)該避免這樣做。
里有一些驅(qū)動模型診斷宏,你應(yīng)該使用它們,以確保信息對應(yīng)于正確 的設(shè)備和驅(qū)動,并且被標(biāo)記了正確的消息級別。這些宏有:dev_err()
, dev_warn()
, dev_info()
等等。對于那些不和某個特定設(shè)備相關(guān)連的信息,
定義了 pr_notice()
, pr_info()
, pr_warn()
, pr_err()
和其他。
寫出好的調(diào)試信息可以是一個很大的挑戰(zhàn);一旦你寫出后,這些信息在遠(yuǎn)程除錯時能提 供極大的幫助。然而打印調(diào)試信息的處理方式同打印非調(diào)試信息不同。其他 pr_XXX() 函數(shù)能無條件地打印,pr_debug() 卻不;默認(rèn)情況下它不會被編譯,除非定義了 DEBUG 或設(shè)定了 CONFIG_DYNAMIC_DEBUG。實際這同樣是為了 dev_dbg(),一個相關(guān)約定是在一 個已經(jīng)開啟了 DEBUG 時,使用 VERBOSE_DEBUG 來添加 dev_vdbg()。
許多子系統(tǒng)擁有 Kconfig 調(diào)試選項來開啟 -DDEBUG 在對應(yīng)的 Makefile 里面;在其他 情況下,特殊文件使用 #define DEBUG。當(dāng)一條調(diào)試信息需要被無條件打印時,例如, 如果已經(jīng)包含一個調(diào)試相關(guān)的 #ifdef 條件,printk(KERN_DEBUG ...) 就可被使用。
14 分配內(nèi)存
內(nèi)核提供了下面的一般用途的內(nèi)存分配函數(shù):kmalloc()
, kzalloc()
, kmalloc_array()
, kcalloc()
, vmalloc()
和 vzalloc()
。請參考 API 文檔以獲取有關(guān)它們的詳細(xì)信息。
傳遞結(jié)構(gòu)體大小的首選形式是這樣的:
p?=?kmalloc(sizeof(*p),?...);
另外一種傳遞方式中,sizeof 的操作數(shù)是結(jié)構(gòu)體的名字,這樣會降低可讀性,并且可能 會引入 bug。有可能指針變量類型被改變時,而對應(yīng)的傳遞給內(nèi)存分配函數(shù)的 sizeof 的結(jié)果不變。
強(qiáng)制轉(zhuǎn)換一個 void 指針返回值是多余的。C 語言本身保證了從 void 指針到其他任何 指針類型的轉(zhuǎn)換是沒有問題的。
分配一個數(shù)組的首選形式是這樣的:
p?=?kmalloc_array(n,?sizeof(...),?...);
分配一個零長數(shù)組的首選形式是這樣的:
p?=?kcalloc(n,?sizeof(...),?...);
兩種形式檢查分配大小 n * sizeof(...) 的溢出,如果溢出返回 NULL。
15 內(nèi)聯(lián)弊病
有一個常見的誤解是 內(nèi)聯(lián)
是 gcc 提供的可以讓代碼運行更快的一個選項。雖然使用內(nèi)聯(lián)函數(shù)有時候是恰當(dāng)?shù)?(比如作為一種替代宏的方式,請看第十二章),不過很多情 況下不是這樣。「inline 的過度使用會使內(nèi)核變大」,從而使整個系統(tǒng)運行速度變慢。因為體積大內(nèi)核會占用更多的指令高速緩存,而且會導(dǎo)致 pagecache 的可用內(nèi)存減少。想象一下,一次 pagecache 未命中就會導(dǎo)致一次磁盤尋址,將耗時 5 毫秒。
「5 毫秒的 時間內(nèi) CPU 能執(zhí)行很多很多指令?!?/strong>
一個基本的原則是如果一個函數(shù)有 3 行以上,就不要把它變成內(nèi)聯(lián)函數(shù)。這個原則的一 個例外是,如果你知道某個參數(shù)是一個編譯時常量,而且因為這個常量你確定編譯器在 編譯時能優(yōu)化掉你的函數(shù)的大部分代碼,那仍然可以給它加上 inline 關(guān)鍵字。kmalloc() 內(nèi)聯(lián)函數(shù)就是一個很好的例子。
人們經(jīng)常主張給 static 的而且只用了一次的函數(shù)加上 inline,如此不會有任何損失, 因為沒有什么好權(quán)衡的。雖然從技術(shù)上說這是正確的,但是實際上這種情況下即使不加 inline gcc 也可以自動使其內(nèi)聯(lián)。而且其他用戶可能會要求移除 inline,由此而來的 爭論會抵消 inline 自身的潛在價值,得不償失。
16 函數(shù)返回值及命名
函數(shù)可以返回多種不同類型的值,最常見的一種是表明函數(shù)執(zhí)行成功或者失敗的值。這樣 的一個值可以表示為一個錯誤代碼整數(shù) (-Exxx=失敗,0=成功) 或者一個 成功
布爾值 (0=失敗,非0=成功)。
混合使用這兩種表達(dá)方式是難于發(fā)現(xiàn)的 bug 的來源。如果 C 語言本身嚴(yán)格區(qū)分整形和 布爾型變量,那么編譯器就能夠幫我們發(fā)現(xiàn)這些錯誤... 不過 C 語言不區(qū)分。為了避免 產(chǎn)生這種 bug,請遵循下面的慣例:
如果函數(shù)的名字是一個動作或者強(qiáng)制性的命令,那么這個函數(shù)應(yīng)該返回錯誤代
碼整數(shù)。如果是一個判斷,那么函數(shù)應(yīng)該返回一個?"成功"?布爾值。
比如, add work
是一個命令,所以 add_work() 在成功時返回 0,在失敗時返回 -EBUSY。類似的,因為 PCI device present
是一個判斷,所以 pci_dev_present() 在成功找到一個匹配的設(shè)備時應(yīng)該返回 1,如果找不到時應(yīng)該返回 0。
所有 EXPORTed 函數(shù)都必須遵守這個慣例,所有的公共函數(shù)也都應(yīng)該如此。私有 (static) 函數(shù)不需要如此,但是我們也推薦這樣做。
返回值是實際計算結(jié)果而不是計算是否成功的標(biāo)志的函數(shù)不受此慣例的限制。一般的, 他們通過返回一些正常值范圍之外的結(jié)果來表示出錯。典型的例子是返回指針的函數(shù), 他們使用 NULL 或者 ERR_PTR 機(jī)制來報告錯誤。
17 不要重新發(fā)明內(nèi)核宏
頭文件 include/linux/kernel.h
包含了一些宏,你應(yīng)該使用它們,而不要自己寫一些 它們的變種。比如,如果你需要計算一個數(shù)組的長度,使用這個宏
#define?ARRAY_SIZE(x)?(sizeof(x)?/?sizeof((x)[0]))
類似的,如果你要計算某結(jié)構(gòu)體成員的大小,使用
#define?FIELD_SIZEOF(t,?f)?(sizeof(((t*)0)->f))
還有可以做嚴(yán)格的類型檢查的 min() 和 max() 宏,如果你需要可以使用它們。你可以 自己看看那個頭文件里還定義了什么你可以拿來用的東西,如果有定義的話,你就不應(yīng) 在你的代碼里自己重新定義。
18 編輯器模式行和其他需要羅嗦的事情
有一些編輯器可以解釋嵌入在源文件里的由一些特殊標(biāo)記標(biāo)明的配置信息。比如,emacs 能夠解釋被標(biāo)記成這樣的行:
-*-?mode:?c?-*-
或者這樣的:
/*
Local?Variables:
compile-command:?"gcc?-DMAGIC_DEBUG_FLAG?foo.c"
End:
*/
Vim 能夠解釋這樣的標(biāo)記:
/*?vim:set?sw=8?noet?*/
不要在源代碼中包含任何這樣的內(nèi)容。
每個人都有他自己的編輯器配置,你的源文件不 應(yīng)該覆蓋別人的配置。這包括有關(guān)縮進(jìn)和模式配置的標(biāo)記。人們可以使用他們自己定制 的模式,或者使用其他可以產(chǎn)生正確的縮進(jìn)的巧妙方法。
19 內(nèi)聯(lián)匯編
在特定架構(gòu)的代碼中,你可能需要內(nèi)聯(lián)匯編與 CPU 和平臺相關(guān)功能連接。需要這么做時 就不要猶豫。然而,當(dāng) C 可以完成工作時,不要平白無故地使用內(nèi)聯(lián)匯編。在可能的情 況下,你可以并且應(yīng)該用 C 和硬件溝通。
請考慮去寫捆綁通用位元 (wrap common bits) 的內(nèi)聯(lián)匯編的簡單輔助函數(shù),別去重復(fù) 地寫下只有細(xì)微差異內(nèi)聯(lián)匯編。記住內(nèi)聯(lián)匯編可以使用 C 參數(shù)。
大型,有一定復(fù)雜度的匯編函數(shù)應(yīng)該放在 .S 文件內(nèi),用相應(yīng)的 C 原型定義在 C 頭文 件中。匯編函數(shù)的 C 原型應(yīng)該使用 asmlinkage
。
你可能需要把匯編語句標(biāo)記為 volatile,用來阻止 GCC 在沒發(fā)現(xiàn)任何副作用后就把它 移除了。你不必總是這樣做,盡管,這不必要的舉動會限制優(yōu)化。
在寫一個包含多條指令的單個內(nèi)聯(lián)匯編語句時,把每條指令用引號分割而且各占一行, 除了最后一條指令外,在每個指令結(jié)尾加上 nt,讓匯編輸出時可以正確地縮進(jìn)下一條 指令:
asm?("magic?%reg1,?#42\n\t"
?????"more_magic?%reg2,?%reg3"
?????:?/*?outputs?*/?:?/*?inputs?*/?:?/*?clobbers?*/);
20 條件編譯
只要可能,就不要在 .c 文件里面使用預(yù)處理條件 (#if, #ifdef);這樣做讓代碼更難 閱讀并且更難去跟蹤邏輯。替代方案是,在頭文件中用預(yù)處理條件提供給那些 .c 文件 使用,再給 #else 提供一個空樁 (no-op stub) 版本,然后在 .c 文件內(nèi)無條件地調(diào)用 那些 (定義在頭文件內(nèi)的) 函數(shù)。這樣做,編譯器會避免為樁函數(shù) (stub) 的調(diào)用生成 任何代碼,產(chǎn)生的結(jié)果是相同的,但邏輯將更加清晰。
最好傾向于編譯整個函數(shù),而不是函數(shù)的一部分或表達(dá)式的一部分。與其放一個 ifdef 在表達(dá)式內(nèi),不如分解出部分或全部表達(dá)式,放進(jìn)一個單獨的輔助函數(shù),并應(yīng)用預(yù)處理 條件到這個輔助函數(shù)內(nèi)。
如果你有一個在特定配置中,可能變成未使用的函數(shù)或變量,編譯器會警告它定義了但 未使用,把它標(biāo)記為 __maybe_unused 而不是將它包含在一個預(yù)處理條件中。(然而,如 果一個函數(shù)或變量總是未使用,就直接刪除它。)
在代碼中,盡可能地使用 IS_ENABLED 宏來轉(zhuǎn)化某個 Kconfig 標(biāo)記為 C 的布爾 表達(dá)式,并在一般的 C 條件中使用它:
if?(IS_ENABLED(CONFIG_SOMETHING))?{
????????...
}
編譯器會做常量折疊,然后就像使用 #ifdef 那樣去包含或排除代碼塊,所以這不會帶 來任何運行時開銷。然而,這種方法依舊允許 C 編譯器查看塊內(nèi)的代碼,并檢查它的正 確性 (語法,類型,符號引用,等等)。因此,如果條件不滿足,代碼塊內(nèi)的引用符號就 不存在時,你還是必須去用 #ifdef。
在任何有意義的 #if 或 #ifdef 塊的末尾 (超過幾行的),在 #endif 同一行的后面寫下 注解,注釋這個條件表達(dá)式。例如:
#ifdef?CONFIG_SOMETHING
...
#endif?/*?CONFIG_SOMETHING?*/
原創(chuàng)不易,歡迎轉(zhuǎn)發(fā)、留言、點贊、分享給你的朋友,感謝您的支持!
長按識別二維碼關(guān)注獲取更多內(nèi)容
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!