Linux 内核代码风格
Linux 内核代码风格
这是一个简短的文档,描述了 linux 内核的首选代码风格。代码风格是因人而异的, 而且我不愿意把自己的观点强加给任何人,但这就像我去做任何事情都必须遵循的原则 那样,我也希望在绝大多数事上保持这种的态度。请 (在写代码时) 至少考虑一下这里 的代码风格。
首先,我建议你打印一份 GNU 代码规范,然后不要读。烧了它,这是一个具有重大象征 性意义的动作。
不管怎样,现在我们开始:
1) 缩进
制表符是 8 个字符,所以缩进也是 8 个字符。有些异端运动试图将缩进变为 4 (甚至 2!) 字符深,这几乎相当于尝试将圆周率的值定义为 3。
理由:缩进的全部意义就在于清楚的定义一个控制块起止于何处。尤其是当你盯着你的 屏幕连续看了 20 小时之后,你将会发现大一点的缩进会使你更容易分辨缩进。
现在,有些人会抱怨 8 个字符的缩进会使代码向右边移动的太远,在 80 个字符的终端 屏幕上就很难读这样的代码。这个问题的答案是,如果你需要 3 级以上的缩进,不管用 何种方式你的代码已经有问题了,应该修正你的程序。
简而言之,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 列能显著增加可读性,并且不 会隐藏信息。
子片段要明显短于母片段,并明显靠右。一种非常常用的样式是将子体与函数左括号对齐。
这同样适用于有着很长参数列表的函数头。
然而,绝对不要打散对用户可见的字符串,例如 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
}
全世界的异端可能会抱怨这个不一致性是……呃……不一致,不过所有思维健全的人 都知道 (a) K&R 是 正确的 并且 (b) K&R 是正确的。此外,不管怎样函数都是特 殊的 (C 函数是不能嵌套的)。
注意结束大括号独自占据一行,除非它后面跟着同一个语句的剩余部分,也就是 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();
}
3.1) 空格
Linux 内核的空格使用方式 (主要) 取决于它是用于函数还是关键字。(大多数) 关键字 后要加一个空格。值得注意的例外是 sizeof, typeof, alignof 和 __attribute__,这 些关键字某些程度上看起来更像函数 (它们在 Linux 里也常常伴随小括号而使用,尽管 在 C 里这样的小括号不是必需的,就像 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() 或者类似的名字,你不应该叫它 cntuser() 。
在函数名中包含函数类型 (所谓的匈牙利命名法) 是脑子出了问题——编译器知道那些类 型而且能够检查那些类型,这样做只能把程序员弄糊涂了。
本地变量名应该简短,而且能够表达相关的含义。如果你有一些随机的整数型的循环计 数器,它应该被称为 i 。叫它 loop_counter 并无益处,如果它没有被误解的 可能的话。类似的, tmp 可以用来称呼任意类型的临时变量。
如果你怕混淆了你的本地变量名,你就遇到另一个问题了,叫做函数增长荷尔蒙失衡综 合征。请看第六章 (函数)。
对于符号名称和文档,避免引入新的“master/slave”(或独立于“master”的“slave”) 和“blacklist/whitelist”。
- “master/slave”推荐替换为:
-
‘{primary,main} / {secondary,replica,subordinate}’ ‘{initiator,requester} / {target,responder}’ ‘{controller,host} / {device,worker,proxy}’ ‘leader/follower’ ‘director/performer’
- “blacklist/whitelist”推荐替换为:
-
‘denylist/allowlist’ ‘blocklist/passlist’
引入新用法的例外情况是:维护用户空间ABI/API,或更新现有(截至2020年)硬件或 协议规范的代码时要求这些术语。对于新规范,尽可能将术语的规范用法转换为内核 编码标准。
Warning
以上主从、黑白名单规则不适用于中文文档,请勿更改中文术语!
5) Typedef
不要使用类似 vps_t 之类的东西。
对结构体和指针使用 typedef 是一个 错误 。当你在代码里看到:
vps_t a;
这代表什么意思呢?
相反,如果是这样
struct virtual_container *a;
你就知道 a 是什么了。
很多人认为 typedef 能提高可读性 。实际不是这样的。它们只在下列情况下有用:
完全不透明的对象 (这种情况下要主动使用 typedef 来 隐藏 这个对象实际上 是什么)。
例如:
pte_t等不透明对象,你只能用合适的访问函数来访问它们。Note
不透明性和“访问函数”本身是不好的。我们使用 pte_t 等类型的原因在于真 的是完全没有任何共用的可访问信息。
清楚的整数类型,如此,这层抽象就可以 帮助 消除到底是
int还是long的混淆。u8/u16/u32 是完全没有问题的 typedef,不过它们更符合类别 (d) 而不是这里。
Note
要这样做,必须事出有因。如果某个变量是
unsigned long,那么没有必要typedef unsigned long myflags_t;
不过如果有一个明确的原因,比如它在某种情况下可能会是一个
unsigned int而在其他情况下可能为unsigned long,那么就不要犹豫,请务必使用 typedef。当你使用 sparse 按字面的创建一个 新 类型来做类型检查的时候。
和标准 C99 类型相同的类型,在某些例外的情况下。
虽然让眼睛和脑筋来适应新的标准类型比如
uint32_t不需要花很多时间,可 是有些人仍然拒绝使用它们。因此,Linux 特有的等同于标准类型的
u8/u16/u32/u64类型和它们的有符号 类型是被允许的——尽管在你自己的新代码中,它们不是强制要求要使用的。当编辑已经使用了某个类型集的已有代码时,你应该遵循那些代码中已经做出的选 择。
可以在用户空间安全使用的类型。
在某些用户空间可见的结构体里,我们不能要求 C99 类型而且不能用上面提到的
u32类型。因此,我们在与用户空间共享的所有结构体中使用 __u32 和类似 的类型。
可能还有其他的情况,不过基本的规则是 永远不要 使用 typedef,除非你可以明 确的应用上述某个规则中的一个。
总的来说,如果一个指针或者一个结构体里的元素可以合理的被直接访问到,那么它们 就不应该是一个 typedef。
6) 函数
函数应该简短而漂亮,并且只完成一件事情。函数应该可以一屏或者两屏显示完 (我们 都知道 ISO/ANSI 屏幕大小是 80x24),只做一件事情,而且把它做好。
一个函数的最大长度是和该函数的复杂度和缩进级数成反比的。所以,如果你有一个理 论上很简单的只有一个很长 (但是简单) 的 case 语句的函数,而且你需要在每个 case 里做很多很小的事情,这样的函数尽管很长,但也是可以的。
不过,如果你有一个复杂的函数,而且你怀疑一个天分不是很高的高中一年级学生可能 甚至搞不清楚这个函数的目的,你应该严格遵守前面提到的长度限制。使用辅助函数, 并为之取个具描述性的名字 (如果你觉得它们的性能很重要的话,可以让编译器内联它 们,这样的效果往往会比你写一个复杂函数的效果要好。)
函数的另外一个衡量标准是本地变量的数量。此数量不应超过 5-10 个,否则你的函数 就有问题了。重新考虑一下你的函数,把它分拆成更小的函数。人的大脑一般可以轻松 的同时跟踪 7 个不同的事物,如果再增多的话,就会糊涂了。即便你聪颖过人,你也可 能会记不清你 2 个星期前做过的事情。
在源文件里,使用空行隔开不同的函数。如果该函数需要被导出,它的 EXPORT 宏 应该紧贴在它的结束大括号之下。比如:
int system_is_up(void)
{
return system_state == SYSTEM_RUNNING;
}
EXPORT_SYMBOL(system_is_up);
6.1) 函数原型
在函数原型中包含参数名和它们的数据类型。虽然 C 语言里没有这样的要求,但在 Linux 里这是提倡的做法,因为这样可以很简单的给读者提供更多的有价值的信息。
不要在函数声明里使用 extern 关键字,因为这会导致代码行变长,并且不是严格 必需的。
写函数原型时,请保持 元素顺序规则 。 例如下列函数声明:
__init void * __must_check action(enum magic value, size_t size, u8 count,
char *fmt, ...) __printf(4, 5) __malloc;
推荐的函数原型元素顺序是:
-
储存类型(下方的
static __always_inline,注意__always_inline技术上来讲是个属性但被当做inline) -
储存类型属性(上方的
__init——即节声明,但也像__cold) -
返回类型(上方的
void *) -
返回类型属性(上方的
__must_check) -
函数名(上方的
action) -
函数参数(上方的
(enum magic value, size_t size, u8 count, char *fmt, ...), 注意必须写上参数名) -
函数参数属性(上方的
__printf(4, 5)) -
函数行为属性(上方的
__malloc)
请注意,对于函数 定义 (即实际函数体),编译器不允许在函数参数之后添加函 数参数属性。在这种情况下,它们应该跟随存储类型属性(例如,与上面的 声明 示例相比,请注意下面的 __printf(4, 5) 的位置发生了变化):
static __always_inline __init __printf(4, 5) void * __must_check action(enum magic value,
size_t size, u8 count, char *fmt, ...) __malloc
{
...
}
7) 集中的函数退出途径
虽然被某些人声称已经过时,但是 goto 语句的等价物还是经常被编译器所使用,具体 形式是无条件跳转指令。
当一个函数从多个位置退出,并且需要做一些类似清理的常见操作时,goto 语句就很方 便了。如果并不需要清理操作,那么直接 return 即可。
选择一个能够说明 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 错误 ,就像这样:
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) 注释
注释是好的,不过有过度注释的危险。永远不要在注释里解释你的代码是如何运作的: 更好的做法是让别人一看你的代码就可以明白,解释写的很差的代码是浪费时间。
一般来说你用注释告诉别人你的代码做了什么,而不是怎么做的。也请你不要把 注释放在一个函数体内部:如果函数复杂到你需要独立的注释其中的一部分,你很可能 需要回到第六章看一看。你可以做一些小注释来注明或警告某些很聪明 (或者槽糕) 的 做法,但不要加太多。你应该做的,是把注释放在函数的头部,告诉人们它做了什么, 也可以加上它做这些事情的原因。
当注释内核 API 函数时,请使用 kernel-doc 格式。详见 如何编写内核文档 和 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.
*/
注释数据也是很重要的,不管是基本类型还是衍生类型。为了方便实现这一点,每一行 应只声明一个数据 (不要使用逗号来一次声明多个数据)。这样你就有空间来为每个数据 写一段小注释来解释它们的用途了。
9) 你已经把事情弄糟了
这没什么,我们都是这样。可能你长期使用 Unix 的朋友已经告诉你 GNU emacs 能自动帮你格式化 C 源代码,而且你也注意到了,确实是这样,不过它 所使用的默认值和我们想要的相去甚远 (实际上,甚至比随机打的还要差——无数个猴子 在 GNU emacs 里打字永远不会创造出一个好程序) (译注:Infinite Monkey Theorem)
所以你要么放弃 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 .
