如何加入Linux内核开发社区(4)
本文翻译自The Linux Foundation的《How to Participate in the Linux Community》(基于2012-03-21最新版本),原作者为Jonathan Corbet(corbet@lwn.net)。 下面是该文章第四章节的中译文。
4、正确地编写代码
关于那个可靠的面向社区的设计过程我们已经说的够多了,任何内核开发项目的证据都是最终的代码。被其他开发者检查的是代码,被(或没有被)合并到主线树的也是代码。因此是代码质量决定了内核开发项目最终的成功。
这一节我们会对内核编码过程进行剖析。我们会首先看看内核开发者可能会出错的几个方面;接下来我们会将关注点转向如何正确地做事以及一些可以在此过程中帮助到我们的工具。
4.1、陷阱
* 编码风格
内核早已拥有了一套标准的编码风格,在Documentation/CodingStyle文件中有关于编码风格的说明。但长久以来,这个文档中所描述的风格策略充其量被视为是建议性的。因此内核中有大量的代码并不符合编码风格准则的要求。这类代码的存在给内核开发者设下了两个陷阱。
第一个陷阱是相信内核编码标准无关紧要并且不是强制性的。而这事儿的真实情况是如果代码没有按照标准编写,新代码将很难被添加到内核中去;许多开发者会要求代码应该在评审之前被重新格式化。像内核这么大规模的代码库需要一种格式一致的代码,这样才能保证开发人员可以快速地理解代码库中的任何一部分。因此这里没有给奇怪格式代码生存的空间。
有时,内核编码风格会与某个雇佣者要求的风格相冲突。这种情况下,在代码可以被合并到内核之前,内核编码风格将取得胜利。将代码放入内核意味着你将在多个方面放弃一些对代码的控制,这其中就包括对代码风格的控制。
另外一个陷阱是假定那些已经存在于内核中代码急需修复代码风格。开发者在开始阶段很可能会将创建修复代码风格的补丁作为一种熟悉开发过程的手段,或作为一种将自己的名字写进内核Changelog文件的手段,或者二者兼具。但纯粹的代码风格修复补丁会被开发社区视为噪音;并很可能会被冷眼对待。因此最好杜绝这类补丁。比较自然合理的做法是在因其他原因修改某段代码时顺便修复其代码风格,不要为了自身的考虑而去修改代码风格。
这个代码风格文档也不应该被当成绝对不能违背的准则。如果你有违反这一风格的好理由(例如,某一行如果按照80列的限制做拆分,可读性就会变得很差),那就按照你的想法去做吧。
* 抽象层
计算机科学教授教育学生应广泛使用抽象层以实现系统的灵活性和信息隐藏。当然内核也广泛使用了抽象;如果不这样的话,没有哪个具有百万行代码的项目可以实现并存活下来。但经验证明过度或过早抽象可能同过早优化一样是有害的。抽象应该被用在需要的层次上并且不要再深入了。
在一个简单的层次上,考虑这样一个只有一个参数的函数,调用者在调用该函数时参数总是传0。只是在有人最终需要使用这个函数提供的额外的灵活性时人们才会记起那个参数。但是到了那时,很可能实现了这个额外参数的代码已经被一种未被察觉的微妙方式改变了– 因为它从未被使用过。或者,当对这个额外灵活性的需求增多时,它的行为已经不再是开发者早先期望的那样的了。核心开发者将会按惯例提交补丁删除无用的参数;通常来讲,这些参数从一开始就不应该加上。
那些隐藏了对硬件访问的抽象层尤其不被赞成使用,因为这些抽象层常常允许一个驱动程序的主要部分被多个操作系统使用。这些抽象层使代码更加难以理解并且很可能引入性能问题;他们不应该被归入Linux内核范畴。
另一方面,如果你发现自己正在从另一个内核子系统拷贝大量重要代码,那么是时候问问你自己将一些代码抽出放入单独的库或在更高层次上实现那个功能是不是更有意义。在内核内部复制相同的代码没有价值。
* #ifdef以及预处理器的一般使用
C预处理器对一些C程序员来说是一种强大的诱惑,这些C程序员将预处理器看作是一种在源文件中嵌入灵活性的手段。但是预处理器不是C语言,过度地使用预处理器将导致代码可读性大大降低,同时也使得编译器进行正确性检查的难度大大增加了。过度使用预处理器通常是一种信号,预示着代码需要一些整理了。
采用#ifdef的条件编译确实是一种强大的特性,并且它已经被用于内核代码中。但我们仍然不希望看到那些不受限制地使用#ifdef代码块的代码。一般来说,#ifdef应该尽可能地被限制在头文件中使用。条件编译代码可用于那些尚未实现完毕的函数,使之变为空函数。编译器接下来会在优化过程去掉对这些空函数的调用。结果我们将得到更加简洁和易理解的代码。
C预处理器宏会带来一些危害,包括可能带有副作用的多次表达式求值以及类型不安全。如果你总想定义一个宏,不妨考虑创建一个内联函数替代这个宏,两者的执行结果是相同的,但内联函数可读性更好,也不会多次对其参数进行求值,并且支持编译器对参数以及返回值进行的类型检查。
* 内联函数
不过内联函数也有他的一个危害之处。程序员们可能会迷恋于因省略函数调用而带来的效率提升,并在源文件中到处使用内联函数。然而那些函数实际上可能降低系统性能。由于在每个调用处这些函数的代码都会被复制一份,最终会导致编译后的内核尺寸膨胀。相应地,这会给处理器的内存缓存带来压力,并可能显著降低执行性能。通常,内联函数应该非常小并且相对较少。毕竟函数调用的消耗并不是那么大;大量创建内联函数是过早优化的一个典型例子。
一般来说,内核程序员忽略缓存效果是冒着风险的。在数据结构课程上学到经典的时空开销转换并不适用于当代的硬件。空间即是时间,因为尺寸更大的程序与更加紧凑的程序运行的要更慢。
* 锁
2006年5月,Devicescape网络协议栈在GPL的授权下大张旗鼓地发布了,并且等待着被主线内核合并。这次捐赠受到了社区的极大欢迎;因为当时Linux对无线网络的支持被认为是不符合标准的,而Devicescape协议栈则许诺修复这一问题。但直到2007年6月份(2.6.22),这份代码也没有被真正合并到内核主线中。究竟发生了什么呢?
这份代码显现出诸多闭门造车的迹象。然而一个更为严重的问题是它不是针对多处理器系统设计的。在这份网络协议栈(现在叫作mac80211)能够被合入主线之前,社区需要一个锁方案来重新对该代码进行改造。
曾几何时,Linux内核代码的开发可以无需考虑多处理器系统的并发问题。不多,现在,就连这篇文章也是在一个双核处理器笔记本上编写的。即使在单处理器系统上,那些为了改善响应速度的工作也会提升并发在内核内部的级别。那些无需考虑锁的内核编码的日子已经一去不复返了。
任何可被不止一个线程并发访问的资源(数据结构、硬件寄存器等)都必须用锁保护起来。开发新代码时应牢记这一要求;即成事实后再进行锁改造将会是一个特别困难的任务。内核开发者应该花时间去好好地了解一下已经存在的锁原语以足够自己为开发任务挑选一个合适的工具。那些缺少对并发关注的代码在通往内核主线的道路上会走得更加艰难。
*Regressions(退步)
最后一个值得一提的危害是:作出一些给现有用户带来破坏的改变(可能带来较大的改进)。这类改变被称作"regression(退步)",内核主线最厌恶regression。如果regression不能在短时间内修复,那些导致regression的改变将极少例外地被清退出内核。最好从一开始就避免regression。
如果因某个regression所带来的改变而受益的人比因其受害的人更多,这个regression是否可能被合法化呢?这里常常引发社区的争论。为什么不可以做出这样一个改变呢:它能给10个系统带来新功能,但只破坏其中一个系统?对于这个问题,Linus在2007年7月给出的最佳答案:
所以,我们不能通过引入新问题的方式来修复bug。那种方式很愚蠢,根本没有人知道你实际上是否带来的真正的进步?是前进两步,后退一步,还是前进一步后退两步呢?(http://lwn.net/Articles/243460/).
一个尤其让人生厌的regression是那种对用户空间ABI(译注:Application Binary Interface,应用程序二进制接口)的改变。一旦一个接口被导出到用户空间,它就必须被无限期地支持。这种情况让创建用户空间接口变得尤其具有挑战性:因为它们不能被以一种不兼容的方式改变,它们必须在一开始时就被正确地创建。为此,用户空间接口总是需要大量的考量、清晰的文档以及大范围的评审。
4.2、代码检查工具
至少在目前,编写无错代码仍旧是一个几乎无人可及的理想。然而,我们可以期望的是,在代码进入内核主线之前,尽可能多的捕捉和修复bug。为达到此目的,内核开发者们设计和实现了一系列工具,这些工具可以自动地捕捉到各种隐蔽的问题。被计算机捕捉到的问题后续将不会折磨用户,因此,顺理成章,我们应该尽可能多地使用这些自动化工具。
第一步就是留心编译器给出的警告。当前版本的gcc可以检测出(并针对…警告)大量潜在的错误。这些警告常常意味着真实的问题。一般来说,提交评审的代码应该不会再产生任何编译器警告了。当关闭警告时,注意务必理解警告的真实原因并且避免进行那种只去除警告但未真正解决问题的"修复"。
注意不是所有编译器警告是默认打开的。使用"make EXTRA_CFLAGS=-W"来编译内核以获得所有警告设置。
内核提供了多个用于打开调试特性的配置选项;其中大多数选项可以在"kernel hacking"子菜单中找到。对于那些用于开发或测试目的的内核来说,多数此类选项都应该被打开。尤其是,你应该打开:
* ENABLE_WARN_DEPRECATED、 ENABLE_MUST_CHECK和FRAME_WARN。打开这几个选项可以获得一些额外的警告设置,这些设置针对的问题诸如使用了不赞成使用的接口或忽略了一个重要的函数返回值等。这些警告的输出可能比较冗长罗嗦,但其他内核部分的警告不会如此,你大可不必担心。
* DEBUG_OBJECTS会增加代码来跟踪内核创建的各种对象的生存期,并且在对象出现故障时给出警告。如果你添加了一个子系统,该子系统创建(或导出)了属于自己的复杂对象,请考虑为该子系统加上对对象调试基础设施的支持。
* DEBUG_SLAB可以查找到大量关于内存分配以及使用的错误;它应该在大多数开发专用的内核上使用。
* DEBUG_SPINLOCK、DEBUG_SPINLOCK_SLEEP和DEBUG_MUTEXES可以找到很多常见的锁错误。
内核中还有很多其他调试选项,其中一些将在下面讨论。有些调试选项将对内核性能产生显著的影响,不应该被一直使用。不过,花些时间了解已有的调试选项很可能会在短时间后给你带来几倍的回报。
一个重量级的调试工具就是锁检查器,或叫做"lockdep"。这个工具可以跟踪系统中每把锁(自旋锁或互斥锁)的加锁和解锁操作、相对于彼此的加锁顺序、当前的中断环境以及更多其他内容。它还能保证始终以相同的顺序进行加锁,保证对所有情况应用相同的中断假设等等。换句话说,lockdep可以找出许多系统可能偶尔死锁的场景。在一个已经部署的系统上,这类问题是很让人头疼的(对开发者和用户都);lockdep支持以一种自动的方式提前发现这类问题。任何重要的代码在提交合入前都应该在lockdep工具的监控下运行。
作为一名勤奋的内核程序员,你将毫无疑问地检查任何可能失败的操作(诸如内存分配)的返回状态。然而,事情的真实情况是,因此进行的失败恢复的路径很可能根本没有经过测试。未测试的代码极可能是有问题的代码;如果所有失败处理路径被执行过多次,你才可能会对你的代码更加有信心。
内核提供了一个故障注入的框架,它可以制造故障,特别是涉及内存分配的地方。在开启故障注入的情况下,内存分配可以按照配置的比例执行失败;这些失败可以被限制在一个特定的代码范围中。在故障注入框架开启的前提下运行可以让程序员们看到代码在出现错误的情况下是如何作出反应的。更多关于如何使用这个工具方面的内容可参见Documentation/fault-injection/fault-injection.txt。
其他类错误可以通过"sparse"静态分析工具查找到。使用sparse,程序员在混淆用户空间与内核空间地址,混用大端法和小端法表示的数量值以及传递对一组特定位标志有要求的整型值时会收到警告。sparse必须单独安装(如果你用的发行版不包含sparse的话,你可以在http://www.kernel.org/pub/software/devel/sparse/下面找到它);当你执行的make命令包含"C=1"时,sparse会被执行。
其他有关可移植性类别的错误最好在代码进行针对其他体系的编译时发现,如果手头没有S/390系统或Blackfin开发板的话,你仍然能够执行这个编译步骤。一套适合x86系统的跨平台编译器可以在下面页面中找到:
http://www.kernel.org/pub/tools/crosstool/
花些时间安装和使用这些编译器可以帮助你避免日后难堪。
4.3、文档
文档常常不仅仅是内核开发规则的例外。即使这样,充足的文档会有助于你的新代码合并入内核,有助于其他开发人员理解你的代码并且也会对你的用户带来帮助。在很多情况下,增加文档已经变成了必不可少的强制要求了。
任何补丁的文档的第一部分内容应该是与之相关的变更日志(changelog)。日志记录应该描述解决了什么问题、解决方案的构成、补丁相关的人员、任何对性能产生的影响以及其他理解该补丁所需要的内容。
任何添加了新用户空间接口–包括新的sysfs或/proc文件–的代码都应该包含一份关于那个接口的说明文档,以便用户空间程序员了解这个接口。关于这类文档应该如何进行格式化以及应该提供哪些信息,请参见Documentation/ABI/README。
Documentation/kernel-parameters.txt描述了内核引导阶段的所有参数。任何添加新参数的补丁都应该在该文档中添加适当的记录。
任何新增的配置选项都必须伴随一份帮助文字,这些文字应该清楚地说明这些选项的功用以及用户何时可能会对它们进行选择。
在多个子系统中使用的内部API信息需要以一种特定格式的注释的方式记录到文档中;这些注释可以被"kernel-doc"脚本以多种方式提取和格式化。如果你正在一个具有kerneldoc注释的子系统上进行开发,你应该视具体情况为外部可用的函数维护和添加注释。即使在尚没有文档记录的区域,为将来添加kerneldoc注释也是无害的;实际上,对于那些刚进入内核开发领域的开发者来说,这可能是一种有益的工作。关于这些注释的格式以及如何创建kerneldoc模板的说明可以参见Documentation/kernel-doc-nano-HOWTO.txt。
读过大量现有内核代码的人常常都会注意到内核代码严重缺少注释。对新代码中注释的期望远远高于之前的代码;没有注释的代码想要合入内核会更加困难。但即便如此,那些具有冗长注释的代码想进入内核依旧是希望渺茫。代码自身应该具有良好的可读性,同时使用注释解释那些不明显、更具技巧的特性。
某些地方应该始终使用注释。内存栅栏(memory barrier)的使用应该始终伴随一行注释,解释这里使用栅栏的必要性。数据结构的加锁规则一般需要在某处给予解释。通常主要的数据结构都需要详细的文档。小块代码间的不明显的依赖需要被指出。任何可能诱使一个代码看门人(code janitor)作出不合规矩地"清理"的地方都需要一个注释解释为何这里要这么做。等等。
4.4、内部API变化
除非是最为严重的情况下,内核提供给用户空间的二进制接口都不能被破坏。相反,内核内部的编程接口则是经常改变的,并且可以在有需要的情况下被改变。如果你发现自己围绕着一个内核API进行开发或只是没有使用一个特定的功能,因为该功能无法满足你的需要,这很可能是一个API需要被改变的信号。作为一个内核开发者,你有权做出这样的改变。
当然,这里还是有一些隐患的。API可以被改变,但这种改变应该是合理的。因此任何导致一个内部API变化的补丁都应该伴随一个描述,该描述包括改变了什么以及这种改变的必要性。这类改变还应该被拆分成多个独立的补丁,而不是放在一个大补丁中。
另外一个隐患是改变内部API的那个开发者通常还要负责修正内核树上那些因API改变而被破坏的代码。对于一个被广泛使用的函数来说,这个责任可能会意味着成百或上千处改变– 多数都可能是与其他开发者所编写的代码的冲突。不用说,这也是一个工作量庞大的工作,因此最好确认你对API改变的合理性是可靠的。
当做出一个不兼容的API改变时,开发者应该尽快能的保证编译器可以捕捉到那些尚未更新的代码。这将有助于你在树内找到所有使用这个接口的代码。它还会警告那些树外代码的开发者有一个需要他们处理的新变化。虽然树外代码的支持不是内核开发者需要担心的事情,但我们还是不要让树外代码的开发者的开发工作变得更难。
© 2012, bigwhite. 版权所有.
Related posts:
关于API变更的最后一段话(也就是文末)什么叫做不兼容的API变更时?前面讲的不就是不兼容的API变更吗,不然为什么需要修正其他地方调用这个API的代码。