Linux 内核代码风格¶
这是一份简短的文档,描述了 Linux 内核首选的代码风格。代码风格是非常个人的,我不会将我的观点强加于任何人,但这是我必须维护的任何东西所遵循的风格,我也更喜欢将其用于大多数其他事情。请至少考虑一下这里提出的观点。
首先,我建议打印一份 GNU 代码标准的副本,并且不要阅读它。烧掉它,这是一个伟大的象征性姿态。
好了,下面开始:
1) 缩进¶
制表符是 8 个字符,因此缩进也是 8 个字符。有一些异端运动试图将缩进深度设置为 4 个(甚至 2 个!)字符,这就像试图将 PI 的值定义为 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();
}
此外,当循环包含多个简单语句时,请使用大括号
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
是一种枪击罪。
GLOBAL 变量(只有在您真正需要它们时才使用)需要具有描述性名称,全局函数也一样。如果您有一个函数用于计算活动用户的数量,则应将其称为 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) 类型定义¶
请不要使用类似 vps_t
这样的东西。为结构体和指针使用 typedef 是一个错误。当你在源代码中看到
vps_t a;
时,它意味着什么?相比之下,如果它说
struct virtual_container *a;
你实际上可以知道 a
是什么。
很多人认为 typedefs 有助于提高可读性
。并非如此。它们仅对以下情况有用
完全不透明的对象(其中 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 个不同的事物,任何更多的东西都会让人感到困惑。你知道你很聪明,但也许你希望在 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 语句会派上用场。如果没有清理需要,则直接返回。
选择说明 goto 的作用或 goto 存在的原因的标签名称。如果 goto 释放 buffer
,则一个好名称的示例可以是 out_free_buffer:
。避免使用像 err1:
和 err2:
这样的 GW-BASIC 名称,因为如果添加或删除退出路径,你必须重新编号它们,并且它们无论如何都使得正确性难以验证。
使用 gotos 的理由是
无条件语句更容易理解和遵循
减少了嵌套
防止了在进行修改时未更新各个退出点而导致的错误
节省了编译器优化冗余代码的工作 ;)
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 bugs
,看起来像这样
err:
kfree(foo->bar);
kfree(foo);
return ret;
此代码中的 bug 是在某些退出路径上 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 语言 文件。
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 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 转换为内联函数,则 FOO(x) = y; 会让你感到头疼。
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 footprint 更大,以及仅仅是因为可用于 pagecache 的内存更少,从而导致整个系统速度变慢。试想一下;pagecache 未命中会导致磁盘寻道,这很容易需要 5 毫秒。这 5 毫秒可以执行大量的 CPU 周期。
一个合理的经验法则是不要将 inline 放在代码行数超过 3 行的函数中。此规则的一个例外是参数已知为编译时常量的情况,并且由于此常量性,您*知道*编译器将能够在编译时优化掉您的大部分函数。有关后一种情况的良好示例,请参见 kmalloc()
内联函数。
人们经常争辩说,向仅使用一次的静态函数添加 inline 总是会带来好处,因为没有空间上的权衡。虽然这在技术上是正确的,但 gcc 能够在没有帮助的情况下自动内联这些函数,并且当出现第二个用户时,删除 inline 的维护问题会超过告诉 gcc 做一些它无论如何都会做的事情的潜在价值。
16) 函数返回值和名称¶
函数可以返回许多不同类型的值,其中最常见的一种是指示函数是否成功或失败的值。这样的值可以表示为错误代码整数(-Exxx = 失败,0 = 成功)或 succeeded
布尔值(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。
bool 函数返回类型和堆栈变量在适当的时候总是可以使用的。鼓励使用 bool 来提高可读性,并且对于存储布尔值来说,它通常是比“int”更好的选择。
如果缓存行布局或值的大小很重要,请勿使用 bool,因为其大小和对齐方式会根据编译的体系结构而变化。针对对齐方式和大小进行了优化的结构体不应使用 bool。
如果一个结构体有许多 true/false 值,请考虑将它们合并为具有 1 位成员的位域,或使用适当的固定宽度类型,例如 u8。
同样,对于函数参数,许多 true/false 值可以合并为单个按位“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 在 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()¶
panic()
应该谨慎使用,并且主要只在系统启动期间使用。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) 参考资料¶
《C 程序设计语言(第二版)》,Brian W. Kernighan 和 Dennis M. Ritchie 著。Prentice Hall, Inc., 1988. ISBN 0-13-110362-8 (平装), 0-13-110370-9 (精装)。
《编程实践》,Brian W. Kernighan 和 Rob Pike 著。Addison-Wesley, Inc., 1999. ISBN 0-201-61586-X。
GNU 手册 - 在符合 K&R 和本文档的情况下 - 适用于 cpp、gcc、gcc internals 和 indent,所有这些都可从 https://www.gnu.org/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/