Linux內核編程規範與代碼風格

source: https://www.kernel.org/doc/html/latest/process/coding-style.html
translated 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);

在二元操作符和三元操作符周圍添加一個空格,例如:

=  +  -  <  >  *  /  %  |  &  ^  <=  >=  ==  !=  ?  :

但是不要在一元操作符之後添加空格:

&  *  +  -  ~  !  sizeof  typeof  alignof  __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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章