走进Linux 操作系统
摘要:本期涉猎了操作系统的来龙去脉后与大家携手步入Linux世界。我们力图展示给大家一幅Linux系统的全景图,并为了加深对linux系统的全面认知,亲手搭建了一个能运行在内存中的试验系统。同时为大家提供了几个shell脚本帮助建立试验系统。
最熟悉的陌生人
用萧亚轩的一首歌形容操作系统给我们的感觉再合适不过了,“最熟悉的陌生人”。
说熟悉,因为几乎每天我们都在有意无意和它打着交道。无论是日常办公,还是畅游网际,我们都无法离开操作系统的帮助。电脑初启,首当其冲运行的就是操作系统。对于许多人而言,电脑就是那扇“窗”。即便远离电脑,我们依然无法逃脱操作系统的“魔掌”。在您收发短信、拨打电话的时候,手机屏幕背后一个嵌入操作系统正在默默为您服务。越来越多的数字设备都开始拥有自己操作系统,虽然它们不同于Bill为我们带来的“窗子”,但并无什么本质的差别。假如有一天,您的电视机开始对您嘘寒问暖,请别忘记,有一个叫做操作系统的东西站在它的背后。
说陌生,因为直接为我们服务的大多属于应用软件,也许是文字处理、也许电子邮件、也许网络游戏。他们都运行于操作系统之上,提供我们所需的服务,这让我们很少直接面对操作系统。对大多数用户而言,操作系统的细节是不可见的,所以虽说我们无时无刻不在使用操作系统,彼此之间却多了一层隔阂。对于操作系统的认识,往往只能停留在间接地、片面地感性基础之上,其内部的运作机制,我们无法一目了然。
无处不在却不以正面相示,让操作系统有了一种“犹抱琵琶”的美,于是操作系统被冠以“最神秘”的称号。
揭开这层神秘面纱,是我们杂志的初衷。我们希望能够诠释操作系统设计思想,和您一起熟悉操作系统提供的各种服务,学习灵活运用这些服务的方法和技巧。我们将以理论讨论结合实例实验方式,让您和我们一起拥有操作系统级开发软件的本领。
好了,开始我们的内核之旅,走近这个“最熟悉的陌生人”吧!
本期目标!
在第一期中,我们将:
提纲挈领地介绍操作系统的基本概念;
理清楚操作系统和其它系统软件或应用软件之间的关系;
了解操作系统的体系结构;
在此基础上,提出操作系统内核概念。
简而言之,希望读者可以在读过本期的内容,能在脑海里勾勒出一幅操作系统的全景图,为日后剖析内核、开发内核创造基础。
学习的最佳方法是实践。
我们的杂志将以实践为主,理论为辅。将实验贯穿到学习、分析当中。
在本期中,为了让读者能比较全面地理解操作系统,并加强对操作系统的感性认识,我们将本期分为两大部分:
第一部分从理论上简要介绍操作系统的内涵,揭示操作系统在软件体系中的特殊地位,然后讨论操作系统的体系结构和演化进程。
第二部重点分析Linux操作系统,介绍Linux操作系统的发源,特色以及内核结构。最后通过和读者一同亲手搭建一个实验操作系统——一个经过裁减具备基本功能的小型Linux,让读者从实践中感受操作系统的体系结构和在计算机系统中所起的作用。
什么是操作系统
操作系统的概念
虽然定义总是生硬、乏味、令人费解,但是它毕竟是概括性最强、最能体现水平的,所以我们还是要在开始就给出操作系统定义:
操作系统是应用程序的运行环境。
够精辟吧!
可能运行环境这个术语令你如坠云端,它太广泛、太抽象了。你一定在问运行环境到底是什么?简单地讲,运行环境是一种即服务和控制于一体地容器。
如果你没有理解环境这个概念,我可以举个并不贴切的社会实例。
在开发区中可以看到许多企业孵化池或产业园,其中入住了各种各样的企业,孵化池或产业园的管理机构会统一为其中的企业办理各种工商、保险、卫生医疗等手续、提供后勤、治安等基础服务,企业需要某些政务服务时,可以通过管理机构去和政府联系,处理相关事宜,而不需要亲自去处理这些和企业业务无关的政务活动,因此可以抽出身来集中精力在业务上。孵化池和产业园为企业提供了统一、普遍地服务和管理,是企业运作的外在环境。操作系统从这个意义上来说,类似于产业园的管理结构,为应用程序——好比企业——提供基础服务和管理。
当然,我们现在并不指望你立刻认识到操作系统深刻内涵,对它的认识需要在不断地使用和思考中消化和积累,在本期杂志中,我们将从各种角度介绍“运行环境”这个术语的真实含义,并在今后各期杂志中分阶段、分层次的展开学习操作系统的各个领域。相信在不久的将来,你就能和我们一起认清它的庐山真面目了。
操作系统产生背景
想要认清一个人,最好是从小认识他。对技术的理解也是如此,我们必须了解它的产生原因和发展过程,才能较为全面的认识该技术所解决的问题,认识它在学科发展中所处的地位。对比技术发展各个阶段的特点,才能认清该技术的优劣。同样,我们想要把握操作系统的特性,还是先把操作系统放在时间轴上看看它的来龙去脉吧!
操作系统并不是计算机出现之初就有的,最初的计算机科学中并不存在操作系统这个概念,所有任务都直接运行于硬件之上。那时的任务大多集中于科学计算领域,系统硬件实现相对简单、直接,任务对I/O操作的要求也比较低,将结果记录到磁带机之类的简单设备中足矣。老程序员们大多采用手工或是用打孔机的方式将将二进制数据和程序输入机器内存,然后执行计算,最后,将结果保存到磁带机上。一旦出现错误,机器上的调试灯会将保存在寄存器中的错误代码反映出来,程序员们会据此去分析错误所在。
随着科学计算任务变得越来越复杂,计算机逐渐被应用到了科学计算以外的其它领域。硬件设备比以前更加丰富和复杂了,I/O操作要求大幅提高,程序规模迅速扩大,需要调试的错误更是直线上升,直接操作硬件,对程序员来说变得越发困难。
于是出现了高级语言、编译系统,帮助程序员简化开发工作;出现了操作系统,帮助程序员管理和操作硬件设备。程序员们可以将精力集中于开发需要的任务,烦琐的如任务装载、分配/释放内存、内存寻址、设备驱动、数据存储等等硬件相关操作统统交给操作系统管理——真可以说是生产力的一次解放。
时代继续发展,多用户多任务时代的来临,使得系统管理更加强调资源共享性。用户直接操作系统资源显然有悖于上述精神,因此资源合理分配与保护更为操作系统发展提供了新的挑战和机遇,同时奠定了操作系统不可或缺的地位,从此,操作系统成为为软件体系中最基础,最重要的组成部分了。
操作系统的任务
从操作系统的起源可以看出,操作系统的核心任务是作为硬件和应用程序之间的一个中间层,或者说是应用程序的一个操作平台,通过它应用程序和系统硬件隔离开,应用程序利用它提供的服务完成硬件相关操作。
总而言之,操作系统方便了应用程序运行,保护了系统资源。具体地讲,操作系统为用户带来了几个方面的好处:
易操作性: 操作系统是用户和计算机之间的接口,它大大简化了用户执行任务的复杂程度。
作为应用程序的执行环境:它为程序员建立应用程序提供了必要的编辑环境、编译环境和调试工具;为程序的执行提供了载入服务和资源分配服务;为数据存取提供了I/O访问服务;为数据格式转化和定位提供了文件操作服务;为程序的安全运行提供了权限控制服务;为程序运行失败提供了错误报告服务等等系统服务,从此,程序员和用户都不再需要关心那些令人生畏的计算机体系结构细节,可以全心全意地开发应用程序了。
有效性:从另一个角度看待操作系统,可以将它认为是一个计算机资源管理系统。
由于系统中资源种类各异,用法也大不相同,如果直接由用户管理这些资源,比如内存分配,时钟计时,I/O驱动,存储维护,势必要求用户具有丰富的软硬件知识,深刻把握计算机系统结构,否则资源将难以合理使用,最终造成系统混乱,甚至崩溃。而且现代的多用户操作系统更是要求系统资源共享,资源必须合理分配给多用户、多任务,只有采用一定的调度策略和分配策略,才能保证资源被公平有效的利用。所以,配置资源成了提高性能的关键——如同资源配置是提高生产力的关键一样。
安全性:安全性是操作系统为我们提供的另一个重要的特点,它为我们提供了多层面的安全保障。
首先,操作系统作为系统硬件和用户的中间平台,禁止应用程序直接操作硬件,禁止应用程序直接访问内存,执行特权指令。多数系统都将应用程序运行限制在用户空间(低特权级),而操作系统则运行于内核空间(高特权级),应用程序只有通过系统调用请求操作系统所提供的接口,才能通过操作系统间接执行和硬件相关的操作或是执行特权指令。因此保护了系统不被恶意的应用程序破坏或非法操作。
其次,多任务多用户操作系统必须保证,不同任务之间信息不能泄漏,因此需要为任务划分各自的私有空间和对其进行访问控制。对不同用户进行相应的授权和认证,可以保护用户各行其是,互不侵犯。
总之,操作系统安全涉及方方面面,健壮的操作系统必须能多方位地保证任务安全执行。
易扩展:计算机技术的高速发展和计算机日益普及,计算机硬件设备不断推陈出新,这要求操作系统提供的服务也能够日新月异,因此要求操作系统具有良好的扩展性。
由于操作系统对系统资源和服务进行了抽象,屏蔽了底层细节,统一了上层接口,添加设备或服务成了一件轻而易举的事,需要做的仅仅是,在设备或服务规定的接口下完成新的实现即刻。
什么是资源?
资源概念在操作系统中使用得相当广泛,内存、磁盘、文件、处理器、时钟等等软硬件都可以划归到资源范畴。资源的概念其实很好理解,概括来讲,系统中的资源指的是系统提供给进程使用的特殊实体,进程通过向操作系统请求获得这些实体,另外,系统分配这些实体给进程前,进程需要挂起等待。凡是满足上述条件的实体就属于资源。
操作系统的演化
和其它任何事务一样,操作系统并非一成不变。迄今为止,它已经经历了半个多世纪的发展,已经形成了一个庞大的家族。从个人计算机到工作站,从通用系统到专用系统,从嵌入式到虚拟机,可谓形式丰富多样。我们难以将所有操作系统囊括,只希望提纲挈领地介绍在操作系统发展进程中具有代表性的几种系统,理清它的演化脉络。
进化历程 :
最早的操作系统是简单的单道批处理系统。它的功能相当简陋,只能串行执行预先组织好的任务组。早先的系统一次只能运行一个任务,每个任务必须先装入,再等执行完后才能装入下一个任务,重复的装入浪费了大量的时间。单道批处理系统的出现,大大的提高了系统吞吐率。
事情并非总如想象般顺利。
由于数据存储时所消耗的时间——I/O操作时间——相比数据处理时间——CPU操作时间——要高出数倍(往往在20倍以上),所以程序运行到I/O操作期间,CPU总是需要停下来(挂起)等待数据传输完成,无形中浪费了大量宝贵的时间,任务组中后续程序的执行也因此被延迟了。如何避免数据传输等待带来的时间浪费呢?能否在进行传输期间,解放CPU去执行别的任务呢?
为解决这个瓶颈,单道批处理系统进化到了多道批处理系统。
所谓多道就实际就是说,处理器(当然现在谈到的都属于单处理器系统)可以交错运行多个程序,某个任务挂起时,运行另一个程序。这样一来, CPU等待数据传输造成的时间浪费问题得以解决,系统吞吐率又一次得到了提高。
计算机的发展使得任务不再仅仅局限于科学计算,越来越多的应用于办公、生活等日常活动中。科学计算中的任务多数执行路径都是固定不变,预先定义好的,只需要给定输入,得到结果期间程序执行中途不需要外界干预,与之不同,办公,生活中的许多任务都必须和用户不断交互,任务结果随时都会因为用户的选择改变。这时的系统变得更公开、更普遍,往往允许多个用户可以同时使用。交互模式和共用模式需要任务响应时间尽可能的快(超过20秒的话,人的思维就容易被打断或变得很不耐烦),这样才能让多个用户都满意,于是操作系统开始采用分时技术,处理器的运行时间分成数片,均分或依照一定权重派发给系统中的用户使用。这种将处理器虚拟给多用户共同使用的方法,不但可以满足快速响应,而且也可以使得所有用户获得计算机完全是在为自己服务的假象。
上面给出了操作系统发展的主流路线:单道批处理——多道批处理——分时系统,除此以外现在还出现了许多分布式操作系统,嵌入系统,不过总体技术思路都仍然脱离不了多道、分时等概念。
操作系统内容
操作系统的演化使得其功能变得愈来愈强大,但结构也越来越复杂。在以方便用户(包括开发人员和终端用户)为宗旨的思想下,操作系统不断集成新功能,新服务。回忆从前大家使用的DOS系统仅仅只需要一张软盘,而如今的windows系统或Linux系统动辄就需要数张光盘,可见已经从过去的麻雀变成了恐龙——虽然它们都有五脏六腑。
虽然变成了恐龙,但是其结构还时相对稳定,清晰的。和软件工程提出的思想一致,操作系统也采取了分层结构,越向上层抽象都越高,越接近用户;相反越向下层,越靠近硬件,抽象也相对接近硬件。而且高层软件依靠下层软件提供的服务,再加上本身提供附加服务为更高层服务。总体来讲呈现倒金子塔形式。
下面我们就简要分析一下操作系统的体系结构,然后再谈谈操作系统设计时需要主要考虑的问题。
操作系统组成
在形形色色的操作系统之中,组成结构不尽相同。因为同样目的实现的手段可以自由选择,所以其组成也有很大差异,我们选取最普遍的操作系统(UNIX)组成结构,向大家揭示操作系统的体系结构的大致框架。对于各种操作系统之间的具体差异,大家可以以下面讲述的结构对比认识。(注意我们这里所说的操作系统属于宏观概念,接近于操作系统发行版,不但包括了内核,还包含了学多系统软件和基础应用软件。)
操作系统最底层的组件是内核,其上层搭建了许多系统软件。系统程序包括三个部分。这三个部分分别是:编译环境、应用程序接口和用户接口。编译环境包含汇编,C 等低高级语言编译程序,连接程序和装载程序,这些程序负责将文本格式的程序语言转变为机器能识别和装载的机器代码;应用程序接口(API)包含内核提供的系统调用接口和语言库,系统调用是为了能让应用程序使用内核服务,语言库函数则是为了方便应用程序开发,所以将一些常用的基础功能预先编译以供使用,比如对C语言来说常用的C库有gun C等;用户接口(AUI)包括我们熟悉的shell(关于shell 应该专门写一个教程)、系统服务程序和常用的应用程序。
这些部分并非所有的操作系统都必须一个不少的包含,不过其中大多数功能都应该提供,尤其内核,系统调用,shell这些基本组件,它们都属于操作系统必备组件,其它组件是否包含需要根据具体系统的要求和应用环境决定,你也可以将其归为操作系统之外的附加部分。
系统程序:系统程序是相对应用程序而言的,应用程序针对终端用户需求完成功能,而系统程序则是为了简化应用程序的开发而存在的,比如数据库系统为了应用程序提供了有效的数据传输,存储服务;还有编程语言的执行环境——它由C库实现——也属于一种系统程序,它为应用程序开发提供了诸如I/O操作例程,图形库,计算库等等基础服务。可见系统程序范围覆盖很广,只要面相服务群体不是最终用户的软件都可以划归到系统软件中来。
内核概念
操作系统最核心,最基础的组件就要属内核了——内核和操作系统的其它系统软件或应用程序本质的区别在于内核运行在高特权级,和硬件直接交互,操作权限几乎不收任何限制,因此内核程序编写也要求格外谨慎,必须保证效率和可靠。
特权级别:现代体系结构中往往为了保护操作系统(内核)专用的数据不被应用程序访问,以免关键数据泄露或系统被破坏,将系统(硬件机制)划分为不同的特权级别,敏感数据存在高特权级,且还规定了一些特权指令,其它级别的任务不能访问敏感数据和使用这些特权指令,只有处于特权级别的任务才有权使用。比如Ox86体系结构中存在4个特权级别(0,1,2,3)Linux操作系统将内核存在0级,其它任务运行在3级。0级被称为内核空间,3级被称为用户空间。
内核设计的主要任务
内核作为操作系统的核心,运行级别最高。其它系统程序都必须通过它才可以使用系统资源,获取系统服务。所以内核使用最为频繁,一切系统行为无论巨细都要通过内核参与。
因此内核运行效率和正确性对整个系统的运行效率和可靠性至观重要,如果内核效率稍微下降,那么在应用程序中就必然造成层层放大。
内核要求高效率,所以它必须自系统运行起就要载入内存,并且在运行期间一直驻留在内存中,直到系统关闭。这是内核与其它应用程序或系统程序的另一个显著区别。虽然说内存今天已经不再是天价了,但是毕竟内存容量有限,所以内核大小不能过大(Linux内核只有几M或十几M,甚至可以裁减得更小),因此内核只应该包含最基础和核心的功能,其它附加功能应该尽量提到用户空间完成。
那么到底有那些功能是操作系统使用最频繁,最需要在内核中实现的呢?内核直接架构于硬件资源之上,因此首先要做的就是对硬件的资源管理。因此内核必须负责:内存管理,进程和进程调度(对CPU的管理),文件系统管理,I/O处理等任务。
我们的杂志核心就是在解释内核原理的基础上,带领大家学习内核级别的开发,也就是说进行核心开发。本期仅仅给大家一个概念上的说明,描述内核设计需要完成的主要任务,至于具体内核各部分的详细讨论在后续期刊中将逐步展开。
内核至少需要包含如下几个模块。
进程管理:进程是操作系统中的执行代码,是任务在系统内的动态化身。内核必须负责将任务抽象为进程,而且必须能将进程执行,能为进程分配资源,维护进程的执行状态,提供进程间通讯方法。更进一步讲,进程管理还必须保证进程运行的可靠性,因此需要提供进程同步,互斥,防死锁等等服务,另外进程调度也是进程管理中的重要任务。
内存管理:计算机存储部件由快到慢、由小到大分为缓存、内存和磁盘。其中最主要和必须是内存,内存管理包括内存的分配和释放,以及访问保护等。另外对使用虚拟内存的系统,内存管理还包含虚拟内存管理,磁盘交换管理,内存影射等等。
文件系统:文件是多数系统中用户使用和管理数据的主要方式,文件系统需要负责用户文件访问,访问权限控制,文件格式转换,数据传输等一系列问题。
设备管理:除了存储设备外,系统还有大量外设需要操作系统管理,比如时钟,网卡,键盘,磁盘等等,设备管理需要负责驱动这些设备为上层调用服务。
I/O管理:操作系统中I/O管理负责处理复杂的I/O操作,其中包括I/O缓冲和磁盘调度等。
另外中断管理也是操作系统内核应该实现的功能。
以上是操作系统内核设计要考虑的主要问题,其中各种模块彼此相互交错、相互利用。不过这些模块的划分并不是绝对的,在实际系统中可能有不同的组合或更细致地划分,因此我们不必追究模块的具体内容,需要关注的是内核究竟需要完成那些功能。
内核模块大致也有层次之分,我们可以这样理解层次含义:直接和硬件作用的是硬件抽象层,和用户更靠近的属于逻辑抽象层。
所谓硬件抽象层,是指管理硬件设备的模块,比如存储管理、设备管理这些模块将硬件功能抽象为内核数据结构和接口函数,以供上层使用。比如磁盘设备驱动,需要将磁盘设备功能抽象为打开open,写入(write),读取(read)等接口函数;内存管理需要将内存抽象为页、段等结构体。然后分配、合并、释放等工作都是通过操作这些抽象得来的结构体,再由这些结构影射到内存的物理实体上去的完成实际操作的。
所谓逻辑抽象层最主要的目的是为了贴近用户需求,最重要的逻辑抽象模块就是文件系统,文件系统的存在完全是从用户角度出发设计的,因为用户最能接受以用文件形式包装的信息,所以文件系统属于逻辑上的抽象,因为物理设备中没有对应文件的实体。
对于进程管理来说,其中进程执行和调度要和处理器打交道,应该说属于硬件抽象层,但其中进程状态维护,进程通讯等更接近用户使用,因此可以归结到逻辑抽象层。
另外I/O管理和中断管理些模块,在内核中属于为其它模块服务的借用力量,它们主要被文件系统或设备管理模块使用,但总之是面向硬件的,所以也可以将其归为硬件逻辑层。
Linux操作系统
在众多商业操作系统和免费操作系统中,Linux占有独到地位,它不但功能强大,接近于工业强度,而且结构设计幽雅,具有良好的扩展性和移植性,接口定义规范,基本和Unix系统兼容。更为重要的优势在于Linux操作系统是最具影响力的开源软件,它的产生揭开了开元运动的新纪元,对自由软件发展起到了前所为有的推动作用。
Linux开放性,也就是它不拘一格的拿来主义精神,吸引了无数软件爱好者热情的投入到其开发中去,因此Linux是当今发展最快,范围最广的开元软件之一。它是社区中大家最乐意讨论和参与的项目,也正是这种开源精神使Linux成为操作系统爱好者最好的良师益友,它在教育意义上的贡献是前所未有的。从这节起我们将进入Linux世界去探索操作系统软件的严谨,去感受Linux的可爱。
Linux操作系统的起源
Linux的第一个版本诞生于1991年,它的作者就是现在大名鼎鼎Linus Torvalds,这个芬兰小伙子据说最初是在做一个作业调度系统的学校家庭作业,后来他突发灵感开始着手将系统改造为一个实用的操作系统,他在开发初期借助了当时最负盛名的教育类操作系统Minx的一些思想和成果,但他的雄心是要将自己这个系统变的比Minx更实用、更强健,因此他决定把自己的系统代码公布于众,并且欢迎任何支援者来修改和扩充Linux系统——这正是我们现在耳闻祥熟的GUN协议的权益——Linux选择了当时在世界上最受推崇的Un ix系统接口标准:POSIX.1来作为自己的内核系统调用接口,从此Linux成为了Unix风格操作系统家族中的新贵,而且是一个代码完全公开的操作系统。
Linux的生命力来自于它的开源思想,自Linus公开Linux代码一来,世界各地的软件工程师和爱好这不断积极地对Linux系统今进行修改和加强,先后将其版本从0.1 提高到2.0 、2.2、2.4到如今的2.6,同时Linux也被从初期的x86平台移植到了Powerpc、Sparc、MIPS、68K等几乎市面上能找到的所有体系结构上。更另人激动的是,拜开源运动之新风,数不胜数的应用软件出现在Linux系统之上,这样大大加强了Linux系统的实用能力。
Linux作为开源软件中的桂冠,越来越受到欢迎,毫无疑问地成为人气最旺,最活跃的gun项目,围绕Linux的社区雨后春笋般的出现,这一切都预示着Linux将在教育领域,在工业领域在政治领域将得前所未有的成功。
Linux 操作系统的技术特点
Linux系统吸收了Unix操作系统的精华思想——“简单就是美”,因此它采用了紧缩内核结构,只在内核中实现那些必要的功能,尽量保持内核精悍短小。至于那些丰富多彩的附加功能统统交给用户空间的库函数或其它系统软件或应用软件完成。
有时大家将直接将Linux内核和 Linux操作系统化等号,这也没错的。而我们上文提到的操作操作系统多数情况不仅仅只内核而且还只内核之上的系统程序,可以说是广义的操作系统概念,希望大家区别。
为了能受益于Unix系统的影响力,Linux采用了Unix的系统调用接口标准POXIS.1,保证了和Unix系统的有限兼容,从而抓住了很大一部分Unix技术人员。
另外Linux起源于小型计、通用算机,并非针对大型和专用计算机设计,因此结构复杂性和规范性都比较适中。
还有就是目前多数Linux操作系统版本都是以服务器为出发点,因此网络功能和系管理能力突出,多数应用也是专为网络管理服务的,对于个人用户所关注的桌面应用和嵌入应用关注的实时性支持尚且有限(今年Linux发展的一个重要议题就是针对桌面和嵌入开发相应的内核版本)。
Linux 内核的特点
Linux是一种是实用性很强的现代操作系统,开发它的中坚力量是软件工程师,因此多以实用性和效率为出发点,很多地方还考虑了工业规范和兼容性等因素,因此不同于教学性操作系统追求理论上的最先进性,Linux系统内核最注重的问题是实用和效率。
下面我们简要归纳一下Linux内核的特色。
第一,Linux内核被设计成单巨内核(monolithic?)结构(相对微内核而言,微内核是一种功能更贴近硬件的核心软件,它一般仅仅包括初等内存管理、同步原语、程间通讯机制、I/O操作和中断管理,这样做有利于扩展性和移植性。但是微内核与诸如文件管理、设备驱动、虚拟内存管理、进程管理等其它上层模块之间需要有较高的通讯开销,所以目前多集中在理论教学领域,对工业应用效率难以保证。),因此效率高,紧凑性强。
第二, 2.6版本前Linux内核是单线程结构——所谓但线程结构是说同一时间只有一个执行线程(内核中的执行程序)允许在内核中运行,不会被调度程序打断运行其它任务,这种内核被成为非抢占的,它的好处在于内核中没有并发任务(单处理器而言),因此避免了许多复杂的同步问题,但其不利影响是非抢占特性延迟了系统响应速度,新任务必须等待当前任务在内核执行退出才能获得运行机会。工业控制领域需要高响应速度,因此2.6版本后由于Robert love等人的贡献,将抢占技术引入了Linux内核,使得其变为内核抢占系统,当然付出的代价是同步操作进一步复杂化了。
第三,为了保证能方便地支持新设备、新功能,又不会无限扩大内核规模,Linux系统对设备驱动或新文件系统等采用了模块化方式,用户在需要时可以现场动态加载,使用完毕可以动态卸载。同时对内核,用户也可以定制,选择适合自己的功能,将不需要的部分剔除出内核。这两种技术都保证了内核的紧凑性和扩展性。
第四,Linux内核纯粹是一种被动调用服务对象,所谓被动是因为它为用户服务的唯一方式是用户通过系统调用来请求在内核空间运行某个函数。内核本身是一种函数和数据结构的集合,不存在运行的内核进程为用户服务(虽然Linux的确存在一种被称为内核线程的进程,但它并不是用来服务于用户的,仅仅作为系统自身的服务目的)。
第五, Linux内核的采用虚拟内存技术使得内存空间虚拟扩展到了4GB之多,其中0-3G属于用户空间,称为用户段,3G-4G属于用户空间,称为内核段。这样使得用户编写程序可以使用远远大于实际内存的存储空间。
第六, Linux的文件系统最大特点是实现了一种抽象文件模型——VFS(虚拟文件系统),该文件系统属于Unix风格。使用虚拟文件系统屏蔽了各种不同文件系统的内在差别,使得用户可以使用同样的方式访问各种不同格式的文件系统,可以毫无区别地在不同介质不同格式的文件系统之间使用VFS提供的统一接口交换数据。这种抽象为Linux带来了无限活力。
第七, Linux提供了一套很有效的延迟执行机制——下半部分,软中断,tasklet和2.6新引入的工作列队等,这些技术保证了系统可以针对任务的轻重缓急,更细粒度的选择执行时机。保证了系统运行时尽量在安全时间(不关中断)。
Linux除了以上提到的特色外,还有许多其它突出特点,我们将在以后各期的介绍中有序地介绍。如果对上述特色有疑问的话,请别着急,后面的学习将为你解答。
Linux 操作系统内核结构
Linux内核虽然实现和Unix系统有很大不同,但是其结构还基本保持和Unix雷同,其中功能也和我们前面提到的操作系统内核要求大体一致。
请见下图
下面我们简要说明一下个模块之间的联系。
用户空间的任何程序如果需要使用内核提供的服务,都必须经过系统调用,因此系统调用层和内核中大多数模块都留有接口,它们或是用来控制系统服务属性(如sys_fnctl设置文件操作属性;sys_nice设置进程时间片),或是从内核提取数据(如sys_time获得由时间中断维护的系统计时),或请求内核分配资源(brk扩展进程堆内存)。
文件系统包含VFS和各种实际文件系统。VFS为实际文件系统抽象了统一接口,而实际文件系统提供自身具体实例操作方法。另外在Linux中和Unix一样设备被巧妙的归属为特殊文件,受文件系统抽象和管理,因此其操作方式和文件系统一致。文件系统将对设备的操作递交给实际的设备驱动处理。
Linux中设备管理将设备被区分为块设备——可以随机访问,如磁盘——和字符设备——只能顺序访问,如键盘。字符设备结构简单,文件系统可将请求直接提交给字符设备驱动处理,但是对于块设备,由于频繁的随机访问需要反复进行磁盘寻址操作,这样会对系统载核的带来沉重负担,因此内核对块设备的请求必须加以整合,比如对请求排队、合并、然后有选择地派发给物理设备;另外读取设备时还需要在内存中进行缓冲磁盘块。因此在请求被提交给块设备前必须经过I/O层处理进行预处理,在磁盘块被读取后必须由I/O层进行块缓冲处理。
中断管理系统负责为设备服务,它相比轮询等方式节约了CPU周期,另外时钟中断还要负责更新系统时间,触发进程调度。
内存是系统中的核心资源之一,是数据存储和传递的必被条件,因此管理系统不但系统调用需要使用,而且几乎系统中所有模块都多多少少地需要使用内存管理系统的函数。文件系统、I/O系统用来缓冲数据都需要分配内存,进程管理中的进程数据存储,地址影射都需要内存,堆的增长也需要动态请求内存;还有就是进程通讯中的一个有效方法就是利用共享内存来实现的。
进程管理除了和内存管理和进程通讯有关外,也和文件系统有重要关联,因为进程资源中文件毫无疑问属于最重要的部分之一,因此进程管理系统也必须和文件系统交互。
中断管理
块设备
字符设备
设备驱动
Ext2
VFS
jfs
vfat
I/O层
系统调用
内存管理
进程管理
进程间通讯
外设备
内存
CPU
Linux 操作系统的文件构成
内核虽说是Linux操作系统的精华所在,是其它程序赖以运行的基础,但是如果一个实用的操作系统仅仅只有内核,而在其上没有丰富、强大的系统程序和应用程序供用户使用,就好比大厦建好了,也通了电,但却没有电梯、电话和办公设备,用户仍然无法入住使用。因此Linux操作系统的发行版除了带有内核以外,还带有大量的系统程序和应用程序,比如最新红帽子系统的发行版本句需要2-3张光盘,其中绝大部分是应用程序。
要想深入学习linux内核,首先需要能熟练使用Linux操作系统,了解整个系统文件构成——正所谓,刨丁解牛,始见无非全牛者——由外至内的学习Linux,再从内向外推敲;从感性深入理性,再由理性返回感性,才会获得最深刻的认识。内核的众多特点最终还是要反映到用户应用上的,所以先熟悉应用无疑会对内核学习有很大裨益。而且学习内核结构的一个重要目的就是推动我们更有效的使用Linux操作系统,无论是从系统管理角度来说或是从程序开发角度上说,掌握内核级别的系统调用、资源分配、中断控制或进程调度等技术都是不凡的价值,可以帮助你有效开发和驾驭系统。
这节我们先在这里简要介绍一下Linux系统的文件构成,将Linux操作系统的外在全貌展现给大家。我们在系统启动后,进入系统所能观察到的就是一系列目录(使用ls或dir)
,认识这些目录构成是学习使用Linux系统的第一步,下面我们就罗列出主要目录并简要描述各自内容。
Linux系统根目录/下包含包含:
bin:该目录存放最常用的基本命令,比如拷贝命令cp、编辑命令vi、删除命令rm等。
boot:该目录包含了系统启动需要的配置文件、内核(vmliuxz)和系统镜像(initrd….img)等。
dev:该目录下存放的是Linux中使用或未使用的外部设备文件(fd代表软盘,hd代表硬盘等),使用这些设备文件可以用操作文件的方式操作设备。
etc:该目录下包含了所有系统服务和系统管理使用的配置文件;比如系统日志服务的配置文件syslog.conf,系统用户密码文件passwd等
home:该目录下包含了除系统管理员外的所有用户的主目录,用户主目录一般以用户登陆帐号命名。
Lib:该目录下包含了系统使用的动态连接库(*.so)和内核模块(在modules下)。
host+found:该目录包含了磁盘扫描检测到的文件碎片,如果你非法关机,那么下次启动时系统会进行磁盘扫描,将损坏的碎片存到该目录下。
mnt:该目录下包含用户动态挂载的文件系统。如果要使用光盘,U盘都一般应该将它们安装到该目录下的特定位置。
proc:该目录属于内存影射的一个虚拟目录,其中包含了许多系统现场数据,比如进程序数,中断情况,cpu信息等等,它其中的信息都是动态生成的,不在磁盘中存储。
root:该目录是系统管理员(root用户)的主目录。
sbin:该目录下包含系统管理员使用的系统管理命令,比如防火墙设置命令iptable,系统停机命令halt等
tmp:该目录下包含一些临时文件。
usr:该目录下一般来说包含系统发布时自带的程序(但具体放什么东西,并没有明确的要求),其中最值得说明的有三个子目录
/usr/src :Linux内核源代码就存在这个目录
/usr/man :Linux中命令的帮助文件
/usr/local : 新安装的应用软件一般默认在该目录下
var:该目录中存放着在不断扩充着的信息,比如日志文件。
以上就是Linux文件系统的原始构成,熟悉它们是应用Linux操作系统的前提,希望大家亲自打开各目录看看。
搭建Linux试验系统实例
进入后续章节讨论的内核前,我们先与读者一同从头构架一个试验操作系统。这样既有助大家熟悉Linux操作系统的组成结构,也会在构建过程中学习介绍一些Linux命令和
使用技巧,加深理解Linux操作系统的运作方式。
实验系统将在保证实用价值的基础上,尽量小巧。希望大家通过亲手构建系统的过程中,能消除对Linux的恐惧感,更希望读者自己能使用裁减的系统,给自己带来成就感和学习热情。
必备的基础知识
对于第一次接触Linux的朋友,仅仅看下面的内容显然不能指望学会Linux的操作方法和系统行为,建议你去找本系统一点的Linux系统教程慢慢咀嚼吧。对于像系统管理员这种大牛,跳过下面内容吧,再高的就去看看新浪体育新闻什么的,别在这瞎转了J。
搭建系统过程中将离不开敲击各种各样的命令,离不开执行大大小小的shell脚本。而最整个过程中重要的是理解系统的运行思路,一切活动的指导思想都要围绕系统运行的步伐,要“顺从“系统运行自己和系统运行服务这一指导思想。所以基础知识也从这几个角度展开。 不过我们蜻蜓点水,不做深究。
基本命令我们首先介绍一组搭建Linux系统需要使用的基本的命令。当登陆到Linux系统上后,出现在我们面前的是一个shell 提示符(# 或 $等),该提示符号告诉我们系统已经准备接受命令了,你可以用键盘输入命令行来操作系统了,你输入的命令将在屏幕上显示出来,并议回车键表述命令输入结束、发送命令给系统的标志
Shell和SHELL编程
Shell是什么?
在你登陆到系统后,系统首先运行的是一个特别的应用程序,它显示一个提示符号表明系统已经准备好开始接受你的命令了,当你键入你要执行的命令后,该应用程序将命令提交给Linux系统去处理,然后等处理完毕再把结果返回给你,这之后她又将回到提示状态,去等待你下次输入命令。这个特殊的“接待”程序就被称为shell,其作用相当是一个内核与用户交流的界面,她周而复始地向内核解释用户命令,因此Shell又被称称为命令解释器。
SHELL作为一种应用程序并非只有唯一一种,目前流行的shell有sh / bash /ksh /tcsh/csh等等,他们其实也就始一个应用程序,你可以使用命令whereis ksh/sh/bash来查看其存在于系统中的具体位置。
有兴趣得话,你可以通过命令 echo $SHELL来观察系统默认的SHELL属于那一种。你也可以在登陆后(使用Ctrl+D可以重新登陆)使用chsh来改变选择使用的shell程序,或干脆直接在默认shell上执行新的shell程序——只要键入新shell名字并回车即可,如果想推出新shell,就再执行exit程序。
各种shell程序各有特点,功能也有强又弱,但是相同点都需要能够执行程序或命令;能够处理程序或命令的输入输出;能够执行shell脚本。(shell 能执行三种不同概念的文件:1命令指shell程序自己内置的基本命令——如 cd 命令,管道 | 命令 >重定向命令——和以二进制文件形式存在的系统命令——如ls cp等。2 程序指用户安装和编译身成的二进制文件;3脚本指包含逻辑关系的程序和命令序列)
shell执行文件需要必要的环境,这些环境包含文件搜索路径,当前目录,用户主目录,默认编辑器等等(你可以从man shell种获得这些信息)。这些信息属于环境变量,可以通过env观察当前系统默认的环境变量,改变这些变量可以通过:变量=设置(如 PATH= /opt)命令方式和修改存在于用户目录下的相关配置文件(如对bash来说配置文件爱你为~/.bashrc,~/.bash_profile)
shell编程
shell编程简单地讲就始讲命令序列化后执行,而不用被编译成二进制可执行文件。这类似于dos下地BAT批处理文件。使用shell程序的意义在于,有些任务无法通过现有的命令完成,必须使用一组命令协作才能完成,而且各种命令之间不是简单的罗列而是按照设定的逻辑关系有机结合。由此可见shell程序需要能够控制各种命令的执行流,能够读写临时数据,因此,shell程序存在自己控制语句和变量,而且对其使用也由相关语法。
Shell程序,也可以成为shell脚本,以普通的Linux文本文件形式存在。可以是用vi等文本编辑器生成,再将其属性改为可执行即可运行。
比如 touch test 生成文件test
chmod u+x test 修改属性
./test
保险其间可以再脚本头先使用#!符号来强制当前shell运行其后的制定shell文件来执行该脚本。
当然shell编程觉非上面说的那样简单,想要真正学习shell编程并能使用它可不那么容易。有兴趣的朋友可以参看有关资料了解shell编程。
系统服务安装过Linux的朋友一定熟悉安装过程种系统会提示你选择何种服务,或安装完毕使用setup命令也可看到一个配置界面其中包含系统服务配置。系统服务包含一系列形形色色的服务,很多服务选项我们闻所未闻,或者仅仅听说过罢了。着很正常,因为服务太多太杂了,很少有人能全部搞清楚这些服务是干什么的。我们这里也不追究所有服务的详细作用,仅仅从系统运行角度介绍一下这些服务的使用方法。(想知道系统到底有那些服务,试试setup命令吧。)
系统服务程序和普通应用程序或系统命令本质是相同的,都是一些二进制文件。但其运行方式却有一些自身特点。系统服务多数情况下都处于后台运行,因此运行结果一般不再屏幕显示(往往被重新定向到/dev/null中),但是为了安全目的或分析目的,大多记录都要求保存到相关日志中;另外系统服务程序运行时多需要进行一定配置,比如ftp服务器有用户访问权限配置,工作目录配置,因此需要从配置文件取数据初始化服务程序。最后就时服务程序很多时随系统启动就开始运行,而不需要用户自己启动。
由于这些特点系统服务程序的启动或停止一般都存在相应的shell脚本文件管理,利用这些脚本可以控制服务程序的配置,启动,日志记录以及关闭服务和清理临时文件操作。这样相比用户手动操作要方便安全得多。
Linux系统中的服务程序运行脚本(启动或关闭)都存放在目录/etc/rc.d/init.d下——Linux系统的文件组织层次遵循FHS规范,包括脚本位置——比如我们启动/停止网络所用的network 等脚本。这些脚本都具有相同的使用方法运行:服务脚本 {start|stop|restart|reload|status}。如果你需要手动启动或停止某项服务,键入/etc/rc.d/init.d/服务脚本名 start|stop 即可,除此方法外也可以利用命令 service服务脚本名 start|stop,它们执行作用相同。
系统服务程序多数情况下随系统启动开始运行,系统关闭停止运行,这也正是你开机或关机时为什么能在屏幕上看到一系列的服务启动[ok] 或服务停止[stop]的原因。那么系统如何启动和关闭这些服务呢?
谈到这里很有必要说一下Linux系统运行级别这个问题。所谓运行级别更通俗的讲就是指定系统的行为,每种运行级别都对应一组该级别应用程序。
运行级
描述
0
系统停止
1
单用户系统,不需要登陆
2
多用户系统但不支持NFS,命令行模式登陆
3
完整多用户模式,命令行模式登陆
4
未用
5
X11图形模式,图形模式登陆
6
重新启动系统
我们可以使用命令init(后问会说明它) 级别来切换系统的运行级别。一般服务器系统使用级别3,如果需要图形界面使用5,对于单用户或嵌入系统使用运行级1即可。
其中级别0和6可以使用来安全停止系统,它们会将除根目录以外的文件系统卸载,并且以只读方式重新安装根文件系统,这样一来防止了破坏文件系统。
言归正传,回到系统服务程序。我们应该能猜到不同的运行级别也对应了不同的系统服务集合。比如运行级别5至少就需要比级别3多启动x服务器和xfs(字体服务器)等。你可以利用命令chkconfig –list来观察每个运行级别下的各种系统服务是否允许。显然级别5开启的服务最多,下来是级别3 。总之,功能越强要求服务越多。
下面的启动部分回告诉大家,系统根文件安装后,首先寻找init程序并运行它,该程序的任务就是从配置文件确定系统的运行级,并且根据级别启动相应的服务程序。具体的过程如下
init程序从inittab中获得系统运行级别X ,后会依次运行/etc/rc.d/rcX.d/中以大写S开头的shell脚本来启动对应的服务。
Linux系统启动的标准流程
对于系统装载过程我们暂时不做介绍,我们假设内核已经被载入内存并且已经完成了异常表、中断表、调度程序、时钟、控制台、内存等初始化,最后进行进程管理器的初始化,从此内核可以开始使用真正的进程了。
初始化完成后,内核创建第一个进程(初始进程),该进程作为系统的第0号进程,在进程描述符表中由task[0]或INIT_TASK表示。该进程进而再创建了一个进程去执行init()函数进行第二阶段的初始化操作,而初始进程(INIT_TASK)本身则去执行idle循环,可见初始进程在内核初始化后唯一的作用就是去使用空闲的CPU时间。
第二阶段的初始化工作要比前一阶段轻松一点,因为现在是由一个真正进程完成它们的,而前一阶段都是由“硬件进程”手工去做的。该阶段,这个由INI_TASK创建的新进程需要初始化总线、网络并启动系统中的各种系统内核后台线程,然后再初始化外设、设置文件格式,在这之后,它要为进入系统做最后的准备——初始化文件系统,安装根文件,打开/dev/console设备,重定向stdin、stdout和stderr到控制台,然后搜索文件系统中的init程序,并使用 execve()系统调用加载执行init程序。系统自此进入了用户态。
init程序接着将依照initab配置文件中的选项依次执行:
1 确定运行级别(1-6)
2 运行rc.sysinit脚本中的的系统服务,如激活交换分区,检查磁盘,加载硬件模块等
3 运行规定级别下的服务:/etc/rc.d/rc*.d/下的S打头的服务,如网络服务S*NETWORK。
4 在指定串口上运行getty程序,getty打开终端线,并设置模式,然后运行login程序。如果用户帐号和密码正确(需要通过/etc/passwd验证),则进入用户的工作目录,并按照其工作目录中的设置执行相应的shell。
到这里用户才可以真正实用操作系统了。
Idle进程是个奇怪的进程,它是在没有别的任务使用CPU时是才使用CPU的,它的存在价值据说可以延长CPU寿命。
内核后台线程是种执行在内核态的进程,它们和用户进程一样受调度程序调度,系统利用它们周期性(不一定固定周期)地执行一些自身管理方面的“家务事”。主要的几种内核线程为:bdflush——清理被写过的内存缓冲区;kupdate——按时将内存缓冲区中的信息更新到磁盘中; Kswapd——将内存页交换到磁盘;keventd——关系系统事件;Ksoftirq——执行软件中断。
搭建实验系统
很抱歉搭建一个Linux操作系统到目前为止还没有一个很标准的流程或规范,不过大体流程都大通小异,无非是首先编译内核——将内核源代码编译成一个可执行的镜像文件,当然编译内核时可能会带有一些模块也需要同期进行编译和安装(是否有模块取决于你的具体选择)。
有了编译后的内核,接着就需要创建一个根文件系统,在其中又需要创建必要的
目录。至于其中使用的软件和库函数你可以选择下载源代码包,然后交叉编译,再进行安装。或者我们偷个懒,从一个发布的完整系统里直接拷贝需要的软件和库,同时将必要的设备文件、配置文件和服务脚本也拷贝过来,你这时所要做得仅仅是去修改一些相关的配置文件就可以拥有一个自己的文件系统了。
内核与文件系统都有了,就可以说一切具备只欠东风,你所需要做得只剩下将内核和文件系统绑定到一起,让系统被引导载入内核,内核载入后可以找到根文件系统,并执行其中的初始化程序。你可别以为这个收尾动作能轻松搞定,往往初学者都在这里要栽跟头。
怎么能在最小的代价学习搭建系统呢?想想看可不是每个网友都能找个空硬盘或者磁盘(看看你的机器,也许连软驱都没)来做新系统的,为了保护原有系统,即便开一个新分区都不能鼓励。所以最好的方法就是用内存模拟一个磁盘,将创建的根文件系统放在其中,系统引导后,就登陆到内存模拟的磁盘上运行。这时你彻底跳出了你的物理硬盘。这种方法有时在嵌入系统中会被使用,或希望断电后数据被抹掉的安全系统中使用。
下面我们就一同做个这样的试验系统,你付出的唯一代价是消耗些时间和无数次击健。
编译内核
第一步要做的工作就是挑选一个合适版本的内核源代码包,然后编译它。不要以为编译内核很神秘,其实它和编译普通程序差不多,内核源代码其实就是“一大堆”程序,编译它就等于分别编译个个程序然后在将它们链接成一个单一的可执行镜像文件。这个镜像就是你在/boot目录下看到的vmlinuz-*(如果你细心的话,一定能发现在该目录下还有一个叫vmlinux的文件。其实这两个文件是一回事,但前面那个是经过压缩的)
正如第一部分所说,Linux内核具有很强的伸缩性,在内核里面许多功能是可选择的,如果需要就可以被编译到内核,不过内核会因此变的肥胖。一种可替代的方式是将某些功能编译成模块放在文件系统内,等你真正需要它时,再由被载入到内核,这样就内核就可以轻装上阵了,启动起来也快许多。
虽然是个试验系统,但还是尽力让它功能做强点吧。所以在编译前,配置内核选项时,除了支持最基本的ext2文件系统,PCI接口,自动装在模块等功能外,再将ext3,JFS,即插即用,网络,SCSI,USB等比较常用的功能加入。再一个就是为了能实现我们的虚拟内存中建立根文件系统,内核还需要支持Ramdisk 和initrd。
内核网络设备选项里包含大量网卡驱动程序,你必须知道自己网卡内型才能正确选择,一般情况都将网卡驱动编译未模块,在系统启动后载入。我们试验系统运行在vmware下,而vmware虚拟网卡驱动为pcnet32,因此这个模块被包含进来了。
编译步骤
先去下载一个内核源代买不用我在多说了吧。如果你实在是个衣来伸手的家伙,好吧告诉你,到网站上荡一个想要版本的内核源代码。如果是gz结尾的压缩源文件,就使用tar xvzf linux-2.4.18.tar.gz解开,如果是gz2结尾的,就使用tar xvjf linux-2.4.18.tar.bz2解开。
内核版本编号可是有点讲究的,简单的说,偶数为稳定版本,奇数为开发版本,所以我们用2.4.18版,一是因为它属于稳定版,再一个就使我机器里以前下载过它,不想再换了J。
然后进入存放解开后的内核原代码的目录(标准系统默认情况下在目录/usr/src/linux下存放该系统的内核源代码),执行命令make menuconfig进行内核功能配置,选择需要的功能以模块形式编译或直接编译到内核。配置信息默认情况下记录在隐含文件.config中,你也可以选择将其记录到自定义文件中,比如可以把信息记录在MiniSys.config中。在以后配置内核时可以方便地导入指定的配置文件。
make menucofig提供给你一个文本图形界面的配置菜单,其中列出了内核所能提供的全部功能,如果你在选项前选则*号,那么该选项被编译到内核中,如果选M则被编译为模块,对于你不清楚的选项可以使用?查看其解释。除了用make meunconfig外你有复古情节的话,可以试试使用make config,它完成同样的功能,不过你得有足够得耐心去忍受刷平一样得命令行选择 。如果你在X环境下(桌面环境)不妨可以使用make xconfig配置,它相比前两种方法得好处就试比较容易看清楚,对眼神不好得网友建议使用它。
保存内核配置后,就执行
make dep /*确保所有的相依关系,例如 include files 都没问题.除非你的电脑真的很慢,否则它不会花太久时间的*/
make clean /*清除核心编译的所有目的档以及其它东西.在重建一个核心之前不要忘记这个步骤*/
make bzImage 或zImage/*编译内核——bz和z格式内核之间最大的差别是对于内核体积大小的限制。zImage内核需要放在实模式1MB的内存之内,其体积受到了限制。而bzImage的内核没有1MB内存限制*/
记住内核编译完了,还必须再编译模块, 即使您在配置内核时没有使用任何模块,也不要跳过此步骤,在编译完 bzImage 后立刻编译并安装模块是个好习惯。而且,如果您真的没有模块需要编译,这个步骤也非常快就结束了。
make modules; /*编译内核模块,凡是配置内核时标记为M功能都将被编译未模块*/
make modules_install。/*这将导致模块被编译而且被安装到 /usr/lib/<内核版本号> 目录下。不过如果你想改变内核镜像或模块的所在目录,都可以通过修改内核源码中的Makefile文件来达到,比如修改INSTALL_MOD_PATH来改变模块安装目录*/
等黄蜂一样的字符风暴再屏幕上停止后,你现在拥有了新内核了。它藏在内核源码目录下arch/i386/boot下叫bzImage或zImage。新内核随带的模块被安装到了lib下的modules目录中。
组建文件系统
组建根文件系统说白了更简单,一来格式化文件系统的宿主设备,二来就是拷贝需要的文件。简单明了吧!我们先来一同拷贝文件吧,等考完了再谈宿主设备的问题,别忘了我们可都是完空手套百狼呀!(除了拷贝文件外,更标准的方法是下栽各种工具包,在本地交叉编译,在进行安装,不过为了省事,我们采用拷贝标准系统文件的方法来构造文件系统,不但方便而且异曲同工。但前提是新系统体系结构——处理器——和我们原料系统一致,如果你想在0x86系统上编译运行在ARM机上的文件系统,那么最好是去下载源代码包重新交叉编译吧)
文件系统基本要求
Linux文件系统的结构上文已经给出,我们这里着手搭建一个精简的文件系统,它包含最基本的目录以及文件,配置文件也尽量修改简洁明了。下面列出文件系统必须包含的内容。
文件系统最小需要包含/dev 、/proc、/bin 、/sbin 、etc 、/lib 、/usr 、/tmp 等目录
需要一组基本命令
支持上述命令的运行库函数,其中也包括编译内核产成的模块
必须的设备文件
一些必要的配置文件
我们要做地就是按部就班地生成和拷贝以上内容,唯一地要就就是你要够心细。
创建根文件系统内容
我们先来建立一个将包含根文件系统内容的新目录“rootfs”(mkdir /rootfs),然后开始在其中生成(拷贝)根文件系统需要地所有目录和文件。
第一步当然是在rootfs目录下建立根目录下地必要地子目录啦,用一行命令就可完成mkdir dev,proc,bin,etc,lib,usr,tmp,sbin 。
第二步拷贝你需要的命令。比如你需要 ls 命令,你先确定它在系统中的位置whereis ls (发现在/bin/ls目录下),然后将该命令拷贝到你工作目录下相同的目录结构下 cp /bin/ls /workdir/bin/ls,但是仅仅拷贝命令文件还不够,还必须考被该命令所用到的动态共享库文件。如何发现命令用到了那些动态共享库呢?很简单,利用ldd /bin/ls 可以察看命令使用的共享库,显示在输出右列的就是被用到的共享库文件(名字中有so)。
比如在我的系统上,该操作输出为:
libtermcap.so.2 => /lib/libtermcap.so.2 (0x4001f000)
libacl.so.1 => /lib/libacl.so.1 (0x40023000)
libc.so.6 => /lib/libc.so.6 (0x40029000)
libattr.so.1 => /lib/libattr.so.1 (0x40149000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
我们要做的是将命令要用到的库文件按照它的目录结构拷贝到我们工作目录下的/lib或/lib/i386。(用户所用到的命令多集中在/bin和/sbin下,另外一些脚本会用到一些出现在/usr/bin和/usr/sbin下的文件,如果你要使用这些脚本,不用说这些命令和它们要用的库都不能少)。但是这些文件并非我们实际想要的,它门只是实际库文件的一个符号链接,系统只所以要使用符号链接,是为了便于库文件升级换代时不影响使用它的应用程序。因此我们单单拷贝符号链接是没有意义,同时也必须将符号链接指向的实际库文件一同拷贝到workdir/lib录下面去。
/lib目录下还有一个重要目录就是modules目录,它里面包含了内核编译产生模块,对于不同版本的模块存放在以版本号命名的文件中。要们可别忘了拷贝这个目录到我们的新系统中。
在原始时期/lib下的这些库就足够用了,但现在的Linux系统对安全多了许多要求,尤其是系统从安全性考虑,增加了许多验证手段,因此往往你还必须具有和安全验证相关的库。这些库不会在命令中直接使用,但却间接地要被系统的安全框架利用到,多数都是由配置文件中说明如何关联,安全框架通过查看配置文件,选择调用具体地验证库(这些配置文件后问会提及)。安全框架方面话题,我们不多说,有兴趣的可以查查 pam 和 nss等的用法。在这里我们不管它三七二十一将在/lib/security/下和pam相关地库和/lib下nss相关地所有libnss*库都考到我们地/lib下的/security下和/lib下。虽然很笨,但确省事。
Linux系统将那些会被多数应用程序频繁使用的库函数,多数都不会以静态的方式编译连接到应用程序中,而是采取动态库的方式,集中存储管理。这样如果多各程序都用到某个共享库,那么该库文件只被调入内存一次,驻留在内存一个拷贝,因此利用共享库大大节约了空间,缩减了执行文件提及。当然天下没有免费的午餐,虽然共享库相比静态库灵活,但却学要而外的路径搜索,而且调入时间也更耗时
链接文件是Unix风格操作系统提供的一个特色之一,其中具体又可分为软链接和硬链接。软链接又称为符号链接,其实就该文件唯一的内容就是包含实际额外文件系统的路径。硬链接则是和被链接文件共享索引节点的(索引节点概念如果还不清楚,那么自己去找找吧)。因此符号链接怕得是实际文件被删除和转移,那么符号链接仍然存在但不再有效了;相反硬链接删除源文件直会使索引节点记数减少,不会破坏硬链接文件的。
第三步建立设备文件,这点很重要但却不费事。由于Linux继承了Unix将设备抽象成特殊文件来使用和管理,所以要想使用系统的外设,比如软硬盘,时钟,系统终端,甚至内存也可以作以为其制作相应的文件来访问。因此我们要建立系统可能用到的所有设备对应的设备文件。至于你具体需要哪些设备文件不能一概而论,你可以打开/dev/目录看看保准里面文件多地让你炫目。不过也别怕多数都是些废物,就我们要建立的实验系统来说用道的设备文件就屈指可数:console 控制台设备,tty* 是由控制台管理的虚拟(我们用ctrl-[1-7]切换的就是这个设备),sda1 SCSI接口设备 (因为我的Linux是运行在vware虚拟机上,而vmware虚拟机使用的存储设备是虚拟的SCSI硬盘,所以需要这个文件。它的用法和使用标准IDE硬盘没什么两样),ram 内存虚拟盘设备(以后我们的系统就运行在内存虚拟盘中),null 空设备(是一个非常有用的字符设备文件,送入这个设备的所有东西都被忽略。如果将任何程序的输出结果重定向到/dev/null,则看不到任何输出信息),zero 零设备 (读取这个设备,只会得到空的内容,所以有时为了获得高压缩率,需要对某空间用全零添充往往就会用到它)
initrd 这是一个特殊的字符设备,它被用来从用户空间向系统内核发送切换运行级别的信息,属于一个虚拟字符设备(比如你向改变运行级别的init 1-6命令,都试通过该设备传达到内核的),关于虚拟字符设备作为用户向内核发命令的利器作用你可看看这个文章。
明确了你需要那些设备文件,可以依次利用mknod命令建立需要的设备文件。建立过程中需要的参数。你可以通过ls – la /dev/设备名命令来查看以下设备属于块设备还是子符号设备,察看主从设备号获得。如果你觉得烦,就用拷贝命令直接从标准系统地/dev/目录下拷贝这些文件吧,不过要配合参数-R否则,你靠过来的可使文件的整个内容而不仅仅是设备文件了。如果那样可就如同把自己往自己衣服口袋里塞,你是永远赛不进去的。
Linux系统将设备分为块设备和字符设备,块设备可以随机访问(B),字符设备只能按顺序访问C。另外一个设备控制器可以控制多个设备,所以有主设备号和从设备号之分。主设备号对应驱动,从设备号用来区分具体设备。
第四步需要建立系统运行需要的配置文件或脚本了。我们还是从简出发,拷贝标准系统的有用文件,然后针对需要进行修修改改。我们试验系统将以多任务多用户环境使用,因此需要登陆密码,也有分组能力,所以需要passwd和group文件,如果系统使用shadow功能隐藏密码,那么还需要文件shadow。登陆首先执行init文件,它可需要不少配置文件呀。首当其冲的便是inittab文件,该文件规定了许多系统运行的基本功能(具体内容参见)。下一步init先执行rc.sysinit脚本来初始化系统,其中会使用到fstab配置文件,它包含了系统启动后挂载的文件系统和目录,对于我们试验系统来说只有两项一个是将/dev/ram作为根文件系统安装到/下,另一个是将proc文件系统安装到/proc目录下。Init执行完rc.sysinit后依照inittab中定义的运行级别进入对应的/etc/rc.d/执行其中S开头的服务运行脚本。不罗嗦了,细节内容别问我了,去看man init吧。你要做的就是把/etc/下initab,rc.sysinit和rc.d目录的所有东西拷贝到你对应系统里。我们实验系统运行级别为3,只启动网络服务服务,因此可以把除了S*network外的S脚本都删除。(当然你也可以改变系统默认的启动流程,让它执行你自己的初始化脚本,这点只需要载inittab中修改 “sysinit:XXX“中的脚本名称)。执行了上述初始化和服务后,系统最后运行rc.local文件,这里你可以放一些你希望开机执行的命令,我们这里放一句“ ok you are welcome !!!”为你进入系统前的问候。
另外要知道登陆是login往往要使用pam验证模块认证用户,所以pam的配置文件也最好拷贝到新系统。很多系统还会用到NSS(名称服务开关,这个服务来帮助客户机器或应用程序获得网络信息,可从本地或从网络某处取得——从DNS或NIS等。诸如getXbyY()等函数都往往会用到这种服务,用户登陆时login很可能就要使用,这取决于你libc的版本),所以/etc/nsswitch.conf需要拷贝,至于如何使用去看man nsswitch.conf吧。
剩下你还要靠被terminfo/termcap文件,它们对设置TERM终端环境变量有用。拷贝modules.conf,它包含了有关模块信息,我们实验系统中的modules.conf中仅仅给pcnet32.o 起了个别名而已。说的我口渴,不说了有什么疑问自己去找资料吧。
差不多完了,对了别忘了吧/root/目录下的那些.开头的用于bash配置的隐藏文件也考到新系统的如root下,这些都是bash的环境参数等东西。
结束动作。ldconfig –r workdir/rootfs(试验文件系统目录) 建立库文件路径缓存 ,从此命令再使用动态连接库时就不必指定目录了,因为它们的路径都被缓存了。(ldconfig 要用动态库配置文件ld.so.conf,试验系统中置空它好了)
安装根文件系统——内核和root文件系统绑定
别混淆,刚刚我们做的是文件系统应该包含的文件。具体文件系统现在才开始做。上面说了需要在系统未来的宿主盘上制作文件系统——进行格式化。如果你手头没有实际设备,Linux提供给你另外两种变通方法 : ramdisk和loop设备(回环设备) 。利用loop设备可以将文件虚拟成一个文件系统进行安装,而ramdisk则是将内存模拟一个块设备用来存放数据。
使用ramdisk或loop设备相比直接使用物理磁盘操作要快一些,也相对安全,不会损坏物理设备。因此在需要创建文件系统的情况下,很多时候都会使用上面两种虚拟技术创建文件系统,然后在将文件系统转移到物理设备中。
我们采取ramdisk作为文件系统的宿主,在上面制作文件系统,然后拷贝我们前面创建的文件系统内容到其上去。然后观察ramdisk的大小后 (可不是其中文件内容的大小,因为其中还包含文件系统本身格式的一些信息),将整个文件系统统转移到某个文件中去(利用dd命令,由于 dd 命令允许二进制方式读写,所以特别适合在原始物理设备上进行输入/输出,制作整个文件系统的镜像),该文件被称为文件系统镜像。虽然Linux对文件后缀没有要求,但这里我们还是习惯以img命名它。
具体做法大该如此:
dd if=/dev/zero of=/dev/ram bs=1k count=20000
mke2fs –m0 /dev/ram 20000
mount /dev/ram /mnt/
cp –av /rootfs/* /mnt/ram
运行df ,注意1k-blocks一栏中/dev/ram的数值,假定为ramsize
umount /dev/ram
dd if=/dev/ram of=ramlinux.img bs=1k count=ramsize
gzip –9v ramlinux.img
第一步是给 /dev/ram设备清出20M的全零空间,然后格式化/dev/ram设备,页就是将格式信心写入/dev/ram中。
接下来,安装/dev/ram设备到/mnt目录下,再把你创建的文件系统内容全部考进来。完成了这步,你才可以说真正有了一个文件系统(文件系统格式信息+文件系统内容)
然后解载设备后,把设备内容(包含文件系统格式和内容)统统转移到名为ramlinux.img的镜像文件中,
最后压缩镜像文件(压缩后名字为ramlinux.img.gz),开始使用/dev/zero清零/ram设备地目的就是为了提高压缩率,因为压缩算法利用统计规律替换字符,所以统一为零会大大提高gzip的压缩率的。
一般标准系统中ramdisk默认大小为4098字节,你不能建立超过该大小的ram盘。但我们搭建的系统大小超过了4096字节,所以必须扩大ramdisk的大小。最简单的方法是在lilo启动时给ramdisk指定大小,实现系统中大概用到20M大空间,所以在Lilo.conf中应该加入“append = “ramdisk_size=20000”这一行,系统启动时就会自动更改ramdisk默认大小了。
系统引导引导过程简述PC打开电源后,先执行ROM中BIOS中的代码,该程序负责将启动设备(软盘、硬盘、光盘)的第一个扇区(0扇区)第一个磁到道的数据载入内存。接着BIOS执行该扇区中的代码(将内核从启动设备中逐步导入到内存)。所以扇区中要么直接存放操作系统内核,要么存放启动装载程序,比如Lilo等,由启动装载程序负责找到内核,装载内核到系统,然后执行内核。
内核被载入内存后的动作上面已经初步介绍了,我们这里要强调的是内核初始化以后紧接着就需要安装根文件系统,那么根文件系统的位置如何确定?(ramdisk size?)
我们必须在创建过程中指定驱动设备,利用命令
rdev filename devicename 设置或在内核原代码目录下的 makefile中修改相关参数,然后编译,因为该信息是备记录在内核中的。
除了跟文件设备外还需要指出根文件系统在宿主设备上的位置,这还需要利用rdev 来实现。该信息也被记录在内核中。(rdev命令很丰富,回忆我们前面谈到的改变ram盘大小的任务都可以通过rdev来修改)
可能很多朋友奇怪自己根本没用过这个命令,这么多年还不照样把系统生级了无数次。的确我们不大使用该命令,因为我们有更酷的工具lilo(当然grub好像现在更流行了),在lilo.conf中的配置如root=* 这些选项其实就是告诉lilo将上述信息写道内核中。
确定了根文件系统位置,将其安装到根目录下,然后找到其中init程序,开始执行系统初始化工作。
安装启动镜像
大家多数都应该对 lilo.conf下的intrd=initrd.img.*有印象吧。你知道initrd.img是干嘛用的吗?
这个文件实际上就是个文件系统镜像,有兴趣的话你可以将它登陆到/mnt下,去看看,它毫无疑问是个微缩的文件系统(该文件使用gzip压缩的,所以先要解压才能安装它。(
mv initrd.img initrd.img.gz;gunzip initrd.img.gz;mount –o loop initrd.img /mnt)。这个文件里的各目录和我们文件系统是完全一样的,但是由于initrd.img是在系统启动后在Ram盘里运行的所以它只包含系统启动时需要的最小命令和库的集合。使用这个萎缩文件系统的目的通常是为了系统启动是尚未安装根文件系统前,用来运行系统以便利用insmod命令装入安装根文件系统需要的模块——比如ext3.o,Buslogic.o等(如果根设备是SCSI或根文件系统是EXT3等,而内核并为将这些功能编译进去,只能以模块方式载入),所以在initrd.img中的lib下会包含需要再入的模块。系统启动后运行intrd.img中的linuxrc脚本来执行模块载入后将根文件系统切换到实际文件系统中(使用pivot_root命令)。
对于我们实验系统来说,因为已经将SCSI和EXT3等模块直接编译进了内核,所以不必通过initrd.img的途径来进行先期模块载入。因此正常情况下initrd.img是不需要的。但是要知道我们制作的根文件系统镜像是放在源标准系统根文件系统下的。所以要使得系统拍托实际物理设备,进入ram盘工作运行,就需要利用intrd.img镜像文件系统在启动期间将物理盘上根文件系统镜像载入ram盘中,然后进入执行。这个工作我们利用linuxrc脚本来实现,具体地讲就是mount源根文件系统,将试验文件系统镜像解压传送到/dev/ram中,然后umount 源根文件系统。从此系统进入我们的实验文件系统开始运行。
Initrd.img也是属于文件系统镜像,它的制作方法和制作根文件系统大通小异。先拷贝需要文件,在编辑脚本(linuxrc),然后制作文件系统镜像。详细过程不再罗索了。
别着急,还有关键一步那就是修改lilo .conf 为实验系统配置启动选项。
boot =实验系统内核
label = ramlinux
initrd = 刚做的initrd镜像
root = /dev/ram
append = “Ramdisk_size = 20000”
最后,执行lilo –r /rootfs 。
ok !
有关内核引导请见附件。
下载搭建脚本和实验系统系统
虽然搭建系统技术简单,但是过程很繁琐,搞不好会丢三拉四,错误百出。为了节约大家的体力,我们编写了几个小脚本帮助搭建系统。利用这几个脚本大家可以轻松地自动建立实验系统。
我们的制作脚本可分为下面几个部分:
mkrootfs.sh—— 收集制作root文件系统所需的所有材料到指定目录。
mkinitrdfs.sh——收集制作initrd镜像所需的所有材料到指定目录。
setup.sh——制作root文件系统镜像和initrd镜像,改写lilo配置文件添加ramlinux启动选项。
连同脚本一同提供给你的还有myboot,myetc 和myroot目录。boot里含有编译好的内核(注意内核是与系统硬件相关的,我的机器是奔三处理器,如果你系统和我不同,那你还是自己在本机上编译试验系统内核吧!不过可以使用我们提供的内核配置文件MinSys.config来选择内核功能,编译完成把内核考贝到myboot下就可以了——或修改mkimage.sh脚本,在最后面的地方修改lilo.conf部分,将”boot=×××”中的XXX用你自己编译的内核代替)、模块和内核配置文件MiniSys.config。etc下包含了供试验系统使用的、已经修改好的配置文件和服务脚本。Boot下是两个bash的配置文件--全部脚本和必要配置文件打包为work.tar.gz。
此外,我们也将按上述方法裁减出来的root文件系统(rootfs.tar.gz)和root文件系统镜像ramlinux.img.gz放在网上以供下载,同时也把initrd的内容(initrdfs.tar.gz)和镜像(initrd.img.gz)放在网上。
如果你要添加或删除文件系统中的某些文件,应该展开roofs.tar.gz,然后在rootfs里面修改,不要把文件系统镜像文件(img文件)以loop方式安装后进行修改,因为我们曾经用zero设备填充过文件系统,所以如果新添加或删除某些内容,可能会破坏里面的一些数据对齐,系统反映给你的就可能会有类似于“bus error”等一类莫名奇妙的错误。
如果你按要求解开了rootfs.tar.gz和initrdfs.tar.gz,那么执行setup.sh既可生成对应的镜像文件ramlinux.img.gz和initrd.img.gz,并会在lilo中添加好对应于试验系统的选项。
Step by step——享受你的操作系统吧
从启动菜单中选择ramlinux开始运行吧!
等等,要输入管理员密码?是的,我们是从原有的系统中裁减的,所以密码自然会继承下来。(如果你用的是我做好的系统,那么用户名自然是root,密码 threeyear)
看见了吗,亲手打造的操作系统已经快步向你走来,有成就感吗?享受你的杰作吧!
附件——Linux系统启动的标准流程
系统的启动是指从计算机加电到显示用户登陆提示的整个过程。我们将在这里对整个流程以及关系到的一些内容做讨论。过程主要可以分为两个阶段:载入内核和准备运行环境,我们分别进行讨论。本部分的讨论只基于i386硬 件架构,但大部分内容是有共通性的。
图一 启动过程 综述
载入内核(将内核载入内存,并将控制权传递给它)
计算机加电到Boot Loader开始工作,硬件含量远大于软件含量,所以这里暂不提及,如果实在有关心的朋友,请先别着急,我们将在下期里讨论它。
这一阶段是 Boot Loader 的主战场。它必须将可执行的内核映像和内核启动所需的额外数据信息从存储介质上载入内存,这并不是件简单的工作,因为除了从硬盘载入,可能还会需要从网络引导服务器这样的外部介质上载入。各种纷繁芜杂的文件系统类型也给载入带来了巨大的挑战。
Boot Loader 可能还需要改变CPU的运行特权级别,然后就可以让内核投入运行了。
除此之外, Boot Loader 还要完成一些其它功能,比如从BIOS中获取系统信息,或者从启动时的命令行参数中提取信息等。有的 Boot Loader 还要扮演引导选择工具的角色,方便用户选择不同的操作系统。
Boot Loader的 职责:
判断到底要载 入什么,这可以要求用户进行选择
载入内核和它 可能需要用到的相关数据,比如initrd或者其它参数
为内核准备好 运行环境,比如,让CPU进入特权模式
让内核投入运 行
Boot Loader的 历史变迁:
早期的Linux只支持软盘引导扇区和 Shoelace 两种 Boot Loader。 Shoelace 是从Minix继承下来的、对文件系统相关的 Boot Loader。它只支持 Minix 文件系统。当时Linux只使用 Minix 一种文件系统,所以这样做并没什么问题。可是, Minix 文件系统存在不能保存创建、修改和访问时间信息;文件名长度限制在14个字节等问题。随着Linux的发展,这些与传统Unix文件系统大相径庭的缺陷越来越让人难以忍受,它已经不适合作为Linux的主要文件系统了。
为了支持其它文件系统的实现,Linux引入了VFS(虚拟文件系统)。这个举措很快就引起了热烈的反响,一大批新的文件系统实现出现了。其中一个 Minix 文件系统的变体,扩展文件系统 Xiafs (根据它的作者命名)突破了 Minix 文件系统的文件名长度限制,将此长度一举提高到全部30个字符。当时文件系统之间的竞争着实激烈,很难看出谁会胜出,甚至搞不清楚会不会有一个最终的“赢家”。
尽管不确定性很大,但是有一点却是清楚的:不管最后哪种文件系统会受到青睐,但是除了 Minix 作为根文件系统,谁也不能从硬盘上启动,因为 Shoelace 只支持Minix文件系统。LILO应运而生了。由于支持多种文件系统(当时内核支持的主流文件系统已经有 Minix ,扩展文件系统 ext , Xiafs 。还有人在移植 BSD 的 FFS ,根本看不出来什么时候是个尽头)在实现和维护上难度太大,而 Boot Loader 也不应该成为人们试验新的文件系统的绊脚石,所以LILO采取了和文件系统无关的设计。
这种设计经受住了时间的考验,被证明是非常成功的。即使在今天,LILO仍旧可以从内核支持的绝大部分文件系统的硬盘上启动。但是,由于ext2历经这么长的时间一直没有大的演变,成为了事实上的标准,所以跟文件系统相关的Boot Loader又渐渐流行了起来。
尽管ext2已经能满足大部分人的日常需要,但是文件系统的设计者们还是在研制以日志机制为特征的新的文件系统,并且已经取得了相当大的进展。考虑到当前又有可能出现多种文件系统的实现同时并存的情况,因此对与文件系统无关的Boot Loader的需求可能会再次变得强劲。
初始化基本的操作环境
一旦内核开始运行,它会初始化内部的数据结构,检测硬件,并且激活相应的驱动程序,为应用软件的准备运行环境。其间包含一个重要操作——应用软件的运行环境必须要有一个文件系统,所以内核必须首先装载root文件系统。由于我们的目的是介绍基本流程,所以相关的硬件初始化细节就不再讨论,相关内容在下一期杂志中会有详细介绍。
硬件初始化完成后,内核着手创建第一个进程——初始进程。说是创建,其实也不尽然,该进程其实是整个硬件上电初始化过程的延续,只不过执行到这里,进程的逻辑已经完备,所以我们就按照进程的创建方式给它进行了“规格化” ——我们把这个初始进程也叫做“硬件进程”,它会占据进程描述符表的第一个位置,所以可以用task[0] 或INIT_TASK表示。该进程进而会再创建一个新进程去执行init()函数,其实,这个新进程才是系统第一个实际有用的进程,它会负责接着执行下一个阶段的初始化操作;而初始进程(INIT_TASK)自己则会开始执行idle循环,也就是说,内核初始化完成之后,初始进程唯一的任务就是在没有任何其它进程需要执行的时候,消耗空闲的CPU时 间(因此初始进程也被称为idle进程)。
下一阶段的初始化工作要比前一阶段轻松一点,因为现在是由一个真正进程接手负责完成它们了,而前一阶段都是由“硬件进程”手工去做的。在此阶段,这个由INIT_TASK创建的新进程需要初始化总线、网络并启动系统中的各种系统内核后台线程,然后再初始化外设、设置文件格式,在这之后,它要为进入系统做最后的准备——初始化文件系统,安装root文件系统,打开/dev/console设备,重定向stdin、stdout和stderr到控制台,然后搜索文件系统中的init程序,并使用 execve()系统调用加载执行init程 序。系统自此进入了用户态。
装载root文件系统
为了装载文件系统,内核需要:1知道root文件系统位于那个存储介质上;2有访问该种介质的驱动程序。最常见的情况是root是ext2文件系统,位于IDE硬盘上。这种情况下需要的操作很简单:将设备号作为参数给内核就可以了,IDE的设备驱动程序通常都会编译进内核的。
如果内核没有相关介质的驱动程序,问题就会变得更为复杂。而这种情况并不罕见,比如Linux的安装盘使用的“通用”内核一般都会碰到。如果内核把所有支持的硬件的设备驱动程序都包含进来,就会变成一个庞然大物;而且一些驱动程序在检测硬件的时候会影响其它设备。
这个问题可以通过initrd机制解决,它允许在装载实际的root文件系统之前先使用RAM文件系统。除了上述两个原因,引入initrd还可以解决内核的动态合成问题。(详见参考资料一。)
不过我们应该注意到,init在整个启动过程中并不是从来就有的,它可以说是一个插件,为了解决以上问题,而被加入启动过程,象图一所示,Linux系统在启动时也可以不选择它。
为什么要引入initrd?
Linux启动过程中肯定要载入内核镜像,在此过程中有些要素必须考虑:
首先,内核镜像不能太大。由于受到各种硬件和兼容性的限制,Linux的内核镜像不能太大,但是这并不容易做到。Linux内核的核心部分本身就不小了;而且还必须加入会使用到的驱动程序。
其次,要支持尽可能多的硬件设备。我们在启动过程中有一件重要工作:挂载root文件系统,因为进一步的数据和应用软件都在其上,所以我们的内核必须能够访问root文件系统。对于一般用户,如果他们使用IDE硬盘上的ext2文件分区作为root文件系统,不会有什么问题。因为不管是IDE硬盘还是ext2文件系统,它们的驱动肯定会包含在内核镜像自身里面。但是,确实存在一些特殊情况:比如说我们希望发行Linux系 统的安装光盘,那么对光盘的驱动,就不一定包含在内核里面了。(有人可能要奇怪了,咦,光盘中的内核镜像不都已经读进来了吗,怎么内核还访问不了光盘呢?注意,读入内核镜像的是 Boot Loader ,内核并不具备 Boot Loader 的功能。)如果没有光盘的驱动,我们又怎么把光盘里的软件包安装到用户的计算机里呢?把驱动程序预先编译到内核里?听起来还不错,可是如果我们除了光盘还有一些其它的安装介质,那么所有这些驱动就会让内核镜像庞大不堪。
而且,还有更严重的问题,各种不同的驱动程序很有可能会发生冲突,特别是以前ISA设备占市场主导地位的时候,这种冲突简直难以避免。
那时的解决办法是发行商提供预先编译好的支持各种设备的不同内核,把每个内核放进一张软盘,随发行包一起交给用户,用户自己选择装有合适内核的软盘进行引导。或者给用户提供制作引导盘的工具,让用户在安装前制作自己的启动盘。当然,哪一种办法都不能让人满意。
唯一的希望在于使用模块化机制。在内核启动的时候调用相应的模块加载驱动程序,然后访问root文件系统。无论是通过内核对设备做进一步的分析还是直接从用户那里得到配置信息,先配置再加载模块的办法,都能有效地避免冲突的发生。
除了在安装的时候需要在挂载root文件系统之前调用相应的模块之外,在完成安装的系统上,我们可能仍然需要在挂载root文件系统之前调用一些模块。这主要是为给计算机进行配置——一般都要针对不同的计算机进行内核配置。
理想情况下,用户按照自己的实际情况配置编译文件,重新编译内核,一步步完成这种工作。但是没有几个用户喜欢这种冗长并且极易出现错误的工作。而且编译和生成内核需要相应的工具,可是大部分用户不需要这些工具。
在安装的过程中可以直接编译一个整体式的内核,但这并不能很好的解决问题:首先,所有的编译工具还需要,其次,编译过程中出现差错导致无法完成任务的概率太大了。所以,我们仍然要使用模块机制:模块机制很可靠,出了错误也只不过不加载对应的模块而已,不会使整个任务失败。而载入模块,象前面说的,也是在挂载root文件系统之前就要得到模块的。
基于以上理由,Linux引入了initrd机制。
initrd做什么
initrd允许系统在启动的时候载入一个RAM盘,这个RAM盘可以被当作一个root文件系统,程序可以在其上运行。(有两重含义,第一,程序在上面;第二,程序的文件系统环境也在上面。)在此之后,可以从别的设备上挂载一个新的root文件系统,先前的root文件系统(initrd)就会被移动到一个目录上去,最终被卸载掉。
为什么要使用RAM盘呢?首先,使用RAM盘能方便的支持以后可能发生的变化;另外,也是为了保持 Boot Loader 工作尽可能的简单。在系统引导时,除了内核镜像之外,Boot Loader把所有相关的信息作为一个文件读入内存,内核在启动中将该文件作为一段连续的内存块看待。也就是把它当作RAM盘来 使用了。正因为如此,这种机制被称作“初始 RAM 盘 (initial RAM Disk)”,缩 写成 initrd。
initrd主要用来把系统的启动划分为两个阶段:初始启动的内核只需保留最精简的驱动程序最小集,此后,在启动必须加载附加的模块时,从initrd中加载。
initrd进行的操作
使用initrd的时候,典型的系统启动的流程变为:
Boot Loader读入内核镜像以及initrd文件
内核将initrd文件转成“普通”的RAM盘,并且释放掉initrd文件占用的内存。
initrd被当作root文件系统,以可读可写(read-write)方式安装。
/linuxrc被执行(它可以是任何可执行文件,包括脚本在内;它以uid0身份执行,基本上能完成所有init程序可以做的工作)
linuxrc安装“实际” 的root文件系统
linuxrc通过pivot_root系统调用将root文件系统放置在root目录下。
常用的启动流 程(比如调用/sbin/init)开始执行。
卸载initrd文件系统。
注意,这是一个典型流程。其实initrd机制可以通过两种方式使用:要么就是作为一个普通的root文件系统使用,这样的话第5、第6两个步骤可以被略过,直接执行/sbin/init(我 们的试验系统就是利用这种方法);要么作为一个过渡环境使用,通过它内核可以继续装载“实际”的root文件系统