UNIX操作系统设计

978-7-115-50523-1
作者: [美] 莫里斯·J. 巴赫(Maurice J.Bach)
译者: 陈葆钰王旭柳纯录冯雪山
编辑: 吴晋瑜

图书目录:

详情

本书以UNIX系统为背景,全面、系统地介绍了UNIX操作系统内核的内部数据结构和算法。本书首先对系统内核结构做了简要介绍,然后分章节描述了文件系统、进程调度和存储管理,并在此基础上讨论了UNIX系统的高级问题,如驱动程序接口、进程间通信与网络等。在每章之后,还给出了大量富有启发性和实际意义的题目。

图书摘要

版权信息

书名:UNIX操作系统设计

ISBN:978-7-115-50523-1

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    [美] 莫里斯•J. 巴赫(Maurice J.Bach)

译    陈葆钰 王 旭 柳纯录 冯雪山

责任编辑 吴晋瑜

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Authorized translation from the English language edition, entitled THE DESIGN OF THE UNIX® OPERATING SYSTEM,1st Edition,ISBN: 0132017997 by BACH,MAURICE J., published by Pearson Education, Inc. Copyright © 1986 Pearson Education, Inc.

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.

CHINESE SIMPLIFIED language edition published by POSTS AND TELECOMMUNICATIONS PRESS, Copyright © 2019.

本书中文简体版由Pearson Education,Inc授权人民邮电出版社有限公司出版。未经出版者书面许可,不得以任何方式或任何手段复制和抄袭本书内容。

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。

版权所有,侵权必究。


本书以UNIX系统Ⅴ为背景,全面、系统地介绍了UNIX操作系统内核的内部数据结构和算法。本书首先对系统内核结构做了简要介绍,然后分章节描述了文件系统、进程调度和存储管理,并在此基础上讨论了UNIX系统的高级问题,如驱动程序接口、进程间通信与网络等。

本书可作为大学计算机科学系高年级学生和研究生的教材或参考书,也为从事UNIX系统研究与实用程序开发人员提供了一本极有价值的参考资料。


UNIX操作系统自1974年问世以来,迅速在世界范围内普及。目前,它不但是小型机、高档微型机、工作站系统的主流操作系统,而且已进入中、大型计算机领地,成为“事实上”的标准操作系统,并被国际标准化组织ISO等考虑和采纳作为分布式处理系统的本地操作系统参考模型。值得一提的是,20世纪90年代以来,随着运行在PC机上的UNIX系统的变体—Linux的出现和迅速普及,UNIX系统更具生命力。

当前,介绍UNIX系统的书籍很多,然而论述UNIX系统内部结构的专著却屈指可数。本书是其中非常引人注目的一本。本书作者Maurice J.Bach多年来在AT&T公司的贝尔实验室工作,对UNIX系统的设计思想有深刻了解,又有讲授UNIX系统的丰富经验。作者在回顾UNIX操作系统的发展演变的基础上,描述了UNIX系统Ⅴ内核内部的数据结构和算法,并对其做了深入浅出的分析。在每章之后,本书还给出了大量富有启发性和实际意义的题目。因而,本书不仅可用作大学本科高年级和研究生操作系统课程的教科书和参考书,也为从事UNIX操作系统的研究人员或UNIX实用程序开发人员提供了极有价值的参考资料。

在本书的翻译过程中,我们尽量保持原著的特色,对书中大量的以C伪码形式描述的算法,仍保持C语言的结构和格式。因而,阅读本书的读者应具备一定的C语言基础。另外,对书中的若干明显错误,我们也一一作了修正。若有错误或不妥之处,恳请指正。

本书是由北京大学计算机系的几位同志合作翻译的:第1、2、3、4、12章由柳纯录翻译;第5、6、8章由冯雪山翻译;第7、9章由王旭翻译;第10、11、13章由陈葆珏翻译。全书由陈葆珏修改、定稿。特别值得一提的是,本书的翻译从一开始就得到了杨美清教授的支持和帮助。她还在百忙之中为本书做了校阅,特此表示衷心的感谢。

译者

于北京


UNIX系统是由Ken Thompson和Dennis Ritchie于1974年在《ACM通讯》中的一篇文章中首次提出的[Thompson 74]。从那时起,UNIX系统得到迅速传播并在计算机工业中得到广泛采用,越来越多的计算机厂家在他们的机器上提供对UNIX系统的支持。UNIX系统在大学里尤其普遍,它通常被用于操作系统的研究及实例分析。

许多专著和文章曾讨论了系统的各个部分,其中有《贝尔系统技术杂志》1978年和1984年的两个专刊[BSTJ 78][BLTJ 84]。还有许多书介绍了UNIX系统的用户接口,特别是如何使用电子邮件、如何准备文件及如何使用“shell”的命令解释程序等;《The UNIX Programming Environment》[Kernighan 84](该书已由机械工业出版社引进出版,中译本名为《UNIX编程环境》)和《Advanced UNIX Programming》[Rochkind 85]等书讨论了程序设计环境。本书则着重描述构成操作系统基础(称为内核)的内部算法和数据结构以及它们与程序员接口之间的关系。因此,本书适用于几种环境。首先,它可用作大学高年级本科生或一年级研究生的操作系统课程的教材。使用本书的同时,读者若能参考系统源代码则将获益匪浅,但也可以独立地学习本书。其次,系统程序员可将本书作为参考书,从而能更好地理解内核是如何工作的,并可以将UNIX系统中采用的算法与其他操作系统的算法加以比较。最后,UNIX系统上的程序员能够更深入地了解他们的程序是如何与系统相互作用的,从而编写出更有效、更高级的程序。

本书的内容及组织形式取自我在AT&T贝尔实验室在1983年和1984年期间讲授的一门课程。尽管这门课集中于阅读系统源代码,但我发现,一旦掌握了算法的基本思想,源代码的阅读和理解就会容易得多。在本书中,我已努力使算法的描述尽可能地简单,从而反映出算法所描述的系统的简单性和精巧性。因此,本书并不是用英文逐行地翻译系统,而是描述了各种算法的主要流程,更重要的是,它描述了各种算法是如何相互作用的。算法用类似C语言的伪码来表示,从而有助于读者理解自然语言的描述;算法的名字对应于内核内部的过程名。书中的各种插图描绘了系统对各种数据结构进行操作时它们之间的关系。在稍后的一些章节中,我们采用许多小的C语言程序来说明一些系统的概念,这些程序的用户是容易明白的。为节省篇幅和清晰,这些例子一般不检查错误条件,而这一点在写程序时是一定要做的。我已经在系统Ⅴ上运行了这些程序;除了某些演示系统Ⅴ的特殊特点的程序外,这些程序也应该能在UNIX系统的其他版本上运行。

原来为课程所准备的许多习题已放在每章的最后,它们是本书的重要组成部分。有些习题是直截了当的,用于说明正文中引入的概念。有些习题比较困难,用来帮助读者在一个较深的层次上理解系统。最后,还有些习题具有研究性质,设计这些题目是为了提出问题以供研究探讨。难度大的题目都标有*号。

本书对UNIX系统的描述基于AT&T所支持的系统Ⅴ,第2版,还包括了一些第3版的新特点。这是我最熟悉的系统,但我还尽力描述了其他版本对UNIX系统的有意义的贡献,特别是BSD对系统的修改。本书回避了与特殊的硬件特性有关的问题,力图以通用的术语描述内核硬件的接口,并忽略特定机器的特殊特点。但是,当与机器有关的问题对理解内核的实现十分重要时,本书则讨论得相对详细一些。至少,对这些问题的探讨会突出操作系统中最依赖于机器的部分。

本书的读者必须具有用高级语言进行程序设计的经验,这是理解本书内容的必备条件,最好还有汇编语言的经验。建议读者具有用UNIX系统工作的经验,并了解C语言[Kernighan 78]。但是,在编写本书时,我努力使没有这种背景的读者也能理解本书的内容。本书的附录含有系统调用的简单描述,它们足以使读者理解书中的表达方式,但并不能作为完整的参考手册。

本书的内容按如下方式组织。第1章简要地描述了用户所看到的系统的特点,并给出了系统结构。第2章描述了内核结构的一般概貌,并引入一些基本概念。其余的章节按系统结构所表示的组成部分,描述其中各个成分。这些章可分为三部分:文件系统、进程控制和高级问题。本书先讨论文件系统,因为其概念比进程控制容易一些。这样,第3章描述了系统缓冲区高速缓存机制,这是文件系统的基础。第4章给出文件系统内部使用的一些算法和数据结构。这些算法使用了第3章中解释的算法,并讨论了管理用户文件所需要的内部操作。第5章说明提供文件系统用户接口的系统调用,这些系统调用使用了第4章的算法来存取用户文件。

第6章转向进程控制,其中定义了进程的上下文,讨论了控制进程上下文的内部内核原语。特别地讨论了系统调用接口,中断处理及上下文切换。第7章给出了控制进程上下文的系统调用。第8章讨论了进程调度问题。第9章的内容是存储管理,其中包括对换和请求调页系统。

第10章讨论了通用驱动程序接口,特别讨论了磁盘驱动程序和终端驱动程序。尽管从逻辑上说设备是文件系统的一部分,但是,因为进程控制的问题要在终端驱动程序中出现,所以,对设备的讨论推迟到这一章。这一章也是通向本书其余章节中所给出的更高级的问题的桥梁。第11章讨论进程间通信和网络问题,其中包括系统Ⅴ的消息、共享存储区及信号量,还有BSD的套接字。第12章解释了紧密耦合的多处理机UNIX系统。第13章研究了松散耦合的分布式系统。

前9章的内容可以在一学期的操作系统课程中完成。其余各章的内容可以在高级讨论班中进行讨论,并同时作各种课题研究。

至此,本人要作几点说明。可以确切地说,本书没有作系统性能方面的讨论,也没有提出任何用于系统安装的配置参数。这些数据会因机器类型、硬件配置、系统版本和实现以及应用类型等的不同而不同。同时,我有意地尽量避免预测UNIX操作系统的未来发展。所讨论的高级问题并不意味着AT&T就要提供这些特别的特性,甚至也不意味着那些特殊的领域正在开发研究中。

Maurice J. Bach


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

如果您是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


UNIX系统自从1969年问世以来已经变得相当流行,它运行在从微处理机到大型机的具有不同处理能力的机器上,并在这些机器上提供公共的执行环境。UNIX系统可分成两部分,第一部分由一些程序和服务组成,其中包括shell程序、邮件程序、正文处理程序包及源代码控制系统等,正是这些程序和服务使得UNIX系统环境如此受欢迎。它们是用户立即可见的部分。第二部分由支持这些程序和服务的操作系统组成。本书给出了该操作系统的详细描述,它着重描述由美国电话电报公司(AT&T)生产的UNIX系统Ⅴ,但也考虑了其他版本所提供的颇有意义的特征。本书考查了在该操作系统中使用的主要数据结构和算法,而这些数据结构和算法最终向用户提供了标准用户界面。

本章是UNIX系统的引言,它回顾了UNIX系统的历史并勾画出了整个系统结构的轮廓。下一章将对该操作系统做更详细的介绍。

1965年,贝尔实验室和通用电气公司及麻省理工学院的MAC课题组一起联合开发一个被称为Multics[Organick 72]的新操作系统。Multics系统的目标是要向大的用户团体提供对计算的同时访问,支持强大的计算能力与数据存储,以及允许用户在需要的时候容易地共享他们的数据。贝尔实验室中后来参加UNIX系统早期开发的许多人当时都参加了Multics工作。虽然Multics系统的原始版本于1969年在GE645计算机上运行了,但它既没能提供预定的综合计算服务,也不清楚自己的开发目标。结果,贝尔实验室退出了这一项目。

在结束了Multics工程上的工作时,贝尔实验室计算科学研究中心的成员们处于缺乏“方便的交互式计算服务”的境况之中[Ritchie 84a]。为了改善他们的程序设计环境,Ken Thompson、Dennis Ritchie及其他人勾画出了一个纸面上的文件系统设计——它后来就演化为UNIX文件系统的早期版本。Thompson编写了若干程序,模拟所建议的文件系统行为,以及模拟在请求调页环境下程序的行为。他甚至为GE645计算机的简单内核进行了编码。与此同时,他用Fortran语言为GECOS系统(Honeywell635)编写了名为“宇宙旅行”的游戏程序。但这个程序是不能令人满意的,因为它很难控制“宇宙飞船”,并且该程序运行开销太大。Thompson后来发现几乎无人问津的PDP-7计算机能提供很好的图形显示和廉价的执行开销。为PDP-7开发“宇宙旅行”程序使Thompson了解到了关于该机器的细节,但是它的程序开发环境要求先在GECOS机上进行程序的交叉汇编,而后把纸带带到PDP-7上输入。为了创建一个较好的开发环境,Thompson和Ritchie在PDP-7上实现了他们的系统设计,其中包括UNIX文件系统、进程子系统的早期版本及少量实用程序。终于,新系统再也不需要把GECOS系统作为开发环境,而能够自我支持了。这个新的系统被命名为UNIX。UNIX是针对Multics的双关语,它是计算科学研究中心的另一名成员Brian Kernighan想出来的。

虽然UNIX系统的早期版本是大有前途的,但直到被用于实际项目之前它并没能发挥出自身的潜力。因此,为给贝尔实验室的专利部门提供一个正文处理系统,1971年UNIX系统被移植到PDP-11上。该系统的特征是规模小:内存中16KB用于系统,8KB用于用户程序;磁盘512KB,每个文件限定长度为64KB。在它初次成功之后,Thompson开始动手为这个系统实现Fortran编译程序。但是在BCPL[Richards 69]的影响下开发出来的却是B语言。B语言是解释性语言,在性能上有所退步——这是这类语言的共同特征。因此,Ritchie把B语言发展成他称之为C的语言,C语言允许产生机器代码、说明数据类型及定义数据结构。1973年,他用C语言重写了UNIX操作系统。这一步在当时并不太引人注目,但对UNIX的外部用户接受它却具有极大的影响。这之后,贝尔实验室的装机数目增加到25个并且形成了一个UNIX系统小组,以提供内部支持。

由于美国电话电报公司1965年与联邦政府签署了反垄断法,所以这时它不能销售计算机产品。但是,美国电话电报公司把UNIX系统提供给了那些请求把UNIX系统用于教育目的的大学。该公司信守了反垄断法的条款,它既没有为UNIX系统做广告,也没有销售和支持UNIX系统。然而,UNIX系统的声望却在稳步增长。1974年,Thompson与Ritchie在《ACM通讯》上发表了一篇描述UNIX系统的文章[Thompson 74],进一步促进了它的可接受性。到1977年,UNIX系统的装机数目已经增长到大约500个,其中有125个在大学。UNIX系统开始在业务电话公司流行起来,为程序开发、网络事务操作服务及实时服务(通过MERT[Lycklama78a])提供了良好的环境。这时,UNIX系统的许可证被提供给商业机构,同时也向大学提供。1977年,交互系统公司(Interactive Systems Corporation)成了UNIX系统的第一个增值转卖商(VAR)[1],他们增强了它,使之在办公室自动化环境中使用。同样,1977年也是标志UNIX系统首次被“移植”到非PDP机(即稍加改变或完全不变而在其他机种上运行)——Interdata 8/32上的一年。

随着微处理机的日益普及,其他公司也把UNIX系统移植到新的机器上,但是它的简单清晰的特点吸引着很多开发者以他们自己的方式增强UNIX系统,结果在基本系统上产生若干变体。从1977年到1982年这一时期,贝尔实验室把若干AT&T变体综合成一个单个系统,这就是大家都知道的商用UNIX系统Ⅲ。随后,贝尔实验室又把若干特征加到系统Ⅲ上,称为新产品UNIX系统Ⅴ[2],1983年1月,美国电话电报公司发布它对系统Ⅴ的正式支持。然而,加利福尼亚大学伯克利分校的人们已经开发了一个UNIX系统的变体,它的最新版本称为4.3BSD(Berkeley Software Distribution),是配在VAX机上的,它提供了一些新的有意义的特征。本书将着重描述UNIX系统Ⅴ,但也偶尔谈及BSD系统中所提供的那些特征。

到1984年年初,世界上大约安装了100 000个UNIX系统,它们运行在从微处理机到大型机的具有显著差异的计算能力的机器上,运行在出自不同的制造厂家的生产线的机器上。没有任何其他操作系统能与之匹敌。UNIX系统的普及与成功可归结为如下一些原因。

简单性与一致性突出了UNIX系统的宗旨,上面列出的大部分原因都讲的是简单性与一致性。

虽然操作系统和很多命令程序是用C语言编写的,但是UNIX系统支持其他语言,包括Fortran、Basic、Pascal、Ada、Cobol、Lisp及Prolog等。UNIX系统能支持具有编译程序或解释程序的任何语言;UNIX系统还能支持一个系统接口,该接口把用户对操作系统服务的请求映射到UNIX系统使用的一组标准请求上。

图1-1绘出了UNIX系统的高层次的体系结构。该图中心的硬件部分向操作系统提供的基本服务将在1.5节中描述。操作系统直接[3]与硬件交互,向程序提供公共服务,并使它们同硬件特性隔离。当我们把整个系统看成层的集合时,操作系统通常称为系统内核,或简称内核(kernel),此时强调的是它同用户程序的隔离。因为程序是不依赖于其下面的硬件的,所以,如果程序对硬件没有做什么假定的话,就容易把它们在不同硬件上运行的UNIX系统之间搬动。比如,那些假定了机器字长的程序比起没假定机器字长的程序来说,就较难于搬到其他机器上。

外层的程序,诸如shell及编辑程序(ed与vi),是通过引用一组明确定义的系统调用而与内核交互的。这些系统调用通知内核为调用程序做各种操作,并在内核与调用程序之间交换数据。图中出现的一些程序属于标准的系统配置,就是大家知道的命令,但是,由名为a.out的程序所指示的用户私用程序也可以存在于这一层。此处的a.out是被C编译程序产生的可执行文件的标准名字。其他应用程序能在较低层的程序之上构建,因此它们存在于本图的最外层。比如,标准的C编译程序cc就处在本图的最外层:它调用C预处理程序、两遍编译程序、汇编程序及装入程序(称为连接-编辑程序),这些都是彼此分开的低层程序。虽然该图对应用程序只描绘了两个级别的层次,但用户能够对层次进行扩充,直到级别的数目适合于自己的需要。确实,为UNIX系统所偏爱的程序设计风格鼓励把现存程序组合起来去完成一个任务。

图1-1 UNIX系统的体系结构

一大批提供了对系统的高级看法的应用子系统及应用程序,诸如shell、编辑程序、SCCS(source code control system)及文档准备程序包等,都逐渐变成了“UNIX系统”这一名称的同义语。然而,它们最终都使用由内核提供的低层服务,并通过系统调用的集合来利用这些服务。系统Ⅴ中大约有64个系统调用,其中将近32个是常用的。它们有简单的可选项,这些可选项使系统调用容易使用,但是却向用户提供了很多能力。系统调用的集合及实现系统调用的内部算法形成了内核的主体,因而本书所要介绍的对UNIX操作系统的研究,就变成了对系统调用及其相互作用的详细研究和分析。简言之,内核提供了UNIX系统全部应用程序所依赖的服务,它也定义了这些服务。本书将频繁使用“UNIX系统”“内核”或“系统”等术语,但其含义是指UNIX操作系统的内核,并且在上下文中是清楚的。

本节简要地介绍UNIX系统的高层特征,诸如文件系统、处理环境及构件原语(比如,管道),后面几章将详细地探讨内核对这些特征的支持。

UNIX文件系统有如下特点。

文件系统被组织成树状,树有一个称为根(root)的节点(记作“/”)。文件系统结构中的每个非树叶节点都是文件的一个目录(directory),树的叶节点上的文件既可以是目录,也可以是正规文件(regular files),还可以是特殊设备文件(special device files)。文件名由路径名(pathname)给出,路径名描述了怎样在一个文件系统树中确定一个文件的位置。路径名是一个分量名序列,各分量名之间用斜杠符隔开。分量是一个字符序列,它指明一个被唯一地包含在前级(目录)分量中的文件名。一个完整的路径名由一个斜杠字符开始,并且指明一个文件,这个文件可以从文件系统的根开始,沿着该路径名的后继分量名所在的那个分支遍历文件树而找到。因此,路径名“/etc/passwd”“/bin/who”及“/usr/src/cmd/who.c”都是图1-2的树中的文件,但“/bin/passwd”及“/usr/src/date.c”则不是。一个路径名不一定非从根开始不可,可以省略掉路径名中的初始斜杠,由相对于正在执行的进程的当前目录来指明。因此,若从目录“/dev”开始,路径名“tty01”标明的是整个路径名为“/dev/tty01”的文件。

图1-2 文件系统树示例

在UNIX系统中,程序不了解核心按怎样的内部格式存储文件,而把数据作为无格式的字节流看待。程序可以按它们自己的意愿去解释字节流,但这种解释与操作系统如何存储数据无关。因此,对文件中数据进行存取的语法是由系统定义的,并且对所有的程序都是同样的。但是,数据的语义是由程序加上去的。比如,文本格式化程序troff希望在文本的每一行的尾部找到“换行”符,而系统记账程序acctcom则希望找到定长记录。两个程序都使用相同的系统服务,以存取文件中的作为字节流存在的数据,而在内部,它们通过分析把字节流解释成适当的格式。如果哪一个程序发现格式是错误的,则它负责采取适当的动作。

从这方面说,目录也像正规文件。系统把目录中的数据作为字节流看待。但是,由于该数据中包含着许多以预定格式记录的目录中的文件名,所以,操作系统以及诸如ls(列出文件名和属性)这样的程序就能够在目录中发现文件。

对一个文件的存取许可权由与该文件相联系的access permissions所控制。存取许可权能够分别对文件所有者、同组用户及其他人这三类用户独立地建立存取许可权,以控制读、写及执行的许可权。如果目录存取许可权允许的话,则用户可以创建文件。新创建的文件是文件系统目录结构的树叶节点。

对于用户来说,UNIX系统把设备看成文件。以特殊设备文件标明的设备,占据着文件系统目录结构中的节点位置。程序存取正规文件时使用什么语法,它们在存取设备时也使用什么语法。读、写设备的语义在很大程度上与读、写正规文件时相同。设备保护方式与正规文件的保护方式相同:都是通过适当建立它们的(文件)存取许可权实现的。由于设备名看起来像正规文件名,并且对于设备和正规文件能执行相同的操作,所以大多数程序在其内部不必知道它们所操纵的文件的类型。

例如,考察图1-3中为一个现存的文件创建新拷贝的程序。假设该程序的可执行版本的名字是copy。终端上的用户通过输入:

copy oldfile newfile

调用该程序。此处,oldfile是一个现存文件名,而newfile是一个新文件名。系统引用main时需要提供argc作为表argv中的参数个数,并且对数组argv的每个成员赋初值,以指向由用户提供的参数。在上述例子中,argc为3,argv[0]指向字符串copy(按惯例,第0个参数是程序名),argv[1]指向字符串oldfile,argv[2]指向字符串newfile。然后该程序检查它被调用时调用者提供的参数个数是否正确。如果正确,则调用系统调用open,试图打开文件oldfile,并对该文件做“只读”操作。如果该系统调用成功,则进一步调用系统调用creat,以便创建newfile。新创建的文件的存取权限将是0666(八进制),即允许所有的用户读写该文件。所有的系统调用在失败时都返回−1。如果open或creat调用失败,则该程序打印出一则消息,并以返回状态值“1”调用系统调用exit,结束程序的执行并指出有些地方出错了。

系统调用open与creat返回一个称为文件描述符(file descriptor)的整数,程序随后就使用这一文件描述符访问该文件。接着,程序调用子程序copy,copy进入一个循环:调用系统调用read,从现存文件中读满一缓冲区的字符,并调用系统调用write,把这些数据写到新文件上。系统调用read返回所读的字节数,当它到达文件尾时返回0。该程序在遇到文件尾时结束循环,或者在系统调用read出了什么错误时(它不检查写错误)结束循环。然后,它从copy返回并以返回状态值“0”调用系统调用exit,以指示该程序成功地结束了。

该程序可以拷贝作为参数提供给它的任何文件——只要它对现存文件具有打开许可权并且对新文件具有建立许可权。此处所说的文件可以是一个可打印字符的文件,比如,程序的源代码,或者它包含不可打印的字符,甚至该程序本身。因此,两个调用

copy copy.c newcopy.c
copy copy newcopy

图1-3 用于拷贝文件的程序

都工作。老文件也可以是一个目录,比如

copy dircontents

就把由名字“.”所表示的当前目录的内容拷贝到正规文件“dircontents”中;新文件中的数据与目录的内容是一个字节、一个字节地相同的,但该文件是一个正规文件(而系统调用mkond创建一个新目录)。最后我们要说的是,任一文件都可能是设备特殊文件。比如

copy /dev/tty terminalread

把在终端上输入的字符(特殊文件/dev/tty是用户终端)读入,并把它们拷贝到文件terminalread上,仅当用户输入字符control-d时才结束。类似地,

copy /dev/tty /dev/tty

则把终端上输入的字符读入并把它们都拷贝回去。

一个程序是一个可执行文件,而一个进程则是一个执行中的程序的实例。在UNIX系统上可以同时执行多个进程(这一特征有时称为多道程序设计或多道任务设计),对进程数目无逻辑上的限制,并且系统中可以同时存在一个程序(如copy)的多个实例。各种系统调用允许进程创建新进程、终止进程、对进程执行的阶段进行同步及控制对各种事件的反应。在进程使用系统调用的条件下,进程便互相独立地执行了。

比如,正在执行图1-4中的程序的进程执行系统调用fork以创建一个新进程。称为子(child)进程的新进程从fork那儿获得一个0返回值,并且调用execl以执行copy程序(图1-3中的程序)。系统调用execl用文件“copy”覆盖子进程的地址空间——当然,我们假设“copy”正处在当前目录中,并且,execl根据用户提供的参数运行该程序。如果execl调用成功了,则它永不返回,这是因为该进程是在新地址空间中执行的,第7章我们将会看到这一点。同时,调用fork的那个进程(父进程)从该调用收到一个非0返回值,然后调用wait将自己的执行挂起,直到copy结束时,打印出“拷贝完毕”的消息,而后退出(每个程序都是在主函数结束时退出,这是在编译处理期间与标准C程序库连接时由C程序库所安排的)。比如,若可执行程序的名字为run,并且一个用户通过

图1-4 创建新进程以拷贝文件的程序

run oldfile newfile

引用该程序的话,则进程把“oldfile”拷贝到“newfile”上并且打印出消息。虽然这个程序只往“copy”程序上添加了一点东西,但它呈现了用于进程控制的四个主要的系统调用:fork,exec,wait及exit。

一般说来,系统调用允许用户编写做复杂操作的程序,其结果是:在其他系统中成为“核心”部分的许多函数却不包含在UNIX系统的内核中。包括像编译程序和编辑程序这样的函数在UNIX系统中都是用户级程序。命令解释程序shell就是这样的程序的一个基本例子,在用户注册到系统中之后就执行该程序。shell把命令行的第一个字解释成命令名:对许多命令,shell都创建子进程,由子进程执行与该名字相联系的命令。把命令行中其余的字视为该命令的参数。

shell允许三种类型的命令。第一种,一个命令是一个可执行文件,它包含将源代码(比如说一个C程序)编译后产生的目标代码。第二种,一个命令是包含一系列shell命令行的可执行文件。最后,一个命令是一个内部shell命令(不是一个可执行文件)。内部命令使shell不仅是命令解释程序,而且是一种程序设计语言,它包括用于循环的命令(for-in-do-done与while-do-done),用于条件执行的命令(if-then-else-if),一个“选择”(case)语句命令,一个改变进程的当前目录的命令(cd)及其他一些命令。shell语法允许模式匹配和参数处理。用户在执行命令时不一定非要知道它们的类型不可。

shell在一个给定的目录序列中搜索命令,而目录序列可以在每次调用shell时由用户请求来改变。通常shell是同步地执行一个命令的,要等候一个命令执行完毕再去读下一个命令行。然而,它也允许异步执行,这时,它不等待前一个命令执行完毕就去读下一个命令行。异步执行的命令被说成是在后台执行的。比如,输入命令

who

引起系统执行存储在文件/bin/who[4]中的程序,它打印当前在系统中注册了的用户名单。当who执行时,shell等候它结束,然后提示用户输入下一个命令。然而输入:

who &

系统则在后台执行程序who,并且shell做好了立即接收下一条命令的准备。

正在UNIX系统中执行着的每个进程都有一个包括当前目录在内的执行环境。一个进程的当前目录是不以斜杠字符开始的所有路径名的起始目录。用户可以执行shell命令cd,即改变目录,以便沿着文件系统树移动,并改变当前目录。命令行

cd /usr/src/uts

把shell的当前目录变成目录“/usr/src/uts”。命令行

cd ../..

把shell的当前目录移到向根结点“靠近”两层结点的目录:命令中的分量“..”指的是当前目录的父目录。

因为shell是用户程序而不是内核的一部分,所以易于修改它、剪裁它以适应特定环境的需要。举例来说,用户能使用C shell,而不是Bourne shell(以它的开发者Steve Bourne命名的),C shell提供一种历史机制,以避免重新输入最近刚使用过的命令。这种机制是标准的系统Ⅴ版本的一部分。或者,某些用户可以仅被允许使用一个有限的shell——正规shell的简化版本。这就是说,系统能同时执行各种shell。用户具有同时执行多个进程的能力,并且如果需要的话,进程能动态创建其他进程,并对它们的执行进行同步。这些特征向用户提供了一个强有力的执行环境。虽然shell的很多能力是从它作为一种程序设计语言的能力及从它对自变量的模式匹配能力引申出来的,但本节着重叙述系统通过shell提供的进程环境。shell的其他重要特征已超出了本书的范围(见[Bourne 78]中关于shell的详细描述)。

正如前面所描述的那样,UNIX系统的宗旨是提供操作系统原语,使用户能编写小的、模块化的程序,并把它们作为构件(building block)去构筑更复杂的程序。重定向I/O(redirect I/O)的能力便是这样的为shell用户可见的一个原语。进程通常可以存取三个文件:它们从标准输入(standard input)文件上读,往标准输出(standard output)文件上写,以及把错误消息写到标准错误(standard error)文件上。在终端上执行的进程一般地是使用终端作为这三个文件,但每个文件都可以被独立地重定向。比如,命令行

ls

把当前目录中的所有文件都列到标准输出上。但是,命令行

ls > output

使用上面提到的系统调用creat把标准输出重定向到当前目录中称为“output”的文件上。类似地,命令行

mail mjb < letter

打开文件“letter”作为它的标准输入,并且把该文件的内容作为邮件寄给注册名为“mjb”的用户。进程能同时进行输入重定向与输出重定向,比如,在命令行

nroff - m m < doc1 > doc1. out 2 > errors

中,文本格式化程序nroff读输入文件doc1,把它的标准输出重定向到文件doc1.out上,并且把错误消息重定向到文件errors上(记号“2>”意味着对文件描述符2进行输出重定向。按惯例,文件描述符2为标准错误文件)。程序ls、mail及nroff不知道它们的标准输入、标准输出或标准错误将是哪一个文件;shell识别出“<”“>”及“2>”等符号,并在执行这些进程之前适当地建立标准输入、标准输出及标准错误文件。

第二个构件原语是管道(pipe),这是允许在读者进程与写者进程之间传递数据流的机制。进程能够把它们的标准输出重新定向到一个管道上,另外一些进程把它们的标准输入重新定向为从这个管道来,即读这个管道。第一类进程写进管道的数据是第二类进程的输入。第二类进程也能对它们的输出重定向,等等,这就要看程序设计的需要了。还有,进程不需要知道它们的标准输出是什么类型的文件;不管它们的标准输出是正规文件,还是管道,还是设备,它们都能工作。当使用较小的程序作为构件组成较大的、较为复杂的程序时,程序员使用管道原语及I/O重定向把几部分断片集成起来。不言而喻,系统确实鼓励这样的程序设计风格,以便使新的程序借助于现存的程序就能工作。

比如,程序grep搜索一组文件(作为grep的参数)中的一个给定的模式:

grep main a.c b.c c.c

在a.c,b.c,c.c中搜索包含字符串“main”的行,把找到的那些行在标准输出上打印出来。下面就是一个输出实例:

a.c:main(argc,argv)
b.c:/* here is the main loop in the program */
c.c:main()

带有任选项“−1”的程序wc对标准输入文件中的行数进行计数。命令行

grep main a.c b.c c.c| wc −1

对上述文件中包含有字符串“main”的行进行计数;来自grep的输出被直接引送到wc命令中。对于前面的grep的输出实例而言,上面的管道化命令的输出是:

3

管道的应用常使得它不需要创建临时文件。

图1-1描绘了紧接在用户应用程序之下的内核层。内核代表用户进程完成各种原语操作,以便支持上面描述过的用户接口。由内核提供的服务有:

内核提供的服务是透明的。比如,它能识别一个给定文件是正规文件还是一个设备,但它向用户进程隐蔽了它们的区别。类似地,它需要对文件中的数据格式化以便于内部的存储,但它向用户进程隐蔽了内部格式,而返回一个非格式化的字节流。最后,它提供必要的服务,使用户级的进程能支持它们必须提供的服务,同时省略那些能够在用户级实现的服务。比如说,内核支持shell作为一个命令解释程序所需要的服务:它允许shell读终端输入、动态地产生进程、同步进程的执行、创建管道以及进行I/O重定向等。一个用户能在不影响其他用户的情况下构造shell的私用版本,通过修改它们的环境来适应规范。像标准的shell一样,这些程序使用相同的内核服务。

UNIX系统上用户进程的执行分成两个级别:用户与内核。当一个进程执行一个系统调用时,进程的执行态从用户态变为核心态:由操作系统执行并试图为用户的请求服务。如果失败,则返回一个错误码。即使用户对操作系统有显式的没提出什么服务请求,操作系统仍然做与用户进程有关的内务操作、中断处理、进程调度、存储管理等。很多机器体系结构(及其操作系统)都支持比这里所说的两个级别更多的级别,然而对于UNIX系统来说,用户态与核心态这两态已经足够用了。

两态之间的区别如下。

简单地说,硬件是按核心态与用户态来观察世界的,而对这两种态下正在执行程序的多个用户是不做区别的。操作系统保存着内部记录以区分正在系统上执行着的多个进程。图 1-5表明了这一区别:内核在横轴方向上把进程A、B、C、D区分开来,硬件在纵轴方向上把执行的态区分开来。

图1-5 多个进程及执行态

虽然系统在执行时必处于两种态之一,但内核是为着用户进程运行的。内核不是与用户进程平行运行的孤立的进程集合,而是每个用户进程的一部分。本书以后将经常提到“内核”分配资源或“内核”进行各种操作,然而其含义是,一个在核心态下执行的进程分配资源或做各种操作。比如,shell通过系统调用读用户终端:正在为该shell进程执行的内核对终端的操作进行控制,并把输入的字符返回给shell。然后shell在用户态下执行,对用户输入的字符流进行解释,执行特定的动作集合,在这些动作集合中又可以请求引用其他系统调用。

UNIX系统允许I/O外围设备或系统时钟异步地中断CPU。当接收到中断的时候,内核保存它的当前上下文(表示进程正在做什么的瞬时冻结映象),判定中断原因,为中断服务。在内核为中断服务之后,内核恢复被中断了的上下文,并且继续进行,就好像什么事也没发生过一样。硬件通常按照中断被处理的次序给设备赋予优先权:当内核为某个中断服务时,它封锁较低优先级的中断,但为更高优先级的中断服务。

例外条件(exception condition)指的是由一个进程引起的非期望事件,例如,非法存储寻址、执行特权指令、除数为零等,它们与来自进程外部的事件所引起的中断是有区别的。例外事件发生在一条指令执行的过程中间,并且系统试图在处理完例外事件之后重新开始执行该指令;中断被认为是在两条指令的执行之间发生,并且系统在对中断服务完毕之后,从下一条指令继续执行。UNIX系统使用一种机制来处理中断及例外条件。

在关键活动期间,有时内核必须阻止中断的发生,因为如果这时允许中断,可能会导致数据的误用。比如,正像在下一章将要看到的那样,当正在对链表操作时,内核可能不希望接收磁盘中断,因为这时处理中断会使指针混乱。计算机通常都有一组特权指令,它们在处理机状态字中建立处理机执行级(processor execution level),把处理机执行级置为一定的值,以便把同级或较低级的中断屏蔽掉,而仅允许较高级的中断。图1-6表明了一组执行级的示例。如果内核屏蔽掉磁盘中断,则除了时钟中断与机器错误中断之外,所有中断都要被阻止。如果它屏蔽了软件中断,则所有其他中断都可能发生。

图1-6 典型的中断级

正如当前正在执行的进程(或者,至少是它的一部分)是驻留在主存中的那样,内核是永远驻留在主存中的。当对一个程序进行编译时,编译程序生成一个程序中的地址集合,它们表示变量和数据结构的地址,或诸如函数之类的指令的地址。编译程序产生的是虚机器地址,如同没有其他程序在物理机器上同时执行一样。

当程序在机器上运行时,内核为它分配主存空间,但是不要求由编译程序生成的虚地址与它们在机器中占据的物理地址完全相同。内核与机器硬件一起协作,建立虚地址到物理地址的转换(translation),把编译程序生成的地址映射成物理的机器地址。该映射依赖于机器硬件的能力,从而UNIX系统中负责映射的部分是依赖于机器的。例如,某些机器具有特殊的硬件以支持请求调页。第6章和第9章将更详细地讨论存储管理问题及它们与硬件的关系。

本章描述了UNIX系统的整个结构、运行在用户态与核心态下的进程间的关系及内核关于硬件所做的假设。进程在用户态或核心态下执行,这里,进程通过明确定义的系统调用集合来利用系统服务。系统设计鼓励程序员编写小程序,每个小程序都只做几个操作但做得很好,然后再使用管道及I/O重定向把这些小程序组合起来去做更复杂的处理。

系统调用使得进程能做要是没有系统调用就做不成的事。除了系统调用服务以外,内核还为用户团体做一般性内务操作、进程调度的控制、主存中的存储管理与进程保护、中断的妥善处理、文件和设备管理以及系统错误条件的处理。UNIX系统内核故意省略很多功能,而这些功能是其他操作系统的组成部分。它提供一小组系统调用,而这些系统调用允许进程在用户级上完成必要的功能。下一章将对内核做更详细介绍,描述它的体系结构及在它的实现中的某些基本概念。

[1] 增值转卖商把具体应用加到计算机系统上以满足特定的市场需要。他们销售的是应用而不是销售这些应用赖以运行的操作系统。

[2] 对系统Ⅳ发生了什么?事实上,系统Ⅳ是UNIX系统的一个内部版本,它被融入系统Ⅴ中去了。

[3] 在UNIX系统的某些实现中,该操作系统同一个核(native operating system)接口,再由核与其下面的硬件接口,并向UNIX系统提供必要的服务。这样的配置可以使其他操作系统及其应用程序能够同UNIX系统平行地运行在装置上。这样的一种配置的经典例子是MERT系统[Lycklama 78a]。更近期的配置包括面向IBM系统/370计算机[Felton 84]及面向UNIVAC1100系列计算机[Bodenstab 84]的实现。

[4] 目录“/bin”包含很多有用的命令,并且通常被包含在shell搜索的目录序列中。

[5] 第12章将考虑多处理机系统,而在那以前,我们都假定为单处理机方式。


上一章给出了对UNIX系统环境的高层次的看法。本章重点放在内核上,对内核的体系结构提出一个总的看法,勾画出它的基本概念和结构,而这些对于了解本书的其余部分是必不可少的。

Christian曾提出,UNIX系统支持文件系统有“空间”而进程有“生命”的假象(见[Christian 83)第239页)。文件和进程这两类实体是UNIX系统模型中的两个中心概念。图2-1给出了内核框图,显示出了各种模块及它们之间的相互关系,尤其是,它显示出了内核的两个主要成分:左边的文件子系统和右边的进程控制子系统。虽然在实际上,由于某些模块同其他模块的内部操作进行交互而使内核偏离该模型,但该图仍可作为观察内核的一个有用的逻辑视图。

图2-1让我们看到了三个层次:用户级、内核级及硬件级。系统调用与库接口体现了图1-1中描绘的用户程序与内核间的边界。系统调用看起来像C程序中普通的函数调用,而库把这些函数调用映射成进入操作系统所需要的原语。这在第6章中有更详细的叙述。然而,汇编语言程序可以不经过系统调用库而直接引用系统调用。程序常常使用像标准I/O库这样的一些其他库程序以提供对系统调用的更高级的使用。在编译期间把这些库连接到程序上,因此,就这里所讨论的目的来说,这些库是用户程序的一部分。下面的一个例子将阐明这一点。

图2-1把系统调用的集合分成与文件子系统交互的部分以及与进程控制子系统交互的部分。文件子系统管理文件,其中包括分配文件空间、管理空闲空间、控制对文件的存取以及为用户检索数据。进程通过一个特定的系统调用集合,比如,通过系统调用open(为了读或写而打开一个文件)、close、read、write、stat(查询一个文件属性)、chown(改变文件所有者)及chmod(改变文件存取许可权)等与文件子系统交互。这些及另外一些有关的系统调用将在第5章介绍。

文件子系统使用一个缓冲机制存取文件数据,缓冲机制调节在内核与二级存储设备之间的数据流。缓冲机制同块I/O设备驱动程序交互,以便启动往内核去的数据传送及从内核来的数据传送。设备驱动程序是用来控制外围设备操作的内核模块。块I/O设备是随机存取存储设备,或者说,它们的设备驱动程序使得它们对于系统的其他部分来说好像是随机存取存储设备。例如,一个磁带驱动程序可以允许内核把一个磁带装置作为一个随机存取存储设备看待。文件子系统还可以在没有缓冲机制干预的情况下直接与“原始”I/O设备驱动程序交互。原始设备,有时被称为字符设备,包括所有不是块设备的设备。

图2-1 系统内核框图

进程控制子系统负责进程同步、进程间通信、存储管理及进程调度。当要执行一个文件而把该文件装入存储器中时,文件子系统与进程控制子系统交互——进程子系统在执行可执行文件之前,把它们读到主存中。这些我们将在第7章看到。

用于控制进程的系统调用有fork(创建一个新进程)、exec(把一个程序的映象覆盖到正在运行的进程上)、exit(结束一个进程的执行)、wait(使进程的执行与先前创建的一个进程的exit相同步)、brk(控制分配给一个进程的存储空间的大小)及signal(控制进程对特别事件的响应)。第7章将介绍这些及其他系统调用。

存储管理模块控制存储分配。在任何时刻,只要系统没有足够的物理存储供所有进程使用,内核就在主存与二级存储之间对进程进行迁移,以便所有进程都得到公平的执行机会。第9章将描述存储管理的两个策略:对换与请求调页。对换进程有时被称为调度程序,因为它为进程进行存储分配的调度,并且影响到CPU调度程序的操作。然而,本书仍将称它为对换程序,以避免与CPU调度程序混淆。

调度程序(scheduler)模块把CPU分配给进程。该模块调度各进程依次运行,直到它们因等待资源自愿放弃CPU,或它们最近一次的运行时间超过一个时间量,从而内核抢占它们。于是调度程序选择最高优先权的合格进程投入运行;当原来的进程成为最高优先权的合格进程时,还会再次投入运行。进程间通信有几种形式,从事件的异步软中断信号到进程间消息的同步传输,等等。

最后,硬件控制负责处理中断及与机器通信。像磁盘或终端这样的设备可以在一个进程正在执行时中断CPU。如果出现这种情况,在对中断服务完毕之后内核可以恢复被中断了的进程的执行:中断不是由特殊的进程服务的,而是由内核中的特殊函数服务的,这些特殊函数是在当前运行的进程的上下文中被调用的。

本节将概述一些主要的内核数据结构,并且更详细地描述图2-1中给出的各模块的功能。

一个文件的内部表示由一个索引节点(inode)给出,索引节点描述了文件数据在磁盘上的布局,并且包含诸如文件所有者、存取许可权及存取时间等其他信息。“索引节点”这一术语是index node的缩写,并且普遍地用于UNIX系统的文献中。每个文件都有一个索引节点,但是它可以有几个名字,且这几个名字都映射到该索引节点上。每个名字都被称为一个联结(link)。当进程使用名字访问一个文件时,内核每次分析文件名中的一个分量,检查该进程是否有权搜索路径中的目录,并且最终检索到该文件所对应的索引节点。例如,如果一个进程调用

open( “/fs2/mjb/rje/sourcefile”,1);

则内核检查“/fs2/mjb/rje/sourcefile”所对应的索引节点。当一个进程建立一个新文件时,内核分配给它一个尚未使用的索引节点。正如我们很快就会看到的那样,索引节点被存储在文件系统中,但是当操控文件时,内核把它们读到内存(in-core)[1]索引节点表中。

内核还包含另外两个数据结构,文件表(file table)和用户文件描述符表(user file descriptor table)。文件表是一个全局核心结构,但用户文件描述符表对每个进程分配一个。当一个进程打开或建立一个文件时,内核在每个表中为相应于该文件的索引节点分配一个表项。这样一共有三种结构表——用户文件描述符表、文件表和索引结点表(inode table),用这三种结构表中的表项来维护文件的状态及用户对它的存取。文件表保存着文件中的字节偏移量——下一次读或写将从那里开始,并保存着对打开的进程所允许的存取权限。用户文件描述符表标识着一个进程的所有打开文件。图2-2表明了这三张表及它们之间的相互关系。对于系统调用open和系统调用creat,内核返回一个文件描述符(file descriptor),它是在用户文件描述符表中的索引值。当执行系统调用read和write时,内核使用文件描述符以存取用户文件描述符表,循着指向文件表及索引节点表表项的指针,从索引节点中找到文件中的数据。第4章和第5章将详细地描述这些结构,此刻,我们只要说使用这三张表可以实现对一个文件的不同程度的存取共享就够了。

图2-2 文件描述符表、文件表和索引节点表

UNIX系统把正规文件(regular file)及目录保存在诸如磁带或磁盘这样的块设备上。由于磁带和磁盘在存取时间上的差别,所以没有什么UNIX系统装置使用磁带实现它们的文件系统。今后,无盘工作站将用得很普遍。在无盘工作站中,文件被存放在一个远程系统上,并通过网络进行存取(见第13章)。然而,为简单起见,下面假设讨论的是有磁盘的系统。一套系统装置可以有若干物理磁盘设备,每个物理磁盘设备包含一个或多个文件系统。把一个磁盘分成几个文件系统可以使管理人员易于管理存储在那儿的数据。内核在逻辑级上只涉及文件系统,而不涉及磁盘,把每个文件系统都当作由一个逻辑设备号标识的逻辑设备(logical device)。由磁盘驱动程序实现逻辑设备(文件系统)地址与物理设备(磁盘)地址之间的转换。除非另有明确的说明,否则,本书在使用“设备”这一术语时总是意味着一个逻辑设备。

一个文件系统由一个逻辑块(logical block)序列组成,每个块都包含512、1024、2048 个字节或512个字节的任意倍数,这要依赖于系统实现。在一个文件系统中,逻辑块大小是完全相同的,但是在一个系统配置中的不同文件系统间逻辑块大小可以是不同的。使用大的逻辑块增加了在磁盘与主存之间的有效数据传送率,因为内核在每次磁盘操作中能传送较多的数据,所以只执行很少几次费时的操作。比如,一次从磁盘读1KB的读操作,会比读两次每次读512B的操作要快。然而,正如将在第5章中看到的那样,如果一个逻辑块太大,将失去有效的存储能力。为简单起见,本书将使用“块”这一术语表示一个逻辑块,并且它将假设一个逻辑块包含1KB数据,除非另有明确说明。

一个文件系统具有如下结构(图2-3):

图2-3 文件系统布局

本节将更进一步介绍进程子系统:先描述一个进程的结构及用于存储管理的若干进程数据结构;然后给出进程状态图的初步看法,并考虑状态转换中的各种问题。

一个进程是一个程序的执行,它是由一系列有格式字节组成的,这些有格式字节被解释成机器指令[以下被称为“正文(text)”]、数据和栈区(stack)。当内核调度各个进程使之执行时,这些进程看起来像是同时执行的。而且,可以有几个进程是一个程序的实例。一个进程循着一个严格的指令序列执行,这个指令序列是自包含的,并且不会跳转到别的进程的指令序列上。它读或写自己的数据和栈区,但它不能读或写其他进程的数据和栈区。进程通过系统调用与其他进程及外界进行通信。

用实际的术语来说,UNIX系统上的进程是被系统调用fork所创建的实体。除了0进程以外,每个进程都是被另一个进程执行系统调用fork时创建的。调用系统调用fork的进程是父进程(parent process),而新创建的进程是子进程(child process)。每个进程都有一个父进程,但一个进程可以有多个子进程。内核用各进程的进程标识号(process ID)来标识每个进程,进程标识号简称为进程ID(或PID,见第6章)。0进程是一个特殊进程,它是在系统引导时被“手工”创建的;当它创建了一个子进程(1进程)之后,0进程就变成对换进程。正如在第7章所解释的那样,1进程被称为init进程,是系统中其他每个进程的祖先,并且享有同它们之间的特殊关系。

用户对一个程序的源代码进行编译以建立一个可执行文件,可执行文件由以下几部分组成:

对于图1-3中的程序,可执行文件的正文是函数main与copy所生成的代码,其中,变量version是初始化数据(放在本程序中仅仅是为让它有初始化数据),数组buffer是未初始化的数据。系统Ⅴ的C编译程序版本在缺省时创建一个分离的正文段,但它支持一种选择,该选择允许数据段中包含程序指令,这是在系统的较老的版本中使用的。

在系统调用exec期间,内核把一个可执行文件装入主存中,被装入的进程至少由被称为正文区、数据区及栈区的三部分组成。正文区和数据区相应于可执行文件中的正文段和数据bss段。但是栈区是自动创建的,而且它的大小在运行时是被内核动态调节的。栈区由逻辑栈帧(stack frame)组成,当调用一个函数时栈帧被压入,当返回时栈帧被弹出。一个称为栈指针(stack pointer)的特殊寄存器指示出当前栈深度。一个栈帧包含着用于函数调用的参数、它的逻辑变量及为恢复先前的栈帧所需要的数据——其中包括函数调用时程序计数器的值及栈指针的值。程序代码包含管理栈增长的指令序列,并且当需要时内核为栈区分配空间。在图1-3所示的程序中,当main被调用时(按惯例,在每个程序中被调用一次),函数main中的参数argc和argv、变量fdold和fdnew就会在栈区上出现。并且,无论何时函数copy被调用,copy中的参数old与new及变量count都在栈区上出现。

因为UNIX系统中的一个进程能在两种态——核心态(kernel mode)或用户态(user mode)下执行,所以UNIX系统中核心栈(kernel stack)与用户栈(user stack)是分开的。用户栈含有在用户态下执行时函数调用的参数、局部变量及其他数据。图2-4中的左半部表明一个进程在copy程序中做系统调用write时进程的用户栈。进程启动过程(此过程是包含在库中的)用两个参数调用函数main,并将第1帧压入用户栈;第1帧含有main的两个局部变量的空间。然后main用两个参数old与new调用copy,并将第2帧压入用户栈中,第2帧包含局部变量count的空间。最后,进程通过调用库函数write来引用系统调用write,每个系统调用都在系统调用库中有一个入口点;系统调用库按汇编语言编码并包含一个特殊的trap指令,当该指令被执行时,它引起一个“中断”,从而导致硬件转换到核心态。一个进程调用一个特定的系统调用库的入口点,正像它调用任何函数一样。对于库函数也要创建一个栈帧。当进程执行特定的指令时,它将处理机执行态转换到核心态,执行内核代码,并使用核心栈。

核心栈中含有在核心态下执行的函数的栈帧。核心栈上的函数及数据项涉及的是内核中的而不是用户程序中的函数和数据,但它的构成与用户栈的构成相同。当一个进程在用户态下执行时,它的核心栈为空。图2-4的右半部给出了一个在copy程序中执行系统调用write的进程的核心栈的表项。在以后的章节中对系统调用write进行详细讨论时,再叙述各算法名称。

图2-4 程序copy的用户栈及核心栈

每个进程在内核进程表(process table)中都有一个表项,并且每个进程都被分配一个u区[3],u区包含仅被内核操纵的私用数据。进程表包含(或指向)一个本进程区表(per process region table),本进程区表的表项指向区表(region table)的表项。一个区是进程地址空间中连续的区域,如正文区、数据区及栈区等。区表登记项描述区的属性,诸如它是否包含正文或数据,它是共享的还是私用的,以及区的“数据”位于主存的何处,等等。从本进程区表到区表的额外伺接级允许彼此独立的进程对区的共享。当一个进程调用系统调用exec时,在释放了进程一直在使用着的老区之后,内核为它的正文、数据和栈分配新区。当一个进程调用系统调用fork时,内核拷贝老进程的地址空间,在可能时允许进程对区共享,否则再建立一个物理拷贝。当一个进程调用系统调用exit时,内核释放进程使用过的区。图2-5展示了一个运行中的进程的有关数据结构:进程表指向本进程区表,本进程区表有指向该进程的正文区、数据区或栈区的区表表项的指针。

图2-5 进程的数据结构

进程表表项及u区包含进程的控制信息和状态信息。u区是进程表表项的扩展,第6章将介绍这两个表的区别。在后几章中讨论的进程表中的字段如下。

u 区包含的是用来描述进程的信息,这些信息仅当进程正在执行时才是可存取的。重要的字段如下。

内核能直接存取正在执行的进程的u区的字段,但不能存取其他进程的u区的字段。在其内部,内核引用结构变量u以存取当前正在运行的进程的u区,并且当另一进程执行时,内核重新安排它的虚地址空间,以使结构变量u引用的是新进程的u区。由于这一实现方式给出了从u区到它的进程表表项的指针,所以内核很容易识别出当前进程。

1.进程上下文

一个进程的上下文(context)包括被进程正文所定义的进程状态、进程的全局用户变量和数据结构的值、它使用的机器寄存器的值、存储在它的进程表项与u区中的值以及它的用户栈和核心栈的内容。操作系统的正文和它的全局数据结构被所有的进程所共享,因而不是进程上下文的一部分。

当执行一个进程时,系统被说成在该进程的上下文中执行。当内核决定它应该执行另一个进程时,它做一次上下文切换(context switch),以使系统在另一个进程的上下文中执行。正如将要看到的,内核仅允许在指定条件下进行上下文切换。当进行上下文切换时,内核保留足够信息,为的是以后它能切换回第一个进程,并恢复它的执行。类似地,当从用户态移到核心态时,内核保留足够信息以便它后来能返回到用户态,并从它的断点继续执行。在用户态与核心态之间的移动是态的改变,而不是上下文切换。再看一下图1-5,当它把上下文从进程A变成进程B时,内核做的是上下文切换;当发生从用户态到核心态或从内核态到用户态的改变时,所改变的是执行态,但仍在同一个进程(例如进程A)的上下文中执行。

内核在被中断了的进程的上下文中对中断服务,即使该中断可能不是由它引起的。被中断的进程可以是正在用户态下执行的,也可以是正在核心态下执行的。内核保留足够的信息以便它在后来能恢复被中断了的进程的执行,并在核心态下对中断进行服务。内核并不产生或调度一个特殊进程来处理中断。

2.进程状态

一个进程的生存周期能被划分为一组状态,每个状态都具有一定的用来描述该进程的特点。第6章将描述所有的进程状态,但现在了解如下状态是重要的:

(1)进程正在用户态下执行。

(2)进程正在核心态下执行。

(3)进程未正在执行,但是它已准备好运行——一旦调度程序选中了它,它就可以投入运行。很多进程可以处于这一状态,而调度算法决定哪个进程将成为下一个执行的进程。

(4)进程正在睡眠。当进程再也不能继续执行下去的时候,如正在等候I/O完成时,进程使自己进入睡眠状态。

因为任何时刻一个处理机仅能执行一个进程,所以至多有一个进程可以处在第一种状态和第二种状态。这两个状态相应于两种执行态:用户态与核心态。

3.状态转换

以上描述的进程状态给出了进程的一种静态观点,但是,实际上,各个进程是按照明确定义的规则连续地在各种状态间移动的。状态转换图是一个有向图,它的节点表示一个进程能进入的状态,而它的边表示引起一个进程从一种状态到另一种状态的事件。如果从第一种状态到第二种状态存在着一条边,则这两种状态之间的状态转换是合法的。可以从一种状态发出多个转换,但是,就处于某种状态的一个进程来说,依赖于所发生的系统事件,完成一个且只完成一个转换。图2-6给出了上述定义的进程状态的状态转换图。

图2-6 进程状态及转换

如前所述,在一个分时方式中,几个进程能同时执行,并且它们可能都要在核心态下运行。如果对它们在核心态下的运行不加以限制,则它们会破坏全局核心数据结构中的信息。通过禁止任意的上下文切换和控制中断的发生,内核可保护它们的一致性。

仅当进程从“核心态运行”状态转移到“在内存中睡眠”状态时,内核才允许上下文切换。在核心态下运行的进程不能被其他进程所抢占,因此内核有时被称为不可抢先(non-preemptive)的,虽然系统并不抢占处于用户态下的进程。因为内核处于不可抢先状态,所以内核可保持它的数据结构的一致性,从而解决了互斥(mutual exclusion)问题——保证在任何时刻至多一个进程执行临界区代码。

比如,让我们考虑图2-7中的示例代码。该代码段要把其地址在指针变量bp1中的数据结构,插入双向链表中地址在指针变量bp中的数据结构之后。如果当内核执行这一代码段时系统允许上下文切换,则会发生如下情形。假设直到注释出现之前内核执行该代码,然后做一个上下文切换,这时双向链表处于非一致性状态:结构bp1一半被插在该链表上,另一半在该链表外。如果进程沿着向前的指针,则它能在该链表上找到bp1;但如果沿着向后的指针,则它不能找到bp1(图2-8)。如果其他进程在原来的进程再次运行之前操控链表上的这些指针,则双向链表结构会被永久性地毁坏。UNIX系统通过一个进程在核心态下执行时不允许上下文切换来防止这种情况发生。如果一个进程进入睡眠从而允许上下文切换,则必须使内核算法的编码实现能够确保系统数据结构处于安全、一致的状态。

图2-7 创建双链表的示例代码

图2-8 由于上下文切换而造成的不正确链表

能引起内核数据的非一致性的有关问题是中断的处理。中断处理能改变内核状态信息。举例来说,如果内核正在执行图2-7中的代码,当执行到注释行时接收了一个中断,并且中断处理程序是如前所述的那样操纵指针,则中断处理程序就会破坏该链表中的信息。若规定在核心态下执行时系统禁止所有的中断,就可以解决这一问题。但是这可能会使中断的服务推迟,或者可能会损害系统吞吐量,为此,改为当进入代码临界区(critical region)时内核把处理机执行级提高,以禁止中断。如果任意的中断处理程序的执行会导致一致性问题的话,那么代码段是临界的。比如,如果一个磁盘中断处理程序操纵图中的缓冲区队列,则内核操纵缓冲区队列的那个代码段是关于磁盘中断处理程序的代码临界区。临界区应小且不经常出现,以便系统吞吐量不大会被它们的存在所影响。其他操作系统解决这一问题的方法是:规定在系统状态下执行时封锁所有的中断,或者采用完善的加锁方案以保证一致性。第12章将面对多处理机系统再回过头来讨论这一问题。这里所给出的解答在那时就不够了。

现在让我们回顾一下本节的内容:内核通过仅当一个进程使自己进入睡眠时才允许上下文切换,以及通过禁止一个进程改变另一个进程的状态来保护它的一致性。它还在代码临界区周围提高处理机执行级,以封锁其他能引起非一致性的中断。进程调度程序定期地抢占用户态下的进程执行,以使进程不能独占式地使用CPU。

4.睡眠与唤醒

一个在核心态下执行的进程在决定它对系统事件的反应上它打算做什么方面有很大的自主权。进程能互相通信并且“建议”各种可供选择的方法,但由它们自己做出最后的决定。正如我们将要看到的,存在着一组进程在面临各种情况时所应服从的规则,但是每个进程最终都是自主地遵循这些规则的。例如,当一个进程必须暂停它的执行(“进入睡眠”)时,它能自由地按自己的意图去做。然而,一个中断处理程序不能睡眠,因为如果中断处理程序能睡眠,就意味着被中断的进程会被投入睡眠。

进程会因为它们正在等待某些事件的发生而进入睡眠,例如:等待来自外围设备的I/O完成;等待一个进程退出;等待获得系统资源;等等。当我们说进程在一个事件上睡眠时,这意味着,直到该事件发生时,它们一直处于睡眠状态;当事件发生时它们被唤醒,并且进入“就绪”状态。很多进程能同时睡眠在一个事件上;当一个事件发生时,由于这个事件的条件再也不为真了,所以所有睡眠在这个事件上的进程都被唤醒。当一个进程被唤醒时,它完成一个从“睡眠”状态到“就绪”状态的状态转换,对于随后的调度来说,该进程就是个合格者了,但它并不立即执行。睡眠进程不耗费CPU资源;内核并不是经常去查看一个进程是否仍处于睡眠状态,而是等待事件的发生,那时把进程唤醒。

举例来说,一个在核心态执行的进程有时必须锁住一个数据结构,如果发生后来它进入睡眠的情况,其他企图操纵该上了锁的数据结构的进程必须检查上锁情况,并且因为别的进程已经占有该锁,则它们去睡眠。内核按如下方式实现这样的锁:

while(条件为真)
      sleep(事件:条件变为假);
置条件为真;

它按如下方式解锁并唤醒睡眠在该锁上的所有进程:

置条件为假;
wakeup(事件:条件变为假);

图2-9描绘了三个进程A、B、C为一个上了锁的缓冲区进行竞争的情况。睡眠的条件是缓冲区处于上锁状态。在任一时刻只能有一个进程在执行,它发现缓冲区是上了锁的,就在缓冲区变为开锁状态的事件上等待。终于,缓冲区的锁解开了,所有的进程被唤醒并且进入“就绪”状态。内核最终选择一个进程(比如说B)执行。进程B执行“while”循环,发现缓冲区处于开锁状态,于是为缓冲区上锁,并且继续执行。如果后来进程B在为缓冲区解锁之前再次去睡眠(例如等候I/O操作的完成),则内核能调度其他进程去运行。如果它选择了进程A,进程A执行“while”循环,发现缓冲区处于上锁状态,那么它就再次去睡眠。进程C可以做同样的事情。最后,进程B醒来并为缓冲区解锁,允许进程A也允许进程C存取缓冲区。因此,“while-sleep”循环保证至多一个进程能获得对资源的存取。

第6章将极其详细地介绍睡眠与唤醒的算法。在此期间它们应被考虑成是“原子的”:一个进程瞬时地进入睡眠状态,并停留在那儿直至它被唤醒。在它睡眠之后,内核调度另一个进程去运行,并切换成后者的上下文。

图2-9 在一个锁上睡眠的多个进程

大多数内核数据结构都占据固定长度的表而不是动态地分配空间,这一方法的优点是内核代码简单。但是它限制了一种数据结构的表项的数目,即为系统生成时原始配置的数目。如果在系统操作期间,内核用完了一种数据结构的表项,则它不能动态地为新的表项分配空间,而是必须向发出请求的用户报告一个错误。此外,如果内核被配置得具有不可能用完的表空间,则因不能用于其他目的而使多余的表空间浪费了。然而,一般都认为内核算法的简单性比挤出主存中每一个仅有的字节的必要性更重要一些。算法通常使用简单的循环来寻找表中的空闲表项,这是一个较易于理解的方法,而且有时比复杂的分配方案更为有效。

管理进程可以非严格地归入为用户团体的公共福利提供各种功能的那类进程。这些功能包括磁盘格式化、新文件系统的创建、修复被破坏的文件系统、内核调试及其他。从概念上说,管理进程与用户进程没有区别:它们都使用为一般用户团体可用的相同的一组系统调用。它们仅在被允许的权限与特权上区别于一般用户进程。例如,文件存取权限允许管理进程操纵对一般用户来说禁止进入的文件。在内部,内核把一个称为超级用户(superuser)的特殊用户区别出来,赋予它特权,这一点我们即将看到。通过履行一次注册-口令序列或通过执行特殊程序可使一个用户成为超级用户。超级用户特权的其他用途将在随后的章节中研究。简而言之,内核不识别一个分离的管理进程类。

本章描述了内核的体系结构:它的两个主要成分是文件子系统与进程子系统。文件子系统控制用户文件中数据的存储与检索。文件被组织到文件系统中,而文件系统被看作一个逻辑设备。像磁盘这样的一个物理设备能包含几个逻辑设备(文件系统)。每个文件系统都有一个用来描述文件系统的结构和内容的超级块,并且文件系统中的每个文件都由索引节点描述,索引节点给出了文件的属性。操控文件的系统调用通过索引节点来实现其功能。

进程有各种状态,并且按照明确定义的转换规则在这些状态之间转移。特别之处在于,在核心态下执行的进程能暂停它们的执行而进入睡眠状态,但是没有哪一个进程能把另一进程投入睡眠状态。内核是不可被抢占的,这意味着,一个在核心态下执行的进程将连续执行,直至它进入睡眠状态或直至它返回到用户态下执行时为止。内核通过实施不可抢占策略以及通过在执行代码临界区时封锁中断来维护它的数据结构的一致性。

本章的其余部分详细描述了图2-1所示的子系统及它们的交互作用。从文件子系统开始,继之以进程子系统。下一章将涉及高速缓冲问题,并描述在第4章、第5章和第7章要介绍的算法中所使用的缓冲区分配算法。第4章考查文件系统的内部算法,包括索引节点的操控、文件的结构及路径名到索引节点的转换。第5章解释若干系统调用,例如,系统调用open、close、read及write,这些系统调用使用了第4章中的算法来访问文件系统。第6章论述进程上下文的基本思想及其地址空间;第7章涉及有关进程管理及使用第6章的算法的系统调用;第8章介绍进程调度;第9章讨论存储管理算法;第10章讲的是设备驱动程序,直到这时终端驱动程序与进程管理之间的相互关系才能被解释;第11章介绍了进程间通信的某些形式;最后两章涉及若干高级专题,包括多处理机系统与分布式系统。

1.考虑如下命令序列:

grep main a.c b.c c.c > grepout &
wc −1 < grepout &
rm grepout &

每一命令行尾部的“&”都通知shell在后台运行这些命令,并且它能并行地执行每个命令。为什么这不等价于如下的命令行?

grep main a.c b.c c.c| wc −1

2.考虑图2-7中的内核代码示例。假设当代码到达注释处时发生上下文切换,并且假设另一进程通过执行如下代码而从链表中摘掉一个缓冲区:

remove(qp)
    struct queue *qp;
{
    qp— >forp — >backp = qp — >backp; 
    qp— >backp— >forp=qp — >forp;
    qp— >forp=qp— >backp = NULL;
}

考虑三种情况:

这三种情况中哪种是原来的进程执行完注释以后的代码时链表的状态?

3.如果内核试图唤醒睡眠在一个事件上的所有进程,但是在唤醒时没有进程睡眠在那个事件上,那么会发生什么情况?

[1] “core”这一术语指的是机器的原始存储,不是指硬件技术。

[2] bss这一名字来自IBM 7090机的汇编伪运算符,它代表“block started by symbol”。

[3] u区中的u代表用户。u区的另一个名称是ublock;本书则总是称它为u区。


相关图书

Linux常用命令自学手册
Linux常用命令自学手册
Linux后端开发工程实践
Linux后端开发工程实践
庖丁解牛Linux操作系统分析
庖丁解牛Linux操作系统分析
轻松学Linux:从Manjaro到Arch Linux
轻松学Linux:从Manjaro到Arch Linux
Linux高性能网络详解:从DPDK、RDMA到XDP
Linux高性能网络详解:从DPDK、RDMA到XDP
跟老韩学Linux架构(基础篇)
跟老韩学Linux架构(基础篇)

相关文章

相关课程