Skip to content

内核编码约定

编码风格 | Linux C编程一站式学习

Linux kernel coding style

Checkpatch

缩进和空白

C 语法本身不强制缩进和空白, 但风格差的代码难读也难维护. 下面以 Linux 内核风格为主整理常见约定.

空白

  1. if / while / for 与后面的 ( 之间加一个空格, 括号内侧不加空格:

    while (1);
    
  2. 双目运算符两侧加空格; 单目运算符与操作数之间不加空格:

    i = i + 1;
    ++i;
    !(i < 1);
    -x;
    &a[1];
    
  3. 后缀运算符与操作数之间不加空格: s.a, foo(arg1), a[i].

  4. , 和 ; 之后加空格, 模仿英文书写习惯:

    for (i = 1; i < 10; i++)
    foo(arg1, arg2);
    
  5. 为突出优先级可写得更紧凑, 但不要误导读者. 例如 a || b && c 容易让人看错优先级.

  6. 接近或超过 80 列的长语句要折行, 折行后与上一行表达式/参数对齐:

    if (sqrt(x*x + y*y) > 5.0
        && x < 0.0
        && y > 0.0)
    
    foo(sqrt(x*x + y*y),
        a[i-1] + b[i-1] + c[i-1]);
    
  7. 长字符串可拆成相邻多个字符串分行书写, 编译器会自动拼接:

    printf("This is such a long sentence that "
           "it cannot be held within a line\n");
    
  8. 变量定义可用 Tab 对齐变量名, 便于阅读:

        int     a, b;
        double  c;
    

缩进

  1. 用缩进体现语句块层次; 只用 Tab 缩进, 不要用空格代替 Tab. 建议编辑器把 Tab 显示宽度设为 8, 与终端一致. 空格与 Tab 混用时, 一旦改显示宽度就会乱.

  2. if / else / while / do / for / switch 的 { } 与关键字同一行 (K&R / 内核风格), 用空格隔开:

    if (...) {
            /* ... */
    } else if (...) {
            /* ... */
    }
    

    { 单独占一行也很常见; 同一项目内保持统一即可.

  3. 函数定义 的 { } 单独占一行 (与控制语句不同):

    int foo(int a, int b)
    {
            /* ... */
    }
    
  4. switch 与其中的 case / default 对齐 (标号相对 switch 不缩进), 标号下的语句再缩进一层. goto 的自定义标号顶格写.

            switch (c) {
            case 'A':
                    /* ... */
            case 'B':
                    /* ... */
            default:
                    /* ... */
            }
    
  5. 逻辑段落之间空一行分隔: 函数与函数之间, 头文件 / 全局变量 / 函数定义之间.

  6. 函数体较长时, 可按相关性分组并用空行隔开. 常见做法: 变量定义后空一行, return 前空一行.

#include <stdio.h>
#include <stdlib.h>

int g;
double h;

int foo(void)
{
        /* ... */
}

int main(void)
{
        int     a, b;
        double  c;

        /* 语句组 */

        return 0;
}

indent

indent 按一组选项重排 C 代码. 它会直接修改原文件, 并留下 *.c~ 备份.

内核自带的格式化脚本是 scripts/Lindent, 现行参数为:

indent -npro -kr -i8 -ts8 -sob -l80 -ss -ncs -cp1 -il0 file.c
  • -npro: 不读 ~/.indent.pro, 避免本机配置盖过这组参数

  • -kr: K&R 风格, 控制语句的 { 与关键字同行

  • -i8 -ts8: 缩进 8 列, 用 Tab. 未加 -nut 时, 每 8 个缩进空格会换成一个 Tab

  • -l80: 行宽 80

  • -sob: 删掉可有可无的空行. 按逻辑分组的空行仍要自己加

  • -ss: for (;;) 这类语句里, 分号前留一个空格

  • -ncs: 强制转换后不加空格, 写成 (int)x

  • -cp1: #else / #endif 旁边的注释放在第 1 列

  • -il0: case 与 switch 对齐, 不再多缩进一层

GNU indent 另有预设 -linux, 一次打开它认为的 "Linux 风格". 其中 -i8 -ts8 -l80 -ncs -sob 与上面相同, 但有两处和 Lindent 不一致: -il1 会让 case 比 switch 多缩进一列, -cp33 把 #else / #endif 旁的注释放到第 33 列. 要对齐内核, 用 Lindent 那一行, 不要用 -linux.

注释

单行注释用 /* comment */, 界定符与文字之间加空格. 多行注释常见写法:

/*
 * Multi-line
 * comment
 */

使用场合

  1. 文件顶部注释: 说明模块信息 (文件名, 作者, 版本历史等), 顶格写不缩进.

  2. 函数注释: 说明功能, 参数, 返回值, 错误码等, 写在函数定义正上方, 与定义之间不留空行, 顶格写. 内核常用类似 kernel-doc 的形式:

    /**
     * radix_tree_insert - insert into a radix tree
     * @root:  radix tree root
     * @index: index key
     * @item:  item to insert
     *
     * Insert an item into the radix tree at position @index.
     */
    int radix_tree_insert(...)
    {
            /* ... */
    }
    
  3. 语句组注释: 对相对独立的一组语句做说明, 写在语句组正上方, 不留空行, 缩进与当前语句组一致.

  4. 行右侧简短注释: 对当前行做特别说明, 与代码之间至少隔一个空格; 同一文件内右侧注释最好上下对齐. 不建议把注释穿插在一行代码中间.

  5. 复杂结构体: 比函数更需要注释, 可说明用途, 锁定规则, 字段含义等.

  6. 复杂宏与变量声明: 也需要注释, 尤其是易误用或有隐含约束的定义.

原则

函数内注释应尽可能少. 注释主要说明代码 能做什么 (接口与意图), 而不是 怎样做. 代码写得足够清晰时, "怎样做" 应一目了然; 需要大段注释才能解释清楚, 往往说明可读性不够. 只有特别需要提醒注意的地方, 才在函数内加注释.

命名原则

清晰可读

命名要清楚, 可用完整单词或易懂缩写. 短词常去元音, 长词常取前几个字母. 常见缩写惯例:

原词 缩写 原词 缩写
count cnt block blk
length len window win
message msg number nr
temporary temp / tmp transmit xmt (词根 trans → x)
internationalization i18n

大小写与分隔

内核风格约定:

  • 变量 / 函数: 全小写 + 下划线, 如 radix_tree_insert

  • 类型 (struct / enum 的名字): 同样全小写 + 下划线, 如 struct radix_tree_root, enum dma_data_direction. 自定义类型也不用驼峰

  • 常量 (宏, 枚举值): 全大写 + 下划线, 如 RADIX_TREE_MAP_SHIFT, DMA_BIDIRECTIONAL. 枚举的类型名仍是小写, 只有枚举常量才大写

不要用匈牙利命名法 (在名字里塞类型前缀, 如 iCnt, pMsg, lpszText).

Linus 对匈牙利命名法的评价

Encoding the type of a function into the name (so-called Hungarian notation) is brain damaged - the compiler knows the types anyway and can check those, and it only confuses the programmer. No wonder MicroSoft makes buggy programs.

内核风格也不采用大小写混合的CamelCase(驼峰命名), 那是Modern C++里更常见的写法. 教材里的 VertexType, MGraph 属于这一类, 不是内核约定.

类型与typedef

名字用 snake_case 还不够, 内核几乎不为结构体和指针做 typedef. 看见变量声明时, 要立刻知道它是结构体, 指针还是整数. 写成带 tag 的 struct, 用的时候把 struct 写出来.

例如下面代码定义了一个无向图的邻接矩阵表示:

struct mgraph {
        char vexs[MAX_VERTEX_NUM];
        int edge[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
        int vexnum, arcnum;
};

struct mgraph g;                 /* 对 */

在408中或者其他书中, 我们通常会这样定义一个图的结构体:

typedef struct {
        /* ... */
} mgraph_t;                      /* 错: 文档点名反对 vps_t 这种写法 */

typedef struct mgraph *mgraph_p; /* 更错: 把指针藏进类型名 */

但在内核中, 这样写是不受欢迎的.

为什么不受欢迎

因为这种写法把类型的真实面目藏了起来. 内核文档的对比是: 看见 vps_t a 你不知道 a 是结构体, 整数还是指针; 写成 struct virtual_container *a 一眼就知道.

回到上面的无向图的邻接矩阵, 它的成员本来就要直接读写 (g.edge[i][j]), 并不是 pte_t 那种「布局随架构变, 只许走访问器」的不透明对象, 用 typedef 装抽象非但没有好处, 弊端倒是不少:

  • mgraph_t a: 看起来像小整数. MAX_VERTEX_NUM 为 500 时, 光 edge[500][500] 就是约 1 MB. 类型被藏住后, 有人会写成 void foo(mgraph_t g) 按值传入, 整张矩阵砸在栈上.

  • mgraph_p p: 看起来像对象, 其实是指针. 容易漏查 NULL; 再写成 mgraph_p *pp 已经是二级指针, 看起来却不像.

  • 匿名 typedef struct { ... } mgraph_t 没有 tag, 头文件里没法 struct mgraph; 做前置声明.

_t 还是 POSIX 留给 size_t, pid_t 的后缀, 用户类型再叫 mgraph_t 既和文档点名批评的 vps_t 同款, 也容易和系统类型撞名.

内核实体都是这个样子: struct task_struct, struct list_head, struct file_operations.

typedef只留给少数情况使用:

  1. 完全不透明, 只能用访问器碰的对象, 如 pte_t

  2. 需要抽象位宽的整数, 如 u8 / u32, 或配置相关可能是 int 也可能是 long 的类型

  3. 偶尔的函数指针类型

typedef char vertex_type 这种别名一般也不写: char 已经够清楚, 别名没有藏起可移植差异. 真要抽象成「以后可能改底层类型」, 才考虑 typedef, 名字仍用 snake_case. 透明结构体不要加 _t (_t 还和 POSIX 保留后缀撞车).

内核 教材常见
结构体 struct mgraph typedef struct { ... } MGraph;
成员 / 函数 vex_num, first_neighbor() vexnum, FirstNeighbor
宏 / 枚举值 MAX_VERTEX_NUM, GRAPH_DIRECTED 同样大写
枚举类型名 enum graph_kind 有时 GraphKind

其他约定

  • 全局变量 / 全局函数: 名字要详细, 宁可多几个单词和下划线, 能一眼看懂用途 (如 radix_tree_insert).

  • 局部变量 / 单文件内部函数: 可简略一些, 但不要太短.

  • 尽量不使用单字母变量; 循环变量 i, j, k 除外.

  • 禁用汉语拼音标识符.

函数设计原则

单个函数应尽量简单, 简单才容易维护.

单一职责

一个函数只做好一件事. 不要做成"万能工具箱": 用途太广往往又长又难复用, 也难修改.

控制复杂度

  • 缩进层次: 一般少于 4 层. 层次过多说明设计过复杂, 应拆成更小的辅助函数 (Helper Function).

  • 函数长度: 建议在 24 行标准终端上不超过两屏; 超过就考虑拆分. 例外: 概念上很简单、只是篇幅长的函数可以接受. 典型例子是一个大 switch 带很多 case (各分支互不影响, 整体复杂度约等于其中一个分支), 如 TCP 状态机.

  • 局部变量数量: 5 到 10 个已经偏多, 再多就难维护, 应考虑拆成多个函数.

命名与注释

  • 函数表示一个动作, 名字通常应含动词, 如 get_current, radix_tree_insert.

  • 比较重要的函数定义上方必须加注释, 说明功能, 参数, 返回值, 错误码等.

函数修饰

  • 没有参数时写成 (void). 声明里的 () 表示参数未说明, 不是没有参数.

    int foo(void);
    
    int foo(void)
    {
        return 0;
    }
    
    /* not this! */
    int foo();
    
    int foo()
    {
        return 0;
    }
    

    函数定义里的空列表在 C99 中虽然等于没有参数, 内核仍要求定义和声明都写 (void). Checkpatch会把 int foo () 报成 FUNCTION_WITHOUT_ARGS.

  • 只在本文件使用的函数应使用 static 修饰.

    static int helper(void)
    {
        return 0;
    }
    

    不加 static 表示该函数具有外部链接. 内核编译打开了 -Wmissing-prototypes 选项, 具有外部链接的函数必须先有原型, 一般放在头文件里.