资源预览内容
第1页 / 共12页
第2页 / 共12页
第3页 / 共12页
第4页 / 共12页
第5页 / 共12页
第6页 / 共12页
第7页 / 共12页
第8页 / 共12页
第9页 / 共12页
第10页 / 共12页
亲,该文档总共12页,到这儿已超出免费预览范围,如果喜欢就下载吧!
资源描述
本章首先从较高层次介绍 Linux 内核源程序的概况,这些都.txt 鲜花往往不属于赏花的人, 而属于牛粪。 。 。道德常常能弥补智慧的缺陷,然而智慧却永远填补不了道德空白人生有三样 东西无法掩盖:咳嗽贫穷和爱,越隐瞒,就越欲盖弥彰。 的的范德萨的地方爱的规格爱你啊好文章第 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 机。 2.2 代码样例 了解 Linux 代码风格最好的方法就是实际研究一下它的部分代码。即使你不完全理解本节所 讨论代码的细节也无关紧要,毕竟本节的主要目的不是理解代码,一些读者可以只对本节进 行浏览。本节的主要目的是让读者对 Linux 代码进行初步了解,为今后的工作提供必要基础。 该讨论将涉及部分广泛使用的内核代码。2.2.1 printk printk(25836 行)是内核内部消息日志记录函数。在出现诸如内核检测到其数据结构出现 不一致的事件时,内核会使用 printk 把相关信息打印到系统控制台上。对于 printk 的调用 一般分为如下几类: ?紧急事件(emergency)例如,panic 函数(25563 行)多次使用了 printk。当内核检测 到发生不可恢复的内部错误时就会调用 panic 函数,然后尽其所能地安全关闭计算机。这个 函数中调用 printk 以提示用户系统将要关闭。 ?调试从 3816 行开始的#ifdef 块使用 printk 来打印 SMP 逻辑单元(box)中每一个处理 器的相关配置信息,但是此过程只有在使用 SMP_DEBUG 标志编译代码的情况下才能够被执行。?普通信息例如,当机器启动时,内核必须估计系统速度以确保设备驱动程序能够忙等待 (busy-wait)一个精确的极短周期。计算这种估计值的函数名为 calibrate_delay(19654 行) ,它既在 19661 行使用 printk 声明马上开始计算,又在 19693 行报告计算结果。另外, 在第 4 章将详细的介绍 calibrate_delay 函数。 如果你已经浏览过这些参照行,你可能已经注意到 printk 和 printf 的参数十分类似:一个 格式化字符串,后跟零个或者多个参数加入字符串中。格式化字符串可能是以一组“” 开始,这里的 N 是从 0 到 7 的数字,包括 0 和 7 在内。数字区分了消息的日志等级(log level) ,只有当日志等级高于当前控制台定义的日志等级(console_loglevel,25650 行) 时,才会打印消息。root 可以通过适当减小控制台的日志等级来过滤不是很紧急的消息。 如果内核在格式化字符串中检测不到日志等级序列,那么就会一直打印消息(实际上,日志 等级序列并不一定要在格式化字符串中出现,可以在格式化文本中查找到它的代码) 。 从 14946 行开始的#define 块说明了这些特殊序列,这些定义可以帮助调用者正确区分对 printk 的调用。简单地说,我称日志等级 0 到 4 为“紧急事件” ,等级 5 到等级 6 为“普通 信息” ,等级 7 自然就是我所说的“调试” (这种分类方法并不意味着其他更好的分类方法没 有用处,而只是目前我们还不关心它而已) 。 在上面讨论的基础上,我们研究一下代码本身。printk 25836:参数 fmt 是 printf 类型的格
网站客服QQ:2055934822
金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号