Linux核心程式設計規範與程式碼風格
source: https://www.kernel.org/doc/html/latest/process/coding-style.htmltranslated by trav, [email protected]
這是一篇闡述Linux核心程式設計程式碼風格的文件,譯者以學習為目的進行翻譯。
1 縮排
Tab的寬度是八個字元,因此縮排的寬度也是八個字元。有些異教徒想讓縮排變成四個字元,甚至是兩個字元的寬度,這些人和那些把 PI 定義為 3 的人是一個路子的。
注意:縮排的全部意義在於清晰地定義語句塊的開始與結束,特別是當你盯著螢幕20個小時之後,你會發現長的縮排寬度的作用。
現在有些人說八個字元的寬度太寬了,這會讓程式碼往右移很遠,在一塊八十字元寬的螢幕上,這樣的程式碼會很難閱讀。對此的回答是,如果你寫的程式碼需要超過三層的縮排,那麼你把一切都搞砸了,你應該修復你的程式。
簡而言之,八個字元寬度的縮排讓程式碼更容易閱讀,並且額外的好處就是提醒你,不要在一個函式裡寫太多層的巢狀邏輯。請記住這個警示。
switch語句的縮排方式是讓case與switch對齊:
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;
對了,不要把多個賦值語句放在同一行,核心的程式碼風格是十分簡潔的,請儘量避免使用複雜的表示式。
除了在註釋、文件和Kconfig中,永遠不要使用空格作為縮排,上面的例子是故意犯的錯誤。
找一個像樣的編輯器,不要在行末留有空格。
2 換行
規範程式碼風格的目的是提高程式碼的可讀性和維護性。
單行的寬度限制為八十列,這是強烈推薦的設定。
任何一行超過八十列寬度的語句都應該拆分成多個行,除非超過八十列的部分可以提高可讀性且不會隱藏資訊。拆分出來的子句長度總是應該比其主句要短,並且應該儘量靠右。這條法則同樣適用於一個有很長的引數列表的函式頭。然而,千萬不要把使用者可見的字串,比如 printk 的資訊,拆分成多行,因為這樣會導致使用 grep 的時候找不到這些資訊。
3 括號與空格
另一個關於 C 程式碼風格的議題就是大括號的位置。這個問題不像縮排那麼具有技術性,我們並不能說某一種風格要在技術上優於另一種風格。但是我們更推薦的,就是有遠見的 Kernighan 和 Ritchie 展示的方式,把左括號放在行末,把右括號放在行首:
if (x is true) { we do y }
這同樣適用於其他非函式的語句塊 (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; }
然而,有一個特殊的例子,就是函式:函式的左括號應該放在行首:
int function(int x) { body of function }
異教徒們會認為這樣的風格是不一致的,但是所有有腦子的人都知道盡管是 K&R 也是不一致的(譯者注:K&R這本書的第一版和第二版有不一致的地方)。除此之外,我們知道函式是很特殊的,在 C 語言中,你不能有巢狀函式。
注意到,右括號一般是單獨成一行的,除非右括號之後緊隨著緊密結合的語句,例如 do-while 語句和 if 語句:
do { body of do-loop } while (condition);
以及
if (x == y) { .. } else if (x > y) { ... } else { .... }
依據:K&R
注意到,這種風格應該在不降低可讀性的前提下儘可能減少空行的數量。想一想,在一塊只有 25 行的螢幕上,無用的換行少了,那麼就有更多的空行來寫註釋。
當單行語句可以解決的時候,不要使用沒必要的括號:
if (condition) action();
以及
if (condition) do_this(); else do_that();
這一點不適用於只有一個 case 有單行,其他 case 有多行的情況:
if (condition) { do_this(); do_that(); } else { otherwise(); }
在一個迴圈中超過一個語句的情況也同樣需要使用括號:
while (condition) { if (test) do_something(); }
3.1 空格
Linux 核心風格的空格主要用在一些關鍵字上,即在關鍵字之後添一個空格。值得關注的例外是一些長得像函式的關鍵字,比如:sizeof ,typeof ,alignof , attribute ,在 Linux 中,這些關鍵字的使用都會帶上一對括號,儘管在 C 語言的使用上並不需要帶上括號。
所以在下面這些關鍵字之後新增一個空格:
if, switch, case, for, do, while
但是不要新增在sizeof ,typeof ,alignof , attribute 之後:
s = sizeof(struct file);
不要在括號周圍多此一舉的新增空格,下面這個例子糟透了:
s = sizeof( struct file );
在宣告指標或者返回值為指標的函式時,星號的位置應該緊靠著變數名或函式名,而不是型別名,例如:
char *linux_banner; unsigned long long memparse(char *ptr, char **retptr); char *match_strdup(substring_t *s);
在二元操作符和三元操作符周圍新增一個空格,例如:
=+-<>*/%|&^<=>===!=?:
但是不要在一元操作符之後新增空格:
&*+-~!sizeoftypeofalignof__attribute__defined
不要在後綴的自增自減一元操作符之前新增空格:
++--
不要在字首的自增自減一元操作符之後新增空格:
++--
不要在結構體成員操作符周圍新增空格:
.->
不要在行末新增多餘的空格。一些編輯器的“智慧”縮排會幫你在行首新增一些空格,好讓你在下一行可以立即寫程式碼。但是某些編輯器不會幫你把多餘的空格給刪掉,儘管你已經寫完了一行程式碼。比如你只想留一行空行,但是編輯器卻“好心”地幫你填上了一些空格。這樣一來,你就在行末添加了多餘的空格。
Git 通常會警告你,讓你除去這些多餘的空格,並且可以幫你刪掉這些東西。但是,如果你讓 Git 一直幫你這樣修補你的程式碼,這很可能導致程式碼行的上下錯亂,之後的自動修補的失敗。
4 命名
C 是一種簡潔粗曠的語言,因此,你的命名也應該是簡潔的。C 程式設計師不會像 Modula-2 和 Pascal 程式設計師那樣使用 ThisVariableIsATemporaryCounter 這種“可愛”的名字,一個 C 程式設計師會把這種變數命名為 tmp ,如此簡潔易寫。
儘管看到一個混合大小寫的名字讓人皺眉,不過對於全域性變數來說,一個具有描述性的名字還是很有必要的。去呼叫一個名為 foo 的全域性函式同樣讓人難以接受。
全域性變數(只有當你真正需要的時候才用它)和全域性函式需要使用描述性的名字。如果你有一個計算活躍使用者數量的函式,你應該起這樣一個名字count_active_users()
或者類似的,而不是這樣一個名字cntusr()
。
起一個包含函式型別的名字(匈牙利命名法)是摧殘大腦的行為,編譯器知道函式的型別並且會檢查型別,這樣的名字不會起到任何幫助,它僅僅會迷惑程式設計師。所以,也難怪微軟做出了那麼多充滿了 bug 的程式。
區域性變數名應該簡短,如果你需要寫一個迴圈,定義一個計數器,在不產生歧義的情況下,你大可命名為 i ,命名為 loop_counter 是生產力很低的行為。同樣地,tmp 可以是任何型別的臨時變數。
如果你擔心會弄混變數名,那麼你遇到了另一個問題,你患上了函式增長荷爾蒙失調綜合症。
5 Typedefs
請不要使用 vps_t 這種東西,這是 typedef 的錯誤用法,當你看到
vps_t a;
這種寫法時,它究竟是個什麼東西?相反,如果是這樣的寫法
struct virtual_container *a;
你就很容易知道 a 代表著什麼。
很多人認為 typedef 是用來幫助提高可讀性的,但是事實往往不是這樣的。typedef 僅僅有如下用處:
a. 封裝物件(typedef 可以方便的隱藏物件)
例如,pte_t 會把物件封裝起來,你僅僅只能通過合適的“訪問函式”(成員函式)來訪問這個物件。
注意:封裝和“訪問函式”(成員函式)本身就不是好東西,我們使用 pte_t 這種東西的理由就是,它指向的物件本身絕對沒有東西可以訪問(我們壓根兒不使用封裝和成員函式那一套)。
b. 指明整數型別,這種抽象可以幫助我們避免一些使用 int 和 long 的疑慮
u8/u16/u32 是完美的使用 typedef 的例子。
注意:你必須要有明確的理由來使用這些用法,如果一些地方使用的本身就是 unsigned long ,那麼你沒有任何理由這樣做
typedef unsigned long myflags_t;
但是如果你有明確的理由來解釋為什麼在某種情況下使用 unsigned int,而在其他情況下使用 unsigned long,那麼大可使用 typedef。
c. 使用 sparse 去新建一個型別來做型別檢查
d. 在某些情況下新建一個與 C99 標準相等的型別
儘管只需要花一小段眼睛和大腦的時間來適應新標準的型別,如 uint32_t,但是一些人還是反對使用他們。
因此,你可以使用 Linux 獨有的 u8/u16/u32/u64 和他們的有符號版本,也可以使用和他們等價的新標準的型別,他們的使用都不是強制的。
當你所編輯的程式碼已經使用了某一種版本時,你應該按照原樣使用相同的版本。
e. 使用者空間中的型別安全
使用者空間中的某些特定的結構體中,我們不能使用 C99 定義的新型別以及上述的 u32,取而代之,我們統一使用 __u32 之類的型別。
也許還有其他情況,但是基本的規則就是,如果你不能滿足上述其中一條情況,你就永遠不要使用 typedef。
通常,一個指標或者一個有可訪問元素的結構體,都不應該使用 typedef。
6 函式
函式應該短小精悍,一個函式只幹一件事。一個函式的程式碼兩個螢幕就應該裝得下(ISO/ANSI標準螢幕大小是80x24),簡單說就是,做一件事並且把它做好。
數的最大長度與函式的複雜度和縮排程度成反比,所以,如果你有一個簡單的函式,函式裡面只是需要處理一個又一個的 case,每個 case 只是幹一些小事,函式長度長一些也沒關係。
然而,如果你的函式十分複雜,你懷疑一個不像你一樣天才的高中生看不懂,你應該遵守函式最大的長度的限制,使用一些有描述性名稱的輔助函式。如果你認為函式的效能至關重要,你可以讓編譯器把這些輔助函式編譯成行內函數,一般情況下編譯器可以比你做得更好。
另一個測量函式的因素是區域性變數的數量,他們不應該超出5-10個這個範圍,否則你就犯了一些錯誤。重新思考這個函式,把它拆分成更小的幾段。人類的大腦一般只能同時關注七件不同的事,更多需要關注的事情意味著更多的困擾。儘管你認為你是個天才,但是你也希望理解一段你兩週之前寫的程式碼。
在原始檔中,用一個空行分割不同的函式,如果函式需要匯出到外部使用,那麼它對應的 EXPORT 巨集應當緊隨在函式之後,例如:
int system_is_up(void) { return system_state == SYSTEM_RUNNING; } EXPORT_SYMBOL(system_is_up);
函式原型中,引數名應該與引數型別引起寫出來,儘管 C 語言允許只寫上引數型別,但是我們更推薦引數名,因為這是一種為讀者提供有價值資訊的簡單方式。
不要在函式原型之前使用extern
關鍵字,因為這是不必要且多餘的。
7 集中函數出口
儘管許多人反對,但是 goto 語句頻繁地以無條件跳轉的形式被編譯器使用。
當函式有多個出口,並且返回之前需要做很多相似的工作時,比如清理空間,這時候 goto 語句是十分方便的。當然了,如果沒有類似的清理工作要在返回之前做,那麼直接返回即可。
根據 goto 的作用來決定一個 label 的名字,如果 goto 語言要去釋放快取,那麼out_free_buffer:
會是一個好名字。避免使用 GW-BASIC 的命名方式,比如err1:
err2:
,因為當你需要新加或者刪除某些函數出口時,你就需要重新排列標籤數字,這會讓程式碼的正確性難以得到保證。
使用 goto 的理由如下:
無條件跳轉易於理解和閱讀
可以減少巢狀
可以減少修改個別函數出口程式碼所造成的錯誤
算是幫助編譯器做了一些優化的工作
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; }
一個常見的 bug 被稱作 one err bug,它長得像這樣:
err: kfree(foo->bar); kfree(foo); return ret;
bug 在於某些 goto 語句跳轉到此時,foo 仍然是 NULL,修復此 bug 的簡單方式就是將一個 label 拆分成兩個,err_free_bar:
和err_free_foo:
:
err_free_bar: kfree(foo->bar); err_free_foo: kfree(foo); return ret;
事實上,你應該進行測試,模擬錯誤情況的發生,測試所有的出口程式碼。
8 註釋
註釋是好的,但是要避免過分註釋。永遠不要去嘗試解釋你的程式碼如何工作,而是花時間在寫出好的程式碼來,解釋一段爛程式碼是浪費時間。
一般來說,你應該去說明你的程式碼做了什麼,而不是怎麼做。同樣地,儘量避免在函式體內寫註釋,如果你的函式如此複雜,以致於你需要在函式體內分幾段註釋來解釋,那麼你應該回到第六節去看看。你可以寫一小段的註釋來標記或者提醒大家哪些地方寫得真聰明(或者真爛),但是不要做得太過分。除此之外,你應該把註釋寫在函式開頭,告訴人們這個函式幹了什麼,為什麼要這樣幹。
當你給 kernel API 進行註釋的時候,請你使用 kernel-doc 的格式。具體參見 https://www.kernel.org/doc/html/latest/doc-guide/index.html#doc-guide
多行註釋推薦的格式如下:
/* * 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/ 中的檔案,推薦的多行註釋格式如下:
/* 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. */
對一些資料和變數進行註釋也是必要的,無論他們是基本型別的還是派生型別的。為了進行註釋,你應該在一行內只宣告一個變數,不要使用逗號進行多個宣告,這讓你有地方對每一個變數進行註釋。
9 你已經弄得一團糟
沒關係,我們都犯過錯。你的那些 Unix 的老手朋友們可能會告訴你,GNU emacs 能幫你自動地對 C 程式碼進行排版,你也注意到它確實可以。但是它預設的排版方式真的很糟糕,事實上,即便是在鍵盤上亂敲也比它來的好看。相信我,無數的猴子在 GNU emacs 上亂敲是不會做出好的程式的。
因此,你可以選擇直接把 GNU emacs 給刪了,或者修一修它,讓它恢復正常。如果你選擇了後者,那麼請把下面的東西拷貝到你的 .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 更好地滿足核心的程式碼風格。
但是即使你不能讓你的 emacs 恢復正常,也有解救方法:使用 indent 。
同樣的問題出現了,GNU indent 和 GNU emacs 有同樣的問題,因此你需要一些命令列選項來進行配置。但是事情也沒那麼糟,因為 GNU indent 的製造者承認 K&R 的權威性,所以你只需要新增命令列引數 -kr -i8 (表示 K&R,8個字元寬的縮排),或者使用 scripts/Lindent 也可以。
indent 有很多命令列選項,特別是註釋的格式化方面,你可以通過 man 幫助頁面來檢視,不過請記住:indent 不是用來修復爛程式的。
注意:你也可以使用 clang-format 來完成這些格式化的工作,具體參見 https://www.kernel.org/doc/html/latest/process/clang-format.html#clangformat
10 Kconfig 配置檔案
對於 Linux 中的 Kconfig 配置檔案,他們的縮排是有所不同的。在 config 定義下的縮排是一個 tab,而裡面的 help 文字是兩個空格,例如:
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.
而對於有可能導致危險的動作(比如特定檔案系統的寫支援),你應該在提示文字中直接指出:
config ADFS_FS_RW bool "ADFS write support (DANGEROUS)" depends on ADFS_FS ...
具體細節參見 Documentation/kbuild/kconfig-language.txt
11 資料結構
對於單執行緒環境裡建立和銷燬的一些資料結構,如果他們對於執行緒外是可見的,那麼總是應該有引用計數。在核心裡,垃圾收集器(GC)是不存在的,這意味著你必須對你使用過的資料進行引用計數。
進行引用計數意味著你可以避免死鎖,允許多個使用者並行訪問資料,並且不用擔心資料因為睡眠或者其他原因而找不到。
注意,鎖不是引用計數的替代品。鎖是為了保持資料的一致性,而引用計數是一種記憶體管理計數。通常這兩種技術都是需要的,我們不要把他們搞混。
當有多個不同類的使用者時,很多資料結構會使用二級引用計數。第二級的引用計數會統計第二級使用者的數量,只有當第二級引用計數遞減至零時,全域性的第一級引用計數才會減一。
這種多級引用計數在記憶體管理(struct mm_struct: mm_users and mm_count)和檔案系統(struct super_block: s_count and s_active)中都有使用。
記住,如果其他執行緒可以發現並使用你的資料結構,而你卻沒有引用計數,那麼這基本就是一個 bug。
12 巨集、列舉與RTL(Real Time Linux)
常量巨集和列舉的命名都是大寫的。
#define CONSTANT 0x12345
當定義一些有關聯的常量時,使用列舉是一個很好的選擇。
定義巨集一般都使用大寫,但是函式巨集可以使用小寫。
通常,我們更推薦把行內函數定義為巨集。
包含多條語句的巨集應該包含在一個 do-while 迴圈體中:
#define macrofun(a, b, c)\ do {\ if (a == 5)\ do_this(b, c);\ } while (0)
使用巨集時應該避免的情況:
1) 影響程式控制流的巨集
#define FOO(x)\ do {\ if (blah(x) < 0)\ return -EBUGGERED;\ } while (0)
這是一個非常壞的壞主意。它看起來像個函式,然而卻會導致呼叫者返回到上一層。巨集的設計不要打斷程式的控制流。
2) 依賴區域性變數的巨集
#define FOO(val) bar(index, val)
這看起來像個好東西,但其實糟透了,並且容易讓人困擾。當其他人閱讀這段程式碼時,他一個細微的改動可能導致嚴重的危害。
3) 帶引數的巨集當作左值
FOO(x) = y;
如果有人把 FOO 變成行內函數,那麼這段程式碼就錯了。
4) 忘了優先順序
#define CONSTANT 0x4000 #define CONSTEXP (CONSTANT | 3)
用巨集來定義常量的時候,必須要括上括號,帶有引數的巨集也要注意。
5) 在定義巨集函式時發生命名衝突
#define FOO(x)\ ({\ typeof(x) ret;\ ret = calc_ret(x);\ (ret);\ })
ret 是一個很容易和區域性變數發生衝突的名字,而 __foo_ret 這樣的名字則很少會發生衝突。
C++ 手冊全面地闡述了巨集定義的細節,gcc 手冊同樣也闡述了組合語言使用的 RTL 規則,具體請自行檢視。
13 列印核心資訊
核心開發者喜歡被視為有素養的,好的英文拼寫和準確的核心資訊能給人留下好的印象,因此,不要使用一些單詞的縮寫,比如 dont,而是 do not 或者 don't。把提示資訊寫得儘可能準確、清晰、無二義。
核心資訊不需要在末尾加上句號
在圓括號中列印數字(%d)沒有任何意義,應該避免這樣幹。
在<linux/device.h>中有許多驅動模型的診斷巨集,你應該使用這些巨集來確保訊息匹配正確的裝置和驅動,並正確的標記它們的級別:dev_err(), dev_warn(), dev_info(), and so forth。對於沒有關聯特定裝置的訊息,<linux/printk.h>中定義了 pr_notice(), pr_info(), pr_warn(), pr_err(), etc。
編寫好的除錯資訊是一項巨大的挑戰,一旦你完成了,這些資訊會對遠端除錯產生巨大幫助。除錯資訊與普通訊息不同,pr_XXX() 函式在任何條件下都會進行列印,而 pr_debug() 卻不是,這些與除錯有關的函式預設都不會被編譯,除非你定義了一個 DEBUG 巨集或者 CONFIG_DYNAMIC_DEBUG 巨集來顯式地讓編譯器編譯他們。還有一個慣例就是使用 VERBOSE_DEBUG 為那些已經開啟 DEBUG 的使用者新增 dev_vdbg() 訊息。
很多子系統在對應的 makefile 裡都有 Kconfig 除錯選項來開啟 -DDEBUG,或者是在檔案裡定義巨集 #define DEBUG。當除錯資訊可以被無條件列印,或者說已經編譯了和除錯有關的 #ifdef 段,那麼 printk(KERN_DEBUG ...) 就可以用來列印除錯資訊。
14 分配記憶體
核心提供了下面這些通用的記憶體分配器:kmalloc(), kzalloc(), kmalloc_array(), kcalloc(), vmalloc(), and vzalloc()。具體細節參見 API 文件。
為一個結構體分配記憶體的形式最好是這樣的:
p = kmalloc(sizeof(*p), ...);
另一種寫出結構體名字的方式(sizeof(struct name))會破壞可讀性並且給 bug 製造了機會:修改結構體名字卻忘了修改對於的 sizeof 語句。
另外,在 malloc 之前新增上一個強制的型別轉換,把空型別的指標轉換為特定型別的指標,這些是多此一舉的操作,他們應當交給編譯器來幹,而不是你。
分配一個數組的形式最好是這樣的:
p = kmalloc_array(n, sizeof(...), ...);
分配一個零陣列的形式最好是這樣的:
p = kcalloc(n, sizeof(...), ...);
兩種形式都會檢查溢位,並且溢位發生時返回一個空指標 NULL。
15 內聯之災
一個很常見的誤解就是,人們認為 gcc 有一種讓他們的程式跑得更快的魔法,就是內聯。然而,內聯往往也有不合適的用法(例如第十二節提到的替換巨集)。inline 關鍵字的泛濫,會使核心變大,從而使整個系統執行速度變慢,因為大核心會佔用更多的CPU快取記憶體,同時會導致可用記憶體頁快取減少。想象一下,一次頁快取未命中就會導致一次磁碟定址,這至少耗費5毫秒。5毫秒足夠CPU執行很多很多的指令。
一個基本的原則就是,如果一個函式有3行以上的程式碼,就不要把它變成行內函數。有一個例外,若某個引數是一個編譯時常數,且你確定因為這個常量,編譯器在編譯時能優化掉函式的大部分程式碼,那麼加上 inline 關鍵字。kmalloc() 就是個很好的例子。
人們經常主張可以給只用一次的靜態函式加上 inline 關鍵字,這樣不會有任何損失。雖然從技術上來說這樣沒錯,但是實際上 gcc 會自動內聯這些函式。
16 函式返回值與名稱
函式可以返回不同種類的值,但是最普遍的就是表示執行成功或失敗的值。這樣的值可以用預先定義好的錯誤碼錶示(-Exxx = failure, 0 = success),或者一個布林值(0 = failure, non-zero = success)
混合兩種方式會使程式碼變得複雜,並且很難找到 bug。如果C語言能明確區分整型和布林型,那麼編譯器會替我們發現這個問題……但是它不會那麼做。為了避免這種問題,一定要謹記如下約定:
如果函式名是一個短語,表示的是一個動作,或者一個命令,那麼返回值應該使用錯誤碼的方式。 如果函式名是一句話,表示的是一個斷言,那麼應該使用布林值的方式。
例如,add work 是一個動作,那麼 add_work() 返回值為0則表示成功,-EBUSY表示失敗。PCI device present是一個斷言,那麼 pci_dev_present() 返回值為1表示成功,0表示失敗。
可匯出(EXPORT)的函式都應該遵守這個約定,私有(static)函式不需要,不過我建議你還是遵守。
如果返回值是一些計算結果,那麼當然不需要管這些東西。一般來說,計算結果出錯了就表示失敗了。典型的例子就是返回一個指標:使用 NULL 或者 ERR_PTR 來表示錯誤。
17 不要重新發明核心巨集
include/linux/kernel.h 標頭檔案裡定義了一些你可以使用的巨集,你應該直接使用他們,而不是重新再定義一些新的巨集。例如,如果你需要計算陣列長度,使用提供的巨集:
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
同樣地,如果你需要計算結構體中某個成員的大小,使用:
#define FIELD_SIZEOF(t, f) (sizeof(((t*)0)->f))
如果需要,裡面還有做型別檢查的 min() 和 max() 巨集。仔細看看標頭檔案中還定義了那些東西,如果裡面有了,你就不要在自己的程式碼中重新定義了。
18 多此一舉的編輯器
有些編輯器可以識別原始檔中的配置資訊,例如 emacs 可以識別這樣的標記:
-*- mode: c -*-
或者這樣的:
/* Local Variables: compile-command: "gcc -DMAGIC_DEBUG_FLAG foo.c" End: */
Vim 可以識別:
/* vim:set sw=8 noet */
不要在原始碼中包含任何類似的內容。每個人都有自己的編輯器配置,你的原始檔不應該影響他們。
19 內聯彙編
在寫一些與體系結構有關的程式碼中,你可能需要使用一些內聯彙編呼叫CPU相關的介面或者和平臺有關的功能,如果有這種需求,你大可使用匯編。但是如果C語言可以乾的事,不要使用匯編。你應該儘可能地使用C語言來控制硬體。
儘可能寫一些輔助函式來實現相同的功能,而不是重複地寫一些相同的程式碼,同時記住,內聯彙編也可以使用C函式的引數。
大的、重要的彙編函式應該獨自寫在一個 .S 檔案中,並且編寫對應的C標頭檔案和函式原型,相應的函式原型應該新增 asmlinkage 關鍵字。
你也許需要標記某些彙編程式碼為 volatile,避免 gcc 誤把一些彙編移除掉。一般情況下,你不需要這樣幹,沒必要的標記會影響優化。
當一條彙編語句裡包含多個指令時,每個指令分行寫,並且除了最後一行外,在其他行的行末新增 \n\t 進行縮排和對齊:
asm ("magic %reg1, #42\n\t" "more_magic %reg2, %reg3" : /* outputs */ : /* inputs */ : /* clobbers */);
20 條件編譯
無論在哪,不要在 .c 檔案中使用條件編譯命令(#if, #ifdef),這樣幹會導致程式碼可讀性降低並且程式碼邏輯混亂。取而代之,應該在 .c 檔案對應的標頭檔案中使用這些條件編譯,並且在每個 #else 分支註明對應的版本資訊。
把同一個版本的所有函式都寫在一個 #ifdef 中,不要在其中寫一部分,而又在外部寫一部分。
在 #endif 之後寫上一個註釋,註明這個 #ifdef 塊對應的內容:
#ifdef CONFIG_SOMETHING ... #endif /* CONFIG_SOMETHING */
References
The C Programming Language, Second Edition by Brian W. Kernighan and Dennis M. Ritchie. Prentice Hall, Inc., 1988. ISBN 0-13-110362-8 (paperback), 0-13-110370-9 (hardback).
The Practice of Programming by Brian W. Kernighan and Rob Pike. Addison-Wesley, Inc., 1999. ISBN 0-201-61586-X.
GNU manuals - where in compliance with K&R and this text - for cpp, gcc, gcc internals and indent, all available from http://www.gnu.org/manual/
WG14 is the international standardization working group for the programming language C, URL: http://www.open-std.org/JTC1/SC22/WG14/
Kernel process/coding-style.rst, by [email protected] at OLS 2002: http://www.kroah.com/linux/talks/ols_2002_kernel_codingstyle_talk/html/