C 語言程式碼風格之 Linux 核心程式碼風格

debugzhang發表於2021-03-26

GitHub: https://github.com/storagezhang

Emai: debugzhang@163.com

華為雲社群:https://bbs.huaweicloud.com/blogs/250379

本文翻譯自 https://www.kernel.org/doc/html/v4.10/_sources/process/coding-style.txt

本文簡短描述了 Linux 核心推薦的程式碼風格。程式碼風格是非常個性化的,但這是 Linux 核心必須維持的準則,對於很多其他領域的程式碼,該規範也具有參考意義。

首先,請列印出 GNU 程式碼規範,不要閱讀,燒掉它們,這是一個很棒的象徵性手勢。

1 縮排

Tab 佔 8 個字元,因此縮排也是 8 個字元。有一些異端運動試圖減少縮排至 4 個字元(甚至 2 個字元),類似的嘗試是將 PI 的值定義為 3。

原因:縮排的整體思想是明確定義控制塊的開始和結束位置。特別是當你連續看螢幕 20 個小時時,你會發現當縮排值較大時,注意到縮排會更加輕鬆。

現在,有些人表示 8 個字元的縮排使程式碼向右移得太遠,難以在寬度為 80 字元的終端螢幕上閱讀。

這個問題的答案是:如果你需要三個以上的縮排級別,無論如何程式都會被搞砸,應當修改程式。

簡而言之,8 字元縮排使程式碼更易於閱讀,並且在函式巢狀過深時發出警告。注意這個警告。

減少 switch 語句中的多級縮排的首選方法是將 switchcase 標籤在同一列對齊而不是縮排 case 標籤:

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 打破長行和長字串

程式碼風格作用於常用工具,使工具增加可讀性和可維護性。

行的長度限制為 80 列,這是一個強優先限值。

除非該行超過 80 列會顯著提高可讀性並且不會隱藏資訊,長度超過 80 列的語句將被分成合理的塊。後代始終比父代短很多,並且基本上位於右側。具有長引數列表的函式頭也是如此。但是切勿破壞諸如 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\) 是正確的。

此外,函式還是很特殊的(你不能將它們巢狀在 C 程式碼裡)。

請注意,閉括號所在的行是空的,除非在其後接同一語句的延續,即 while 在 do 語句後或 elseif 語句後,像這樣:

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();
}

3.1 空格

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,這樣更容易編寫,但同時也更難理解。

雖然不贊成使用大小寫混合的命名方式,但描述性名稱命名全域性函式是必須的。

全域性變數(僅在確實需要它們時才使用)與全域性函式一樣,都需要使用具有描述性的名稱命名。如果現在有一個函式用來統計活躍使用者數,應該將它命名為 count_active_users() 或類似的,而不應該命名為 cntusr()

將函式的型別編碼為名稱(所謂的匈牙利命名法)是不好的——編譯器無論如何都知道它們的型別並且可以檢查它們,這樣只會使程式設計師感到困惑。難怪 \(MicroSoft\) 寫出了很多古怪的程式。

區域性變數名和應該簡單明瞭,切中要害。如果你有一些隨機的整數迴圈變數,可以將它命名為 i。如果該程式沒有可能被誤解,命名為 loop_counter 是沒有必要的。同樣,tmp 幾乎可以用於任何型別的儲存臨時值的變數。

如果你害怕混淆你的區域性變數名,你會遇到另一個問題,它通常被稱為“生長激素功能失調綜合徵”。參見第六章(函式)。

5 Typedefs

請不要使用諸如 vps_t 之類的型別別名。對結構體和指標使用此類別名是一種錯誤。當你在原始碼中看到

vps_t a;

這是什麼意思?

相反,如果是這樣

struct virtual_container *a;

你就能知道 a 實際上到底是什麼。

許多人認為 typedefs 提高了可讀性。但並不是這樣,它們僅對以下內容有用:

  1. 完全不透明的物件(typedef 用於隱藏物件的內容)。

    示例:pte_t 等不透明的物件,你只能通過合適的訪問函式進行訪問。

    注意:不透明性和訪問函式並不是一種好的方法。之所以將它們用於類似 pte_t 的物件,是因為它們確實存在不可訪問的資訊。

  2. 明確整數型別,這種抽象有助於避免混淆 intlongu8/u16/u32 是完美的 typedefs

    注意:

    如果如果有任何量是 unsigned long,那就沒有理由使用 typedef unsigned long myflags_t

    但是如果有明確的理由說明為什麼在某些情況下它可能是 unsigned long,而在另一些配置下可能是 unsigned long,那就繼續使用 typedef

  3. 當你使用 sparse 建立一個用於型別檢查的新型別。

  4. 在某些特殊情況下,與 C99 標準型別相同的新型別。

    儘管只需要很短的時間就可以使眼睛和大腦適應像這樣的標準型別 uint32_t,但是無論如何,有些人還是反對使用它們。

    因此,Linux 特定型別 u8/u16/u32/u64 及其有符號型別被允許定義別名,儘管它們在新程式碼中不是必須的。

    當編輯已使用一種或另一組型別的現有程式碼時,應當遵循該程式碼中的現有選擇。

  5. 在使用者空間保持型別安全。

    在使用者空間可見的某些結構體中,我們不能使用 C99 標準型別,也不能使用類似 u32 的形式。因此,我們應當在所有與使用者空間共享的結構的體中使用 __u32 和類似型別。

也許還有其他情況,但是一個基本規則是:除非你可以清楚地匹配上述規則之一,否則不要使用 typedef

通常,指標或者具有可以合理地直接訪問元素的結構體永遠都不應該使用 typedef

6 函式

函式應該簡潔而優美,每個函式只做一件事。它們應該適合一到兩個文字(\(ISO/ANSI\) 螢幕的尺寸是 80 \(\times\) 24)。

函式的最大長度與該函式的複雜度和縮排程度成反比。因此,如果你有一個概念上很簡單的函式,但是有一個長而是簡單的 case 語句,通過這些語句完成很多不同的小任務,那麼讓該函式更長是可行的。

但是,如果你有一個複雜的函式,並且懷疑一般人無法理解該函式的全部含義,那麼你應該更加嚴格地遵守最大長度限制,並使用具有描述性名稱的輔助函式(如果你認為該輔助函式對效能至關重要,可以將其設定為行內函數)。

函式的另一個度量是區域性變數的數量,它們不應該超過 5-10 個,否則就是你的函式有什麼問題。重新規劃該函式,考慮將其拆分為較小的部分。人腦通常可以輕鬆地跟蹤大約 7 種不同的事物,再多就會變得混亂。你知道自己很聰明,但是也許你想輕鬆理解 2 周前做的工作。

在原始檔中,用一個空行分割函式。如果需要匯出該函式,則 EXPORT 巨集應該緊跟在函式結束的括號行之後。例如:

int system_is_up(void)
{
        return system_state == SYSTEM_RUNNING;
}
EXPORT_SYMBOL(system_is_up);

函式原型應該包含引數名稱及其資料型別。儘管這不是 C 語言所必需的,但我們建議在 Linux 中這樣做,因為它是一種簡單的為讀者新增有效資訊的方法。

7 函式集中退出

儘管有些人不贊同使用 goto 語句,但是編譯器經常以無條件跳轉指令的形式使用與 goto 語句等效的語句。

當函式可能從多個位置退出,並且必須執行一些常規工作(例如清理)時,goto 語句就會派上用場。如果不需要清理,則直接返回。

標籤命名需要說明 goto 的功能或 goto 存在的原因。當一個 goto 是用來釋放 buffer 的空間,一個好的標籤命名是 out_free_buffer:。避免使用像 err1:err2: 這樣的 GW-BASIC 命名,因為當你新增或刪除退出路徑時,你需要重新命名它們,並且它們會使正確性驗證變得非常困難。

使用 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;
}

注意常見的錯誤型別,例子如下:

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/)看到更多細節。

多行註釋推薦的風格如下:

/*
 * 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 更好地與 ~/src/linux-trees 目錄下的核心風格的 C 檔案相容。

但是即使你無法使用 emacs 進行理想的格式化,仍然還有其他方法:使用 indent

同樣,GNU indent 有一些與 GNU emacs 相同的失效配置,這就是為什麼你需要為其提供一些命令列選項。但這還不算太糟,因為即使是 GNU indent 的開發者也意識到 \(K\&R\) 的權威(GNU 的開發者並不邪惡,他們只是在這類問題上受到嚴重誤導),所以你只需要給 indent 提供選項 -kr -i8K&R, 8 character indents 標準),或使用 scripts/Lindent,它的縮排風格是最新風格。

indent 有很多選項,尤其是在對註釋重新格式化的時候,你可能需要仔細看一下 man 手冊。但請記住,indent 不是針對不良程式設計的解決方案。

10 Kconfig 配置檔案

對於原始碼樹中的所有 Kconfig* 配置檔案,它們的縮排有所不同。config 定義下的行以一個 tab 為單位縮排,而幫助文字以兩個空格縮排。例如:

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 資料結構

對於在單執行緒環境內建立和銷燬,並線上程之外可見的資料結構,應該使用引用計數。在核心中不存在垃圾回收(並且核心之外的垃圾回收是緩慢且低效的),這意味著你必須使用引用計數記錄所有的使用情況。

引用計數意味著你可以避免加鎖,允許多個使用者並行訪問資料結構,並且不必擔心正在使用的資料結構突然消失(僅僅因為程式 sleep 了一會兒或者做了一會兒其他的事情)。

注意:加鎖並不能代替引用計數。加鎖用於保持資料結構的一致性,而引用計數是一種記憶體管理技術。通常二者都是必須的,不要相互混淆。

很多資料結構都有 2 級引用計數,當存在不同等級的使用者時,自級計數器統計子級使用者的數量,當子級計數器變為 0 時將全域性計數器減 1。

這種多級引用計數的例子可以在記憶體管理(struct mm_struct: mm_users and mm_count)中找到,也可以在檔案系統(struct super_block: s_count and 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)

使用巨集時應該避免下列情況:

  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 則不太可能與已有的區域性變數發生衝突。

cpp 手冊詳盡描述了巨集,gcc 內部手冊還介紹了在核心中經常與組合語言一起使用的 RTL。

13 列印核心訊息

核心開發者喜歡被看作是有學問的人。一定要注意核心訊息的拼寫,給人留下好印象。不要使用類似 dont 之類的蹩腳單詞,使用 do notdon't 代替它。

核心訊息要簡潔、清晰並且無歧義。

核心訊息不必以句號結尾。

應避免列印帶括號的數字,因為這樣毫無意義。

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_DEBUGdev_dbg() 也是如此,通常的約定是使用 VERBOSE_DEBUG 將 dev_vdbg() 的訊息新增到由 DEBUG 啟用的訊息中。

許多子系統中通過 Kconfig 除錯選項來開啟相關 Makefile 中的 -DDEBUG。其他情況下在特定的檔案中使用#debug DEBUG 定義 DEBUG 巨集。當除錯資訊需要無條件列印,例如它已經在與除錯相關的 #ifdef 中,可以使用 printk(KERN_DEBUG ...)

14 分配記憶體

核心提供以下通用的記憶體分配函式:kmalloc()kzalloc()kmalloc_array()kcalloc()vmalloc()vzalloc()。關於它們更詳細的資訊,請參考 API 文件。

推薦的傳遞結構體大小的形式如下:

p = kmalloc(sizeof(*p), ...);

使用 struct name 的形式損害可讀性,並且當指標變數的型別發生了變化,但是傳遞給記憶體分配函式的與之相關的 sizeof 沒進行修改時,很容易引入 bug。

void 指標型別的返回值進行型別轉換是多餘的。將 void 指標轉換為其它任何型別的指標是由 C 語言保證的。

推薦的分配陣列的形式如下:

p = kmalloc_array(n, sizeof(...), ...);

推薦的分配所有元素的初始值為 0 的陣列的形式如下:

p = kcalloc(n, sizeof(...), ...);

兩種形式都需要檢查分配大小為 n * sizeof(...) 的記憶體是否成功,當分配不成功時會返回 NULL

15 內聯隱患

人們似乎普遍認為 GCC 有一個神奇的“使我更快”的加速選項——inline。雖然可以適當地使用內聯(比如作為替代巨集的一種方法,參閱第 12 章),但通常情況下不是這樣的。大量使用 inline 關鍵字會導致一個更大的核心,這會反過來會減慢整個系統的速度,因為這樣會導致 CPU icache 的佔用量會更大,並且用於頁面快取的記憶體會更少。頁面快取沒命中會導致磁碟查詢,這很容易就會花費掉 5 毫秒,然而有很多 CPU 週期本該可以進入這 5 毫秒。

一個合理的經驗法則是不要對包含多於三行程式碼的函式進行內聯。該規則的例外是:其中一個引數是編譯時確定的常量,並且由於這個常量你知道編譯器將能夠在編譯時優化函式的大部分內容。這種例外情況的好示例是 kmalloc() 行內函數。

人們經常爭辯說給靜態的並且只使用一次的函式增加內聯是一個勝利,因為不用在空間上做權衡。儘管從技術上講這是正確的,但實際上 GCC 有能力在沒有幫助的情況下自動對它們進行內聯。並且當第二個使用者出現時需要我們手動刪除內聯內容所導致的維護問題,超過了告訴 GCC 去做一些它已經做了的事情的潛在價值。

16 函式返回值和函式名稱

函式可以返回許多不同種類的值,其中最常見的是返回表明函式執行是成功還是失敗的值。表明成功或失敗,可以使用整型值(負數代表失敗,0 代表成功),或布林值(0 代表失敗,非 0 代表成功)。

將二者混合使用會給 bug 的排查帶來麻煩。如果 C 語言能很好的區分 integers 和 booleans,那麼編譯器將會幫助我們發現這些錯誤,但事實並非如此。為防止此類 bug,請始終遵守以下約定:

  • 如果函式名是一個動作或命令,函式應該返回 integer 型別的錯誤碼;
  • 如果函式名是謂語,函式應該返回 boolean 型別的錯誤碼。

例如:

  • add work 是一個命令,函式 add_work() 返回 0 代表成功,返回 -EBUSY 代表失敗。
  • PCI device present 是一個謂語,pci_dev_present() 函式在發現匹配的裝置時返回 1 代表成功,返回 0 代表沒發現匹配的裝置。

所有的匯出函式和公有函式必須遵守此約定。私有(靜態)函式不需要嚴格遵守,但也建議這麼做。

函式返回實際計算的結果(而不是表明函式是否計算成功)的函式不受此規則約束。通常它們通過返回一些超出範圍的結果來表明失敗。典型的例子是返回指標的函式,他們用 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 語言標頭檔案中定義相關的 C 語言函式原型。彙編函式的 C 語言函式原型需要使用 armlinkage

你可能需要將彙編語句標記為 volatile,以防止防止 GCC 把他們優化掉,但不一定總是要這麼做,而且多餘的做法可能會限制優化。

當編寫包含多條指令的單個內聯彙編語句時,將每條指令單獨放在帶引號的字串中並獨佔一行,並且除了最後一個字串以外,所有字串都以 \n\t 結尾,這樣做才能使以彙編形式輸出時下一條指令能正確地縮排。

asm ("magic %reg1, #42\n\t"
     "more_magic %reg2, %reg3"
     : /* outputs */ : /* inputs */ : /* clobbers */);

20 條件編譯

儘量不要在 .c 檔案中使用預處理條件(#if, #ifdef),因為這樣做會使程式碼難以閱讀,更難理解程式碼的邏輯。相反,在標頭檔案中使用預處理條件來定義那些在 .c 檔案中使用的函式,在 #else 中提供 no-op stub 的版本,然後在 .c 檔案中無條件地呼叫這些函式。編譯器將不會為呼叫 stub 生成任何程式碼,從而產生相同的效果,並且邏輯更容易理解。

最好編譯出整個函式,而不是部分函式或部分表示式。與其將 ifdef 放在表示式中,不如將表示式的一部分或全部分解到一個單獨的輔助函式中,然後將預處理條件應用於該函式。

如果某個函式或變數在特定的配置中可能不需要被使用,編譯器會針對定義但沒使用的變數發出警告,將該定義標記為 __maybe_unused 而不是用預處理條件包裹它(如果函式或變數總是不使用則應該刪除它)。

在程式碼中儘量使用 IS_ENABLE 巨集將 Kconfig 中的符號轉換為 C 語言的布林表示式,並且在普通的 C 語言條件語句中使用它:

if (IS_ENABLED(CONFIG_SOMETHING)) {
        ...
}

編譯器將不斷地摺疊這些條件,並像 #ifdef 一樣包含或排除程式碼塊,所以這不會增加任何執行時的開銷。然而這種方法仍然允許 C 編譯器檢視其中的程式碼並檢查其正確性(語法、型別、符號引用等)。因此當條件不滿足是,塊中程式碼引用的符號也不存在,這種情況下,你仍然需要使用 #ifdef

在任何具有一定複雜度的 #if 或 #ifdef 塊的末尾 #endif 所在行的後面放置註釋來標註使用的條件表示式,例如:

#ifdef CONFIG_SOMETHING
...
#endif /* CONFIG_SOMETHING */

附錄 參考

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 greg@kroah.com at OLS 2002: http://www.kroah.com/linux/talks/ols_2002_kernel_codingstyle_talk/html/

相關文章