本文是翻譯自 內核源碼 的 Documentation/process/coding-style.rst(最后的提交時間是 2021 年 2 月 12 日)。
網上雖然已經有很多人做了很好的翻譯,但都是很早的版本,所以我還是想自己翻譯一次最新的編碼風格(當然,我借助了某歌翻譯),讓自己加深印象。
如果發現有看不懂的地方,不用懷疑,是我翻譯得不對,請告訴我:[email protected]。
這是一個簡短的文檔,描述了 Linux 內核的首選編碼風格。 編碼風格非常個人化,我不會對任何人強加我的見解,但這是我必須要維護的代碼(指 Linux 內核代碼)的編碼風格,對于其他項目代碼,我也希望使用它。 寫內核代碼時請至少考慮本文提出的風格。
首先,我建議打印出 GNU 編碼標準,然后不要閱讀。 燒掉它們,這是一個很棒的象征性動作。
無論如何,我們開始:
1) 縮進
制表符(Tab
鍵)是 8 個字符,因此縮進也是 8 個字符。 有一些異端做法試圖使制表符變成 4 個(甚至 2 個!)字符,這類似于嘗試將 PI 的值定義為 3。
理由:縮進的目的是明確定義控制塊的開始和結束位置。 特別是當你連續看了 20 個小時的屏幕后,如果縮進較大則作用更大(指更容易分辨縮進)。
現在,有些人會聲稱具有 8 個字符的縮進會使代碼向右移得太遠,并使得在 80 個字符的終端屏幕上難以閱讀。 答案是,如果你需要三個以上的縮進級別,那么無論如何你的代碼有問題了,應該修復程序。
簡而言之,8 字符縮進使內容更易于閱讀,并具有在嵌套函數太深時發出警告的作用。 注意該警告。
緩解 switch
語句中多個縮進級別的首選方法是在同一列中對齊 switch
及其從屬 case
標簽,而不是對 case
標簽進行兩次縮進。 例如:
-
switch (suffix) {
-
case 'G':
-
case 'g':
-
mem <<= 30;
-
break;
-
case 'M':
-
case 'm':
-
mem <<= 20;
-
break;
-
case 'K':
-
case 'k':
-
mem <<= 10;
-
fallthrough;
-
default:
-
break;
-
}
除非要隱藏某些內容,否則不要在一行上放置多個語句:
-
if (condition) do_this;
-
do_something_everytime;
不要使用逗號來避免使用花括號:
-
if (condition)
-
do_this(), do_that();
始終對多個語句使用花括號:
-
if (condition) {
-
do_this();
-
do_that();
-
}
也不要將多個賦值語句放在一行上。 內核編碼風格非常簡單。 避免使用棘手的表達式。
除了注釋,文檔和 Kconfig 外,空格都不用于縮進,前面的例子是故意的。
選用一個好的編輯器,不要在行尾留空格。
2) 把長的行和字符串打散
編碼風格是關于使用通用工具來維持可讀性和可維護性。
單行長度的首選限制是 80 列。
長度超過 80 列的語句應分為合理的片段,除非超過 80 列會顯著提高可讀性且不會隱藏信息。
后面的片段應該短于原來的語句,并且基本上位于靠右放置。 一個典型的例子是將后面的片段與函數左括號對齊。
這些相同的規則適用于帶有長參數列表的函數頭,如下所示:
-
/* 注意:這個例子是我(陳孝松)寫的 */
-
void func(int a, int b, int c, int d, int e, int f, int g, int h, int i
-
int j, int k)
-
{
-
...
-
}
但是,切勿破壞諸如 printk
消息之類的用戶可見的字符串,因為這會破壞 grep
為它們顯示的功能。
3) 大括號和空格的放置
3.1) 大括號
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
-
}
全世界的異端人士都聲稱這種不一致性是……嗯……是不一致的,但是所有思維健全的人都知道(a)K&R 是正確的,(b)K&R 是正確的。 此外,函數是很特殊的(在 C 語言中函數是不能嵌套的)。
陳孝松注:K & R:《The C Programming Language》一書的作者 Kernighan 和 Ritchie。
請注意,結束大括號單獨一行,除非在其后跟著同一條語句的剩余部分,也就是 do
語句中的 while
,或者 if
語句中的 else
,例如:
-
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();
如果條件語句的只有一個分支是單個語句,則不適用; 這時請在兩個分支中都使用大括號:
-
if (condition) {
-
do_this();
-
do_that();
-
} else {
-
otherwise();
-
}
另外,當循環中包含了多個單行的簡單語句時,請使用大括號:
-
while (condition) {
-
if (test)
-
do_something();
-
}
3.2) 空格
Linux 內核使用空格的方式(主要)取決于是用于函數還是關鍵字。 (大多數)在關鍵字之后加一個空格。 值得注意的例外是 sizeof
、typeof
、alignof
和 attribute
,它們看起來有點像函數(并且在 Linux 中通常與小括號一起使用,盡管它們在語言中不是必需的,例如:struct fileinfo info;
聲明后的 sizeof info
) 。
因此,在這些關鍵字之后加一個空格:
-
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);
在大多數二元和三元運算符的兩側(每邊)使用一個空格,例如以下任意一個:
-
= + - < > * / % | & ^ <= >= == != ? :
但一元運算符后不要加空格:
-
& * + - ~ ! sizeof typeof alignof __attribute__ defined
后綴遞增和遞減一元運算符前沒有空格:
-
++ --
前綴遞增和遞減一元運算符后沒有空格:
-
++ --
.
和 ->
結構成員操作符前后沒有空格。
不要在行尾留空格。 某些具有“智能”縮進的編輯器將在適當的情況下在新行的開頭插入空格,因此你可以立即開始鍵入下一行代碼。 但是,如果沒有在這一行輸入代碼,則某些編輯器不會刪除空格,就像你留下一個只有空白的行。 結果,行尾帶有空格的行就產生了。
當 git 發現補丁包含了行尾空格的時候會警告你,并且可以有選擇地為你去掉尾隨空格; 但是,如果打一系列補丁,這樣做會導致后面的補丁失敗,因為你改變了補丁的上下文。
4) 命名
C 是一種簡樸的語言,你的命名也應是這樣。 與 Modula-2 和 Pascal 程序員不同,C 程序員不會使用諸如 ThisVariableIsATemporaryCounter
之類的可愛名稱。 C 程序員將該變量命名為 tmp
,該變量更容易編寫,而且更容易理解。
但是,雖然不贊成使用大小寫混合的名稱,但全局變量還是需要使用具備描述性的名稱。 把全局函數命名為 foo
是一種難以饒恕的錯誤。
全局變量(僅在確實需要它們時才使用)與全局函數一樣,都需要具有描述性名稱。 如果你有一個統計活動用戶數量的函數,則應命名為 count_active_users()
或類似名稱,而不應該命名為 cntusr()
。
在函數名中包含函數類型(所謂的匈牙利命名法)是愚蠢的 - 編譯器知道類型而且能夠檢查類型,這樣做只能把程序員弄糊涂。
陳孝松注:這里曾經還有一句話:難怪微軟總是制造出有問題的程序。在 2021 年 2 月 12 日這句話被刪除了。
局部變量名稱應簡短明了。 如果你有一些隨機整數循環計數器,則應命名為 i
。 如果沒有可能被誤解,則命名為 loop_counter
是無用的。 同樣,tmp
可以用來命名任意類型的臨時變量。
如果你害怕混淆你的局部變量名稱,那么你會遇到另一個問題,稱為叫做函數增長荷爾蒙失衡綜合癥。 請參見第 6 章(函數)。
對于符號名稱和文檔,請避免引入“主/從”(或獨立于“主”的“從”)和“黑名單/白名單”的新用法。
推薦的“主/從”替代方案是:
-
'{primary,main} / {secondary,replica,subordinate}' '{initiator,requester} / {target,responder}' '{controller,host} / {device,worker,proxy}' 'leader / follower' 'director / performer'
推薦的“黑名單/白名單”替代方案是:
-
'denylist / allowlist' 'blocklist / passlist'
引入新用法的例外情況是維護用戶空間 ABI/API,或者更新用于強制使用這些術語的現有(截至 2020年)硬件或協議規范的代碼。 對于新規范,盡可能將術語的規范用法轉換為內核編碼標準。
5) typedef
請不要使用 vps_t
之類的東西。 對結構體和指針使用 typedef
是錯誤的。 當你看到
-
vps_t a;
出現在代碼中,是什么意思? 相反,如果這樣
-
struct virtual_container *a;
你就知道 a
是什么了。
許多人認為 typedef
有助于提高可讀性。 不是這樣的。它們僅在下列情況下有用:
- 鴻蒙官方戰略合作共建——HarmonyOS技術社區
- 完全不透明的對象(這時
typedef
主動用于隱藏對象是什么)。
例如:pte_t
等不透明對象,你只能使用適當的訪函數來訪問他們。
注意:不透明和“訪問函數”本身并不好。 之所以使用諸如pte_t
等類型的原因在于真的是完全沒有任何共用的可訪問信息。- 清楚的整數類型,這層抽象有助于避免混淆到底是
int
還是long
。
u8/u16/u32
是沒問題的typedef
,不過它們更符合情況 (4) 而不是這里。
再次注意:需要有一個原因。 如果某個變量類型是unsigned long
,則沒有必要這樣
typedef unsigned long myflags_t;
但是,如果有明確的原因,比如有些情況可能是unsigned int
,而在其他情況下可能是unsigned long
,那么一定要繼續使用typedef
。- 當你使用
sparse
從字面上創建用于類型檢查的新類型時。陳孝松注:sparse
誕生于 2004 年,是由 Linus 開發的,目的就是提供一個靜態檢查代碼的工具, 從而減少 Linux 內核的隱患。- 在某些特殊情況下,與標準 C99 類型相同的新類型。
盡管眼睛和大腦只需要很短的時間就習慣了uint32_t
這樣的標準類型,但是仍然有人反對使用它們。
因此,Linux 特有的等同于標準類型的u8/u16/u32/u64
類型和它們的有符號類型是被允許的 -- 盡管它們在你自己的新代碼中不是必需的。
編輯已使用了某類型集的現有代碼時,應遵循該代碼中的現有選擇。- 可以在用戶空間中安全使用的類型。
在用戶空間可見的某些結構體中,我們不能要求C99類型而且不能用上面提到的u32
類型。 因此,我們在與用戶空間共享的所有結構體中使用__u32
和類似的類型。
也許還有其他情況,但是基本的規則應該永遠不要使用 typedef
,除非你可以明確符合上述規則中的一個。
通常,指針或結構體中的元素可以合理被訪問到,那么就不應該是 typedef
。
6) 函數
函數應該簡短而漂亮,并且只完成一件事。 它們應該一屏或兩屏顯示完(眾所周知,ISO/ANSI
屏幕大小為 80x24
),并且可以做一件事并且做好。
函數的最大長度與該函數的復雜度和縮進級數成反比。 因此,如果你有一個理論上很簡單的函數,只是一個很長(但很簡單)的 case
語句,那么需要在每個 case
語句做很多小事情,這樣的函數可以很長。
但是,如果功能很復雜,并且懷疑天分不是很高的一年級高中生甚至可能根本不了解該函數的功能,則應該更加嚴格地遵守長度限制。 使用具有描述性名稱的輔助函數(如果你認為它們的性能至關重要,則可以讓編譯器內聯它們,效果比寫一個復雜的函數要好)。
函數的另一種衡量標準是局部變量的個數。 它們不應超過 5-10 個,否則函數就有問題了。 重新考慮一下函數的實現,并將其拆分為更小的函數。 人腦通??梢暂p松地跟蹤約7種不同的事物,如果更多的話就會變得混亂。 即使你再聰明,你也可能會記不清兩個星期前做過的事。
在源文件中,用一個空行分隔函數。 如果導出了該函數,則該函數的 EXPORT
宏應緊跟在結束大括號的下一行。 例如:
-
int system_is_up(void)
-
{
-
return system_state == SYSTEM_RUNNING;
-
}
-
EXPORT_SYMBOL(system_is_up);
在函數原型中,最好包含參數名稱和其數據類型。 盡管 C 語言沒要求必須這樣做,但在 Linux 中提倡這樣做,因為這樣可以很簡單的給讀者提供更多的有價值的信息。
請勿將 extern
關鍵字與函數原型一起使用,因為這會使行更長,并且并非絕對必要。
7) 集中的函數退出途徑
盡管某些人認為已過時,但是 goto
語句的等價物經常以無條件跳轉指令的形式被編譯器使用。
陳孝松注:equivalent of the goto statement 翻譯為
goto
語句的等價物 似乎不大通順,如果你有更好的翻譯請告訴我。
當函數從多個位置退出并且必須執行一些常規工作(例如清理)時, goto
語句會派上用場。 如果不需要清理,則直接返回。
選擇標簽名稱,要能說明 goto
的功能或 goto
存在的原因。 如果 goto
中轉到釋放 buffer
的地方,則標簽名字為 out_free_buffer:
將是一個很好的例子。 避免使用諸如 err1:
和 err2:
之類的 GW-BASIC 名稱,因為如果你添加或刪除出口路徑,則必須重新編號它們,并且它們無論如何都會使正確性難以驗證。
陳孝松注:GW-BASIC 是 BASIC 的一個方言版本,這個版本的 BASIC 最早是微軟在 1984 年為康柏(2002 年康柏公司被惠普公司收購)開發的。
使用 goto
的基本原理是:
- 無條件語句更易于理解和跟蹤
- 嵌套程度減少
- 防止在進行修改時忘記更新某個單獨的退出點而導致錯誤
- 節省了編譯器的工作,以優化冗余代碼 :wink:
-
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;
-
}
要注意的一種常見錯誤是 one err bugs
,如下所示:
-
err:
-
kfree(foo->bar);
-
kfree(foo);
-
return ret;
此代碼中的錯誤是在某些出口路徑上 foo
為 NULL
。 通常,此問題的解決方法是將其分為兩個錯誤標簽 err_free_bar:
和err_free_foo:
:
-
err_free_bar:
-
kfree(foo->bar);
-
err_free_foo:
-
kfree(foo);
-
return ret;
理想情況下,你應該模擬錯誤以測試所有出口路徑。
8) 注釋
注釋是好的,但也有過度注釋的危險。 永遠不要嘗試在注釋中解釋代碼是如何工作的:更好 的做法是讓別人一看代碼就可以明白,解釋寫的很差的代碼是浪費時間。
通常,你希望你的注釋告訴別人你的代碼做了什么,而不是怎么做的。 另外,請盡量避免在函數體內添加注釋:如果函數太復雜以至于需要單獨注釋其中的某些部分,則可能應該回到第6章看一看。 你可以加一些小注釋,注明或警告某些特別聰明(或糟糕)的做法,但不要加太多。 你應該將注釋放在函數的頭部,告訴人們它做了什么,以及這么做的原因。
在注釋內核 API 函數時,請使用 kernel-doc 格式。詳細信息請參考:Documentation/doc-guide/ <doc_guide>
和scripts/kernel-doc
。
長(多行)注釋的首選風格是:
-
/*
-
* 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.
-
*/
-
/*
-
* 這是Linux內核源代碼中多行注釋的首選風格。
-
* 請始終使用這種風格。
-
*
-
* 說明:左側是星號列,開始和結束的行幾乎是空白的。
-
*/
對于 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.
-
*/
-
/* net/和drivers/net/中的文件的首選注釋風格如下所示。 *
-
* 它幾乎與一般的首選注釋風格相同,但是開始的行不是幾乎空白的。
-
*/
注釋數據(無論是基本類型還是衍生類型)也很重要。 為此,每行僅使用一個數據聲明(不要使用逗號來一次聲明多個數據)。 這為你留出了對每個數據寫一段小注釋的空間,以解釋其用途。
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)))
-
(dir-locals-set-class-variables
-
'linux-kernel
-
'((c-mode . (
-
(c-basic-offset . 8)
-
(c-label-minimum-indentation . 0)
-
(c-offsets-alist . (
-
(arglist-close . c-lineup-arglist-tabs-only)
-
(arglist-cont-nonempty .
-
(c-lineup-gcc-asm-reg c-lineup-arglist-tabs-only))
-
(arglist-intro . +)
-
(brace-list-intro . +)
-
(c . c-lineup-C-comments)
-
(case-label . 0)
-
(comment-intro . c-lineup-comment)
-
(cpp-define-intro . +)
-
(cpp-macro . -1000)
-
(cpp-macro-cont . +)
-
(defun-block-intro . +)
-
(else-clause . 0)
-
(func-decl-cont . +)
-
(inclass . +)
-
(inher-cont . c-lineup-multi-inher)
-
(knr-argdecl-intro . 0)
-
(label . -1000)
-
(statement . 0)
-
(statement-block-intro . +)
-
(statement-case-intro . +)
-
(statement-cont . +)
-
(substatement . +)
-
))
-
(indent-tabs-mode . t)
-
(show-trailing-whitespace . t)
-
))))
-
(dir-locals-set-directory-class
-
(expand-file-name "~/src/linux-trees")
-
'linux-kernel)
這將使 emacs
更好地配合 ~/src/linux-trees
下 C 文件的內核編碼風格。
但是,即使你無法使 emacs
進行合理的格式化,也并不意味著你失去了一切:還可以用 indent
。
現在,再次,GNU indent
具有與 GNU emacs
有問題的設定,所以你需要給它一些命令選項。 但是,這并不算太糟,因為即使 GNU indent
的作者也認同 K&R 的權威(GNU 的人并不是壞人,他們在此問題上受到嚴重誤導),所以你只需給 indent
指定選項-kr -i8
( 代表 “K&R, 8 character indents”),或使用 scripts/Lindent
(以最時髦的方式縮進)。
indent
有很多選項,尤其是重新格式化注釋時,你可能需要看一下手冊頁。 但是請記住:indent
不能修正壞的編程習慣。
請注意,你還可以使用 clang-format
工具來幫助你遵循這些規則,快速自動地重新格式化部分代碼,并查看完整文件,以發現編碼風格錯誤,錯別字和可能的改進。 它對于排序 #includes
,對齊變量/宏,重排文本和其他類似任務也很方便。更多詳細信息,請參見文件 Documentation/process/clang-format.rst <clangformat>
。
10) Kconfig 配置文件
對于整個源代碼樹中的所有 Kconfig* 配置文件,縮進有些不同。 緊挨 在 config
定義下面的行縮進一個制表符,幫助信息則再多縮 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.
嚴重危險的功能(例如對某些文件系統的寫支持)應在其提示字符串中突出顯示這一點:
-
config ADFS_FS_RW
-
bool "ADFS write support (DANGEROUS)"
-
depends on ADFS_FS
-
...
有關配置文件的完整文檔,請參閱文件 Documentation/kbuild/kconfig-language.rst
。
11) 數據結構
在創建和銷毀它們的單線程環境之外具有可見性的數據結構應始終具有引用計數。 在內核中,不存在垃圾回收(并且在內核之外,垃圾回收是緩慢且效率低下的),這意味著你絕對必須引用計數所有使用情況。
引用計數意味著你可以避免上鎖,并允許多個用戶并行訪問數據結構 - 不必擔心這個數據結構僅僅因為暫時不被使用就消失了,因為休眠一段時間或做了其他事情。
請注意,上鎖不能代替引用計數。 上鎖用于保持數據結構的一致性,而引用計數是一種內存管理技術。 通常兩者都是必需的,并且不要相互混淆。
當存在不同 classes
的用戶時,許多數據結構的確可以具有兩級的引用計數。 子類計數器對子類用戶的數量進行計數,并且當子類計數器變為零時,全局計數器減一。
這種多級引用計數(multi-level-reference-counting
)的示例可以在內存管理(struct mm_struct
:mm_users
和 mm_count
)以及文件系統代碼(struct super_block
: s_count
和 s_active
)中找到。
請記?。喝绻硪粋€線程可以找到你的數據結構,并且你沒有對其的引用計數,則幾乎可以肯定有一個bug。
12) 宏、枚舉和 RTL
定義常量的宏名稱和枚舉中的標簽均使用大寫字母。
-
#define CONSTANT 0x12345
定義多個相關常量時,最好使用枚舉。
宏的名字請用大寫字母,但是類似于函數的宏可以用小寫字母命名。
通常,內聯函數比類似于函數的宏更可取。
具有多個語句的宏應包含在 do
- while
語句塊中:
-
#define macrofun(a, b, c)\
-
do {\
-
if (a == 5)\
-
do_this(b, c);\
-
} while (0)
使用宏時應避免的事情:
- 鴻蒙官方戰略合作共建——HarmonyOS技術社區
-
影響控制流程的宏:
-
#define FOO(x)\
-
do {\
-
if (blah(x) < 0)\
-
return -EBUGGERED;\
-
} while (0)
非常不好。 它看起來像一個函數,但是會導致調用它的函數退出。不要打亂讀者大腦里的語法分析器。 -
-
依賴于一個固定名字的本地變量的宏
-
#define FOO(val) bar(index, val)
也許看起來不錯,但是當人們閱讀代碼時,它看起來很混亂,并且很容易因不相關的更改而被破壞。 -
-
帶參數的宏作為左值:
FOO(x) = y;
,如果有人將FOO
變成一個內聯函數就會出錯。 -
忘記優先級:使用表達式定義常量的宏必須將表達式用括號括起來。 帶參數的宏也要注意此類問題。
-
#define CONSTANT 0x4000
-
#define CONSTEXP (CONSTANT | 3)
-
-
在類似于函數的宏中定義局部變量時,名稱空間沖突:
-
#define FOO(x)\
-
({\
-
typeof(x) ret;\
-
ret = calc_ret(x);\
-
(ret);\
-
})
ret
是局部變量的通用名稱 -__foo_ret
與現有變量發生沖突的可能性較小。
cpp 手冊詳盡地處理了宏。 gcc internals 手冊還介紹了RTL,內核里的匯編語言經常用到RTL。陳孝松注:
RTL:寄存器傳遞語言,又譯為暫存器轉換語言、寄存器轉換語言,一種中間語言,使用于編譯器中。 -
13) 打印內核消息
內核開發者應該是受過良好教育的。 請注意內核消息的拼寫,以給人留下深刻的印象。 不要使用不正確的收縮,例如 dont
; 而要使用 do not
或 don't
。 使消息簡單、明了、無歧義。
內核消息不必以句點(即點號)終止。
括號中的數字(%d
)沒有任何價值,應避免使用。
<linux/device.h>
中有許多驅動模型診斷宏,你應使用這些宏來確保消息與正確的設備和驅動程序匹配,并以正確的級別進行標記: dev_err()
、dev_warn()
、dev_info()
等。 對于與特定設備無關的消息,<linux/printk.h>
定義了 pr_notice()
、pr_info()
、pr_warn()
、pr_err()
等。
寫出好的調試消息可以是一個很大的挑戰。 一旦有了它們,它們將為遠程故障排除提供巨大幫助。 但是,調試消息的打印方式與打印其他非調試消息的方式不同。 雖然其他 pr_XXX()
函數無條件打印,但 pr_debug()
不會; 除非定義了 DEBUG
或設置了 CONFIG_DYNAMIC_DEBUG
,否則編譯器會忽略它。dev_dbg()
也是如此,并且相關的約定使用 VERBOSE_DEBUG
將 dev_vdbg()
消息添加到已由 DEBUG
啟用的消息中。
許多子系統具有 Kconfig 調試選項,可以在相應的 Makefile 中打開 -DDEBUG
。 在其他情況下,特定文件定義了 #define DEBUG
。 并且當應無條件打印調試消息時(例如,如果它已經在與調試相關的 #ifdef
中),可以使用 printk(KERN_DEBUG ...)
。
14) 分配內存
內核提供以下一般用途的內存分配函數:kmalloc()
、kzalloc()
、kmalloc_array()
、 kcalloc()
、vmalloc()
、vzalloc()
。 請參閱 API 文檔以獲取有關它們的更多信息: Documentation/core-api/memory-allocation.rst <memory_allocation>
。
傳遞結構體大小的首選形式如下:
-
p = kmalloc(sizeof(*p), ...);
另外一種傳遞方式中,sizeof
的操作數是結構體的名字,這樣會降低可讀性,并且可能會引 入 bug。有可能指針變量類型被改變時,而對應的傳遞給內存分配函數的 sizeof
的結果不變。
陳孝松注:有可能出現以下情況:
int *p; /* 最開始是 char *p, 后來修改成 int *p */
p = kmalloc(sizeof(char), ...);
強制轉換 void
指針的返回值是多余的。 C 語言保證了從 void
指針到任何其他指針類型的轉換是沒問題的。
分配數組的首選形式如下:
-
p = kmalloc_array(n, sizeof(...), ...);
分配初始化為零的數組的首選形式如下:
-
p = kcalloc(n, sizeof(...), ...);
兩種形式都檢查分配大小 n * sizeof(...)
上的溢出,如果發生,則返回 NULL
。
這些通用的分配函數在不帶 __GFP_NOWARN
的情況下使用時,都會在失敗時發出堆棧轉儲,因此在返回 NULL
時不會有其他失敗消息。
15) 內聯弊病
有一個常見的誤解是內聯函數(inline
)是 gcc 提供的可以讓代碼運行更快的一個選項。 雖然可以適當使用內聯函數(例如,作為替換宏的一種方法,請參見第 12 章),不過很多情況下不是這樣。 大量使用 inline
關鍵字會導致內核變大,這會降低整個系統的速度,這是因為 CPU 的 icache 占用量更大,而且會導致 pagecache 的可用內存減少。 考慮一下:pagecache 未命中會導致磁盤查找,這很容易花費 5 毫秒。5 毫秒的時間內 CPU 能執行很多指令。
一個基本的原則是不要對其中包含多于 3 行代碼的函數進行內聯。該規則的例外情況是參數已知為編譯時常量,并且由于該常量,你確定編譯器將能夠在編譯時優化大部分函數。 一個很好的示例就是 kmalloc()
內聯函數。
人們經常主張說,將 inline
添加到 static
且僅使用一次的函數,不會有任何損失,因為沒有什么好權衡的。 盡管從技術上講這是正確的,但是 gcc
能夠在沒有幫助的情況下自動內聯這個函數,而且其他用戶可能會要求移除 inline
,由此而來的爭論會抵消 inline
自身的潛在價值,得不償失。
16) 函數返回值及命名
函數可以返回許多不同類型的值,最常見的值之一是指示函數成功還是失敗的值。 這樣的值可以表示為錯誤代碼整數(-Exxx = failure,0 = success)或是否成功的布爾值(0 = failure, non-zero = success)。
混合使用這兩種表達方式是難于發現的 bug 的來源。 如果 C 語言能嚴格區分整數和布爾值,則編譯器會為我們找到這些錯誤……但是 C 語言不區分。 為防止此類錯誤,請始終遵循以下約定:
如果函數的名字是一個動作或者強制性的命令,則該函數應返回錯誤代碼整數。 如果是一個判斷,則函數應返回表示是否“成功”的布爾值。
例如,add work
是一條命令,add_work()
函數成功時返回 0
,失敗時返回 -EBUSY
。 同樣,PCI device present
是一個判斷,如果成功找到匹配的設備,pci_dev_present()
函數將返回 1
,否則將返回 0
。
所有 EXPORT
函數必須遵守此約定,所有公共函數也應遵守此約定。 私有(static
)函數不是必需的,但建議這樣做。
返回值是計算的實際結果而不是指示計算是否成功的函數不受此規則的約束。 通常,它們通過返回超出范圍的結果來指示失敗。 典型的例子是返回指針的函數。 他們使用 NULL
或 ERR_PTR
機制來報告錯誤。
17) 使用布爾
Linux 內核的布爾類型是 C99 _Bool
類型的別名。 布爾值只能是 0
或 1
,并且隱式或顯式轉換為布爾值,會自動將值轉換為 true
或 false
。 使用布爾型時,!! 不需要結構體,從而消除了一類錯誤。
使用布爾值時,應使用 true
和 false
定義,而不是 1
和 0
。
在適當的時候使用可以使用布爾返回類型的函數和堆棧變量。 鼓勵使用布爾值來提高可讀性,并且在存儲布爾值時通常比使用整型類型更好。
如果緩存行的布局或值的大小很重要,請不要使用布爾,因為其大小和對齊方式會根據編譯的體系結構而變化。 針對對齊和大小進行了優化的結構不應使用布爾值。
如果結構體具有許多 true
/false
,請考慮將它們合并到具有 1 個位成員的位域中,或使用適當的固定寬度類型(例如 u8
)。
類似地,對于函數參數,可以將許多 true
/false
值合并為單個按位的 flags
參數,并且如果調用位置具有裸露的 true
/false
常量,則 flags
通常是更具可讀性的替代方法。
否則,在結構體和參數中限制使用布爾可以提高可讀性。
18) 不要重新發明內核宏
頭文件 include/linux/kernel.h
包含許多你應該使用的宏,而不要自己寫一些它們的變種。 例如,如果你需要計算數組的長度,請利用宏:
-
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
同樣,如果需要計算某些結構成員的大小,請使用:
-
#define sizeof_field(t, f) (sizeof(((t*)0)->f))
如果需要,還有 min()
和 max()
宏會進行嚴格的類型檢查。 你可以自己看看那個頭文件里還定義了什么你可以拿來用的宏,如果有定義的話,你就不應在你的代碼里自己重新定義。
19) 編輯器模式行(配置信息)和其他內容
一些編輯器可以解釋用特殊標記表示的嵌入在源文件中的配置信息。 例如,emacs
解釋標記如下的行:
-
-*- mode: c -*-
或者這樣的:
-
/*
-
Local Variables:
-
compile-command: "gcc -DMAGIC_DEBUG_FLAG foo.c"
-
End:
-
*/
vim
解釋如下標記:
-
/* vim:set sw=8 noet */
不要在源文件中包含任何這些。 人們具有自己的個人編輯器配置,并且你的源文件不應覆蓋它們。 這包括用于縮進和模式配置的標記。 人們可以使用他們自己定制的模式,或者使用其他可以產生正確的縮進的巧妙方法。
20) 內聯匯編
在特定于體系結構的代碼中,你可能需要使用內聯匯編與 CPU 或平臺功能交互。 必要時不要猶豫。 但是,當 C 可以完成這項工作時,請不要隨意使用內聯匯編。 你可以并且應該在可能的情況下用 C 操作硬件。
考慮編寫簡單的輔助函數,這些函數包裝內聯匯編的常用位,而不是重復編寫稍有變化的函數。 請記住,內聯匯編可以使用 C 參數。
大型的,非平凡(大量的)的匯編函數應放在 .S
文件中,并在 C 頭文件中定義相應的 C 原型。 匯編函數的 C 原型應使用 asmlinkage
。
你可能需要將 asm
語句標記為 volatile
,以防止 GCC 在未發現任何副作用的情況下將其刪除。 但是,你不一定總是需要這樣做,因為這樣做可能會限制優化。
當編寫包含多個指令的單個內聯匯編語句時,將每條指令放在單獨的行中,放在單獨的帶引號的字符串中,用 \n\t
結束除最后一個字符串以外的每個字符串,以正確縮進匯編輸出中的下一條指令:
-
asm ("magic %reg1, #42\n\t"
-
"more_magic %reg2, %reg3"
-
: /* outputs */ : /* inputs */ : /* clobbers */);
21) 條件編譯
盡可能不要在 .c
文件中使用預處理條件(#if
、#ifdef
); 這樣做會使代碼更難閱讀,邏輯也更難遵循。 而是在頭文件中使用此類條件,以定義在那些 .c
文件中使用的函數,在 #else
情況下提供 no-op(無操作) stub 版本,然后從 .c
文件中無條件調用這些函數。 編譯器將避免為 stub calls 生成任何代碼,從而產生相同的結果,但是邏輯將易于遵循。
最好編譯出整個函數,而不是編譯部分函數或表達式的一部分。 不要將 ifdef
放入表達式中,而是將部分或全部表達式封裝成函數,然后調用該函數。
如果你具有在特定配置中可能未使用的函數或變量,并且編譯器會警告其定義未使用,請將該定義標記為 __maybe_unused
,而不是將其包裝在預處理條件中。 (但是,如果函數或變量始終不使用,請將其刪除。)
在代碼內,在可能的情況下,使用 IS_ENABLED
宏將 Kconfig 符號轉換為 C 布爾表達式,并在普通的 C 條件中使用它:
-
if (IS_ENABLED(CONFIG_SOMETHING)) {
-
...
-
}
編譯器將不斷折疊條件,并像 #ifdef
一樣包含或排除代碼塊,因此這不會增加任何運行時開銷。 但是,這種方法仍然允許C編譯器查看塊中的代碼,并檢查其是否正確(語法,類型,符號引用等)。 因此,如果塊內的代碼引用了如果不滿足條件將不存在的符號,則仍然必須使用 #ifdef
。
在任何重要的 #if
或 #ifdef
代碼塊的末尾(多行),在同一行的 #endif
后面放置注釋,并注明所使用的條件表達式。 例如:
-
#ifdef CONFIG_SOMETHING
-
...
-
#endif /* CONFIG_SOMETHING */
附錄 I) 參考
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 https://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 :ref:process/coding-style.rst <codingstyle>
, by [email protected] at OLS 2002: http://www.kroah.com/linux/talks/ols_2002_kernel_codingstyle_talk/html/
原文鏈接:https://linux.cn/article-13494-1.html