第七章:集中的函数退出途径
虽然被某些人声称已经过时,但是goto语句的等价物还是经常被编译器所使用,具体形式是
无条件跳转指令。
当一个函数从多个位置退出并且需要做一些通用的清理工作的时候,goto的好处就显现出来
了。
理由是:
- 无条件语句容易理解和跟踪
- 嵌套程度减小
- 可以避免由于修改时忘记更新某个单独的退出点而导致的错误
- 减轻了编译器的工作,无需删除冗余代码;)
int fun(int a)
{
int result = 0;
char *buffer = kmalloc(SIZE);
if (buffer == NULL)
return -ENOMEM;
if (condition1) {
while (loop1) {
...
}
result = 1;
goto out;
}
...
out:
kfree(buffer);
return result;
}
第八章:注释
注释是好的,不过有过度注释的危险。永远不要在注释里解释你的代码是如何运作的:更好
的做法是让别人一看你的代码就可以明白,解释写的很差的代码是浪费时间。
一般的,你想要你的注释告诉别人你的代码做了什么,而不是怎么做的。也请你不要把注释
放在一个函数体内部:如果函数复杂到你需要独立的注释其中的一部分,你很可能需要回到
第六章看一看。你可以做一些小注释来注明或警告某些很聪明(或者槽糕)的做法,但不要
加太多。你应该做的,是把注释放在函数的头部,告诉人们它做了什么,也可以加上它做这
些事情的原因。
当注释内核API函数时,请使用kernel-doc格式。请看
Documentation/kernel-doc-nano-HOWTO.txt和scripts/kernel-doc以获得详细信息。
Linux的注释风格是C89“/* ... */”风格。不要使用C99风格“// ...”注释。
长(多行)的首选注释风格是:
/*
* 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.
*/
注释数据也是很重要的,不管是基本类型还是衍生类型。为了方便实现这一点,每一行应只
声明一个数据(不要使用逗号来一次声明多个数据)。这样你就有空间来为每个数据写一段
小注释来解释它们的用途了。
第九章:你已经把事情弄糟了
这没什么,我们都是这样。可能你的使用了很长时间Unix的朋友已经告诉你“GNU emacs”能
自动帮你格式化C源代码,而且你也注意到了,确实是这样,不过它所使用的默认值和我们
想要的相去甚远(实际上,甚至比随机打的还要差——无数个猴子在GNU emacs里打字永远不
会创造出一个好程序)(译注:请参考Infinite Monkey Theorem)
所以你要么放弃GNU emacs,要么改变它让它使用更合理的设定。要采用后一个方案,你可
以把下面这段粘贴到你的.emacs文件里。
(defun linux-c-mode ()
"C mode with adjusted defaults for use with the Linux kernel."
(interactive)
(c-mode)
(c-set-style "K&R")
(setq tab-width 8)
(setq indent-tabs-mode t)
(setq c-basic-offset 8))
这样就定义了M-x linux-c-mode命令。当你hack一个模块的时候,如果你把字符串
-*- linux-c -*-放在头两行的某个位置,这个模式将会被自动调用。如果你希望在你修改
/usr/src/linux里的文件时魔术般自动打开linux-c-mode的话,你也可能需要添加
(setq auto-mode-alist (cons '("/usr/src/linux.*/.*\\.[ch]$" . linux-c-mode)
auto-mode-alist))
到你的.emacs文件里。
不过就算你尝试让emacs正确的格式化代码失败了,也并不意味着你失去了一切:还可以用“
indent”。
不过,GNU indent也有和GNU emacs一样有问题的设定,所以你需要给它一些命令选项。不
过,这还不算太糟糕,因为就算是GNU indent的作者也认同K&R的权威性(GNU的人并不是坏
人,他们只是在这个问题上被严重的误导了),所以你只要给indent指定选项“-kr -i8”
(代表“K&R,8个字符缩进”),或者使用“scripts/Lindent”,这样就可以以最时髦的方式
缩进源代码。
“indent”有很多选项,特别是重新格式化注释的时候,你可能需要看一下它的手册页。不过
记住:“indent”不能修正坏的编程习惯。
第十章:Kconfig配置文件
对于遍布源码树的所有Kconfig*配置文件来说,它们缩进方式与C代码相比有所不同。紧挨
在“config”定义下面的行缩进一个制表符,帮助信息则再多缩进2个空格。比如:
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.
仍然被认为不够稳定的功能应该被定义为依赖于“EXPERIMENTAL”:
config SLUB
depends on EXPERIMENTAL && !ARCH_USES_SLAB_PAGE_STRUCT
bool "SLUB (Unqueued Allocator)"
...
而那些危险的功能(比如某些文件系统的写支持)应该在它们的提示字符串里显著的声明这
一点:
config ADFS_FS_RW
bool "ADFS write support (DANGEROUS)"
depends on ADFS_FS
...
要查看配置文件的完整文档,请看Documentation/kbuild/kconfig-language.txt。
第十一章:数据结构
如果一个数据结构,在创建和销毁它的单线执行环境之外可见,那么它必须要有一个引用计
数器。内核里没有垃圾收集(并且内核之外的垃圾收集慢且效率低下),这意味着你绝对需
要记录你对这种数据结构的使用情况。
引用计数意味着你能够避免上锁,并且允许多个用户并行访问这个数据结构——而不需要担心
这个数据结构仅仅因为暂时不被使用就消失了,那些用户可能不过是沉睡了一阵或者做了一
些其他事情而已。
注意上锁不能取代引用计数。上锁是为了保持数据结构的一致性,而引用计数是一个内存管
理技巧。通常二者都需要,不要把两个搞混了。
很多数据结构实际上有2级引用计数,它们通常有不同“类”的用户。子类计数器统计子类用
户的数量,每当子类计数器减至零时,全局计数器减一。
这种“多级引用计数”的例子可以在内存管理(“struct mm_struct”:mm_users和mm_count)
和文件系统(“struct super_block”:s_count和s_active)中找到。
记住:如果另一个执行线索可以找到你的数据结构,但是这个数据结构没有引用计数器,这
里几乎肯定是一个bug。
第十二章:宏,枚举和RTL
用于定义常量的宏的名字及枚举里的标签需要大写。
#define CONSTANT 0x12345
在定义几个相关的常量时,最好用枚举。
宏的名字请用大写字母,不过形如函数的宏的名字可以用小写字母。
一般的,如果能写成内联函数就不要写成像函数的宏。
含有多个语句的宏应该被包含在一个do-while代码块里:
#define macrofun(a, b, c) \
do { \
if (a == 5) \
do_this(b, c); \
} while (0)
使用宏的时候应避免的事情:
1) 影响控制流程的宏:
#define FOO(x) \
do { \
if (blah(x) < 0) \
return -EBUGGERED; \
} while(0)
非常不好。它看起来像一个函数,不过却能导致“调用”它的函数退出;不要打乱读者大脑里
的语法分析器。
2) 依赖于一个固定名字的本地变量的宏:
#define FOO(val) bar(index, val)
可能看起来像是个不错的东西,不过它非常容易把读代码的人搞糊涂,而且容易导致看起来
不相关的改动带来错误。
3) 作为左值的带参数的宏: FOO(x) = y;如果有人把FOO变成一个内联函数的话,这种用
法就会出错了。
4) 忘记了优先级:使用表达式定义常量的宏必须将表达式置于一对小括号之内。带参数的
宏也要注意此类问题。
#define CONSTANT 0x4000
#define CONSTEXP (CONSTANT | 3)
cpp手册对宏的讲解很详细。Gcc internals手册也详细讲解了RTL(译注:register
transfer language),内核里的汇编语言经常用到它。
第十三章:打印内核消息
内核开发者应该是受过良好教育的。请一定注意内核信息的拼写,以给人以好的印象。不要
用不规范的单词比如“dont”,而要用“do not”或者“don't”。保证这些信息简单、明了、无
歧义。
内核信息不必以句号(译注:英文句号,即点)结束。
在小括号里打印数字(%d)没有任何价值,应该避免这样做。
<linux/device.h>里有一些驱动模型诊断宏,你应该使用它们,以确保信息对应于正确的
设备和驱动,并且被标记了正确的消息级别。这些宏有:dev_err(), dev_warn(),
dev_info()等等。对于那些不和某个特定设备相关连的信息,<linux/kernel.h>定义了
pr_debug()和pr_info()。
写出好的调试信息可以是一个很大的挑战;当你写出来之后,这些信息在远程除错的时候
就会成为极大的帮助。当DEBUG符号没有被定义的时候,这些信息不应该被编译进内核里
(也就是说,默认地,它们不应该被包含在内)。如果你使用dev_dbg()或者pr_debug(),
就能自动达到这个效果。很多子系统拥有Kconfig选项来启用-DDEBUG。还有一个相关的惯例
是使用VERBOSE_DEBUG来添加dev_vdbg()消息到那些已经由DEBUG启用的消息之上。
第十四章:分配内存
内核提供了下面的一般用途的内存分配函数:kmalloc(),kzalloc(),kcalloc()和
vmalloc()。请参考API文档以获取有关它们的详细信息。
传递结构体大小的首选形式是这样的:
p = kmalloc(sizeof(*p), ...);
另外一种传递方式中,sizeof的操作数是结构体的名字,这样会降低可读性,并且可能会引
入bug。有可能指针变量类型被改变时,而对应的传递给内存分配函数的sizeof的结果不变。
强制转换一个void指针返回值是多余的。C语言本身保证了从void指针到其他任何指针类型
的转换是没有问题的。
第十五章:内联弊病
有一个常见的误解是内联函数是gcc提供的可以让代码运行更快的一个选项。虽然使用内联
函数有时候是恰当的(比如作为一种替代宏的方式,请看第十二章),不过很多情况下不是
这样。inline关键字的过度使用会使内核变大,从而使整个系统运行速度变慢。因为大内核
会占用更多的指令高速缓存(译注:一级缓存通常是指令缓存和数据缓存分开的)而且会导
致pagecache的可用内存减少。想象一下,一次pagecache未命中就会导致一次磁盘寻址,将
耗时5毫秒。5毫秒的时间内CPU能执行很多很多指令。
一个基本的原则是如果一个函数有3行以上,就不要把它变成内联函数。这个原则的一个例
外是,如果你知道某个参数是一个编译时常量,而且因为这个常量你确定编译器在编译时能
优化掉你的函数的大部分代码,那仍然可以给它加上inline关键字。kmalloc()内联函数就
是一个很好的例子。
人们经常主张给static的而且只用了一次的函数加上inline,如此不会有任何损失,因为没
有什么好权衡的。虽然从技术上说这是正确的,但是实际上这种情况下即使不加inline gcc
也可以自动使其内联。而且其他用户可能会要求移除inline,由此而来的争论会抵消inline
自身的潜在价值,得不偿失。
第十六章:函数返回值及命名
函数可以返回很多种不同类型的值,最常见的一种是表明函数执行成功或者失败的值。这样
的一个值可以表示为一个错误代码整数(-Exxx=失败,0=成功)或者一个“成功”布尔值(
0=失败,非0=成功)。
混合使用这两种表达方式是难于发现的bug的来源。如果C语言本身严格区分整形和布尔型变
量,那么编译器就能够帮我们发现这些错误……不过C语言不区分。为了避免产生这种bug,请
遵循下面的惯例:
如果函数的名字是一个动作或者强制性的命令,那么这个函数应该返回错误代码整
数。如果是一个判断,那么函数应该返回一个“成功”布尔值。
比如,“add work”是一个命令,所以add_work()函数在成功时返回0,在失败时返回-EBUSY。
类似的,因为“PCI device present”是一个判断,所以pci_dev_present()函数在成功找到
一个匹配的设备时应该返回1,如果找不到时应该返回0。
所有导出(译注:EXPORT)的函数都必须遵守这个惯例,所有的公共函数也都应该如此。私
有(static)函数不需要如此,但是我们也推荐这样做。
返回值是实际计算结果而不是计算是否成功的标志的函数不受此惯例的限制。一般的,他们
通过返回一些正常值范围之外的结果来表示出错。典型的例子是返回指针的函数,他们使用
NULL或者ERR_PTR机制来报告错误。
第十七章:不要重新发明内核宏
头文件include/linux/kernel.h包含了一些宏,你应该使用它们,而不要自己写一些它们的
变种。比如,如果你需要计算一个数组的长度,使用这个宏
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
类似的,如果你要计算某结构体成员的大小,使用
#define FIELD_SIZEOF(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 */
不要在源代码中包含任何这样的内容。每个人都有他自己的编辑器配置,你的源文件不应
该覆盖别人的配置。这包括有关缩进和模式配置的标记。人们可以使用他们自己定制的模
式,或者使用其他可以产生正确的缩进的巧妙方法。
附录 I:参考
The C Programming Language, 第二版, 作者Brian W. Kernighan和Denni
M. Ritchie. Prentice Hall, Inc., 1988. ISBN 0-13-110362-8 (软皮),
0-13-110370-9 (硬皮). URL: http://cm.bell-labs.com/cm/cs/cbook/
The Practice of Programming 作者Brian W. Kernighan和Rob Pike. Addison-Wesley,
Inc., 1999. ISBN 0-201-61586-X. URL: http://cm.bell-labs.com/cm/cs/tpop/
cpp,gcc,gcc internals和indent的GNU手册——和K&R及本文相符合的部分,全部可以在
http://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 ... ingstyle_talk/html/ |