第十一章:数据结构
如果一个数据结构,在创建和销毁它的单线执行环境之外可见,那么它必须要有一个引用计数器。内核里没有垃圾收集(并且内核之外的垃圾收集慢且效率低下),这意味着你绝对需要记录你对这种数据结构的使用情况。
引用计数意味着你能够避免上锁,并且允许多个用户并行访问这个数据结构——而不需要担心这个数据结构仅仅因为暂时不被使用就消失了,那些用户可能不过是沉睡了一阵或者做了一些其他事情而已。
注意上锁不能取代引用计数。上锁是为了保持数据结构的一致性,而引用计数是一个内存管理技巧。通常二者都需要,不要把两个搞混了。
很多数据结构实际上有2级引用计数,它们通常有不同“类”的用户。子类计数器统计子类用户的数量,每当子类计数器减至零时,全局计数器减一。
这种“多级引用计数”的例子可以在内存管理(“struct mm_struct”:mm_users和mm_count)和文件系统(“struct super_block”:s_count和s_active)中找到。
记住:如果另一个执行线索可以找到你的数据结构,但是这个数据结构没有引用计数器,这里几乎肯定是一个bug。
第十二章:宏,列举(enum)和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)
非常不好。它看起来像一个函数,不过却能导致“调用”它的函数退出;
don't break the internal parsers of those who will read the code.
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,不会有任何损失,因为这种情况下没有什么好权衡的。虽然从技术上说,这是正确的,不过gcc可以在没有提示的情况下自动使其内联,而且其他用户可能会要求移除inline,此种维护上的争论会抵消可以告诉gcc来做某些事情的提示带来的潜在价值。不管有没有inline,这种函数都会被内联。
第十六章:函数返回值及命名
函数可以返回很多种不同类型的值,最常见的一种是表明函数执行成功或者失败的值。这样的一个值可以表示为一个错误代码整数(-E**=失败,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: &quot;gcc -DMAGIC_DEBUG_FLAG foo.c&quot;
End:
*/
Vim能够解释这样的标记:
/* vim:set sw=8 noet */
不要在源代码中包含任何这样的内容。每个人都有他自己的编辑器配置,你的源文件不应该覆盖别人的配置。这包括有关缩进和模式配置的标记。人们可以使用他们自己定制的模式,或者使用其他可以产生正确的缩进的巧妙方法。
更多嵌入式学习可以群聊:305711544 |