UNIX 分时系统
作者:互联网
UNIX 分时系统
摘要
UNIX 是一套通用的、多用户、交互式操作系统,被用于数字设备公司 (DEC) 的 PDP-11/40 和 11/45 计算机。它拥有着即使是较大型的操作系统也很少见的功能,包括:(1)集成了可装卸分卷的层次文件系统;(2)互相兼容的文件、设备和进程间通信;(3)能够初始化异步进程;(4)系统命令语言可根据用户选择;(5)超过 100 个子系统,包括十几种语言。本文讨论了文件系统和用户命令界面的特征和实现。
1. 引言
UNIX 有三个版本。最早的版本(约 1969-70 年)在数字设备公司的 PDP-7 和 -9 计算机上运行。第二个版本在没有保护的 PDP-11/20 计算机上运行。本文只介绍 PDP-11/40 和 /45 [1] 系统,因为它更现代化,它与旧的 UNIX 系统的诸多差别,只在于重新设计了一些有缺陷或缺失的功能。
自 PDP-11 UNIX 于 1971 年 2 月投入实践以来,用在了大约 40 个设备上;这些设备所搭载的系统一般都比本文所述的要小。它们当中大多数都用于诸如编写和格式化专利申请和其他文字材料、收集和处理贝尔系统内各种交换机的故障数据、以及记录和检查电话服务单之类的应用。我们自己的设备主要用于操作系统、语言、计算机网络和其他计算机科学课题的研究,也用于文档编写。
也许 UNIX 最重要的成就是表明,一个强大的交互式操作系统并不需要昂贵的设备或人力。UNIX 可以在成本仅为 40,000 美元的硬件上运行,而在主要系统软件上只花了不到 2 人年的工作量。虽然 UNIX 所包含的一些功能,甚至在一些大得多的系统中也很少提供,但我们希望 UNIX 的用户仍能意识到,该系统最重要的特点是简单、优雅和易于使用。
除了系统本身外,UNIX 下的主要程序有:汇编器、基于 QED 的文本编辑器 [2]、链接加载器、符号调试器、带有类型和结构的类似 BCPL [3] 的语言编译器(C)、BASIC 方言的解释器、文本格式化程序、Fortran 编译器、Snobol 解释器、自上而下的编译器 - 编译器(TMG)[4]、自下而上的编译器 - 编译器(YACC)、表格字母生成器、宏处理器(M6)[5] 和换行索引程序。
此外,还有一系列的维修、实用、娱乐、新奇的程序。这些程序都是本地编写的。值得注意的是,该系统是完全自给自足的。所有的 UNIX 软件都是在 UNIX 下维护的;同样,UNIX 文档也是由 UNIX 编辑器和文本格式化程序生成和格式化的。
2. 硬件和软件环境
安装我们 UNIX 的 PDP-11/45 是一台 16 位字(8 位字节)的计算机,核心内存为 144KB;UNIX 占用 42K 字节。然而,这个系统包括了非常多的设备驱动程序,并为 I/O 缓冲区和系统表分配了大量的空间;一个能够运行上述软件的最小系统,总共只需要 50K 字节的核心。
PDP-11 有一个 1M 字节的固定头磁盘,用于文件系统的存储和交换;四个移动头磁盘驱动器,每个驱动器在可装卸磁盘盒上提供 2.5M 字节;以及一个使用 40M 字节可装卸磁盘组的移动头磁盘驱动器。此外,还有一个高速纸带读卡器-打孔机、九轨磁带和 D-磁带(一种可对单个记录进行寻址和重写的磁带设施)。除控制台打字机外,还有 14 个变速通信接口,连接到 100 系列数据集上,还有一个 201 数据集接口,主要用于向公用行式打印机拼接打印输出。还有一些独一无二的设备,包括 Picturephone® 接口、语音应答装置、语音合成器、照相排字机、数字交换网络,以及在泰克 611 存储管显示器上生成矢量、曲线和字符的卫星 PDP-11/20。
UNIX 软件大部分是用前面提到的 C 语言编写的 [6]。该操作系统的早期版本是用汇编语言编写的,但在 1973 年夏天,我们用 C 语言重写了一版。新系统的规模比旧系统大三分之一。由于新系统不仅更容易理解和修改,而且还包括许多功能上的改进,包括多程序编程和在几个用户程序之间共享重入代码的能力,我们认为这种体积的增加是相当可以接受的。
3. 文件系统
UNIX 最重要的工作是提供一个文件系统。从用户的角度来看,文件有三种:普通磁盘文件、目录和特殊文件。
3.1 普通文件
普通文件包含了用户放在上面的任何信息,例如符号或二进制(对象)程序。系统不会对普通文件的结构有任何预设。文本文件只是由一串字符组成,行与行之间用换行符划分。二进制程序是程序开始执行时将出现在核心内存中的字序列。一些用户程序所操作的文件更结构化:汇编器生成和加载器处理的对象文件都具有特定的格式。但是,文件的结构是由使用它们的程序控制的,而不是由系统控制的。
3.2 目录
目录提供了文件名和文件本身之间的映射,从而在整个文件系统中形成了一种结构。每个用户都有一个自己的文件目录;他也可以创建子目录,将各组文件放进不同的目录,以便处理。目录在形式上与普通的文件完全一样,只是无权限的程序不能对其进行写入,也就是说,目录的内容是由系统控制的。但是,任何有适当权限的用户都可以像读其他文件一样读目录。
系统会维护几个目录供自己使用。其中一个是根(root)目录。系统中的所有文件都可以根据路径中一连串的目录找到。这种搜索的起点往往是根目录。另一个系统目录包含所有常规使用的程序;也就是所有的命令(commands)。然而,正如我们将看到的那样,一个程序要想被执行,并不一定要放在这个目录中。
文件以不超过 14 个字符的序列命名。当向系统指定文件名时,可以采用路径名(path name)的形式,路径名是由斜线 “/” 分隔的目录名序列,以文件名结束。如果该序列以斜线开头,则从根目录开始搜索。文件名 /alpha/beta/gamma 会让系统在根目录下搜索 alpha,然后在 alpha 目录下搜索 beta,最后在 beta 目录下找到 gamma。gamma 可能是一个普通的文件,一个目录,或者一个特殊的文件。作为一种极端情况,名称 “/” 指的是根目录本身。
不以 “/” 开头的路径名则让系统从用户的当前目录开始搜索。因此,alpha/beta 这个名字指定了当前目录的子目录 alpha 中名为 beta 的文件。最简单的一类文件名,例如 alpha,指的是一个本身就在当前目录中的文件。作为另一种极端情况,空文件名指的就是当前目录。
同一个非目录文件可以出现在若干个目录中,名称也可以不一样。这种特性称为链接技术(linking);指向这个文件的目录项有时也被称为链接(link)。UNIX 与其他允许链接的系统不同的是,所有指向同一文件的链接都具有同等的地位。也就是说,一个文件并不存在于某一特定的目录中;文件的目录项仅仅由它的名称和一个指向真实文件描述信息的指针组成。因此,一个文件独立于任何目录项而存在,尽管在实践中,一个文件会与最后一个链接一起消失。
每个目录至少有两项数据。目录中的名称指的是目录本身。因此,一个程序可以在不知道其完整路径名的情况下,以 “.” 为名读取当前目录。按照惯例,“…” 指的是展现该目录的父目录,也就是指创建它的目录。
目录结构被限制为有根树的形式。除了特殊的条目 “.” 和“…”之外,每个目录都必须作为一个条目出现在另一个目录中,也就是它的父目录中。这样做是为了在编写程序时能更简单地访问该目录结构的子树,但更重要的是,避免了层次结构的局部分离。如果允许任意链接到目录,那么就很难检测到从根目录到目录的最后一个连接何时被切断。
3.3 特殊文件
特殊文件构成了 UNIX 文件系统最不寻常的特征。UNIX 支持的每个 I/O 设备都至少与一个这样的文件相关联。特殊文件的读写就像普通的磁盘文件一样,但读写的请求会导致相关设备的激活。每个特殊文件的目录项都在目录 /dev 中,同时也可以像普通文件一样链接到这些文件中的任意一个。因此,例如,要打纸带(punch paper tape),可以写在文件 /dev/ppt 上。每个通信线路、每个磁盘、每个磁带机以及物理核心内存都存在特殊的文件。当然,活动的磁盘和核心特殊文件是受到保护的,不能随意访问。
这样处理 I/O 设备有三方面的好处:文件和设备 I/O 尽可能地相似;文件名和设备名的语法和含义相同,因此,以文件名作为参数的程序也可以传入设备名;最后,特殊文件与普通文件有着相同的保护机制。
3.4 装卸式文件系统
虽然文件系统的根总是存储在同一个设备上,但整个文件系统的层次结构并不一定要保留在这个设备上。有一个 mount 系统请求,它有两个参数:一个是现有普通文件的名称,一个是直接访问的特殊文件的名称,该特殊文件所关联的存储卷(如磁盘组)应该具有独立文件系统结构,且有着它自己的目录层次。mount 的效果是使原先对普通文件的引用改为对可装卸卷上文件系统的根目录的引用。实际上,mount 用一个全新的子树(存储在可装卸卷上的层次结构)取代了当前层次结构树的一个叶子(普通文件)。挂载后,可装卸卷上的文件和永久文件系统中的文件几乎没有区别。以我们自己的设备为例,根目录安装在固定头磁盘上,而包含用户文件的大盘驱动器由系统初始化程序挂载,四个小型磁盘驱动器可供用户挂载自己的磁盘组。通过在其相应的特殊文件上写入,生成可挂载的文件系统。有个实用程序可以创建一个空的文件系统,也可以直接复制一个现有的文件系统。
处理不同设备上文件的规则是相同的,但有一个例外:一个文件系统层次结构与另一个文件系统层次结构之间不得有任何链接。强制对此进行限制,是为了避免繁琐的簿记工作,否则,当可装卸卷最终被卸载时,就需要确保删除这些链接。需要特别指出,在所有文件系统的根目录中,不管是否可装卸,“…” 的名称都是指目录本身,而不是指其父目录。
3.5 保护
虽然 UNIX 的访问控制方案相当简单,但它有一些不同寻常的特点。系统中的每个用户都被分配了一个独特的用户识别号。当一个文件被创建时,它被标记为其所有者的用户 ID。对于新文件,还给出了一组 7 位的保护位。其中 6 位分别指定了文件所有者和所有其他用户的读、写和执行权限。
如果第七位为真,则每当文件作为程序执行时,系统会将当前用户的用户 ID 临时改为该文件创建者的用户 ID。这种用户 ID 的改变只在执行调用它的程序时有效。set-user-ID 功能提供了一种拥有特权的程序,这些程序能使用其他用户无法访问的文件。举例来说,一个程序可能会保有一个会计文件,该文件除了程序本身之外,既不应被读取,也不应被更改。如果该程序的 set-user-ID 位被打开,它就可以访问该文件,而该程序的使用者所执行的其他程序,则不具有该文件的访问权限。由于任何程序都可以拿到其调用者的实际用户 ID,具有 set-user-ID 特权的程序也可以根据需要取得其调用者的权限。这种机制可以让用户执行一些经过谨慎设计的、调用了特权系统入口(system entry)的命令。例如,有一个只有 “超级用户”(如下)才能调用的系统入口,它可以创建一个空目录。如上所述,目录应该有 “.” 和 “…” 两个条目。创建目录的命令由超级用户拥有,并设置了 set-user-ID 位。在检查了其调用者对创建指定目录的授权后,创建目录,并为 “.” 和 “…” 设置条目。
由于任何人都可以在自己的文件上设置 set-user-ID 位,所以这种机制一般不需要管理员干预。例如,这种保护方案很容易解决 [7] 中提出的 MOO 会计问题(译注:实际上是指一个游戏的积分系统不被一般用户任意篡改的问题)。
系统中有一个特殊的用户 ID(即 “超级用户” 的 ID)不受通常的文件访问限制;因此(例如)可以绕开保护系统不必要的干扰,直接编写用于转储和重新加载文件系统的程序。
3.6 I/O 调用
我们通过对 I/O 的系统调用的设计,消除了各种设备和访问方式的差异。没有 “随机” 和顺序 I/O 的区别,系统也不强迫规定逻辑记录的大小。普通文件的大小是由写进文件的最高字节数决定的,没有必要也不可能预先确定文件的大小。
为了说明 UNIX 中 I/O 的精髓,下面总结了一些基本的调用。我们用一种匿名语言指明所需的参数,而不涉及机器语言编程的复杂性。对系统的每一次调用都有可能导致错误返回,为了简单起见,在调用序列中不做表示。
要读取或写入一个假定已经存在的文件,必须通过以下调用打开它:
filep = open(name, flag)
name 表示文件的名称,可以是任意的一个路径名。flag 参数表示文件是读、写还是 “更新”,即同时读和写。
返回值 filep 被称作文件描述符。它是一个小整数,作为该文件的标识,用于后续调用读、写或其他操作。
要创建一个新的文件或完全重写一个旧的文件,可以使用 create 系统调用。当指定的文件不存在时,它会创建一个新文件,若存在,则将旧文件截至零长度。create 也会打开新建的文件以供写入,就像 open 那样,会返回一个文件描述符。
文件系统中没有用户可见的锁,也没有限制打开一个文件进行读写的用户数量;虽然当两个用户同时对一个文件进行写入时,文件的内容有可能会错乱,但在实际操作中,并没有出现困难。我们认为,在我们的环境中,锁既不必要,也不足以防止同一文件中各用户之间的干扰。说它不必要,是因为我们面对的并不是若干个独立的进程维护的大型单文件数据库。说它不足以,则是因为一般意义上的锁,即阻止一个用户在另一个用户正在读取的文件上写入,仍不能防止混乱,例如,当两个用户都在编辑同一个文件,但他们的编辑器会先将文件复制一份出来。
应该说,当两个用户同时进行在同一文件上写东西、在同一目录下创建文件或删除对方打开的文件等相互干扰的活动时,系统有充分的内部互锁来维持文件系统的逻辑一致性。
除了下面的说明,读和写是都是连续的。这意味着,如果文件中的某个字节是最近被写入(或读取)的,那么下一个 I/O 调用就会隐式地指向该字节的下一个字节。对于每一个打开的文件,都有一个指针,由系统维护,它表示下一个要读或写的字节。如果读或写了 n 个字节,指针就会前进 n 个字节。
一个文件打开后,可以作如下的调用:
n = read(filep, buffer, count)
n = write(filep, buffer, count)
在 filep 代表的文件和 buffer 代表的字节数组之间,会传输至多 count 个字节的数据。返回值 n 是实际传输的字节数。在写的情况下,n 与 count 会一致,除非一些特殊的情况,如 I/O 错误或特殊文件的物理媒介终止;而在读的情况下,即使不出现异常,n 也可以小于 count。如果读指针很接近文件的末端,以至于读取 count 个字符会导致读到末端之外,那么传输的字节数会让指针刚好抵达文件末端;另外,对于打字机类的设备肯定不会读取超过一行的输入。当 read 调用返回的 n 等于零时,表示文件的结束。对于磁盘文件,当读指针前进到文件当前的大小时,就会出现这种情况。而对于打字机,可以通过由该打字机规定的转义序列来生成文件结束标识(EOF)。
写入到文件的字节只会改变由写指针的位置和 count 计数所指定的那些字节,文件的其他部分保持不变。如果最后一个写入的字节位于文件的末端之外,文件将按需进行扩充。
要随机(直接访问)I/O,只需将读或写的指针移动到文件中的适当位置即可:
location = seek(filep, base, offset)
与文件 filep 相关联的指针,将根据起点 base,从文件的开头、中间或结尾,移动 offset 个字节。offset 可以是负数。对于某些设备 (如纸带和打字机),seek 调用会被忽略。指针距离文件开头的实际偏移量将返回在 location 中。
3.6.1 其他 I/O 调用
还有几个与 I/O 和文件系统有关的系统入口,这里暂不讨论。例如:关闭文件、获取文件的状态、改变文件的保护模式或所有者、创建目录、生成现有文件的链接、删除文件。
4. 文件系统的实现
如上文 §3.2 所述,一个目录项只包含一个关联文件的名称和一个指向文件本身的指针。这个指针是一个整数,称为文件的 i-number(index number)。当访问某个文件时,它的 i-number 将被用作索引,对一个由该目录所在设备的已知区域内所保存的系统表(即 i-list)进行查找。由此找到的条目(文件的 i-node)包含的文件描述如下:
- 所有者
- 保护位
- 物理磁盘或磁带存放文件内容的地址
- 文件大小
- 最后修改时间
- 文件的链接数,即该文件在目录中出现的次数
- 一个二进制位,表明该文件是否为目录
- 一个二进制位,表明该文件是否为特殊文件
- 一个二进制位,表明该文件是 “大文件” 还是 “小文件”
open 或 create 系统调用的目的是通过搜索显式或隐式命名的目录,将用户给出的路径名转换成 i-number。一旦文件被打开,它的设备、i-number 和读写指针就会被存储在一张系统表中,该表由 open 或 create 返回的文件描述符进行索引。因此,后续对文件调用读或写时提供的文件描述符,可以很容易地关联到访问文件所需的信息。
创建一个新的文件时,会给它分配一个 i-node,并建立一个包含文件名和 i-node编号(译注:即 i-number)的目录项。要链接到一个现有的文件,需要创建一个带有新名称的目录项,从原来的文件条目中复制 i-number,并递增 i-node 的链接数(link-count)字段。移除(删除)一个文件,则是通过递减其目录项所指向的 i-node 的链接数,并删除该目录项。如果链接数减到 0,文件占用的任何磁盘块都会被释放,i-node 也会被解除分配。
任何具有文件系统的固定或移动磁盘上的空间都被划分为若干 512 字节的块,逻辑地址从 0 分配到上限,具体上限取决于设备。在每个文件的 i-node 上有一块区域可存放 8 个设备地址。一个(非特殊)小文件 可直接填入不超过 8 个的块;在这种情况下,i-node 上存的是块本身的地址。对于(非特殊)大文件,8 个设备地址中的每一个都可以指向间接的块,一个间接块可存放 256 个块地址用来组成文件。这种文件能够大到 8⋅256⋅512 即 1,048,576(2^20)个字节。
上述讨论适用于普通文件。当对一个在 i-node 中提示为特殊文件的文件发出 I/O 请求时,后 7 个设备地址字是不重要的,该列表被解释为一对构成内部设备(device)名称的字节。这些字节分别指定了设备类型和子设备号。设备类型表示该设备上的 I/O 将由哪种系统程序处理;子设备号则用于选择,例如,选择连接到某一控制器上的磁盘驱动器、选择数个打字机接口中的一个。
在这种环境下,mount 系统调用 (§3.4) 的实现非常简单。mount 维护了一个系统表(译注:一个映射关系),它的自变量是 mount 过程中指定的普通文件的 i-number 和设备名,对应的值是指定的特殊文件的设备名。在 open 或 create 过程中扫描路径名时,会对每一组 (i-number,device) 进行搜索,如果发现匹配,i-number 就会被替换成 1(也就是所有文件系统中根目录的 i-number),设备名则替换成表中对应的值。
在用户看来,文件的读和写都是同步的,没有缓冲。也就是说,在 read 调用返回后,数据立即可以使用,反之,在 write 之后,用户的工作空间可以重新使用。实际上系统维护了一个相当复杂的缓冲机制,大大减少了访问文件所需的 I/O 操作次数。下面假设进行了一次 write 调用,指定传输一个字节。
UNIX 将搜索各个缓冲区,查看受影响的磁盘块当前是否驻留在核心内存中;如果没有,则从设备中读入。然后,受影响的字节会在缓冲区中被替换,并在待写块的列表中新增一项。此后 write 调用便直接返回,尽管实际的 I/O 可能要到以后才会完成。相反,如果是读取一个字节,系统会判断该字节所在的二级存储块是否已经在系统的某个缓冲区中;如果是,则可以立即返回该字节。如果没有,则将该块读入缓冲区,并挑出该字节。
一个以 512 字节为单位读写文件的程序比每次只读或写一个字节的程序有优势,但收益并不是很大,它主要来自于避免系统开销。一个不进行大容量 I/O 或不常用的程序,用尽可能小的单位进行读写仍然是合理的。
i-list 的概念是 UNIX 的一个不同寻常的特征。在实践中,这种组织文件系统的方法被证明是相当可靠和容易处理的。对系统本身来说,它的一个优点是每个文件都有一个简短的、无歧义的名字,这个名字以一种简单的方式与访问文件所需的保护、地址和其他信息相关联。它还允许一种相当简单和快速的算法来检查文件系统的一致性,例如验证每个设备中包含有用信息的部分和可自由分配的部分是否不相交,加起来是否能填满设备上的空间。这种算法是独立于目录层次的,因为它只需要扫描线性排列的 i-list。同时,i-list 的概念还产生了某些在其他文件系统组织中没有的奇特性。例如,有一个问题是,由于一个文件的所有目录项都具有平等的地位,那么一个文件所占的空间应该对谁进行收费?一般来说,向文件的所有者收费是不公平的,因为有可能一个用户创建一个文件,另一个用户链接到该文件,而第一个用户可以删除该文件。第一个用户仍然是文件的所有者,但应该向第二个用户收费。最简单合理公平的算法似乎是将费用均摊到拥有文件链接的用户。当前版本的 UNIX 由于不收取任何费用,避开了这一问题。
4.1 文件系统的效率
为了说明 UNIX 的总体效率,特别是文件系统的效率,我们记录了对一个 7621 行程序的汇编耗时。汇编在机器上单独运行;总时钟时间为 35.9 秒,每秒钟 212 行。时间分配如下:63.5% 的汇编器执行时间,16.5% 的系统开销,20.0% 的磁盘等待时间。我们不试图对这些数字进行任何解释,也不与其他系统进行比较,只是指出我们对系统的整体性能基本上是满意的。
5. 进程和映像
一个映像(image)是一个计算机执行环境。它包括核心映像、通用寄存器值、打开文件的状态、当前目录等。一个映像就是一台伪计算机的当前状态。
一个进程(process)就是一个映像的执行。当处理器代表进程执行时,映像必须驻留在核心中;在其他进程执行期间,映像仍然驻留在核心中,除非出现一个活动的、优先级较高的进程,迫使它被换出到固定头磁盘中。
映像的用户核心部分分为三个逻辑段。程序文本段开始于虚拟地址空间的 0 位置。在执行过程中,该段受到写保护,并且在执行同一程序的所有进程之间共享一个副本。在虚拟地址空间中程序文本段上方的第一个 8K 字节边界处,开辟了一个非共享的、可写的数据段,该数据段的大小可以通过系统调用进行扩展。从虚拟地址空间的最高地址往下是栈段,随着硬件的栈指针的波动,栈段自动向下扩充。
5.1 进程
除非 UNIX 正在引导自己启动运行,只有使用 fork 系统调用才能产生一个新的进程。
processid = fork(label)
当 fork 被一个进程执行时,它会分裂成两个独立执行的进程。这两个进程拥有原始核心映像的独立副本,并共享任何打开的文件。新进程的不同之处仅在于其中一个被认为是父进程:在父进程中,控制权直接从 fork 返回,而在子进程中,控制权被传递给位置标记 label。fork 调用返回的 processid 是另一个进程的 ID。
因为父进程和子进程的返回点不一样,所以 fork 后存在的每个映像都可以判断它是父进程还是子进程。
5.2 管道
进程可以使用跟文件系统 I/O 相同的 read 和 write 系统调用与相关进程进行通信。调用
filep = pipe()
返回一个文件描述符 filep,并创建一个称为管道(pipe)的进程间通道。这个通道就像其他打开的文件一样,通过 fork 调用在映像中从父进程传递到子进程。使用管道文件描述符进行读取时,会先等待另一个进程使用同一管道的文件描述符写入。此时,数据在两个进程的映像之间传递。两个进程都不需要知道,它们读写的是管道而非普通文件。
虽然通过管道进行的进程间通信是一个相当有价值的工具(见 §6.2),但它不是一个完全通用的机制,因为管道必须由相关进程共同的祖先建立。
5.3 程序的执行
另一个重要的系统原语是
execute(file, arg1, arg2, ..., argn)
它请求系统读入并执行以 file 命名的程序,并传入字符串参数 arg1,arg2,…,argn。通常情况下,arg1 应该是与 file 相同的字符串,这样程序就可以确定它被调用的名称。使用 execute 的进程中的所有代码和数据都会从该文件替换,但打开的文件、当前目录和进程间的关系都不会改变。只有当调用失败时,例如因为找不到文件或因为其执行权限位没有设置,才会从 execute 原语中返回;它类似于 “跳转” 机器指令,而非子程序调用。
5.4 进程同步
另一个操控进程的系统调用
processid = wait()
会使其调用者暂停执行,直到它的一个子进程完成执行。然后 wait 返回被终止进程的 processid。如果调用进程没有子进程,则会采取错误返回。wait 也可以呈现来自子进程的某些状态,甚至还可以呈现来自孙子进程或更远的祖先进程的状态;参见 §5.5。
5.5 终止
最后,
exit(status)
会终止一个进程,销毁它的映像,关闭它打开的文件,通常还会消除该进程。当通过 wait 原语通知父进程时,父进程可以拿到指定的状态(status);如果父进程已经终止,则祖父进程可以得到该状态,以此类推。进程也可能由于各种非法行为或用户产生的信号而终止(下文 §7)。
6. 外壳程序(Shell)
对于大多数用户来说,与 UNIX 的通信是借助于一个叫做 Shell 的程序进行的。Shell 是一个命令行解释器:它读取用户输入的行,并将其解释为执行其他程序的请求。一条命令行,在最简的形式下,由命令名称和命令参数组成,所有参数用空格分隔:
command arg1 arg2 ... argn
Shell 将命令名和参数分割成独立的字符串。然后寻找一个名字为 command 的文件,command 可以是一个路径名,包含 “/” 字符来指定系统中的任何文件。若找到了 command,则将其带入核心并执行。Shell 收集到的参数可以被命令访问。当命令执行完毕后,Shell 恢复自己的执行,并通过一个提示字符表示准备接受另一条命令。
如果找不到文件 command,Shell 会在命令前加上字符串 /bin/,并再次尝试寻找文件。目录 /bin 包含了所有需要普遍使用的命令。
6.1 标准 I/O
上文 §3 中对 I/O 的讨论似乎意味着,程序使用的每一个文件都必须由该程序打开或创建,才能得到文件的文件描述符。然而,由 Shell 执行的程序,一开始就有两个打开的文件,其文件描述符分别为 0 和 1。当这样的程序开始执行时,文件 1 是用来写入的,最好将其理解为标准输出文件。除了下面指出的情况外,这个文件就是用户的打字机(译注:本文出现的打字机包含打印输出功能,可以理解为现在的终端)。因此,希望写入提示或诊断信息的程序通常使用文件描述符 1。相反地,文件 0 则默认用于读取,希望读取用户输入的信息的程序通常会读取这个文件。
Shell 能够改变这两个文件描述符默认的打字机显示器和键盘的标准分配。如果一行命令的其中一个参数以 “>” 为前缀,文件描述符 1 将在命令执行期间,引用 “>” 后面所命名的文件。例如,
ls
通常会在打字机上列出当前目录下各文件的名称。命令
ls >there
创建一个名为 there 的文件,并将列出来的结果放在文件中。因此,参数 “>there” 的意思是,“把输出放入 there”。另一方面,
ed
通常会进入编辑器,它通过打字机接受用户的请求。命令
ed <script
将 script 解释为编辑器命令的一个文件,因此 “<script” 的意思是,“从 script 中获取输入”。
虽然 “<” 或 “>” 后面的文件名看起来是命令的一个参数,但实际上它完全由 Shell 解释,根本没有传递给命令。因此,命令的内部不需要特殊的代码来处理 I/O 重定向,只需要在适当的地方使用标准的文件描述符 0 和 1 即可。
6.2 过滤器
这里对标准 I/O 的概念进行了扩展,从而能够将一个命令的输出引导到另一个命令的输入。由竖线隔开的命令序列会让 Shell 同时执行所有命令,并安排将每个命令的标准输出传给序列中下一个命令的标准输入。因此,在命令行
ls | pr –2 | opr
中,ls 列出了当前目录中的文件名;它的输出被传递给 pr,pr 用日期标题对输入进行分页。参数 “-2” 表示双列。同样地,pr 的输出也会被输入到 opr 中,这个命令会把它的输入汇集到一个文件中,以便离线打印。
这一过程也可以用比较笨重的方式来实现:
ls >temp1
pr –2 <temp1 >temp2
opr <temp2
然后再删除临时文件。在没有重定向输出和输入的能力的情况下,一个更笨重的方法是要求 ls 命令能接受将其输出分页、以多栏格式打印、并安排其输出脱机传送的用户请求。事实上,期望 ls 等命令的作者提供如此广泛的输出选项是令人吃惊的,实际上出于效率的考虑,这也是不明智的。
像 pr 这样把标准输入(处理后)复制到标准输出的程序称为过滤器(filter)。一些我们认为有用的过滤器可以进行字符翻译、输入的排序以及加密和解密。
6.3 命令分隔符:多任务
Shell 提供的另一个功能比较简单。命令不需要在不同的行中,而可以用分号来分隔。
ls ; ed
将首先列出当前目录的内容,然后进入编辑器。
一个相关的功能比较有趣。如果一个命令后面跟着 “&”,Shell 不会等待命令结束后再提示,而是立即准备接受新的命令。比如说
as source >output &
会对 source 进行汇编, 并将诊断输出到 output;无论汇编时间多长,Shell 都会立即返回。当 Shell 不等待命令的完成时,会打印出运行该命令的进程的 ID。这个 ID 可以用来等待命令完成或终止该命令。在一行中可以多次使用 “&”:
as source >output & ls >files &
在后台同时进行汇编和列表。在上面使用 “&” 的例子中,提供了打字机以外的输出文件;如果不这样,各种命令的输出就会混在一起。
Shell 还允许在上述操作中使用括号。例如:
(date; ls) >x &
将当前的日期和时间,接上当前目录的列表,打印到文件 x 上。Shell 也会立即返回,等待下一个请求。
6.4 Shell作为命令:命令文件
Shell 本身就是一个命令,可以递归调用。假设文件 tryout 包含以下几行
as source
mv a.out testprog
testprog
mv 命令使文件 a.out 重命名为 testprog。a.out 是汇编程序的 (二进制) 输出,准备用于执行。因此,如果在控制台上输入上述三行代码,source 将被汇编,产生的程序被命名为 testprog,并执行 testprog。当这几行放在 tryout 文件中时,命令
sh < tryout
会让 Shell 程序 sh 来顺序执行那些命令。
Shell 还有更多的功能,包括替换参数和从目录中指定的文件名子集构建参数列表的能力。还可以根据字符串比较或给定文件存在与否,条件地执行命令,以及在命令序列文件中进行控制转移。
6.5 Shell的实现
现在可以了解 Shell 运作的概要了。大多数时候,Shell 都在等待用户输入命令。当敲入换行符来结束当前行时,Shell 的 read 调用就会返回。Shell 对命令行进行分析,将参数变成适合 execute 的形式。然后调用 fork。子进程的代码当然还是 Shell 的代码,它会试图以适当的参数调用 execute。如果可以执行,就会带入并开始执行给定名称的程序。同时,由 fork 产生的另一个进程,也就是父进程,会等待子进程死亡。此后,Shell 知道命令已经结束,于是键入它的提示符并读取打字机以获得另一条命令。
在这个框架下,后台进程的实现就稀松平常了;每当命令行包含 “&” 时,Shell 仅仅是不会再去等待它所创建的执行该命令的线程。
令人高兴的是,所有这些机制都与标准输入输出文件的概念很好地融合在一起。当一个进程被 fork 原语创建时,它不仅继承了父进程的核心映像,而且还继承了父进程中当前打开的所有文件,包括文件描述符为 0 和 1 的文件。 当然,Shell 使用这两个文件来读取命令行和写入提示及诊断,且在普通情况下,它的子进程——命令程序——会自动继承这些文件。但是,当给出一个带有 “<” 或 “>” 的参数时,子程序就会在执行 execute 前,让标准的 I/O 文件描述符 0 或 1,引用相应参数所指定名称的文件。这很容易做到,因为根据约定,当打开(或创建)一个新文件时,会分配一个最小的未使用的文件描述符;所以只需要关闭文件 0(或 1),然后打开指定的文件即可。由于运行命令程序的进程在运行完后会直接终止,所以当进程死亡时,“<” 或 “>” 后指定的文件与文件描述符 0 或 1 之间的关联会自动结束。因此 Shell 不需要知道自身的标准输入和输出的文件的实际名称,因为它根本不需要重新打开这些文件。
过滤器是标准 I/O 重定向的简单扩展,用管道代替了文件。
在一般情况下,Shell 的主循环永远不会终止。(主循环包括 fork 返回的属于父进程的那个分支,也就是会执行 wait,然后读取下一条命令行的分支)。让 Shell 终止的一种方式是在其输入文件上探到一个文件结束(EOF)条件。因此,当 Shell 作为命令与给定的输入文件一起执行时,如
sh < comfile
comfile 中的命令将被执行,直到到达 comfile 的结束点;然后 sh 调用的 Shell 实例将终止。由于该 Shell 进程是另一个 Shell 实例的子进程,在后者中执行的等待将返回,从而可以处理下一条命令。
6.6 初始化
用户键入命令的 Shell 实例本身就是另一个进程的子进程。UNIX 初始化的最后一步是创建一个进程,并(由 execute)调用一个名为 init 的程序。init 的作用是为用户拨号接入的各个打字机信道创建一个进程。init 的各个子实例会打开相应的打字机来输入和输出。由于调用 init 时没有文件打开,所以在每个进程中,文件描述符 0 会指向打字机键盘,文件描述符 1 会指向打印机。每个进程都会打出一条消息,请求用户登录,并等待,读取打字机输入的用户回应。一开始,没有人登录,所以每个进程只是挂起。最后,有人输入了自己的名字或其他 ID。init 的相应实例被唤醒,接收登录行,并读取密码文件。如果找到了用户名,并且他能够提供正确的密码,init 就会切换到用户的默认当前目录,将进程的用户 ID 设置为登录者的 ID,并执行 execute 启动一个 Shell。至此,该 Shell 已经可以接收命令,而登录协议就此完成。
同时,init 的主流路径(即那些之后会成为 Shell 的子实体的父体)会执行一个 wait。如果其中一个子进程终止,无论是因为 Shell 探到了文件结束,还是因为用户输入了错误的名称或密码,init 的这条路径就会简单地重新创建这个已经失效的进程,而这个进程又会重新打开相应的输入和输出文件,并再次打出登录提示。因此,用户只需键入文件结束序列就可以注销,省得使用 Shell 命令。
6.7 其他程序作为外壳程序
上文所述的 Shell 是为了让用户充分使用系统的设施而设计,因为它执行任何程序时都有适当保护模式。然而,有时我们会想要一个不同的系统界面,但这同样很容易实现。
回顾一下,当用户通过提供名字和密码成功登录后,init 通常会调用 Shell 来解释命令行。用户在密码文件中的条目可以包含一个程序的名称,在登录后将调用这个程序,而不是 Shell。这个程序可以自由地以任何方式解释用户的信息。
例如,对秘书编辑系统的用户而言,其密码文件条目中指定使用的是编辑器 ed 而不是 Shell。这样,当编辑系统的用户登录后,他们就在编辑器中,可以立即开始工作;同时,也可以防止他们调用不想让他们使用的 UNIX 程序。实践证明,允许暂时离开编辑器,以执行格式化程序和其他实用程序是可取的。
UNIX 上的一些游戏(如国际象棋、21 点、3D 井字棋)体现了一种更为严格的限制环境。每一款游戏的密码文件中都有一个条目,指定要调用的相应的游戏程序而不是 Shell。当人们以其中一个游戏的玩家身份登录时,会发现自己被限制在游戏中,无法探寻 UNIX 这个整体中可能更为有趣的地方。
7. 陷阱
PDP-11 硬件可以检测到一些程序故障,例如对不存在的内存的引用,未实现的指令,以及在需要偶数地址的地方使用奇数地址。这类故障会导致处理器陷入系统例程。当捕捉到非法操作时,除非有其他安排,否则系统会终止进程,并将用户的映像写入当前目录下的核心(core)文件。然后可以使用调试器来确定故障发生时程序的状态。
当一个程序正在循环,或产生了不想要的输出,又或者其用户有了别的想法时,可以使用中断(interrupt)信号中止该程序,该信号通过输入 “delete” 字符产生。除非采取了特殊的行动,否则这个信号只是使程序中止执行,而不产生核心映像文件。
还有一个退出(quit)信号,用于强制生成核心映像。因此,即使在不预先安排的情况下中止意外循环的程序,仍然能够检查其核心映像。
硬件产生的故障以及中断和退出信号,可以通过请求,被进程忽略或捕获。例如,Shell 会忽略退出信号,以防止退出导致用户注销。编辑器会捕获中断信号,并退回到命令层。这很有用,因为可以停止较长的打印输出,而不丢失正在进行中的工作(编辑器操作的是所编辑文件的副本)。没有浮点硬件的系统,会捕获未实现指令,并解释浮点指令。
8. 展望
也许矛盾的是,UNIX 的成功在很大程度上是由于它并非为任何预定的目标而设计。当时我们之中有一个人(Thompson)对现有的计算机设施不满意,发现了很少被使用的 PDP-7 系统,并开始着手创造一个更友好的环境,于是编写了第一个版本。这一基本上属于个人的工作取得了足够的成功,引起了其余作者和其他人的兴趣,后来又以此购置了 PDP-11/20,专门用来支持文本编辑和格式化系统。然后又轮到 11/20 被淘汰,UNIX 已经被证明是有用的,足以说服管理层投资于 PDP-11/45。我们整个工作的目标,如果说有的话,总是关注于与机器建立一种舒适的关系,以及探索操作系统中的创意和发明。我们并不迫于去满足他人的要求,对于这种自由,我们心存感激。
回顾起来,可以看到影响 UNIX 设计的三个因素。
首先,因为我们是程序员,自然而然地就将系统设计成能够方便地编写、测试和运行程序。为了编程的方便,我们希望该系统最突出的表现就是被做成交互式使用,尽管最初的版本只支持一个用户。我们相信,一个设计得当的交互式系统比 “批处理” 系统用起来更有效率、更令人满意。而且这样的系统相当容易适配非交互式使用,反之则不然。
其次,对系统及其软件的大小一直存在着相当严格的限制。另一方面,出于对合理的效率和表现力的需求,这种大小的限制所促进的不仅仅是经济上的节省,还有一种设计上的优雅。这可能是 “苦难救赎” 哲学的一个简陋版本,但在我们的案例中,它起了作用。
第三,几乎从一开始,该系统就能够而且确实做到了对自身的维护。这一事实比看起来更重要。如果一个系统的设计者被强制使用该系统,他们很快就能意识到该系统在功能上和表面上的缺陷,并有强烈的动机在为时已晚之前纠正这些缺陷。由于所有的源程序总是可以在网上获得,而且很容易修改,所以每当发明、发现或有人提议新的想法时,我们会愿意修改和重写系统及其软件。
本文所讨论的 UNIX 的各个方面至少清楚地展示了上述设计考虑中的前两个方面。例如,从编程的角度来看,文件系统的接口是非常方便的。设计了尽可能低级的接口,从而消除各种设备和文件之间的区别,以及直接访问和顺序访问之间的区别。不需要大型的 “访问方法” 例程来隔离程序员与系统调用;事实上,所有的用户程序要么直接调用系统,要么用一个只有几十条指令的小库程序,缓冲若干字符,一次性读取或写入。
编程方便的另一个重要方面是没有结构复杂的 “控制块”,其中一部分结构由文件系统或其他系统调用来维护和依赖。一般来说,程序地址空间的内容归属程序自身,我们尽量避免对地址空间内的数据结构施加限制。
考虑到所有程序都应该可以将任何文件或设备作为输入或输出来使用,从空间效率的角度来看,将设备依赖的考量放进操作系统中也是可取的。唯一的取舍似乎在于,是在所有程序中加载处理每个设备的例程,占用比较大的空间,还是在实际需要的时候依靠某种方法动态地链接到适合每个设备的例程,花费更多的硬件资源。
同样,进程控制方案和命令界面也被证明是方便和有效的。由于 Shell 是作为一个普通的、可交换的用户程序来运行的,因此它在系统中不占用任何固有的空间,而且可以用很少的开销达到预期的效果。特别是,在这一框架下,由于 Shell 是作为一个进程来执行的,而这个进程又会派生出其他进程来执行命令,所以 I/O 重定向、后台进程、命令文件和用户可选系统接口等概念在实现上都变得稀松平常。
8.1 影响
UNIX 的成功并不那么在于新的发明,而在于充分地利用了一套经过精心挑选的富饶的思想,尤其在于它体现了这些思想可以成为实现一个小而强大的操作系统的关键。
我们实现的 fork 操作,基本上是在 Berkeley 分时系统中出现的 [8]。在很多方面,我们受到了 Multics 的影响,它展现了 I/O 系统调用的独特形式 [9],也提出了 Shell 的名称和它的一般功能。Multics 的早期设计也给我们提示了 Shell 应该为每条命令创建一个进程的概念,尽管在那个系统中,后来出于效率的考虑,放弃了这个概念。TENEX [10] 也采用了类似的方案。
9. 统计数字
下面介绍 UNIX 的统计数字,以显示该系统的规模,并说明这种规模的系统是如何使用的。在我们的用户中,那些不参与文档编写的用户往往使用该系统进行程序开发,特别是语言工作。重要的 “应用” 程序很少。
9.1 总体
72 | 用户数 |
14 | 最大同时使用用户数 |
300 | 目录数 |
4400 | 文件数 |
34000 | 512 字节二级存储块使用数 |
9.2 每天 (一天24小时,一周7天)
有一个 “后台” 进程,以尽可能低的优先级运行;它用以吸收任何闲置的 CPU 时间。它之前被用来生成常数 e-2 的百万位近似值,而现在正在生成复合伪数(基数 2)。
1800 | 命令数 |
4.3 | CPU 运行小时数(不包括后台) |
70 | 连接小时数 |
30 | 不同用户数 |
75 | 登录次数 |
9.3 命令CPU使用(截至 1%)
15.7% | C 编译器 | 1.7% | Fortran 编译器 |
15.2% | 用户程序 | 1.6% | 移除文件 |
11.7% | 编辑器 | 1.6% | 磁带归档 |
5.8% | Shell(作为命令使用且包含命令用时) | 1.6% | 文件系统一致性检查 |
5.3% | 国际象棋 | 1.4% | 库维护工具 |
3.3% | 列出目录 | 1.3% | 连接/打印文件 |
3.1% | 文档格式化工具 | 1.3% | 分页和打印文件 |
1.6% | 备份转储工具 | 1.1% | 打印磁盘占用 |
1.8% | 汇编器 | 1.0% | 复制文件 |
9.4 命令访问(截至 1%)
15.3% | 编辑器 | 1.6% | 调试器 |
9.6% | 列出目录 | 1.6% | Shell(作为命令使用) |
6.3% | 移除文件 | 1.5% | 打印磁盘可用 |
6.3% | C 编译器 | 1.4% | 列出进程执行 |
6.0% | 连接/打印文件 | 1.4% | 汇编器 |
6.0% | 用户程序 | 1.4% | 打印参数 |
3.3% | 列出登录用户 | 1.2% | 复制文件 |
3.2% | 重命名/移动文件 | 1.1% | 分页和打印文件 |
3.1% | 文件状态 | 1.1% | 打印当前日期/时间 |
1.8% | 库维护工具 | 1.1% | 文件系统一致性检查 |
1.8% | 文档格式化工具 | 1.0% | 磁带归档 |
1.6% | 有条件地执行另一个命令 |
9.5 可靠性
我们对可靠性的统计比其他的统计要主观得多。以下结果是尽我们一起回忆的真实情况。时间跨度在一年以上,机器是一台老式 11/45。
其中一个文件系统有一次丢失(五个磁盘中的一个),原因是软件无法应对某个硬件问题,陷入了反复断电。该磁盘上的文件已作了三天的备份。
“崩溃” 是指计划外的系统重启或停止。大约每隔一天就有一次崩溃;其中大约三分之二是由与硬件有关的困难引起的,如电力骤降以及处理器莫名其妙地中断到随机位置。其余的则是软件故障。最长的不间断运行时间约为两周。服务电话平均每三周一个,但非常集中。总运行时间约为我们 365 天 24 小时计划的 98%。
鸣谢
我们感谢 R.H.Canaday、L.L.Cherry 和 L.E.McMahon 对 UNIX 的贡献。我们特别感谢 R.Morris、M.D.McIlroy 和 J.F.Ossanna 的创造性、深思熟虑的批评和不断的支持。
参考文献
- Digital Equipment Corporation. PDP-11/40 Processor Handbook, 1972, and PDP-11/45 Processor Handbook. 1971.
- Deutsch, L.P., and Lampson, B.W. An online editor. Comm. ACM 10, 12 (Dec, 1967) 793–799, 803.
- Richards, M. BCPL: A tool for compiler writing and system programming. Proc. AFIPS 1969 SJCC, Vol. 34, AFIPS Press, Montvale, N.J., pp. 557–566.
- McClure, R.M. TMG—A syntax directed compiler. Proc. ACM 20th Nat. Conf., ACM, 1965, New York, pp. 262–274.
- Hall. A.D. The M6 macroprocessor. Computing Science Tech. Rep. #2, Bell Telephone Laboratories, 1969.
- Ritchie, D.M. C reference manual. Unpublished memorandum, Bell Telephone Laboratories, 1973.
- Aleph-null. Computer Recreations. Software Practice and Experience 1, 2 (Apr.–June 1971), 201–204.
- Deutsch, L.P., and Lampson, B.W. SDS 930 time-sharing system preliminary reference manual. Doc. 30.10.10, Project GENIE, U of California at Berkeley, Apr. 1965.
- Feiertag. R.J., and Organick, E.I. The Multics input-output system. Proc. Third Symp. on Oper. Syst. Princ., Oct. 18–20, 1971, ACM, New York, pp. 35–41.
- Bobrow, D.C., Burchfiel, J.D., Murphy, D.L., and Tomlinson, R.S. TENEX, a paged time sharing system for the PDP-10. Comm. ACM 15, 3 (Mar. 1972) 135–143.
标签:文件,Shell,一个,用户,UNIX,进程,分时系统 来源: https://blog.csdn.net/lyndon_li/article/details/112974978