Author:Once Day Date:2023年12月27日
漫漫长路,才刚刚开始…
本文档主要内容翻译于《Linux kernel coding style》
配套的clang-format配置可参考文档:
本文总结并且描述Linux内核的编码风格,这是一份存在很久的文档,其实每一种编码风格都有其侧重点,也不会面面俱到,对于我们来说,不妨放下内心的偏见,博纳众家之长,丰富认知和经验。
这是一个简短的文档,描述了linux内核的首选编码风格。编码风格是非常个人化的,我不会把我的观点强加给任何人,但这是我必须能够维护的任何东西,我也希望它适用于大多数其他东西。请至少考虑一下这里提出的观点。首先,我建议打印一份GNU编码标准的副本,不要读它。烧了它们,这是一个很好的象征性姿态。
原文档开篇非常有趣,准备烧掉GNU编码规范,在我看来,就是需要尊重已有的编码风格,不能拿其他地方或者个人经验去搪塞。对于大型C语言项目来说,整齐划一的风格,对于维护和学习,有很大的帮助。
这是一个持续的学习过程,并非一朝一夕之事,如果哪天编码时,有那么一丝疑惑,不妨拿出来看看。
后续的内容,直接机翻于英文文档,中间添加了一些注释说明,描述了个人的一些理解。
制表符是8个字符,因此缩进也是8个字符。有些异端运动试图将缩进深度设置为4(甚至2!)个字符,这类似于试图将PI的值定义为3。
理由:缩进背后的整个想法是清楚地定义控制块的开始和结束位置。特别是当你连续盯着屏幕20个小时的时候,你会发现如果你有大的缩进,你会更容易看到缩进是如何工作的。
现在,有些人会声称8个字符的缩进会使代码向右边偏移太多,而且在80个字符的终端屏幕上很难阅读。答案是,如果如果你需要超过3层的缩进,你就完蛋了,应该修复你的程序。
(注: 现在惯例是使用space缩进,而不是tab,并且缩进以4个字符为单位,tab也等价解释为4个空白字符)
简而言之,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;
}
(注: switch风格建议case和switch同一列,并且标明fallthrough
的情况)
不要把多个语句放在一行中,除非你有什么要隐藏的:
if (condition) do_this;
do_something_everytime;
不要使用逗号来避免使用大括号:
if (condition)
do_this(), do_that();
总是对多个语句使用大括号:
if (condition) {
do_this();
do_that();
}
也不要将多个任务放在一行上。内核编码风格超级简单。避免复杂的表达。除了注释、文档和Kconfig之外,空格从来不用于缩进,上面的例子是故意打破的。找一个好的编辑器,不要在行尾留空格。
编码风格是指在使用常见工具时,能具备可读性和可维护性。
单行长度的首选限制是80列。超过80列的语句应该分成合理的块,除非超过80列会显著提高可读性并且不会隐藏信息。
子参数总是比父函数短得多,并且被放置在右边。一种非常常用的样式是将子参数与函数的左括号对齐。这些相同的规则适用于具有长参数列表的函数头文件。
但是,永远不要破坏用户可见的字符串,如printk
消息,因为这会破坏对它们进行grep
的能力。
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中嵌套它们)。
请注意,右括号在它自己的一行上是空的,除非它后面跟着同一语句的延续,即do
语句中的while
或if
语句中的else
,例如下面这样的:
do {
body of do-loop
} while (condition);
if (x == y) {
..
} else if (x > y) {
...
} else {
....
}
另外,请注意,这种大括号的放置还减少了空(或几乎空)行的数量,而不会损失可读性。因此,由于屏幕上的新行不是可再生资源(想想这里的25行终端屏幕),您有更多的空行可以放置注释。
在单个语句就可以完成的地方,不要不必要地使用大括号。
(注: 当前惯例一般是不再省略大括号,因为容易出隐形BUG,毕竟现在的屏幕非常大,远不止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();
}
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);
在大多数二元和三元操作符(两边)周围使用一个空格,例如以下任何一个:
= + - < > * / % | & ^ <= >= == != ? :
但是一元操作符(unary operators)后面没有空格:
& * + - ~ ! sizeof typeof alignof __attribute__ defined
后置和前缀自增和自减一元操作符前没有空格:
++ --
在.
和->
结构体成员操作符周围没有空白字符:
book.name
book_p->name
不要在行尾留下尾随空格。一些带有智能缩进的编辑器会在新行开头适当地插入空白,因此您可以立即开始键入下一行代码。但是,如果您最终没有在那里放置一行代码,例如如果您留下空白行,则某些此类编辑器不会删除空白。因此,您最终会看到包含尾随空格的行。
Git会警告你关于引入尾随空格的补丁,并可以选择性地为你删除尾随空格;但是,如果应用一系列补丁,这可能会使该系列中的后续补丁由于更改其上下文行而失败。
C是一种简朴语言,您的命名约定应该遵循它。
与Modula-2和Pascal程序员不同,C程序员不会使用像ThisVariableIsATemporaryCounter
这样可爱的名字。C程序员会把这个变量称为tmp,这样写起来容易得多,而且理解起来一点也不困难。
然而,尽管混合大小写的名称是不受欢迎的,但全局变量的描述性名称是必须的。将全局函数调用为foo是一种不合适的行为。全局变量(仅在真正需要时使用)需要具有描述性名称,就像全局函数一样。如果您有一个计算活动用户数量的函数,您应该调用count_active_users()
或类似的函数,而不应该调用cntusr()
。
将函数的类型编码到名称中(所谓的匈牙利符号)是愚蠢的,编译器无论如何都知道这些类型,并且可以检查这些类型,这只会使程序员感到困惑。
局部变量名应该简短,切中要害。如果你有一些随机的整数循环计数器,它可能应该被称为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年)硬件或协议规范的代码时强制使用这些术语。对于新的规范,尽可能将术语的规范用法转换为内核编码标准。
请不要使用vps_t
之类的东西。对结构体和指针使用typedef
是错误的。当你看到一个:
vps_t a;
在原文中,它是什么意思? 相反,如果它说:
struct virtual_container *a;
你可以知道a
是什么。很多人认为typedefs
有助于可读性,不是这样的,它们仅对以下情况有用:
完全不透明的对象(主动使用typedef
来隐藏对象是什么)。例如:pte_t
等不透明对象,您只能使用适当的访问器函数访问。
注意: 不透明性和访问器函数本身并不好。我们将它们用于pte_t
等内容的原因是,那里确实绝对没有可共同访问的信息。
清晰的整数类型,抽象有助于避免混淆是int
还是long
,比如u8/u16/u32
是非常好的类型。
注意: 再说一遍,这需要一个理由。如果某个东西是unsigned long
,那么就没有理由这样做Typedef unsigned myflags_t
;
但是,如果有明确的理由说明为什么它在某些情况下可能是unsigned int
,而在其他配置下可能是unsigned long
,那么无论如何都要使用typedef
。
当您使用spare
(一个设计用于在Linux内核代码中查找可能的编码错误的软件工具)来创建一个新的类型进行类型检查时。
在某些特殊情况下,与标准C99类型相同的新类型。虽然眼睛和大脑只需要很短的时间就能适应像uint32_t
这样的标准类型,但有些人还是反对使用它们。因此,linux
特定的u8/u16/u32/u64
类型及其与标准类型相同的带符号的等价类型是允许的。
尽管它们在您自己的新代码中不是强制性的。当编辑已经使用一组或另一组类型的现有代码时,您应该遵循该代码中的现有选择。
可在用户空间中安全使用的类型。在用户空间可见的某些结构中,我们不能要求C99
类型,也不能使用上面的u32
形式。因此,我们在与用户空间共享的所有结构体中使用_u32
和类似的类型。
也许还有其他情况,但规则基本上应该是永远不要使用typedef
,除非你能清楚地匹配这些规则之一。一般来说,指针或结构体的元素可以合理地直接访问,则不应该使用typedef
定义。
函数应该短小精悍,只做一件事。它们应该适合一个或两个屏幕的文本(ISO/ANSI
屏幕尺寸是80x24,我们都知道),并做一件事,把它做好。
函数的最大长度与该函数的复杂度和缩进程度成反比。所以,如果你有一个概念上很简单的函数,它只是一个很长的(但很简单的)case语句,你必须为很多不同的情况做很多小的事情,那么有一个更长的函数是可以的。
但是,如果您有一个复杂的函数,并且您怀疑一个不太有天赋的高一学生可能甚至不理解这个函数是关于什么的,那么您应该更严格地遵守最大限制。使用具有描述性名称的帮助函数(如果您认为这对性能至关重要,您可以要求编译器将它们内联,并且它可能比您做得更好)。
函数的另一个度量是局部变量的数量。它们不应该超过5-10个,否则你就做错了。重新思考这个功能,把它分成更小的部分。人类的大脑通常可以很容易地记住大约7件不同的事情,如果多了,它就会感到困惑。你知道你很聪明,但也许你想知道两周后你做了什么。
在源文件中,用一个空行分隔函数。如果导出函数,它的EXPORT宏应该紧跟着结束函数的大括号行。例如:
int system_is_up(void)
{
return system_state == SYSTEM_RUNNING;
}
EXPORT_SYMBOL(system_is_up);
在函数原型中,包括参数名和它们的数据类型。尽管C语言不需要这样做,但在Linux中首选这样做,因为这是为读者添加有价值信息的简单方法。
不要在函数声明中使用extern关键字,因为这会使行变长,并且不是严格必要的。在编写函数原型时,请保持元素的顺序有规律。例如,使用这个函数声明示例:
__init 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
{
...
}
推荐的函数原型元素顺序是:
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
)尽管有些人不赞成使用goto
语句,但是编译器经常以无条件跳转指令的形式使用goto
语句。
当函数从多个位置退出并且需要执行一些常见的工作(如清理)时,goto
语句会派上用场。如果不需要清理,那么直接返回即可。
选择标签名称,说明goto
的作用或goto
存在的原因。一个好名字的例子可以是out_free_buffer
: 如果goto
释放缓冲区。避免使用像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;
}
需要注意的一种常见错误类型是如下所示的错误:
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;
理想情况下,您应该模拟错误以测试所有退出路径。
注释是好的,但也有过度注释的危险。永远不要试图在注释中解释你的代码模型是如何工作的: 最好把代码写得很明显,解释写得不好的代码是浪费时间。
一般来说,你希望你的注释告诉你代码做了什么,而不是如何做。此外,尽量避免在函数体中放置注释: 如果函数非常复杂,以至于需要对其中的部分进行单独注释,那么应该考虑是否函数过于臃肿了。
你可以对一些特别聪明(或丑陋)的事情做些小的注释或警告,但尽量避免过度。相反,将注释放在函数的头部,告诉人们它做了什么,以及为什么这样做。
注释内核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.
*/
对于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.
*/
注释数据也很重要,无论它们是基本类型还是派生类型。为此,每行只使用一个数据声明(多个数据声明不使用逗号)。这样你就可以在每件物品上留下小小的注释,解释它的用法。
在创建和销毁它们的单线程环境之外具有可见性的数据结构应该始终具有引用计数。在内核中,垃圾收集是不存在的(在内核之外的垃圾收集是缓慢和低效的),这意味着您必须对所有的使用进行引用计数。
引用计数意味着您可以避免锁定,并允许多个用户并行访问数据结构,而不必担心因为他们睡了一会儿或做了其他事情而导致结构突然从他们下面消失。
注意,锁定不能替代引用计数。锁用于保持数据结构的一致性,而引用计数是一种内存管理技术。通常这两种都是需要的,它们不能相互混淆。
当有不同类的用户时,许多数据结构确实可以有两层引用计数。子类计数计算子类用户的数量,当子类计数为零时,全局计数只减少一次。这种多级引用计数的例子可以在内存管理(结构体mm_struct
: mm_users
和mm_count
)和文件系统代码(结构体super_block
: s_count
和s_active
)中找到。
请记住:如果另一个线程可以找到您的数据结构,而您没有对其进行引用计数,那么几乎可以肯定存在错误。
在枚举中定义常量和标签的宏的名称大写。
#define CONSTANT 0x12345
在定义多个相关常量时,首选枚举。欢迎使用大写的宏名,但类似函数的宏可以用小写命名。一般来说,内联函数比类似函数的宏更可取。
带有多条语句的宏应该被封装在do - while块中:
#define macrofun(a, b, c) \
do { \
if (a == 5) \
do_this(b, c); \
} while (0)
使用宏时要避免的事情:
影响控制流的宏,是个很糟糕的主意。它看起来像一个函数调用,但退出了调用函数; 不要破坏那些将阅读代码的人大脑解析过程。
#define FOO(x) \
do { \
if (blah(x) < 0) \
return -EBUGGERED; \
} while (0)
依赖于具有魔术名称的局部变量的宏,可能看起来是一件好事,但当人们阅读代码时,它就像地狱一样令人困惑,并且容易因看似无害的更改而损坏。
#define FOO(val) bar(index, val)
带有参数作为左值的宏: FOO(x) = y;
如果有人把FOO变成内联函数,就会咬你一口。
忘记优先级: 使用表达式定义常量的宏必须将表达式括起来。注意使用参数的宏的类似问题。
#define CONSTANT 0x4000
#define CONSTEXP (CONSTANT | 3)
在类似函数的宏中定义局部变量时发生命名空间冲突:
#define FOO(x) \
({ \
typeof(x) ret; \
ret = calc_ret(x); \
(ret); \
})
ret
是局部变量的通用名称,_foo_ret
不太可能与现有变量冲突。
内核提供了以下通用内存分配器:kmalloc()
,kzalloc()
,Kmalloc_array()
,kcalloc()
,vmalloc()
,vzalloc()
。请参考API文档关于他们的更多信息。文档/核心api/memory-allocation.rst
。
传递结构体大小的首选形式如下:
p = kmalloc(sizeof(*p), ...);
另一种形式是将struct名称拼写出来,这种形式会损害可读性,并且在指针变量类型被更改,但相应的sizeof
没有传递给内存分配器时,可能会引入错误。
强制转换一个空指针返回值是多余的。C编程语言保证了从void
指针到任何其他指针类型的转换。
分配数组的首选形式如下:
p = kmalloc_array(n, sizeof(...), ...);
分配一个归零数组的首选形式如下:
p = kcalloc(n, sizeof(...), ...);
这两种形式检查分配大小n * sizeof(…)
是否溢出,如果发生则返回NULL
。当不使用_GFP_NOWARN
时,这些通用分配函数都会在失败时发出堆栈转储,因此在返回NULL
时没有必要发出额外的失败消息。
似乎有一个常见的误解,gcc有一个神奇的“让我更快”的加速选项称为内联。虽然使用内联是合适的(例如,作为替换宏的一种手段),但通常不是这样。
大量使用内联关键字会导致更大的内核,这反过来又降低了整个系统的速度,因为CPU的icache
占用更大,而且用于页面缓存的可用内存更少。想想看,pagecache
缺失会导致磁盘寻道,这通常需要5毫秒,在这5毫秒中有很多cpu周期。
一个合理的经验法则是不要使用代码超过3行的内联函数。这条规则的例外情况是,参数已知是编译时常数,并且由于这种常数,您知道编译器将能够在编译时优化您的大部分函数。
有关后一种情况的一个好例子,请参阅kmalloc()
内联函数。人们经常认为,向静态且只使用一次的函数中添加内联总是一种胜利,因为没有空间被浪费。
虽然这在技术上是正确的,但gcc
能够在没有帮助的情况下自动内联这些内容。而且其他用户可能会要求移除 inline,由此而来的争论会抵消 inline 自身的潜在价值,得不偿失。
函数可以返回许多不同类型的值,其中最常见的一种是指示函数是否成功或失败的值。这样的值可以表示为错误码整数(-Exxx
=失败,0 = 成功)或成功布尔值(0=失败,非零=成功)。
将这两种表示混合在一起会产生难以发现的bug。如果C语言包含整数和布尔值之间的强烈区别,然后编译器会为我们找到这些错误……但事实并非如此。为了防止此类错误,请始终遵循以下约定:
例如,add work
是一个命令,add_work()
函数返回0
表示成功,返回-EBUSY
表示失败。以同样的方式,PCI device present
是一个谓词,如果pci_dev_present()
函数成功找到匹配的设备,则返回1
,否则返回0
。
所有导出的函数都必须遵守这个约定,所有公共函数也是如此。私有(静态)函数不需要这样做,但建议这样做。
如果函数的返回值是计算的实际结果,而不是计算是否成功的指示,则不受此规则的约束。通常,它们通过返回一些超出范围的结果来表示失败。典型的例子是返回指针的函数,它们使用NULL
或ERR_PTR
机制来报告失败。
Linux内核bool
类型是C99
_Bool
类型的别名。bool
值只能求值为0
或1
,隐式或显式转换为bool
会自动将值转换为true
或false
。当使用bool
类型时!!
不需要构造,这就消除了一类bug。
当使用bool
值时,应该使用true
和false
定义,而不是1
和0
。Bool
函数的返回类型和堆栈变量总是可以在适当的时候使用。鼓励使用bool
来提高可读性,并且对于存储布尔值来说,bool
通常是比int
更好的选择。
如果缓存栈布局或值的大小很重要,不要使用bool
,因为它的大小和对齐方式基于编译的架构,因此往往具有较大差异。对对齐和大小进行优化的结构不应该使用bool
。
如果一个结构体有很多真/假值,考虑将它们合并到一个1位成员的位域中,或者使用一个合适的固定宽度类型,比如u8。
类似地,对于函数参数,许多true/false
值可以合并到单个按位使用的flags
参数中,如果调用点具有裸true/false
常量,则flags
通常是更具可读性的替代方案。
总之,在结构体和参数中有限地使用bool可以提高可读性。
在特定于体系结构的代码中,可能需要使用内联汇编与CPU或平台进行对接功能,必要时不要犹豫。
但是,当C可以完成这项工作时,不要随意使用内联汇编。在可能的情况下,您可以并且应该从C中获取硬件数据。考虑编写简单的helper
函数来包装内联汇编的公共位,而不是稍有变化就反复地写新函数。
请记住,内联汇编可以使用C
参数。大型的、重要的汇编函数应该放在.s
文件中,在C
头文件中定义相应的C
原型。汇编函数的C
原型应该使用asmlinkage
。
你可能需要把汇编语句标记为 volatile,用来阻止 GCC 在没发现任何副作用后就把它移除了。你不必总是这样做,尽管,这不必要的举动会限制优化。
在写一个包含多条指令的单个内联汇编语句时,把每条指令用引号分割而且各占一行, 除了最后一条指令外,在每个指令结尾加上 \n\t
,让汇编输出时可以正确地缩进下一条指令:
asm ("magic %reg1, #42\n\t"
"more_magic %reg2, %reg3"
: /* outputs */ : /* inputs */ : /* clobbers */);
只要可能,不要在.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 */
对于整个源码树中的所有Kconfig*
配置文件,缩进都有所不同。配置定义下的行用一个制表符缩进,而帮助文本则额外缩进两个空格。例子:
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
。
内核开发人员喜欢被看作是有文化的。一定要注意内核消息的拼写,给人留下好印象。不要使用不正确的缩略词,比如don
,用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()
等。
提出好的调试消息可能是一个相当大的挑战:一旦你拥有了它们,它们就可以一个巨大的帮助远程故障排除。但是,调试消息打印的处理方式与打印其他非调试消息有很大的不同。当其他pr_XXX()
函数无条件打印时,pr_debug()
没有。
默认情况下,除非定义了DEBUG
或设置CONFIG_DYNAMIC_DEBUG
,否则它不会被编译出来。对于dev_dbg()
也是如此,并且有一个相关的约定VERBOSE_DEBUG
将dev_vdbg()
消息添加到已经被DEBUG
启用的消息中。
许多子系统都有Kconfig
调试选项,可以在相应的Makefile
中打开-DDEBUG
;在其他情况下,特定文件中使用#define DEBUG
。
当需要无条件打印调试消息时,比如它已经在与调试相关的#ifdef
节中,可以使用printk (KERN_DEBUG…)
。
头文件include/linux/kernel.h
包含许多应该使用的宏,而不是自己显式地编写它们的一些变体。例如,如果需要计算数组的长度,可以利用宏:
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
类似地,如果需要计算某个结构成员的大小,使用:
#define sizeof_field(t, f) (sizeof(((t*)0)->f))
如果需要的话,还有min()
和max()
宏可以执行严格的类型检查。请随意阅读该头文件,看看还有哪些已经定义但不应该在代码中复制的内容。
一些编辑器可以解释嵌入在源文件中的配置信息,用特殊的标记表示。例如,emacs
解释这样标记的行:
-*- mode: c -*-
或者如下:
/*
Local Variables:
compile-command: "gcc -DMAGIC_DEBUG_FLAG foo.c"
End:
*/
Vim解析如下所示的标记:
/* vim:set sw=8 noet */
不要在源文件中包含这些内容。每个人都有自己的编辑器配置,所以你的源文件不应该覆盖它们。这包括用于缩进和模式配置的标记。人们可能会使用自己的自定义模式,或者可能有一些其他神奇的方法来使缩进正确工作。
一般来说,使内核崩溃的决定权属于用户,而不是内核开发人员。
避免使用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
是一个可用的内核选项,许多用户都设置了这个选项。这就是为什么上面写着“不要轻易警告”的原因。但是,panic_on_warn
用户的存在并不是避免明智地使用WARN*()
的有效理由。这是因为,启用panic_on_warn
的人已经显式地要求内核在WARN*()
触发时崩溃,这样的用户必须准备好去面对更有可能崩溃的系统和处理其带来的后果。
对编译时断言使用BUILD_BUG_ON()
,使用BUILD_BUG_ON()
是可以接受和鼓励的,因为它是一个编译时断言,在运行时没有影响。
sparse
工具介绍Sparse是一个设计用于在Linux内核代码中查找可能的编码错误的软件工具。Linus Torvalds启动了Sparse的开发,以帮助对Linux内核代码进行静态分析。它可以检查包括类型安全性在内的多种问题。
Sparse的一个功能是定义新的类型,这些类型即使可能基于相同的底层C数据类型,但与其他类型是不同的。这些被称为“不透明类型”。这在内核中很有用,可以防止混淆不同类型的句柄或标识符。
例如,你可能有多种在内核中由整数表示的标识符:
如果没有额外的类型检查,C语言中没有什么能阻止程序员不小心将文件描述符传递给期望进程ID的函数,因为它们都是由整数表示的。
Sparse允许创建新的类型,这样如果有人试图在期望另一个类型的地方使用一个类型,编译器(通过Sparse的检查)会报告警告。这种类型检查比标准C提供的更加严格,它有助于防止错误地将错误类型的值传递给函数的错误。
这里是一个如何用Sparse定义新类型的例子:
typedef int __attribute__((noderef, address_space(1))) my_new_type;
在这行代码中,__attribute__((noderef, address_space(1)))
是一个Sparse特定的属性,告诉Sparse这是一个在单独地址空间中的新类型的指针类型,而my_new_type
是一个与普通整数不同的新类型,用于类型检查。
使用这些Sparse注解,Linux内核开发者可以创建一个类型系统,捕捉到C语言本身不会捕捉到的某些类型的错误。Linux内核编码风格文档可能包含如何使用Sparse及其注解来提高内核代码安全性和可靠性的指导原则。