2015年(6)
分类: LINUX
2015-03-14 11:32:55
原文地址:Linux内核探索之路——关于方法 作者:lli_njupt
Linux内核探索之路
——给那些想从LiNuX内核找点乐趣的人。
一个不能回避的尴尬问题:研究Linux内核是不是必须要通过研读那些错综复杂的“邪恶”代码,才能真正理解它?
关于方法
“术业要有专攻”。还记得大学时候训练英语的听说能力。每天到多媒体教室上一个多小时的课,但是一个学期下来,英语听说水平不但没有任何进展,还导致了对英语学习的厌恶,直到最后不了了之。后来实在不行,有时候需要用,用到时无法交流就是无比的尴尬,脸上绝对是“火辣辣的”!后来有个室友说背新概念管用,于是背了几篇,还是不行,交流的时候还是“哑口无声”,无话可说。室友又说这个要把一本书背得滚瓜烂熟才行,那样才能做到脱口而出。大道理谁都明白,能做得到的没几个,我相信在我没背滚瓜烂熟之前,我早就泄气了。
如果你学习一样东西,在较短的时间内都不能应用,不能让你体验一下小有成就的感觉,那么能够坚持下来的人就会很少。学习的动力一开始可能是兴趣,可能是好奇,也可能是某种非自愿。但无论如何一开始都是志在必得,胸有成竹的,至少是相信自己不会半途而废。但是不好的方法会很快耗尽最初的激情,而最坏的结果是让你放弃,让你认为这个东西太难了,可能根本不适合自己。这里不是讲革命大义,但是方法论在小事上依然有效。那么什么方法算得上是好的方法呢?适合这个人的方法就能适合其他人吗?那么每个人都要一套独到的方法了?好的方法总有一些通用性,不然也就不能认为是好的方法了。笔者认为它应该具有以下的特性:
1.提纲挈领,由浅入深,深入浅出。大部分当前还能存在的方法基本上都具有这个特点,如果你要学习英语,那么就要从26个英文字母开始学习,当然了对于英语国家的人,是从咿呀学语开始,但无论如何,没有人一开始就看虚拟语气,就探讨英美发音。好的开端,意味着成功的一半。
2.可持续性。保持学习的动力,进一步激发学习的兴趣。这一点是非常之重要。背新概念固然可以学习好的句法,逻辑性,当然还有些新的单词,但是如果用它来学习口语,则确实有些方法失当。我记得我最后学习口语的方法采用的是英语九百句,它把句子分类,比如交际问候,待人接物等,每一个分类都是一组对话:我累了,我烦了,我喜欢,我恨你,我受不了,我想骂人……然后一天学习几组对话,跟着MP3一句一句的重复,然后等到周末就可以去英语角找老外胡侃了,你知道当你敢于站在别人大声说出你背了几十遍的英语后,会感觉到多么的成功,尽管那又是多么的微不足道。最终这就促成了好的循环,这种循环不仅使你逃脱了学习过程的乏味,还让你感觉到自己找到了好的方法,树立了自信!在Linux内核代码学习中不好的方法之一就是看满篇代码引用的书籍或者资料。
3.循序渐进性,能够自然而合理的划分学习阶段,这与第二个特点相互呼应。多数时候这要根据个人的学习进展来决定,但是好的方法能够自然而然的体现学习的阶段性,阶梯型。与此同时在无形中要限制学习的速度,这一点是为了牵制大多数人急于求成的心态。很多人,包括笔者,之所以走弯路,最后走上放弃的道路,是因为“急功近利”。总想着走捷径,速成,最后就是“欲速则不达”!切记切记,慢即是快的道理。笔者一直认为将一本书看一百遍和草草看一百本书的人是完全生活在不同的精神境界。再回到学英语的例子,那些不同的对话分类并不是随随便便的,而是有承前启后,步步上升作用的。比如一开始是家人之间的对话,简单到一两个单词,最后则是商业交际,涉及到金融贸易。在Linux内核代码学习中不好的方法之二就是好高骛远,一口气,两三天把想把问题搞个一清二楚。
4.具有普遍性,也即针对大多数人,它都是有效的。我想如果一个方法如果具备了以上的特性,那么这个方法就基本上具备了普遍性,至少适应一个地区,或者一个年龄层次的人没有问题。当然为了针对特定的人群,可能要一些方式的变化:比如26个英文字母,对于成年人只要给出音标,写法即可,但对于学龄前儿童,则要用图画颜色来表达。在Linux内核代码学习中,多画图,多总结则具有一般性。另外有的人有了很好的汇编或者硬件基础就可以从Bootloader开始看,有的人则不行,但是可以从相对简单的内核功能模块看起,这也具有一般性。
5.与时俱进。这是方法论的最可怕之处,也是理想方法的终极境界,对于个人的大多数事情,应该不需要到这个层次,当然达到这个境界的方法正无时无刻不在地球的各个地方发挥着作用,否则那些方法随着时间必将淘汰。与时俱进的方法体现在Linux内核代码学习上,需要到达一定的程度,此时已经具有了前瞻性,探索性。
好了,说了许多似乎和学习Linux内核还没有沾太大边呢!笔者并不建议一个只写过少许用户空间C程序的人直接去看内核代码,因为用户空间和内核空间相差的不仅仅是一个系统调用。笔者认为一个人有看内核代码的想法或者需求有以下几种:追踪BUG追到了内核;内核暴力不合作,抛出oops信息拒绝继续执行;Linux到底干了啥的好奇心。还有一种可能就是看着别人研究内核,自己也要试试,盲从一把。不管是哪种情况,此时你可能要自顾自得折腾一番,然后大多数时候灰头土脸的回来了,然后在头大的同时得出一个结论:那帮家伙也太牛x了,这玩意咋搞出来的?此时你如果比较幸运就会开始思考学习内核的方法了,但是多数时候是胡乱买了一些书,然后看了几(“几”很有可能是“一”)章之后这本书便被束之高阁了。怎么办呢?有什么好方法?
如果你已经在Linux平台上工作或者学习了很久,至少大多数Shell命令用的还可以,对Linux这个黑盒子已经有了自己的感觉和理解,那么这确实是一个好的开端。很多计算机系的人在大学毕业时很可能都不具备这个基础。到这里,你如果自认为还没达到这个级别,那么请回头打牢基础,否则笔者就要害人走弯路了,这个阶段至少要有半年,记住“经济基础决定上层建筑”,现在你的基础有多深厚基本决定了你以后理解内核的效率,准确性以及能够达到的深度。
对Linux的初步感觉是,它和Windows基本上是两码事,这个东西的命令很强劲,但是要记住的东西也挺多。如果你平时的工作是在Linux上编写应用程序,那么已经自然而然进入第二阶段了。无论是从工作的积累,还是通过参考书籍,互联网资源以及同事间的探讨,这个阶段都要尽量做到以下几件事:
1. 能够熟练和准确的运用C语言的语法和技巧,熟悉GCC的一些特性,一些更深入的要求是能够研究一些glibc库函数的实现,这样会帮助你对神秘的glibc有实际的理解,与此同时它能使你养成严谨代码的写法。
2. 了解一些编译器的用法,如果是嵌入式开发,那么这是必须的。首先了解这些选项的作用和优化的原理,在此基础上了解GCC编译的整个过程:预处理,汇编以及链接。更深一步的要求是对链接器的原理,ELF文件的构造和特定CPU架构的汇编语言有正确的理解和全局的把握。
3. 尽管已经罗列了这么多,似乎还和Linux内核没沾上大的边。第三点就是在这个阶段编程时尽量广泛的使用Linux上不同应用的系统调用:
文件操作:读写,链接,权限控制,文件锁。
进程间通信:信号,管道,套接字。
网络应用:TCP/IP,UDP,RAW Socket,NETLINK等。
多进程和线程的应用等。
其中进程间通信和网络应用是Linux的精华所在,如果错过了它们,可以说是一很大的遗憾。
尽管上面罗列的这些知识点已经让人望而却步,但是把它们放在一个较长的时间阶段里来实现,是相当现实的。如果你是一个刚刚接触Linux不久的新手,可能要惊叹:哇塞,还是算了吧,以后日子太苦了。没有对Linux的热情,那么又何必把精力放在这里呢,放在自己喜欢的领域将会有更大的成就。当然只有热情是远远不够的,如果没有长期的可实践的计划,那么和做一场梦有什么区别呢?切记“慢即是快”。
在第二个阶段完成的代码量至少要几万行,并且它们最终看起来和开源项目的代码间没有明显的区别。另外应该在这段时间形成自己的经验代码,最好形成一个或者几个库:针对网络的,文件操作的,锁机制的,进程间通信的等等!库中的代码应该清晰明了,和标准的函数库没有本质的区别,你可以熟练引用这些库函数来实现应用并能大概讲清楚它实现的流程。
好了,百川东到海,在经历了一年或者几年(具体的时间并不重要,但是要记住,欲速则不达——再次强调)的Linux平台的C语言编程之后,在此时,当然也很有可能是在第二阶段的某些时候,你肯定蠢蠢欲动了。最后的探宝之路开始了,并且你要相信过去的那些日子已经让你几乎最高效率的来到了埋藏宝贝的城堡门前,你已经走了一条捷径。
尽管大部分的项目已经不能难住你,还是要清楚的知道:这才是开始。只有抱有谦逊的心,才能走的更远。在此时:
1. 你应当(笔者认为是必须)准备一块开发板,它最好能够让你做一切事情,包括擦除Bootloader而不会报废,另外你应该(几乎是必须)拥有该开发板的所有软硬件资料,其中包含Bootloader和Linux内核的源码,电路图,芯片文档。这保证你不会形而上学的在那猜测,然后似是而非的理解。这也让你有勇气指出那些前人写过的经典之作中也不免错误或者翻译不合理的存在。为什么不用PC或者虚拟机呢?大多数时候是可以的,但有以下几点需要注意:对于PC你很有可能需要多台电脑;你可能无法得到PC的硬件资料;由于在分析代码时可能需要不停重启,PC重启时间过长;PC只能让你研究x86等。对于虚拟机:如果要研究一些底层的原理,虚拟机不是硬件,你可能无法获取详细的配置信息;虚拟机可能让CPU工作在不同寻常的模式。
2. 选择你喜欢的或者工作需要的内核版本,笔者绝不推荐你选择0.1版本,也不推荐2.4版本,你应该选择Linux2.6.11或者更高的2.6版本,这基于以下理由:如果你有足够的能力让0.1版本在你的开发板上跑起来,你对Linux内核的理解似乎不是一般境界了,你让它在PC上跑起来都相当困难。2.4版本已经是10年前的产物,这方面的书籍虽然也有很多,知名的有《Linux内核源代码情景分析》,但是你在论坛上询问2.4版本的内核问题,多数时候得到的回应是沉默。最后一个理由:2.6版本在2.4版本上有了天翻地覆的变化,比如强化了面向对象,统一的设备模型,名字空间的引入等等。这些功能举足轻重。为何不选择最新的3.0版本?本质上3.0在代码和功能上和高版本的2.6内核没有太大的变化,另外根据3.0编写的书籍应该还没有出现。Linux.2.6.24或者以上的版本是一个推荐的选择,因为它们是笔者在工作中遇到的最多的内核版本,我想BSP提供商不会选择一个比较糟糕的内核版本作为基础来开发。
3. 从哪里下手?你这么快就准备下手了?好吧,了解内核代码的目录存放,了解内核的Makefile编写方式,对它们的理解均会在研究中不断深化。然后呢,你可以看看内核启动都经历了哪些阶段:Bootloader引导阶段,解压缩阶段等等。什么时候写代码啊?如果这个时候你还不是迫不及待,那么你的热情还不是太足够。尽管内核提供了printk帮助打印和追踪,但是笔者还是推荐你封装一下printk或vprintk,形成自己的一系列DEBUG函数:比如打印行号函数名等,以及添加一些醒目的颜色,放在自己的单独.c文件中,然后把它编译到内核,另外定义一个.h文件放在include/linux下并声明它们。在你当前研究的内核文件中添加该头文件,然后……
从哪个功能模块研究内核?这个问题有些仁者见仁,智者见智了。通常有两种方式:研究自己喜欢或者工作需要的特定部分;从Bootloader启动开始一直到start_kernel调用结束。本质上这是两种不同的方法,对个人的知识水平要求也有很大的不同。
第一种方法因为针对自己关注的功能模块,所以必然已经对它有所了解,所以切入进去相对简单,并且效果明显,可以迅速提升业务能力。这种方式也是在第二阶段中偶尔去研究内核功能的一种可行方法。但是这种方法有一个明显的不足:由于内核的各类功能模块并非相互独立,所以如果这个模块在某些时候引用了另一个模块的函数,对它的理解很可能陷入似是而非的假象阶段,如果你尝试不停的往下挖掘,很快你就像堆栈一样溢出了。
第二种方法也有很多人采用,但是这种方法确实不太适合新手,至少新手应该通过第一种方法研究了一些内核相对简单的实现,比如Proc文件系统,设备驱动模型等。之所以不太适合新手,是因为对Bootloader的知识可能欠缺,另外就是对内核镜像文件的构成要有所深入。当然不仅如此:对应开发板的汇编语言的掌握,对应开发板的硬件架构的了解。它的缺点是很显然的,理解周期长,一开始要分析很多汇编代码,并且它们是与硬件息息相关的。当然它的优点也毋庸置疑:整体的把握,会将大多数第一种方法的似是而非给解决掉,并且这种掌握是系统的。这有点像空中俯瞰迷宫。
几乎没有人能够完全使用第一种或者第二种方法来掌握Linux的大部分实现的原理(只能说是大部分,在Linux内核某些阴暗的角落很久都不会有人涉及)。对Linux的理解总是循着“循环往复,总体向上”的路线发展。对这两种方法应该合理运用,首先采用第一种方法,在恰当的时候切入第二种,这应该是比较好的路线图。
由于笔者还在路上,这里不能给出一个理想的功能模块分析的顺序,但是参考经典书籍的章节分布可能是一个不错的选择。
最后的啰嗦:内核中通常不会出现过于庞大的函数,如果是,那么基本是在做一些平行处理。你可以容易找到switch语句,如果不幸没有,那么你一定要细心找到其中的关键函数,一般如果不是在初始化或者注销阶段,应该不会包含太多的关键函数,然后再对关键函数一一击破,击破的过程中要尝试记录关键数据结构的状态变化。总结成一句话就是以关键函数和关键数据结构为核心。内核中最让人易于产生迷惑的地方:对位的操作,不经过一段时间你很难弄明白它们到底对应程序的哪些功能或者状态,好在当前的内核在努力避免直接对它们的操作,而是封装在一系列的宏中。内核中对很多操作总结提炼成了一类模式的操作:锁,位,链表等等,这是面向对象的一种努力,这也是研究过程中的另外一种收获,注意对它们的总结和积累。另外——还是看代码(笔者认为它包含内核文档)吧!
基于对与时俱进方法论的实践,该章节有可能被补充……
因为实在不敢恭维chinaunix博客的排版,所以附 Linux 内核探索之路.pdf