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 语句中的 while
或 if-else 语句中的 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.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
,这更容易编写,而且理解起来也绝不更难。
然而,虽然不鼓励使用混合大小写的名称,但全局变量的描述性名称是必须的。将全局函数命名为 foo
是一种严重的错误。
全局变量(仅在你确实需要时才使用)和全局函数一样,需要有描述性的名称。如果你有一个函数用于计算活动用户数量,你应该将其命名为 count_active_users()
或类似的名称,而不应该将其命名为 cntusr()
。
将函数类型编码到名称中(所谓的匈牙利表示法)是愚蠢的——编译器无论如何都知道类型并可以检查它们,它只会混淆程序员。
局部变量名应该简短扼要。如果你有一个随机的整数循环计数器,它可能应该命名为 i
。如果不可能被误解,将其命名为 loop_counter
是没有意义的。类似地,tmp
可以是任何用于保存临时值的变量类型。
如果你担心混淆局部变量名,你还有另一个问题,这被称为函数增长荷尔蒙失衡综合征。参见第 6 章(函数)。
对于符号名称和文档,避免引入“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 年)硬件或协议规范的代码时,如果这些规范强制使用这些术语。对于新规范,尽可能将规范中的术语用法转换为内核编码标准。
5) Typedefs¶
请不要使用诸如 vps_t
之类的东西。对结构体和指针使用 typedef 是一个错误。当你看到一个
vps_t a;
在源代码中,它意味着什么?相反,如果它写着
struct virtual_container *a;
你实际上可以知道 a
是什么。
许多人认为 typedef 有助于提高可读性
。并非如此。它们仅适用于:
完全不透明的对象(其中 typedef 被积极用于隐藏对象的真实类型)。
例如:
pte_t
等不透明对象,你只能通过适当的访问器函数来访问它们。注意
不透明性和
访问器函数
本身并非总是好事。我们为 pte_t 等类型使用它们的原因是,这些类型中实际上绝对没有任何可移植访问的信息。清晰的整数类型,其中抽象有助于避免混淆是
int
还是long
。u8/u16/u32 是完全可以接受的 typedef,尽管它们更适合归类到 (d) 而不是此处。
注意
再次强调——这需要一个理由。如果某个东西是
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 语句的函数,其中你需要为许多不同的情况做许多小事情,那么函数长一点也是可以的。
然而,如果你有一个复杂的函数,并且你怀疑一个资质平平的高一学生可能都无法理解该函数是关于什么的,那么你更应该严格遵守最大长度限制。使用带有描述性名称的辅助函数(如果你认为它对性能至关重要,可以要求编译器将其内联,而且它可能会比你做得更好)。
衡量函数的另一个标准是局部变量的数量。它们不应超过 5-10 个,否则你可能做错了什么。重新思考该函数,并将其拆分为更小的部分。人脑通常可以轻松跟踪大约 7 种不同的事物,再多就会感到困惑。你知道自己很聪明,但也许你希望在两周后还能理解自己做了什么。
在源文件中,用一个空行分隔函数。如果函数是导出的,则其对应的 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 语句会派上用场。如果不需要清理,则直接返回即可。
选择能说明 goto 作用或存在原因的标签名称。一个好的名称示例可以是 out_free_buffer:
,如果该 goto 释放 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/ 和 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 中敲打也永远不会写出好的程序)。
所以,你可以要么放弃 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 字符 缩进
),或者使用 scripts/Lindent
,它会以最新风格进行缩进。
indent
有很多选项,特别是在注释重新格式化方面,你可能需要查看手册页。但请记住:indent
并非糟糕编程的解决方案。
请注意,你还可以使用 clang-format
工具来帮助你遵循这些规则,自动快速地重新格式化部分代码,并审查整个文件以发现编码风格错误、拼写错误和可能的改进。它还方便用于排序 #includes
、对齐变量/宏、重新排版文本以及其他类似任务。有关详细信息,请参阅文件 Documentation/dev-tools/clang-format.rst。
如果你使用的编辑器与 EditorConfig 兼容,一些基本的编辑器设置,如缩进和行尾符,将自动设置。更多信息请参阅 EditorConfig 官方网站:https://editorconfig.org/
10) Kconfig 配置文件¶
对于整个源代码树中的所有 Kconfig* 配置文件,缩进方式有所不同。在 config
定义下的行缩进一个制表符,而帮助文本则额外缩进两个空格。例如:
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
...
有关配置文件的完整文档,请参阅文件 Kconfig Language。
11) 数据结构¶
在其创建和销毁的单线程环境之外可见的数据结构应始终具有引用计数。在内核中,没有垃圾回收(在内核之外,垃圾回收也很慢且效率低下),这意味着你绝对必须对所有使用进行引用计数。
引用计数意味着你可以避免加锁,并允许多个用户并行访问数据结构——而且不必担心数据结构突然从他们下方消失,仅仅因为他们休眠或做了一些其他事情。
请注意,锁定不是引用计数的替代品。锁定用于保持数据结构的一致性,而引用计数是一种内存管理技术。通常两者都需要,且不应相互混淆。
当存在不同类别
的用户时,许多数据结构确实可以具有两级引用计数。子类别计数统计子类别用户的数量,并在子类别计数降至零时只递减一次全局计数。
这种多级引用计数
的例子可以在内存管理(struct mm_struct
:mm_users 和 mm_count)和文件系统代码(struct super_block
:s_count 和 s_active)中找到。
请记住:如果另一个线程可以找到你的数据结构,而你没有对其进行引用计数,那么你几乎肯定有一个错误。
12) 宏、枚举和 RTL¶
定义常量和枚举标签的宏名称使用大写字母。
#define CONSTANT 0x12345
定义多个相关常量时,首选枚举。
大写的宏名称值得称赞,但类似函数的宏可以使用小写名称。
通常,内联函数优于类似函数的宏。
包含多个语句的宏应封装在 do-while 块中
#define macrofun(a, b, c) \
do { \
if (a == 5) \
do_this(b, c); \
} while (0)
带有未使用参数的类函数宏应该被 static inline 函数替代,以避免未使用变量的问题
static inline void fun(struct foo *foo)
{
}
由于历史原因,许多文件仍然采用“强制转换为 (void)”的方法来评估参数。然而,这种方法并不可取。内联函数解决了“具有副作用的表达式被多次评估”的问题,避免了未使用变量的问题,并且由于某种原因,它们通常比宏有更好的文档。
/*
* Avoid doing this whenever possible and instead opt for static
* inline functions
*/
#define macrofun(foo) do { (void) (foo); } while (0)
使用宏时应避免的事项:
影响控制流的宏
#define FOO(x) \
do { \
if (blah(x) < 0) \
return -EBUGGERED; \
} while (0)
是一个非常糟糕的主意。它看起来像函数调用,但却退出了调用
函数;不要破坏代码阅读者的内部解析器。
依赖于具有魔术名称的局部变量的宏
#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 not
或 don't
。使消息简洁、清晰、明确。
内核消息不必以句号结尾。
在括号中打印数字 (%d) 没有附加价值,应避免使用。
<linux/dev_printk.h> 中有许多驱动模型诊断宏,你应该使用它们来确保消息与正确的设备和驱动程序匹配,并标记正确的级别:dev_err()、dev_warn()、dev_info() 等。对于不与特定设备关联的消息,<linux/printk.h> 定义了 pr_notice()
、pr_info()
、pr_warn()
、pr_err()
等。当驱动程序正常工作时,它们应保持静默,因此除非出现问题,否则首选使用 dev_dbg/pr_debug。
想出好的调试消息可能相当具有挑战性;一旦你有了它们,它们对于远程故障排除会非常有帮助。然而,调试消息的打印方式与打印其他非调试消息不同。其他 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
传递结构体大小的首选形式如下:
p = kmalloc(sizeof(*p), ...);
拼写出结构体名称的替代形式会损害可读性,并且当指针变量类型改变而传递给内存分配器的相应 sizeof 未改变时,会引入错误的机会。
强制转换返回值为 void 指针是多余的。C 语言保证了从 void 指针到任何其他指针类型的转换。
分配数组的首选形式如下:
p = kmalloc_array(n, sizeof(...), ...);
分配清零数组的首选形式如下:
p = kcalloc(n, sizeof(...), ...);
这两种形式都会检查分配大小 n * sizeof(...) 是否溢出,如果发生溢出则返回 NULL。
这些通用分配函数在未使用 __GFP_NOWARN 的情况下失败时都会发出栈转储,因此在返回 NULL 时没有必要发出额外的失败消息。
15) 内联症¶
似乎有一种普遍的误解,认为 gcc 有一个神奇的“让我更快”的加速选项叫做 inline
。虽然使用内联可能是合适的(例如作为替换宏的一种方式,参见第 12 章),但通常并非如此。大量使用 inline 关键字会导致内核变得更大,进而导致整个系统变慢,原因在于 CPU 的 icache 占用空间更大,并且用于页面缓存的内存更少。试想一下;一次页面缓存未命中会导致磁盘寻道,这很容易花费 5 毫秒。这 5 毫秒可以消耗大量的 CPU 周期。
一个合理的经验法则是,不要对包含超过 3 行代码的函数使用 inline。此规则的例外情况是参数已知为编译时常量,并且由于此常量的特性,你知道编译器能够在编译时优化掉你的大部分函数。关于后一种情况的一个好例子,请参阅 kmalloc()
内联函数。
人们常争辩说,对静态且只使用一次的函数添加 inline 总是能带来好处,因为没有空间上的权衡。虽然这在技术上是正确的,但 gcc 能够在没有帮助的情况下自动内联这些函数,并且当出现第二个用户时移除 inline 的维护问题,其重要性超过了提示 gcc 做它反正会做的事情的潜在价值。
16) 函数返回值与名称¶
函数可以返回多种不同类型的值,其中最常见的是指示函数成功或失败的值。这样的值可以表示为错误码整数(-Exxx = 失败,0 = 成功)或一个 成功
布尔值(0 = 失败,非零 = 成功)。
混淆这两种表示方式是难以发现的错误的温床。如果 C 语言在整数和布尔值之间有严格的区别,那么编译器就会为我们找到这些错误……但它没有。为了帮助防止此类错误,请始终遵循以下约定:
If the name of a function is an action or an imperative command,
the function should return an error-code integer. If the name
is a predicate, the function should return a "succeeded" boolean.
例如,add work
是一个命令,add_work() 函数成功返回 0,失败返回 -EBUSY。同样,PCI device present
是一个谓词,pci_dev_present()
函数如果成功找到匹配设备则返回 1,否则返回 0。
所有 EXPORTed 函数都必须遵守此约定,所有公共函数也应如此。私有(静态)函数不必如此,但建议遵守。
返回值是计算实际结果而非指示计算是否成功的函数不受此规则约束。它们通常通过返回超出范围的结果来指示失败。典型的例子是返回指针的函数;它们使用 NULL 或 ERR_PTR 机制来报告失败。
17) 使用 bool¶
Linux 内核的 bool 类型是 C99 _Bool 类型的别名。bool 值只能评估为 0 或 1,隐式或显式转换为 bool 会自动将值转换为 true 或 false。使用 bool 类型时不需要 !! 构造,这消除了一类错误。
处理 bool 值时,应使用 true 和 false 定义,而不是 1 和 0。
布尔函数返回类型和栈变量在适当的时候总是可以使用的。鼓励使用布尔类型以提高可读性,并且通常是存储布尔值的比“int”更好的选择。
如果缓存行布局或值的大小很重要,请勿使用 bool,因为其大小和对齐方式因编译架构而异。针对对齐和大小进行优化的结构体不应使用 bool。
如果一个结构体有许多真/假值,请考虑将它们合并到一个具有 1 位成员的位域中,或者使用适当的固定宽度类型,例如 u8。
类似地,对于函数参数,许多真/假值可以合并到一个单一的按位“flags”参数中,如果调用点有裸露的 true/false 常量,那么“flags”通常会是一个更具可读性的替代方案。
否则,在结构体和参数中有限地使用 bool 可以提高可读性。
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 情况下提供无操作的存根版本,然后从 .c 文件无条件地调用这些函数。编译器将避免为存根调用生成任何代码,产生相同的结果,但逻辑将保持易于跟踪。
优先编译掉整个函数,而不是函数的部分或表达式的部分。与其在表达式中放置 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 */
22) 不要使内核崩溃¶
一般来说,使内核崩溃的决定权在于用户,而不是内核开发者。
避免 panic()¶
使用 WARN() 而非 BUG()¶
不要添加使用任何 BUG() 变体的新代码,例如 BUG()、BUG_ON() 或 VM_BUG_ON()。相反,请使用 WARN*() 变体,最好是 WARN_ON_ONCE(),并可能带有恢复代码。如果没有合理的方法至少部分恢复,则不需要恢复代码。
“我太懒了,不想做错误处理”不是使用 BUG() 的借口。无法继续的重大内部损坏可能仍然使用 BUG(),但需要充分的理由。
使用 WARN_ON_ONCE() 而非 WARN() 或 WARN_ON()¶
WARN_ON_ONCE() 通常优于 WARN() 或 WARN_ON(),因为给定的警告条件(如果发生的话)通常会多次出现。这可能会填满并循环内核日志,甚至可能使系统运行缓慢,导致过多的日志本身成为一个额外的问题。
不要轻易使用 WARN¶
WARN*() 旨在用于意想不到的、不应该发生的情况。WARN*() 宏不得用于正常操作期间预期会发生的情况。例如,它们不是前置或后置条件断言。再次强调:WARN*() 不得用于预期会轻易触发的条件,例如用户空间操作。如果你需要通知用户问题,pr_warn_once() 是一种可能的替代方案。
不必担心 panic_on_warn 用户¶
关于 panic_on_warn 再多说几句:请记住 panic_on_warn
是一个可用的内核选项,并且许多用户会设置此选项。这就是上面有“不要轻易使用 WARN”的原因。然而,panic_on_warn 用户的存在并非避免明智地使用 WARN*() 的有效理由。那是因为,任何启用 panic_on_warn 的人都明确要求内核在 WARN*() 触发时崩溃,因此此类用户必须准备好应对系统更容易崩溃的后果。
使用 BUILD_BUG_ON() 进行编译时断言¶
使用 BUILD_BUG_ON() 是可以接受并鼓励的,因为它是一个在编译时生效且在运行时没有影响的断言。
附录 I) 参考资料¶
The C Programming Language, Second Edition by Brian W. Kernighan and Dennis M. Ritchie. Prentice Hall, Inc., 1988. ISBN 0-13-110362-8 (平装), 0-13-110370-9 (精装)。
The Practice of Programming by Brian W. Kernighan and Rob Pike. Addison-Wesley, Inc., 1999. ISBN 0-201-61586-X。
GNU 手册 - 在符合 K&R 和本文的情况下 - 关于 cpp、gcc、gcc internals 和 indent,均可从 https://gnu.ac.cn/manual/ 获取
WG14 是编程语言 C 的国际标准化工作组,URL: http://www.open-std.org/JTC1/SC22/WG14/
Kernel CodingStyle,作者 greg@kroah.com,OLS 2002 年发表:http://www.kroah.com/linux/talks/ols_2002_kernel_codingstyle_talk/html/