|
作者:empty 页数:1575 出版社:empty |
第1章预备知识
1.1Linux内核简介
在计算机技术的发展史上, Unix操作系统的出现是一个重要的里程碑, 早期的Unix曾免费供美国及一些西方国家的大学和科研机构使用,并凡提供源代码,这一方面为高校和科研机构普及使用计算机提供了条件;另一方面,也是更重要的,为计算机软件的核心技术“操作系统”的教学和实验提供了条件。特别是Unix内核第6版的源代码, 在相当长的段时期内是大学计算机系高年级学生和研究生使用的教材, 甚至可以说, 美国当时整整一代的计算机专业人员都是读着Unix的源代码成长的。反过来, 这也促进了Unix的普及和发展, 并且在当时形成了一个Unix产业。事实上, 顾硅谷的形成和发展, 也可以看到Unix起着重要的作用。Unix两大主流之一的BSD就是在加州大学伯克利分校开发的。后来, Unix成了商品, 其源代码也受到了版权的保护, 再说也日益复杂和庞大了, 而第6版则又慢慢显得陈旧了, 便逐渐不再用Unix内核的源代码作为教材了(但是直到现在还有在用的) ,在这种情况下, 出于教学的需要, 荷兰的著名教授AndrewS.Tane nba un编写了一个小型的“类Unix”操作系统Minix, 在PC机上运行, 其源代码在20世纪80年代后期和90年代前期曾被广泛采用, 但是, Minix虽说是“类Unix“, 其实离Unix相当远。首先, Minix是个所谓“微内核”, 与Unix内核属于不同的设计, 功能上更是不可同日而语。再说Unix也不仅仅是内核, 还包括了其“外壳”Shell和许多工具性的“实用程序”, 如果内核提供的支持不完整, 就不能与这些成分结合起来形成Unix环境, 这样, Minix虽然不失为一个不错的教学工具, 却缺乏实用价值, 看到Minix的这个缺点, 当时的一个芬兰学生Linus Torvalds就萌生了一个念头, 即组织一些人, 以Minix为起点, 基本上按照Unix的设计, 并凡博采各种版木之长, 在PC机上实现, 开发出一个真正可以实用的Unix内核。这样, 公众就既有免费的(现代) Unix系统, 又有系统的源代码, 且不存在版权问题。可是, Tanenbaum教授的日光却完全在教学上,因此并不认为这是一个好主意,没有采纳这个建议。
起来。由于所实现的基本上是Unix, Linus Torvalds就把它称为Linux.那时候互联网虽然还不像现在这么晋及, 但是在大学和公司中已经用得很多了, Linus Torvalds在基木完成了Linux内核的第一个版毕竟是“初生牛哦不怕虎”, 加上自身的天赋和勤奋, 还有公益心, Linus Torvalds就自己动手丁了本以后就把它放在了互联网上,一来是把自己写的代码公诸于众,二来是邀请有兴趣的人也来参与,他的这种做法很快便引起了热烈的反应, 并且与美国“自由软件基金会”FSF的主张正好不谋而合,当时FSF已经有计划要开发一个类Unix(但又不是Unix, 所以称为GNU, 这是 Gnu is Not Unix”的缩写) 的操作系统和应用环境, 而Linux的出现正是适得其时, 适得其所, 于是, 由Linus Torvalds主持的Linux内核的开发、改进与维护, 就成了FSF的主要项目之一。同时, FSF的其它项目, 如GNU的C编译gee、程序调试工具gdb, 还有各种Shell和实用程序, 乃至Web服务器Apache、浏览器Mozilla(实际上就是Netscape) 等等, 则正好与之配套成龙。人们普遍认为自由软件的开发是软件领域巾的一个奇迹。这么多志愿者参与,只是通过互联网维持松散的组织,居然能有条不素地互相配合,开发出高质量的而且又是难度较大的系统软件,实在令人赞叹,
那么, Linux与它的前身Minix的区别何在呢?简单地说, Minix是个“微内核“, 而Linux是个“宏内核 :Minix是个类Uni的教学用模型, 而Linux其本上就是Unx, 而且是Unix的延续和发展, 其至是各种Unix版木与变种的集大成者,大家知道,传统意义下的操作系统,其内核应具备多个方面的功能或成分,既包含用于管理属于应用层的“进程”的成分,如进程管理,也包含为这些进程提供各种服务的成分,如进程间通信,设备驱动和文件系统等等,内核中提供各种服务的成分与使用这些服务的进程之间实际上就形成一种典型的 Client Server”的关系.其实, 这些服务提供者并不一定非得都留在内核中不可, 他们本身也可以被设计并实现某些“服务进程”,其中必须要解在内核中的成分其实只有进程间通信。如果把这些服务提供者从内核转移到进程的层次上,那么内核本身的结构就可以大大减小和简化。而各个服务进程,既然已从内核中新离出来,便可以单独地设计,实现以及调试,更重要的是可以按实际的需要来配置和启动, 基于这样的想法, 各种“微内核“(Micro*Kernel) 便应运而生, 特别是对于一些专用的系统,主要是实时系统和“嵌入式”系统(Embedded System) , 微内核的思想就很有吸引力, 究其原因, 主要是因为通常这些系统都不带磁盘, 整个系统都必须放在EPROM中, 常常受到存储空间的限制, 而所需要的服务又比较单一和简单, 所以, 几乎所有的嵌入式系统和实时系统都采用微内核, 如PSOS,VxWorks等。当然, 微内核也有缺点, 将这些服务的提供都放在进程层次上, 再通过进程间通信(通常是报文传递)提供服务,势必增加系统的运行开销,降低了效率,在Linux里, 这个问题就解决得比较好.Linux既允许把设备驱动程序在编译时静态地连接在内核Linux最初是在Intel 80386 平台”上实现的, 但是已经被移植到各种主要的CPU系列上, 包括与微内核相对成, 传统的内核结构就称为“宏内核”(Macro+Kernel) , 或称为“一体化内核”(Monolithic Kern eD。通用式的系统由于所需的服务面广而量大, 一体化内核就更为合适, 作为一种通用式系统, Linux采用一体化内核是很自然的事,传统的Unix内核是“全封闭”的。
如果要往内核中加个设备(增加种服务) , 早期一般的做法是编写这个设备的驱动程序,开变动内核源程序中的某些数据结构(设备表),再重新编译整个内核,并重新引导整个系统.这样做当然也有好处,如系统的安全性更能得到保证,但其缺点也是很明显的,那就是太僵化了。在这样的情况下,当某一个公司开发出一种新的外部设备时(比方说,一台彩色扫描仪) , 它就不可能随同这新的设备提供一片软盘或光盘给用户, 使得用户只要运行一下 setup”就可以把这设备安装上了(像对DOS/Windows那样) , 有能力修改Linux内核的设备表, 并重新编译内核的用户毕竟不多,中,一如传统的驱动程序那样;也允许动态地在运行时安装,称为“模块”;还允许在运行状态下当需要用到某一模块时由系统自动安装,这样的模块仍然在内核中运行,而不是像在微内核中那样作为单独的进程运行,所以其运行效率还是得到保证。模块,也就是动态安装的设备驱动程序的实现(详见设备驱动程序一章) , 是很大的改进, 它使Linux设备驱动程序的设计、实现、调试以及发布都大大地简化,其至可以说是发生了根本性地变化。Alpha、M68K、MIPS、SPARC、PowerPC等等(Pentium、PentiumII等等均属于i 386系列) 。可以说Linux内核是现今覆盖面最广的体化内核。同时, 在同一个系列的CPU上, Linux内核还支持不同的系统结构, 它既支持常规的单CPU结构, 也支持多CPU结构。不过, 本书将专注于i386CPU并且以单CPU结构为主, 但是最后有一章专门讨论多CPU结构。
在安装好的Linux系统中, 内核的源代码位于/ust/s re/linux。如果是从GNU网站下载的Linux内核lins x分利色含与系选到导、内存管理,系统调用的进入和返母、中断处理以值得一提的是, Linux的源代码看似庞大, 其实对于每一个具体的内核而言井不是所有的c和h文Wam ning:The document was created wth Spe.PDF for.NET.在结束本节之前, 还要介绍一下有关Linux内核版本的一些规定。
通常, 在说到Linux时, 是指它的内核加上运行在内核之上的各种管理程序和应用程序。严格地Linux内核的版本在发行上有自己的规则, 可以从其版本号加以识别。版本号的格式为 x.yy.zz”出于Linux源代码的开放性, 公众随吋都可以从网上下载最新的版本, 包括还在开发中、尚未稳Linux的内核基本上只有一种来源, 那就是出Linus主持开发和维护的内核版本。但是有很多公司对于大多数用户,由发行商提供的这些发行版起着十分重要的作用,让用户自行配置和生成整个的tar文件, 则展开以后在一个叫linux的子目录中。以后本书中谈到源文件的路径时, 就总是从linux这个节点开始.Linux源代码的组成, 大体如下所示,件都会用到,而是在编译(包括连接)时根据系统的配置有选择地使用.例如,虽然源代码中包含了用来支持各种不同CPU的代码, 但编译以后每一个具体的内核都只是针对一种特定CPU的。再如,在net子目录下包含了各种网卡的驱动程序, 但实际上通常只会用到一种网卡, 而且各种网卡的驱动程序实际上大同小异。
说, 内核只是操作系统的一部分, 即其核心部分, 但是, 人们往往把Linux的内核就称为Linux, 所以在讲到Linux时有时候是指整个操作系统, 有时候则是指其内核, 安根据上下文加以区分。在本书中,如无特别说明, 则Linux通常是指其内核。其中x介于0到9之间,而yy、zz则介于0到99之间.通常数字愈高便说明版本愈新。一些版本号后面有时会见到pNN的字样, NN是介于0到20之间的数字。它代表对某一版的内核“打补丁”或修订的次数。如0.99p15,代表这是对版本0.99的内核的第15次修订。定、因而还不能发行的阪本,因此,需要有一套编号的方案,使用户看到一个具体的版本号就可以知道是属于“发行版”还是“开发版”, 所以Linux内核的版本编号是有规则的。在版本号x.yyz z中, x的不同号码标志着内核在设计上或实现上的重大改变,yy一方面表示版木的变迁,一方面标志着版木的种类,即“发行版”或“开发版”如果yy为偶数便表示是一个相对稳定、已经发行的版本;若为奇数则表示还在开发中,目前还不太稳定、或者在运行中可能出现比较大的问题的版本,开发中的版本一旦通过测试以及试运行,证明已经稳定下来,就可能会发布一个yy的值为偶数的发行版。之后,开发者们又将创建下一个新的开发版本,但是有时候也会在历经了几个开发版以后才发布一个发行版.至于zz,则代表着在内核增加的内容不是很多、改动不是很大时的变迁,只能算是同一个版木。例如,版本巾2.0.34升级到2.0.35只意味着版本2.0.34中的一些小缺陷被修复,或者代码有了一些小的改变,“发行版”和“开发版”的zz是独立编号的,因此并没有固定的对应关系。例如,当开发版2.3的版本号达到2.3.99时,相应的发行版还只是2.2.18。
Linux内核的0.0.2版在1991年首次公开发行, 2.2版在1999年1月发行.Linux内核的改进是相当频繁的,几乎每个月都在变。木书最初采用的是2.3.28版,最后成书付印时则以正式发行的2.4.0版为依据,在发行Linux操作系统不同的发行版(distribution) , 如RedHat Caldera等等。虽然不同的发行版本中所采用的内核在版本上有所不同,但其来源基本一致.各发行版的不同之处一般表现在安装程序、安装界面、软件包的多少、软件包的安装和管理方式等方面,在特殊情况下也有对内核代码稍作修改的(如汉化)。不同的发行版由不同的发行商提供服务,不同的发行商对自己所发行版本的定位也有不同, 各厂商所能提供的售后服务、技术支持也各不相同。由此可见, 原则上全世界只有个Linux, 所谓“某某Linux”只是它的一种发行版本或修订版本, 另外, 不要把Linux内核的版本与发行商自己的版本(如”RedHat 6.0 ) 混淆, 例如, Caldera 2.2版的内核是2.2.5版.系统是相当困难的, 因为那样用户不但要自己下载内核源程序, 自己编译安装, 还要从不同的FTP站点下载各种自出软件添加到自己的系统中,还要为系统加入各种有用的工具,等等。而所有这些工作都是很费时费力的事情。Linux的发行) 商正是看到了这一点, 替用户做了这些工作, 在内核之上集成了大量的应用软件.并且,为了安装软件,发行厂商同时还提供了用于软件安装的工具性软件,以利
手用户安装管理。由于组织新的发行版时并没有一个统的标准,所以不同厂商的发行版各有特点也各有不足。Linux内核的终极的来源虽然只有一个, 但是可以为其改进和发展作出贡献的志愿者人数却并无限制,同时,考虑到一些特殊的应用,一些开发商或机构往往对内核加以修改和补充,形成一些针对特殊环境或要求的变种。例如, 针对“嵌入式”系统的要求, 有人就开发出Embedded Linux:针对有“硬实时”要求的系统, 有人就开发了RTLinux:针对于持式计算机的要求, 有人就开发出了Baby Linux:等等。当然, 中文Linux也是其中的一类。每当有新的Linux内核版本发布时, 这些变种版本通常也很快就会推出相应的新版本.根据FSF对白由软件版权的规定(GPL) , 这些变种版本对内核的修改与补充必须公开源代码。
Linux内核源代码, 是有版权保护的, 只不过这版权归公众(或者说全人类) 所有, 出自出软件基金会FSF管理。FSF为所有的GNU软件制定了一个公用许可证制度, 称为GPL(General Public License) ,也叫Copyleft, 这是与通常所讲的版权即Copy r ght截然不同的制度.Copyright即通常意义下的版权,保护作者对其作品及其衔生品的独古权, 而Copyleft则允许用户对作品进行复制、修改, 但要求用户许多人以为, 既然Linux是免费的公开软件, 那就无所谓版权的问题了。其实不然。Linux以及承担GPL规定的一些义务。按GPL规定, 允许任何人免费地使用GNU软件, 并且可以用GNU软件的源代码重构可执行代码。进一步, GPL还允许任何人免费地取得GNU软件及其源代码, 并且再加以发布甚至出售, 但必须要符合GPL的某些条款。简而言之, 这些条款规定GNU软件以及在GNU软件的基础加以修改而成的软件, 在发布(或转让、出售) 时必须要申明该软件出白GNU(或者源自GNU) , 井日必须要保证让接收者能够共享源代码, 能从源代码重构可执行代码。换言之, 如果一个软件是在GNU源代码的基础加以修改、扩充而来的, 那么这个软件的源代码就也必须对使用者公开(注意产品的出售与源代码的公开并不定相矛盾).通过这样的途径,自由软件的阵容就会像滚雪球样越滚越大。不过, 如果一个软件只是通过某个GNU软件的用户界面(APT) 使用该软件, 则不受GPL条款的约味或限制。总之, GPL的主要目标是:使白由软件及其衍生产品继续保持开放状态, 从整体上促进软件的共享和重复使用, 具体到Linux的内核来说, 如果你对内核源代码的某些部分作了修改,或者在你的程序中引用了Linux内核中的某些段落, 你就必须加以申明并且公开你的源代码。但是,如果你开发了个用户程序,只是通过系统调用的界面使用内核,则你自己拥有完全的知识产权,不受GPL条款的限制。
说到高尚, 此处顺便多说几句。美国管经出过两木很有些影响的书, 一木叫Undocumented DOS,另一本叫Undocumented Windows, 两本书均被列入DOS/Windows系统程序员的必备工具书。在这两本书中, 作者们(Andrew Schulman.David Maxey以及Matt Pict rck等)列举了经过他们辛勤努力才破译和总结出来的DOS/Windows API(应用程序设计界面) 实际上提供了但却没有列入Microsoft技术资料的许多有用(而且重要) 的功能。作者们认为, Microsoft没有将这些功能收入其技术资料的原因是无法用疏忽或遗漏加以解释的, 而只能是故意向用户隐瞒。Microsoft既是操作系统的提供者, 同时又是一个应用程序的开发商,通过向其它的应用程序开发商隐瞒一些操作系统界面上的技术关键,就使那些开发商无法与Microsoft公平竞争, 从而使Microsoft可以通过对关键技术的午断达到对DOS/Windows应用软件市场的垄断。作者们在书中指责Microsoft这样做不仅有道德上的问题, 也有法律上的问题。是古涉及法律问题姑且不论,书中所列的功能确实都是存在的,可以通过实验证实,也确实没有写入Microsoft向客户提供的技术资料。
要是将FSF与Microsoft放在一起, 则二者恰好成为鲜明的对比。差别之人, 读者不难做出自己的结论.GPL的正文包含在一个叫COPYING的文件中。在通过光盘安装的Linux系统中, 该文件的路径名为/usr/scr linux/COPYING。而在下载的Linux内核tar文件中, 经过解压后该文件在顶层目录中, 有兴趣或有需要的读者可以(而且应该)仔细阅读
1.2Intel X86CPU系列的寻址方式
Intel可以说是资格最老的微处理器芯片制造商了, 历史上的第个微处理芯片4004就是Intel制造的。所谓X 86系列, 是指Intel从16位微处理器8086开始的整个CPU芯片系列, 系列中的每种型号都保持与以前的各种型号兼容,主要有8086、8088、80186、80286、80386、80486以及以后各种型号的Pentium芯片。自从IBM选择8088用于PC个人计算机以后, X 86系列的发展就与IBM PC及其放容机的发展休戚相关了。其中80186并不广为人知, 就与IBM当初决定停止在PC机中使用80186有关。限于篇幅, 本书不对这个系列的系统结构作全面的介绍, 而只是结合Linux内核的存储管理对其寻址方式作一些简要的说明
在X86系列中,8086和8088是16位处理器,而从80386开始为32位处理器:80286则是该系列从8088到80386,也就是从16位到32位过渡时的一个中间步骤。80286虽然仍是16位处理器,但是在寻址方式上开始了从“实地址模式”到“保护模式”的过渡。当我们说个CPU是16位”或32位”时, 指的是处理器中“算术逻辑单元”(ALU) 的宽度。系统总线中的数据线部分, 称为“数据总线”, 通常与ALU具有相同的宽度(但有例外) .那么“地址总线”的宽度呢?最自然的地址总线宽度是与数据总线一致。这是因为从程序设计的角度来说,一个地址, 也就是一个指针, 最好是与一个整数的长度一致。但是, 如果从8位CPU寻址能力的角度来考虑,则这实际上是不现实的,因为一个8位的地址只能用来寻访256个不同的地址单元,这显然太小了,所以,般8位CPU的地址总线都是16位的。这也造成了些8位CPU在内部结构上的些不均匀性, 在8位CPU的指令系统中常常会发现一些实际上是16位的操作。当CPU的技术从8位发展到16位的时候,本来地址总线的宽度是可以跟数据总线一致了,但是当时人们已经觉得由16位地址所决定的地址空间(64K)还是太小,还应该加大.加到多大呢?结合当时人们所能看到的微型机的应用前景, 以及存储器芯片的价格, Intel决定采用IM, 也就是说64K的16倍, 那时觉得应该是足够了。确实,IM字节的内存空间在当时已经很使些程序员激动不已了,那时候配置齐全的小型机,甚至大型机也只不过是4M字节的内存空间。在计算机的发展史上,几乎每一个技术决策,往往很快就被事后出现的事实证明是估计不足的。
既然Intel决定了在其16位CPU, 即8086中采用IM字节的内存地址空间, 地址总线的宽度也就相应地确定了, 那就是20位。这样, 一个问题就摆在了Intel的设计人员面前:虽然地址总线的宽度是20位, 但CPU中ALU的宽度却具有16位, 也就是说可直接加以运算的指针的长度是16位的。如何来填补这个空隙呢?可能的解决方案当然有很多种, 例如, 可以像在一些8位CPU中那样, 增设一些20位的指令专用丁地址运算和操作, 但是那样又会造成CPU内部结构的不均匀性, 再如, 当时的
显然,在实地址模式上是无法建造起现代意义上的“操作系统”的,Warning:The document was created wth Spe.PDF for.NET.指令中的地址在数据段。这一点与实地址模式相同。(2)根据段寄存器的内容,找到相应的“地址段描述结构”.(3)从地址段描述结构中得到基地址。(6)将指令中发出的地址作为位移,与基地相加而得出实际的“物理地址”,PDP·11小型机也是16位的, 但是结合其MMU(内存管理单元) 可以将16位的地址映射到24位的地址空间, 结果, Intel设计了一种在当时看来还不失巧妙的方法, 即分段的方法。Intel在8086CPU中设置了四个“段寄存器”:CS、DS、SS和ES.分别用于可执行代码即指令、数据、堆栈和其它。每个段寄存器都是16位的,对应于地址总线中的高16位。每条“访内”指令中的“内部地址”都是16位的, 但是在送上地址总线之前都在CPU内部自动地与某个段寄存器中的内容相加,形成个20位的实际地址。这样,就实现了从16位内部地址到20位实际地址的转换,或者“射”。这里要注意段寄存器中的内容对应于20位地址总线中的高16位,所以在相加时实际上是拿内部地址中的高12位与段寄存器中的16位相加,而内部地址中的低4位保留不变。这个方法与操作系统理论中的“段式内存管理”相似,但并不完全一样,主要是没有地址空间的保护机制。对于每一个由段寄存器的内容确定的“基地址”,一个进程总是能够访问从此开始的64K字节的连续地址空间,而无法加以限制。同时,可以用来改变段寄存器内容的指令也不是什么“特权指令”,也就是说,通过改变段寄存器的内容,一个进程可以随心所欲地访问内存中的任何一个单元,而丝毫不受到限制。不能对一个进程的内存访问加以限制, 也就谈不上对其它进程以及系统本身的保护.与此相应, 一个CPU如果缺乏对内存访问的限制,或者说保护,就谈不上什么内存管理,也就谈不上是现代意义上的中央处理器,由于8086的这种内存寻址方式缺乏对内存空间的保护,所以为了区别于后米出现的“保护模式”,就称为“实地址模式”针对8086的这种缺陷, Intel从80286开始实现其“保护模式”(Protected Mode, 但是早期的80286只能从实地址模式转入保护模式,却不能从保护模式转回实地址模式)。同时,不久以后32位的80386CPU也开发成功了, 这样, 从8088/8086到80386就完成了一次从比较原始的16位CPU到现代的32位CPU的飞跃, 而80286则变成这次飞跃的个中间步骤, 从80386以后, Intel的CPU历经80486、Pentium、PentiumⅡ等等型号, 虽然在速度上提高了好几个量级, 功能上也有了不小的改进, 但基本上属于同一种系统结构中的改进与加强, 而并无重大的质的改变, 所以统称为i 386结构.或i386CPU.下面我们将以80386为背景,介绍i386系列的保护模式。
80386是个32位CPU, 也就是说它的ALU数据总线是32位的。我们在前面说过, 最自然的地址总线宽度是与数据.总线致。当地址总线的宽度达到32位时,其寻址能力达到了4G(4千兆),对于内存来说似乎是足够了。所以, 如果新设计一个32位CPU的话, 其结构应该是可以做到很简洁、很白然的.但是,80386却无法做到这一点。作为一个产品系列中的一员,80386必须维持那些段寄存器.还必须支持实地址模式,与此同时又要能支持保护模式。而保护模式是完全另搞一套,还是建立在段寄存器的基础上以保持风格上的一致, 并且还能节约CPU的内部资源呢?这对于Intel的设计人员无疑又是一次挑战。Intel选择了在段寄存器的基础上构筑保护模式的构思, 并且保留段寄存器为16位(这样才可以利用原有的四个段寄存器),但是却又增添了两个段寄存器FS和GS.为了实现保护模式,光是用段寄存器来确定一个基地址是不够的,至少还得要有一个地址段的长度,并且还需要一些其它的信息,如访问权限之类.所以, 这里需要的是一个数据结构, 而并非一个单纯的基地址, 对此, Intel设计人员的基本思路是:在保护模式下改变段寄存器的功能,使其从个单纯的基地址(变相的基地址)变成指向这样一个数据结构的指针。这样, 当一条访内存指令发出一个内存地址时, CPU就可以这样来归纳出实际上应该放上数据总线的地址:(1)根据指令的性质来确定应该使用哪个段寄存器,例如转移指令中的地址在代码段,而取数(4)将指令中发出的地址作为位移,与段描述结构中规定的段长度相比,看着是否越界。(5)根据指令的性质和段描述符叶的访问权限来确定是否越权。在运行时则使用其在CPU中的“影子”。从“保护”的角度考虑, 在由(指令给出的) 内部地址(或虽然段描述结构存储在内存中, 在实际使用时却将其装载入CPU中的一组“影了”结构, 而CPU者说“逻辑地址“)转换成物理地址的过程中,必须要在某个环节上对访问权限进行比对,以防止不其备特权的用户程序通过玩弄某些诡计(例如修改段寄存器的内容,修改段描述结构的内容等),得以非法访问其它进程的空间或系统空间,的实际实现,table register) , 另一个是局部性的段描述表寄存器L DTR(local descriptor table register) , 分别可以用来指向存储在内存中的一个段描述结构数组,或者称为段描述表,由于这两个寄存器是新增设的,不存在与原有的指令是否兼容的问题,访问这两个寄存器的专用指令便设计成“特权指令”.明白了这个思路,80386的段式内存管理机制就比较睿易理解了(还是很复杂).下面就是此机制首先, 在80386CPU中增设了两个寄存器:一个是全局性的段描述表寄存器G DTR(global deser ipt or在此基础上,段寄存器的高13位(低3位另作它用)用作访问段描述表中具体描述结构的下标(index) , 如图1.1所示G DTR或L DTR中的段描述表指针和段寄存器中给出的下标结合在一起, 才决定了具体的段描述表项在内存中的什么地方, 也可以理解成, 将段寄存器内容的低3位屏蔽掉以后与G DTR或L DTR中的基地址相加得到描述表项的起始地址,因此就无法通过修改描还表项的内容来玩弄诡计,从而起到保护的作用,每个段描述表项的大小是8个字节,每个描述表项含有段的基地址和段的大小,再加上其它一些信息
以这里的位段type为例, “:4”表示其宽度为4位。整个数据结构的大小为64位, 即8个字节。读者一定会问:为什么把段描述项定义成这样一种奇怪的结构?例如,为什么基地址的高8位和低24位不连在一起?最自然也最合理的解释就是:开始时Intel的意图是24位地址空间, 后来又改成32位地址空间,这也可以从段长度字段也是拆成两节得到印证:当g标志位为1时,长度的单位为4KB.而段长度字段的低16位的容量是64K,所以一个段的最人可能长度为64Kx4K=256M,而这正是24位地址空间的大小。所以, 可以看出, Intel起先意欲使用24位地址室间, 不久又认识到应该用32位,但是80286已经发售出去了, 于是就只好修修补补。当时的Intel确实给人一种“小脚女人走路”的感觉,每当个段寄存器的内容改变时(通过MOV、POP等指令或发生中断等事件) , CPU就把由这段寄存器的新内容所决定的段描述项装入CPU内部的一个“影子”描述项, 这样, CPU中有几个段寄存器就有几个影子描述项,所以也可以看作是对段寄存器的扩充。扩充后的段寄存器分成两部分,一部分是可见的(对程序而言),还与原先的段寄存器一样;另一部分是不可见的,就是用来存放影子描述项的空间, 这一部分是专供CPU内部使用的。在80386的段式内存管理的基础上,如果把每个段寄存器都指向同个描述项,而在该描述项中则将基地址设成0,并将段长度设成最人,这样便形成一个从0开始覆盖整个32位地址空间的一个整段, 由于基地址为0, 此时的物理地址与逻辑地址相同, CPU放到地址总线上去的地址就是在指令中给出的地址。这样的地址有别于由“段寄存器/位移量”构成的“层次式”地址, 所以Intel称其为“平面(Flat) ”地址。Linux内核的源代码(更确切地应该说是gcc) 采用平面地址。这里要指出, 平面地址的使用并不意味着绕过了段描述表、段寄存器这一整套段式内存管理的机制,而只是段式内存管理的一种使用特例.要了解完整的细节可以参阅Intel的有关技术资料。关于80386的段式内存管理就先介绍这些,以后随着代码分析的进展视需要再加以补充。读者想利用80386对段式内存管理的硬件支持,可以实现段式虚存管理。如前所述,当一个段寄存器的内容改变时, CPU要根据新的段寄存器内容以及G DTR或L DTR的内容找到相应的段描述项并将其装入CPU中, 在此过程中, CPU会检查该描述项中的p标志位(表示”present”) , 如果p标志位为0,就表示该描述项所指向的那一段内容不在内存中《也就是说, 在磁盘上的某个地方) , 此时CPU会产生一次异常(exception, 类似于中断) , 而和应的服务程序使可以从磁盘交换区将这一段的内容读入内存中的某个地方,并据此设置描述项中的基地址,再将p标志位设置成1。相应地,内存中暂时不用的存储段则可以写入磁盘,并将其描述项中p标志位改成0。对段式内存管理的支持只是i386保护模式的一个组成部分.如果没有系统状态和用户状态的分离,以及特权指令(只允许在系统状态下使用)的设立,那么尽管有了前述的段式内存管理,也还不能起到保护的效果, 前面已经提到过特权指令的设置, 如用来装入和存储G DTR和L DTR的指令LG DT/LL DT和SGD T/SLD T等就都是特权指令。正是由于这些指令都只能在系统状态(也就是在操作系统的内核中) 使用, 才使得用户程序不但不能改变G DTR和L DTR的内容, 还因为既无法确知其段描述表在内存中的位置,又无法访问其段描述表所在的空间(只能在系统状态下才能访问),从而无法通过修改段描述项来打破系统的保护机制。那么,80386怎么米分隔系统状态和用户状态,并且提供在两种状态之间切换的机制呢?级别, 其中0级为最高, 3级为最低。每一条指令也都有其适用级别, 如前述的LG DT, 就只有在0级的状态下才能使用, 而一般的输入/输出指令(IN, OUT) 則规定为0级或1级。通常, 用户的应
80386并不只是像一般CPU通常所做的那样, 划分出系统状态和用户状态, 而是划分成四个特权用程序都是3级,一般程序的当前运行级别由其代码段的局部描述项(即由段寄存器CS所指向的局部段描述项) 中的dpl字段决定(dpl表示“deser ipt or privilege lev eD) 。当然, 每个描述项中的dpl字段都是在0级状态下内内核设定的, 而全局段描述的dpl字段, 则又有所不同, 它是表示所需的级别。前面讲过.16位的段寄存器中的高13位用作下标来访问段描述表,而低3位是干什么的呢?我们还是通过一段伪代码来说明:}寄存器;段插述表指不位, D表示GDT, 1表不LDT*//*Requested Privilege Level, 整求的优光纸*/当段寄存器CS中的ti位为1时,表示要使用全局段描述表,为0时,则表示要使用局部段描述表。而rpl则表示所要求的权限。当改变一个段寄存器的内容时, CPU会加以检查, 以确保该段程序的当前执行权限和段寄存器所指定要求的权限均不低于所要访问的那一段内存的权限dpl.至于怎样在不同的执行权限之间切换,我们将在进程调度、系统调用和中断处理的有关章节中讨论, 此外, 除了全局段描述表指针G DTR和局部段描述表指针L DTR两个寄存器外, 其实i386CPU中还有个中断向量表指针寄存器I DTR、与进程(在Intel术语中称为“任务”, Task) 有关的寄存器TR以及描述任务状态的“任务状态段”TSS等, 这些都将在其它章节中有需要时再加以介绍, Intel在实现i 386的保护模式时将CPU的执行状态分成四级, 意图是为满足更为复杂的操作系统和运行环境的需要,有些操作系统,如OS/2中,也确实用了。但是很多人都怀疑是否真有必要搞得那么复杂,事实上,几乎所有广泛使用的CPU都没有这么复杂.而且, 在80386上实现的各种Unix版本, 包括Linux, 都只用了两个级别, 邮0级和3级, 作为系统状态和用户状态。本书在以后的讨论中将沿用Unix的传统称之为系统状态和用户状态。
1.3i386的页式内存管理机制
学过操作系统原理的读者都知道,内存管理有两种,一种是段式管理,另一种是页式管理,而页式管理更为先进。从80年代中期开始, 页式内存管理进入了各种操作系统(以Unix为主) 的内核,一时成为操作系统领域的一个热点,Intel从80286开始实现其“保护模式”, 也即段式内存管理。但是很快就发现, 光有段式内存管理而没有页式内存管理是不够的, 那样会使它的X 86系列逐渐失去竞争力以及作为主流CPU产品的地位。因此,在不久以后的80386中就实现了对页式内存管理的支持,也就是说,80386除了完成开完善从80286开始的段式内存管理的同时还实现了页式内存管理。前面讲过,80386的段式内存管理机制,是将指令中结合段寄存器使用的32位逻辑地址映射(转换)成同样是32位的物理地址。之所以称为“物理地址”,是因为这是真正放到地址总线上去,并用以寻访物理上存在着的具体内存单元的地址。但是,段式存储管理机制的灵活性和效率都比较差。方面“段”是可变长度的,这就给盘区交换操作带来了不便:另一方面,如果为了增加灵活性而将一个进程的空间划分成很多小段时,就势必要求在程序中频繁地改变段寄存器的内容,同时,如果将段分小,虽然一个段描述表中可以容纳8192个描述项(因为有13位下标),也未必就能保证足够使用。所以,比较好的办法还是采用页式存储管理。本来,页式存储管理并不需要建立在段式存储管理的基础之上,这是两种不同的机制。可是,在80386中,保护模式的实现是与段式存储密不可分的,例如,CPU的当前执行权限就是在有关的代码段描述项中规定的。读过Unix早期版本的读者不妨将此与PDP-11中的情况作一对比。在PDP-11中CPU的当前执行权限存放在一个独立的奇存器PSW中, 而与任何其它的数据结构没有关系。因此,在80386中,既然决定利用部分已经存在的资源,而不是完全另起炉灶,那就无法绕过段式存管来实现页式存管。也就是说,80386的系统结构决定了它的页式存管只能建立在段式存管的基础上。这也意味着,页式存管的作用是在由段式存管所映射而成的地址上再加上一层地址射, 由于此时由段式存管映射而成的地址不再是“物理地址”了, Intel就称之为“线性地址”于是,段式存管先将逻辑地址映射成线性地址,然后再由页式存管将线性地址映射成物理地址:或者,当不使用页式存管时,就将线性地址直接用作物理地址,80386把线性地址空间划分成4K字节的页面,每个页面可以被映射至物理存储空间中任意一块4K字节大小的区间(边界必须与4K字节对齐),在段式存管中,连续的逻辑地址经过映射后在线性地址空间还是连续的。但是在页式存管中,连续的线性地址经过映射后在物理空间却不一定连续(其灵活性也正在于此)。这里需要指出的是,虽然页式存管是建立在段式存管的基础上,但一旦启用了页式存管, 所有的线性地址都要经过页式映射, 连G DTR与L DTR中给出的段描述表起始地址也不例外,可以看出,在页面目录中共有21-1024个目录项,每个目录项指向一个页面表,而在每个页面表中又共有1024个页面描述项, 类似于G DTR和L DTR, 又增加了一个新的寄存器CR 3作为指向当前出于页式存管的引入,对32位的线性地址有了新的解释(以前就是物理地址)。
empty