2.4.2 GNU C
像所有自视清高的Unix内核一样,Linux内核是用C语言编写的。让人略感惊讶的是,内核并不完全符合ANSI C标准。实际上,只要有可能,内核开发者总是要用到gcc提供的许多语言的扩展部分。(gcc是多种GNU编译器的集合,它包含的C编译器既可以编译内核,也可以编译Linux系统上用C语言写的其他代码。)
内核开发者使用的C语言涵盖了ISO C99标准和GNU C扩展特性。这其中的种种变化把Linux内核推向了gcc的怀抱,尽管目前出现了一些新的编译器如Intel C,已经支持了足够多的gcc扩展特性,完全可以用来编译Linux内核了。最早支持gcc的版本是 3.2,但是推荐使用gcc 4.4或之后的版本。Linux内核用到的ISO C99标准的扩展没有什么特别之处,而且C99作为C语言官方标准的修订本,不可能有大的或是激进的变化。让人感兴趣的,与标准C语言有区别的,通常也是人们不熟悉的那些变化,多数集中在GNU C上。就让我们研究一下内核代码中所使用到的C语言扩展中让人感兴趣的那部分吧,这些变化使内核代码有别于你所熟悉的其他项目。
1. 内联(inline)函数
C99和GNU C均支持内联函数。inline这个名称就可以反映出它的工作方式,函数会在它所调用的位置上展开。这么做可以消除函数调用和返回所带来的开销(寄存器存储和恢复)。而且,由于编译器会把调用函数的代码和函数本身放在一起进行优化,所以也有进一步优化代码的可能。不过,这么做是有代价的(天下没有免费的午餐),代码会变长,这也就意味着占用更多的内存空间或者占用更多的指令缓存。内核开发者通常把那些对时间要求比较高,而本身长度又比较短的函数定义成内联函数。如果一个函数较大,会被反复调用,且没有特别的时间上的限制,我们并不赞成把它做成内联函数。
定义一个内联函数的时候,需要使用static作为关键字,并且用inline限定它。比如:
- static inline void wolf(unsigned long tail_size)
内联函数必须在使用之前就定义好,否则编译器就没法把这个函数展开。实践中一般在头文件中定义内联函数。由于使用了static作为关键字进行限制,所以编译时不会为内联函数单独建立一个函数体。如果一个内联函数仅仅在某个源文件中使用,那么也可以把它定义在该文件开始的地方。
在内核中,为了类型安全和易读性,优先使用内联函数而不是复杂的宏。
2. 内联汇编
gcc编译器支持在C函数中嵌入汇编指令。当然,在内核编程的时候,只有知道对应的体系结构,才能使用这个功能。
我们通常使用asm()指令嵌入汇编代码。例如,下面这条内联汇编指令用于执行x86处理器的rdtsc指令,返回时间戳(tsc)寄存器的值:
- unsigned int low, high;
- asm volatile("rdtsc" : "=a" (low), "=d" (high));
- /* low和high分别包含64位时间戳的低32位和高32位 */
Linux的内核混合使用了C语言和汇编语言。在偏近体系结构的底层或对执行时间要求严格的地方,一般使用的是汇编语言。而内核其他部分的大部分代码是用C语言编写的。
3. 分支声明
对于条件选择语句,gcc内建了一条指令用于优化,在一个条件经常出现,或者该条件很少出现的时候,编译器可以根据这条指令对条件分支选择进行优化。内核把这条指令封装成了宏,比如likely()和unlikely(),这样使用起来比较方便。
例如,下面是一个条件选择语句:
- if (error) {
- /* ... */
- }
如果想要把这个选择标记成绝少发生的分支:
- /* 我们认为error绝大多数时间都会为0...*/
- if (unlikely(error)) {
- /* ... */
-
- }
相反,如果我们想把一个分支标记为通常为真的选择:
- /* 我们认为success通常都不会为0 */
- if (likely(success)) {
- /* ... */
-
- }
在你想要对某个条件选择语句进行优化之前,一定要搞清楚其中是不是存在这么一个条件,在绝大多数情况下都会成立。这点十分重要:如果你的判断正确,确实是这个条件占压倒性的地位,那么性能会得到提升;如果你搞错了,性能反而会下降。正如上面这些例子所示,通常在对一些错误条件进行判断的时候会用到unlikely()和likely()。你可以猜到,unlikely()在内核中会得到更广泛的使用,因为if语句往往判断一种特殊情况。