Linux内核源代码分析-第二章 代码初识-1

来源:互联网 发布:linux 获取cpu使用率 编辑:程序博客网 时间:2024/06/05 02:11

第2章 代 码 初 识
本章首先从较高层次介绍Linux内核源程序的概况,这些都是大家关心的一些基本特点。
随后将简要介绍一些实际代码。最后介绍如何编译内核。
2.1 Linux内核源程序的部分特点
在过去的一段时期,Linux内核同时使用C语言和汇编语言来实现。这两种语言需要一定的
平衡:C语言编写的代码移植性较好、易于维护,而汇编语言编写的程序则速度较快。一
般只有在速度是关键因素或者一些因平台相关特性而产生的特殊要求(例如直接和内存管
理硬件进行通讯)时才使用汇编语言。
正如实际中所做的,即使内核并未使用C++的对象特性,部分内核也可以在g++(GNU的C++
编译器)下进行编译。同其他面向对象的编程语言相比较,相对而言C++的开销是较低的
,但是对于内核开发人员来说,这已经是太多了。
内核开发人员不断发展编程风格,形成了Linux代码独有的特色。本节将讨论其中的一些
问题。
2.1.1 gcc特性的使用
Linux内核被设计为必须使用GNU的C编译器gcc来编译,而不是任何一种C编译器都可以使
用。内核代码有时要使用gcc特性,本书将陆续介绍其中的一部分。
一些gcc特有代码只是简单地使用gcc语言扩展,例如允许在C(不只是C++)中使用inline
关键字指示内联函数。也就是说,代码中被调用的函数在每次函数调用时都会被扩充,因
而就可以节约实际函数调用的开销。
一般情况下,代码的编写方式比较复杂。因为对于某些类型的输入,gcc能够产生比其他
输入效率更高的执行代码。从理论上讲,编译器可以优化具有相同功能的两种对等的方法
,并且得到相同的结果。因此,代码的编写方式是无关紧要的。但在实际上,用某种方法
编写所产生的代码要比用另外一些方法编写所产生的代码执行速度快许多。内核开发人员
知道怎样才能产生更高效的执行代码,这不断地在他们编写的代码中反映出来。
例如,考虑内核中经常使用的goto语句—为了提高速度,内核中经常大量使用这种一般要
避免使用的语句。在本书中所包含的不到40 000行代码中,一共有500多条goto语句,大
约是每80行一个。除汇编文件外,精确的统计数字是接近每72行一个goto语句。公平地说
,这是选择偏向的结果:比例如此高的原因之一是本书中涉及的是内核源程序的核心,在
这里速度比其他因素都需要优先考虑。整个内核的比例大概是每260行一个goto语句。然
而,这仍然是我不再使用Basic进行编程以来见过的使用goto频率最高的地方。
代码必需受特定编译器限制的特性不仅与普通应用程序的开发有很大不同,而且也不同于
大多数内核的开发。大多数的开发人员使用C语言编写代码来保持较高的可移植性,即使
在编写操作系统时也是如此。这样做的优点是显而易见的,最为重要的一点是一旦出现更
好的编译器,程序员们可以随时进行更换。
内核对于gcc特性的完全依赖使得内核向新的编译器上移植更加困难。最近Linus对这一问
题在有关内核的邮件列表上表明了自己的观点:“记住,编译器只是一个工具。”这是对
依赖于gcc特性的一个很好的基本思想的表述:编译器只是为了完成工作。如果通过遵守
标准还不能达到工作要求,那么就不是工作要求有问题,而是对于标准的依赖有问题。
在大多数情况下,这种观点是不能被人所接受的。通常情况下,为了保证和程序语言标准
的一致,开发人员可能需要牺牲某些特性、速度或者其他相关因素。其他的选择可能会为
后期开发造成很大的麻烦。
但是,在这种特定的情况下,Linus是正确的。Linux内核是一个特例,因为其执行速度要
比向其他编译器的可移植性远为重要。如果设计目标是编写一个可移植性好而不要求快速
运行的内核,或者是编写一个任何人都可以使用自己喜欢的编译器进行编译的内核,那么
结论就可能会有所不同了;而这些恰好不是Linux的设计目标。实际上,gcc几乎可以为所
有能够运行Linux的CPU生成代码,因此,对于gcc的依赖并不是可移植性的严重障碍。
在第3章中我们将对内核设计目标进行详细说明。
2.1.2 内核代码习惯用语
内核代码中使用了一些显著的习惯用语,本节将介绍常用的几个。当通读源代码时,真正
重要的问题并不在这些习惯用语本身,而是这种类型的习惯用语的确存在,而且是不断被
使用和发展的。如果你需要编写内核代码,你应该注意到内核中所使用的习惯用语,并把
这些习惯用语应用到你的代码中。当通读本书(或者代码)时,看看你还能找到多少习惯
用语。
为了讨论这些习惯用语,我们首先需要对它们进行命名。为了便于讨论,笔者创造了这些
名字。而在实际中,大家不一定非要参考这些用语,它们只是对内核工作方式的描述而已

一个普通的习惯用语,笔者称之为“资源获取”(resource acquisition idiom)。在这
个用语中,一个函数必须实现一系列资源的获取,包括内存、锁等等(这些资源的类型未
必相同)。只有成功地获取当前所需要的资源之后,才能处理后面的资源请求。最后,该
函数还必须释放所有已经获取的资源,而不必考虑没有获取的资源。
我采用“错误变量”这一用语(error variable idiom)来辅助说明资源获取用语,它使
用一个临时变量来记录函数的期望返回值。当然,相当多的函数都能实现这个功能。但是
错误变量的不同点在于它通常是用来处理由于速度的因素而变得非常复杂的流程控制中的
问题。错误变量有两个典型的值,0(表示成功)和负数(表示有错)。
这两个用语结合使用,我们就可以十分自然地得到符合模式的代码如下:
(注意变量err是使用错误变量的一个明确实例,同样,诸如out之类的标号则指明了资源
获取用语的使用。)
如果执行到标号out2,则都已经获取了r1和r2资源,而且也都需要进行释放。如果执行到
标号out1(不管是顺序执行还是使用goto语句进行跳转到),则r2资源是无效的(也可能
刚被释放),但是r1资源却是有效的,而且必需在此将其释放。同理,如果标号out能被
执行,则r1和r2资源都无效,err所返回的是错误或成功标志。
在这个简单的例子中,对err的一些赋值是没有必要的。在实践中,实际代码必须遵守这
种模式。这样做的原因主要在于同一行中可能包含有多种测试,而这些测试应该返回相同
的错误代码,因此对错误变量统一赋值要比多次赋值更为简单。虽然在这个例子中对于这
种属性的必要性并不非常迫切,但是我还是倾向于保留这种特点。有关的实际应用可以参
考sys_shmctl(第21654行),在第9章中还将详细介绍这个例子。
2.1.3 减少#if和#ifdef的使用
现在的Linux内核已经移植到不同的平台上,但是我们还必须解决移植过程中所出现的问
题。大部分支持各种不同平台的代码由于包含许多预处理代码而已经变得非常不规范,例
如:
这个例子试图实现操作系统的可移植性,虽然Linux关注的焦点很明显是实现代码在各种
CPU上的可移植性,但是二者的基本原理是一致的。对于这类问题来说,预处理器是一种
错误的解决方式。这些杂乱的问题使得代码晦涩难懂。更为糟糕的是,增加对新平台的支
持有可能要求重新遍历这些杂乱分布的低质量代码段(实际上你很难能找到这类代码段的
全部)。
与现有方式不同的是,Linux一般通过简单函数(或者是宏)调用来抽象出不同平台间的
差异。内核的移植可以通过实现适合于相应平台的函数(或宏)来实现。这样不仅使代码
的主体简单易懂,而且在移植的过程中还可以比较容易地自动检测出你没有注意到的内容
:如引用未声明函数时会出现链接错误。有时用预处理器来支持不同的体系结构,但这种
方式并不常用,而相对于代码风格的变化就更是微不足道了。
顺便说一下,我们可以注意到这种解决方法和使用用户对象(或者C语言中充满函数指针
的struct结构)来代替离散的switch语句处理不同类型的方法十分相似。在某些层次上,
这些问题和解决方法是统一的。
可移植性的问题并不仅限于平台和CPU的移植,编译器也是一个重要的问题。此处为了简
化,假设Linux只使用gcc来编译。由于Linux只使用同一个编译器,所以就没有必要使用
#if块(或者#ifdef块)来选择不同的编译器。
内核代码主要使用#ifdef来区分需要编译或不需要编译的部分,从而对不同的结构提供支
持。例如,代码经常测试SMP宏是否定义过,从而决定是否支持SMP机。 

0 0