UNIX时分系统

D. M. Ritchie 和 K. Thompson

摘要

Unix是一个用于数字设备公司更大的PDP-11和Interdata 8/32计算机,通用目的、多用户、交互式的操作系统。它提供了许多更大的操作系统上罕见的特征,包括

    i   一个结合了可卸载卷的层次文件系统,
    ii  文件、设备和进程间I/O的一致性,
    iii 启动异步进程的能力,
    iv  根据用户选择的系统命令语言,
    v   超过一百个子系统、一打语言,
    vi  高度可移植性。

本文论述文件和用户命令接口的状态和实现。

版权提示:版权所有1974,计算机协会公司,本文经许可再版。本文电子版是出现在贝尔系统技术期刊(1978年7月-8月)第6期,第2部分,第57篇文章的再版。它依次也是ACM通讯1974年7月,第7期,位于第365-375页上第17篇文章的修订版。该篇文章是第四次ACM关于操作系统原则大会上文章的修订版,IBM Thomas J. Watson Research Center, Yorktown Heights, New York, October 15-17, 1973。版本间的差异主要体现在C. ACM和BSTJ版本间。我们更新了页码,并加入了关于可移植性的材料。

I. 简介

Unix时分系统已经有了四个版本。最早的版本(约在1969-70年间)运行在数字设备公司PDP-7和-9计算机上。第二个版本运行在无保护的PDP-11/20计算机上。第三版加入了多道程序设计,运行在PDP-11/34, /40, /45, /60和/70计算机上;它是本文在上一次发表中描述的版本,也是在今天最广为使用的版本。这篇文章只描述第四版,当前系统运行在PDP-11/70和Interdata 8/32计算机上。事实上,不同系统间的差异非常小。相对于本文最初发表后的大部分修改,除了风格,都是与文件系统的实现细节相关。

自PDP-11的Unix在1971年2月运行以来,有超过600个安装被交付使用。大部分被用于计算机科学教育,文档和其它文字材料的准备和格式化,贝尔系统内部不同交换机产生的数据的收集和处理,以及纪录和检验电话服务订单。我们自己安装它主要是用于操作系统、语言、计算机网络和其它计算机科学研究,也包括文档准备。

或许Unix最重要的成就是验证了一个强大的交互式使用的操作系统,不需要昂贵的设备和人力投入:它可以运行在成本小到$40,000的硬件上,以及在主要系统软件上的花费小于两人-年。然而,我们希望用户能发现,这个系统的最重要特性是它的简单、优雅和容易使用。

除了操作系统特有的部分,Unix下一些主要的程序包括

    C编译器;
    基于QED的文本编辑器;
    汇编器、连接装载器、符号调试器;
    排版和设置程序

包括成打的语言:Fortran 77, Basic, Snobol, APL, Algol 68, M6, TMG, Pascal。还有在本地计算机上编写的,许多维护、工具、娱乐和新奇的程序。数以千计的Unix用户组成的社区,贡献了更多的程序和语言。值得注意的是,这个系统完全是自我支撑的。所有的Unix软件都维护在这个系统上;同样,这篇文章和所有其它关于这个问题的文章,都是用Unix文本编辑器和格式化程序来生成和格式化的。

II. 硬件和软件环境

研究Unix系统所安装于其上的PDP-11/70是16位字长(8位字节),有768K字节核心内存;系统内核占用90K字节,约在代码和数据表间等分。然而,这个系统包括大量设备驱动程序,并且为I/O缓冲和系统表分配了适当空间;一个最小的系统只需要小到96K核心内存,就能运行上面提到的软件。存在甚至更大的安装;例如,参见PWB/UNIX系统中的描述。也有小得多,可是有些限制的版本的系统。

我们自己的PDP-11有两个200-M可移头磁盘,用于文件系统存贮和交换。有20个连接到300至1200波特率数据设备的可变速度的通信接口,还附加一个12线到9600波特率的终端和卫星计算机。也有几个2400和4800波特率的用于机器间交换文件的同步通信接口。最后,有多种多样的其它设备,包括9轨磁带、一个行式打印机、一个声音合成器、一个排字机、一个数字交换网络和一个国际象棋机器。

Unix软件的优势在于使用上面提及的C语言编写。这个操作系统的一些早期版本用汇编语言编写,但是在1973年夏天的时候,它被用C重写。新系统的大小比原系统约大1/3。因为新系统变得不仅容易理解和修改,而且包括许多功能上的改进,包括多道程序设计,和在几个用户程序间共享可重入代码的能力,我们认为在大小上的增长是可以接受的。

III. 文件系统

这个系统最重要的功能是提供了文件系统。从用户的观点来看,存在三种文件:普通磁盘文件、目录和特殊文件。

3.1 普通文件

一个文件包含用户放入的任何信息,例如,符号或二进制(对象)程序。系统没有要求任何特殊的结构。一个文本文件简单地由一串字符组成,由换行字符分行。二进制程序是像当程序开始执行时,出现在核心内存中的字序列。一些用户程序会更有结构地操作文件;例如,汇编器生成的,和装载器接受的,是一个特殊格式的对象文件。但是,文件的结构由使用它们的程序,而不是系统控制。

3.2 目录

目录提供文件名和文件本身之间的联系,并因此导致文件系统的结构作为一个整体。每一个用户有一个他自己的文件的目录;他也可以创建一个子目录,以包含一组可被方便共同对待的文件。一个目录表现得几乎与普通文件完全一致,除了它不能被没有特权的程序写入,所以系统控制目录的内容。然而,任何具有适当许可的人,都可以像其它文件一样读一个目录。

系统维护几个目录供自己使用。其中之一是root目录。系统中的所有文件,都能按照直到所需文件的目录链被找到。这种搜索的起点通常是root。其它系统目录包含通常使用的程序;既是,所有的命令。像将要看到的,然而,它并没有必要意味着,这些目录中的程序可以执行。

文件使用14个或更少的字符序列命名。在系统中指定一个文件名,它可能是以路径名的形式,它是一个由斜线“/”分割的目录名序列,并以一个文件名终止。如果序列以斜线开始,搜索会从root目录开始。名字/alpha/beta/gamma导致系统在root下搜索alpha目录,然后在alpha下搜索beta,最后在beta内搜索gamma。gamma可以使一个普通文件,一个目录或一个特殊文件。作为一种限制的情况,名字“/”指根目录本身。

一个不以“/”开始的路径名,导致系统从用户的当前目录开始搜索。因此,名字alpha/beta指名为beta的文件在当前目录的子目录alpha中。最简单种类的名字,例如alpha,指在当前目录下找到的文件本身。作为另一种限制的情况,一个空文件名指当前目录。

同样的非目录文件可以出现于不同名字的几个目录下,这个功能被称作链接;一个文件的一个目录入口有时被称作链接。Unix系统在与其它,允许到一个文件的所有链接有相等的状态的系统不同。即是,一个文件不存在于一个特殊目录;一个文件的目录条目仅由自己的名字组成和一个的确描述文件信息的指针。因此一个目录项文件单独存在,然而,实际上一个文件与它的最后一个链接一起消失。

每一个目录至少有两个条目。每一个目录中的名字“.”表示目录自身。因此一个程序可以读当前目录,而不用知道完整路径名。名字“..”按惯例指向它所置身其中的父目录,即是,指向它创建于其中的目录。

目录结构被强制具有一个根树的形式。除了特殊的条目“.”和“..”,每个目录必须只能是一个,出现在一个其它目录中的条目,即是它的父目录。理由是为了简化编写访问目录结构中的子树的程序,并且更重要的是,避免部分目录层次的分割。如果允许有到目录的任意链接,检测何时从根到一个目录的最后连接被切断将会非常困难。

3.3 特殊文件

特殊文件构成Unix文件系统最不通常的功能。每一个被支持的I/O设备至少关联一个此类文件。特殊文件像普通磁盘文件一样被读写,但读写要求导致激活关联设备。每个存在于目录/dev内的特殊文件条目,然而可以生成一个链接指向这些文件之一,如同到一个普通文件。因此,例如为了写入一个磁带,就应该写入文件/dev/mt。每一条通信线路、磁盘、磁带驱动器以及对物理主存都对应一个特殊文件。当然,活动磁盘和内存特殊文件被保护,不可被随意访问。

用这种方式处理I/O设备有三种优势:文件和I/O设备尽可能相似;文件和设备名有相同的语法和含义,所以一个需要一个文件名作为参数的程序,可被传入一个设备名;最后,特殊文件使用与常规文件一样的保护机制。

3.4 可移动文件系统

尽管文件系统的根总是存储于样同的设备,整个文件系统层次没有必要都位于这个设备。有一个mount系统请求带有两个参数:现存的普通文件的名字,和关联存储卷(例如一个磁盘组)具有一个包含本身目录层次的,独立文件系统结构的特殊文件的名字。mount的作用是另指向普通文件的引用,指向可移动卷上的文件系统根目录。安装替换层次树(普通文件)中一个页,以整个新子树(存储在可移动卷中的层次树)。安装之后,实质上可移动卷和永久文件系统间的文件没有区别。例如在我们的安装中,root目录存在于我们一个磁盘驱动器的一个小分区中。然而其它包含用户文件的驱动器,在系统初始化序列中被安装。一个可安装文件系统通过写入它对应的特殊文件生成。一个工具程序可用于生成一个空文件系统,或者简单地拷贝一个现有文件系统。

在同等对待不同设备上的文件的规则上,只有一种例外:没有链接可以存在一个文件系统层次和另一个间。这个限制被强加以避免,在可移动卷卸载后保证移除连接,而被要求的详细簿记。

3.5 保护

尽管访问控制模式非常简单,它有一些不寻常的特征。系统中每一个用户被赋予一个唯一的识别号码。当一个文件被创建时,它被标记上它的属主用户id。新文件被赋予一组十个保护位。这些中的九个分别指定,文件属主、属主所在组其它用户和其余用户的读、写和可执行许可。

如果第十个位被置上,当这个文件被当作一个程序执行,系统将临时改变当前用户的用户标志(此后称为用户ID)为文件属主的用户ID。用于ID的这个改变只在通过调用执行程序时有效。set-user-ID特性提供了可以访问,其它用户不可访问文件的特权程序。例如,一个程序可能有一个除了程序本身以外,不能被读写的帐户文件。如果这个程序的set-user-ID位被设置。它可以访问这个用户调用其它程序被禁止访问的文件。因为任何程序的调用者的真实用户ID总是有效,set-user-ID程序可以使用任何要求的手段,令它们自己符合他们调用者的信任状。这种机制用于允许用户执行被谨慎编写的,调用特权系统资源的命令。例如,有一个系统资源只能被超级用户调用生成一个空目录。像在前面指出来的一样,目录被期待具有“.”和“.”条目。生成一个目录的命令由超级用户所有,并且设置了set-user-ID位。在它检测它的调用者的授权以生成指定的目录,它创建目录并产生“.”和“..”的入口。

因为任何人可能在他自己的文件上设置set-user-ID位,这个机制通常不需要管理员干涉便可使用。例如,这个保护模式很容易解决由“Aeph-null”造成的MOO簿记问题。

系统识别一个特殊的用户ID(那即是超级用户)可以不受限制访问文件;因此(例如)程序可以写到dump,并且不需要保护系统的不必要干涉重载入文件系统。

3.6 I/O调用

I/O系统调用被设计成,以消除不同设备和访问方式间的差别。“随机”和“顺序”I/O间没有区别,系统也没有强加任何逻辑的纪录大小。一个普通文件的大小由写入它的字节数目决定。预测文件的大小没有必要也不可能。

为了例证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等于0,则是已到达文件尾。对磁盘文件来说,当读指针等于文件的当前大小时,会发生这种情况。在终端通过使用一个依赖于终端所使用的转义序列,也可能产生一个文件尾字符。

写入的字节只影响隐含在写指针位置和count的那部分文件;文件其余部分不会被改变。如果最后一个字节越过文件尾,文件会任意增长。

为了进行随机(直接访问)I/O,只需要移动读或写指针到文件中相应的位置。

    location = lseek(filep, offset, base)

与filep关联的指针根据base的取值,从指针的当前位置,或从文件尾,被移动到一个从文件开始处偏移offset个字节的位置。offset可以是一个负值。对一些设备(比如,纸带或终端)搜索调用被忽略。实际从文件的开始处到被移动的指针的偏移量,被返回在location中。

有一些附加的与系统和I/O及文件系统相关的条目,不会被讨论。例如:关闭一个文件,获取文件状态,更改文件保护模式或者属主,创建一个目录,产生一个现有文件的链接,删除一个文件。

IV. 文件系统的实现

在以上3.2节已提及,一个目录入口只包含关联文件的名字和到文件本身的指针。这个指针是一个被叫做文件的i-number的整数(表示索引数值)。当一个文件被访问时,它的i-number被用作一个存储在目录所在的设备上的,一个已知位置的系统表(i-list)中的索引。因此该被搜索到的条目(文件的i-node),包含文件的描述:

    i   属主的用户和组ID
    ii  它的保护位模式
    iii 存储文件内容的物理磁盘或磁带地址
    iv  它的大小
    v   创建、最后使用、和最后修改时间
    vi  文件的链接数,即是,它在目录中出现的次数
    vii 指示该文件是否为一个目录、一个普通文件、或一个特殊文件的代码

系统调用open或create的目的是通过搜索显式或隐式命名的目录,把用户提供的路径名转换为一个i-number。一旦一个文件被打开,它的设备、i-number,和读/写指针都被存储在系统表,可被open或create返回的文件描述符索引到。因此,在后续对文件的read或write调用中,文件描述符可被轻易关联到反问文件必要的信息。

当创建一个新文件时,一个i-node被分配给它,并且一个包含文件名和i-node数值的目录条目被生成。创建一个现存文件的链接,会引起产生一个带有一个新名字的目录条目,从原始文件条目拷贝i-number,并增加目录条目相关的i-node的link-count字段值,并删除该目录条目。如果link-count减少为0,此文件占用的磁盘块被释放并且i-node被丢弃。

包含一个文件系统的所有磁盘上的空间,逻辑上被分成许多512字节块,地址从0开始直到设备的上限。每个文件的i-node需要占用13个设备地址的空间。对于非特殊文件,开始的10个设备地址,指向文件中开始的10个块。如果文件大于10个块,11个设备地址指向一个包含文件中最多128附加块地址的间接块。更大的文件使用i-node的第12个设备地址,指向一个128间接块的二级接块,每个指向文件中的128个块。如果要求,第13个设备地址三级块。因此,文件在概念上会增长至[(10+128+128^2+128^3)*512]字节。一旦打开,低于5120的字节数能在一次单次磁盘访问中被读取;从5120到70,656范围的字节需要两次访问;70,656到8,459,264的字节需要三次访问;从那里到最大文件(1,082,201,088)的字节需要四次访问。实际上,一个设备缓存机制(见下面)在消除大部分间接提取方面,证明了是有效的。

前面的讨论适用于普通文件。当向一个i-node指示它是特殊的文件产生一个I/O请求,后12个设备地址字不是实质的,并且第一个指明一个内部设备名称,它被解释为一对数字表示,分别是,一个设备类型和子设备号。设备类型指示哪个系统例程将处理那个设备上的I/O;子设备号选择,例如,一个附上特殊控制器的磁盘驱动器,或几个相似终端接口中的一个。

在这个环境中,mount系统调用(3.4节)的实现是非常直接的。mount维持一个参数为在mount时指定的,普通文件的i-number和设备名称的系统表,并且它对应的值是所指示的特殊文件的设备名。在这个表中搜索在open或create时扫描路径名,找到的每个i-number/device对;如果找到一对匹配值,i-number被替换为根目录和被表值替换的设备名。

对用户来说,读和写文件看起来是同步和无缓冲的。即是说,在read调用返回后,数据立刻就有效了。相反,写入文件之后,用户的工作空间可被重用。实际上,系统维持一个相当复杂的缓冲机制,减少了访问文件时所需的I/O操作数量。假设一个write调用用以传送一个单字节。系统将搜索它的缓冲区,检查当前影响到的磁盘块是否在主存中。如果不是,它将从设备读取。然后受影响的字节在缓冲区中被替换,并且在被写入缓冲区列表中生成一个条目。然后write调用的返回可能发生,尽管真实的I/O直到更晚些时候才完成。相反,如果读取一个单字节,系统判断是否放置字节的第二存储块,已在系统缓冲区之一。如果是这样,字节能被立刻返回。如果不是,块被读取到缓冲并且字节被提取出来。

系统许可一个程序访问一个文件的顺序块,并异步预读下一个块。这在很大程度上减少了大多数程序的运行时间,而并没有增加系统的负担。

一个程序以512个字节为单位读或写文件,比一次只读或写一个单字节有一个优点,但是这种益处并不大;它主要是为了避免系统开销。一个程序如果被使用得不多或没有进行很大批量的I/O操作,它以所期望的单位读或写可能是非常合理。

i-list是Unix里面一个不同寻常的概念。实际上,这种组织文件系统的方法,已经证明是非常可靠和容易处理。对系统自身来说,它的长处之一是每一个文件有一个关于保护、地址和其它访问文件需要的信息的,简单方式的短的、明确的名字的事实。它也允许一个检查文件系统一致性的相当简单和快速的算法,例如,每个包含有用信息的设备的部分,和泄露的资源耗尽设备空间的校验。这个算法独立于目录层次,因为它只需要扫描线性组织的i-list。i-list的概念同时导致,一些其它文件系统组织里没有的特性。例如,有一个关于谁负责文件占用空间的问题,因为一个文件的所有目录条目有等同的状态。要求所有者负责一个文件通常是不公平的,因为一个用户可能创建了一个文件,另一个可能链接到它,第一个用户可能删除文件。第一个用户仍然是文件属主,但它由第二个用户负责。最简单的相当好的算法,看起来将在向链接到一个文件的用户间,相等地传递文件的负责权限。许多安装通过根本不要求花费避免了这个问题。

V. 进程和映像

一个映像是一个计算机的执行环境。它包括一个内存映像、通用寄存器值、打开文件的状态、当前目录等等。一个映像是一个伪计算机的当前状态。

一个进程是一个映像的执行。当处理器在执行一个进程时,映像必须存在于主存中;在执行其它进程的当中,它仍保留在主存中除非一个活动的、更高优先级的进程,迫使它换出到磁盘。

一个映像的用户存储部分被分为三个逻辑段。程序的正文(text)段开始于虚拟地址空间的地址0。在执行当中,这个段是写保护的,并且它的一个单一拷贝被执行这个程序的所有进程共享。在开始虚拟地址空间中,在程序正文段之上的硬件保护字节边界,开始于一个未保护、可写的数据(data)段,它的尺寸可以通过系统调用被扩展。虚拟地址空间中从最高地址开始的是一个栈(stack)段,它随着栈指针移动自动向下增长。

5.1 进程

除了当系统正在引导自身(bootstrapping)开始进行工作,一个新进程只能通过使用系统调用fork产生:

    processid = fork()

当fork在一个进程中被调用时,这个进程被分成两个独立的可执行进程。这两个进程有独立的原始内存映像拷贝,并且共享所有打开的文件。新进程仅与父进程不同:在父进程中,返回的processid真实地标识着子进程,并且永远不会为0,然而在子进程中,返回值总是0。

因为在父进程和子进程中,由fork返回的值是可区别的,每一个进程可以判断它是父还是子进程。

5.2 管道

进程可以与相关进程使用同样用于文件系统I/O的系统调用read和write通信。系统调用:

    filep = pipe()

返回一个文件描述符filep,并且创建一个叫做管道的进程间通道。这个通道,像其它打开的文件一样,从父进程被传递到该映像中,由fork调用创建的子进程。一个使用一个管道文件描述符的read调用,等待直道另一个进程使用同一个管道的文件描述符调用write。在这里,数据在两个进程的映像间传递。没有一个进程需要知道使用的是一个管道,而不是普通文件。

尽管通过管道的进程间通信是一个非常有价值的工具(见6.2节),它不是一个完全通用的机制,因为管道必须由涉及到的进程的共同的祖先建立。

5.3 程序的执行

另一个主要的系统特征由

    execute(file, arg1, arg2, ... , argn)

调用,它要求系统读取并执行file命名的程序,并向它传递字符串参数arg1, arg2, ..., argn。调用execute的进程中的所有代码和数据,被file替换,但是打开的文件、当前目录,和进程间关系未受改变。仅在调用失败时,例如因为file不能被找到或它的可执行位模式未被设置,从execute返回;它类似一个“跳转”机器指令而不是一个子例程调用。

5.4 进程同步

另一个进程控制系统调用:

    processid = wait(status)

导致它的主调者暂停程序的执行,直道它的一个子进程完成执行。然后wait返回被终止进程的processid。如果调用进程没有子进程,会返回一个错误。某些来之子进程的状态也有效。

5.5 终止

最后:

    exit(status)

终止一个进程,销毁它的映像,关闭它打开的文件,并删除它。父进程会通过wait得到通知,状态对它有效。进程也可以作为各种非法活动或用户生成信号的终止(见下面VII节)。

VI. SHELL

对大多数用户,与系统通信是通过一个叫shell的程序进行的。shell是一个命令行解释器:它读取用户键入命令行,并把它们解释为一个对其它程序的执行请求。(其它文献完整描述了shell,所以本文只讨论它的工作原理。) 一个命令最简单的形式由命令名跟随命令参数组成,它们之间用空格分割:

    command arg1 arg2 ... argn

shell把命令名和参数分为单独的字符串。系统会搜索一个名为command的文件;command可以是一个包含“/”字符,指定系统中任意文件的路径名。如果command被找到,它被载入内存并执行。shell得到的参数可以被命令访问。当命令完成,shell恢复它自己的执行,并且打印一个提示符,显示它已准备好接受另一个命令。

如果一个文件command不能被找到,shell通常在command之前缀上一个字符创,比如/bin/,并再次尝试查找文件。目录/bin通常包含被使用的命令。(搜索的目录序列可被用户要求改变)。

6.1 标准I/O

在上面第III节,对I/O的讨论看起来隐含表示,程序使用的每个文件必须在程序中被open或create,以得到该文件的一个文件描述符。然而shell执行的程序,从三个打开的文件开始,文件描述符是0、1、2。当这样一个程序开始执行,文件1被打开用于写入,并最好被理解为标准输出文件。除了在如下所示的情形下,这个文件即用户的终端。因此希望输出提示性信息的程序,通常使用文件描述符1。相反,文件0被打开用于读取,并且希望读取用户输入的程序,读这个文件。

shell能从用户终端打印机和键盘,改变这些文件描述符的标准分配。如果一个命令的参数之一被前缀“>”,文件描述符1在这个命令期间,将指向“>”后面的名称的文件。例如:

    ls

通常在打字终端上列出当前目录中的文件清单。命令:

    ls > there

创建一个名为there的文件并且把列出的文件清单放置于其中。因此参数> there表示“把输出到there。” 另一方面:

    ed

通常进入该编辑器,因应用于从键盘的请求。命令

    ed < script

把script解释为编辑器命令的一个文件;因此“< script”表示“从script获取输入。”

尽管“<”或“>”之后的文件名,表现为命令的一个参数,实际上它完全由shell解释并且根本没有被传递给命令。因此并没有特别的编码用以处理,每个命令中要求的I/O重定向;命令只在适当的地方需要标准文件描述符0和1。

文件描述符2,像文件1,通常关联终端输出流。当由“>”给出一个输出重定向请求,文件2仍然联系着终端,所以命令将产生一个诊断信息,并不会只在输出文件中安静结束。

6.2 过滤

标准I/O思想的一个扩展,是把一个命令的输出,引入作为另一个的输入。一个有竖线分隔的命令序列,导致shell同步执行所有命令,并且安排每一个命令的标准输出,作为下序列中一个命令的输入。因此,在命令行:

    ls | pr -2 | opr

ls列出当前目录中的文件名;它的输出被传递到pr,它对输入添加日期标头进行分页。(参数“-2”要求双列输出。) 同样地,pr的输出是opr的输入;这个命令把它的输出送至一个脱机打印文件。

这个过程更笨拙一点,可以被这样完成:

    ls > temp1
    pr -2 < temp1 > temp2
    opr < temp2

后面再加上一个对临时文件的移除。在不能够重定向输出和输入的情况下,一个更要笨拙的方法是,要求ls命令接受用户分页输出的请求。为了打印多列输出格式,并且安排输出可以离线递交。它真的会令人感到惊讶,并且事实上因为效率的原因也不明智,如果期望比如ls等一些命令的作者提供如此多样的输出选项。

一个比如pr的程序拷贝它的标准输入到它的标准输出(经过处理)被称作过滤器。一些我们发现有用的过滤器执行字符翻译,根据模式选择行,对输入排序,以及加密和解密。

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分析命令行,把参数转换为一种适合执行的形式。fork被调用。子进程,它的代码自然还是shell的,尝试用适当的参数执行一个execute调用。如果成功,它将载入并执行给定名字的程序。同时,由父进程fork产生的其它进程,等待子进程结束。当这个发生时,shell知道命令已被执行完毕,所以它打印它的提示符并读取键盘输入以获得另一个命令。

在这个框架下,后台进程的实现是微不足道的;当一个命令行包含“&”,shell仅仅不去等待它产生的进程执行命令。

开心的是,所有这些机制与标准输入输出文件的思想非常吻合。当一个进程被fork创建,它不仅继承父进程的内存映像,而且继承父进程当前所有打开的文件,包括文件描述符为0,1和2的文件。shell当然使用这些文件读取命令行,并且输出它的提示符和诊断,并且在普通的情况下,它的子命令自动编写它们。然而当一个参数带有“<”或“>”被给出,刚好在它之前的子进程,执行execute,使标准I/O文件描述符(分别为0或1)指向给定文件。这很容易,因为根据协议,当一个新文件被打开(或创建),最小未被使用的文件描述符被赋值;它只需要关闭文件0(或1)并且打开指定文件。因为命令程序在其中运行的进程简单地终止,当它通过在“<”或“>”后指定的文件和文件描述符0或1间的组合,在进程结束时自动终止。因此shell不需要知道它自己的标准输入和输出文件的真实名字,因为它永远不需要再打开它。

过滤是标准I/O重定向加上管道而不是文件的直接扩展。

在普通情况下,shell的主循环永远不会终止。(主循环包括父进程fork返回的分支;即是,分支等待,然后读取另一个命令行。)导致shell终止的一种情况,是在它的输入文件中发现一个end-of-file条件,因此当shell作为一个命令与一个文件一起执行,像在:

    sh < comfile

comfile中的命令将被执行直到comfile的文件尾;然后sh调用的shell实例将终止。因为这个shell进程是另一个shell实例的子进程,后续执行的wait将返回,并且另一个命令之后可以被处理。

6.6 初始化

用户键入命令的shell实例自身是另一个进程的子进程。系统初始化的最后一个步骤是生成一个单进程并调用(通过execute)一个init程序。init的角色是为每一个终端通道生成一个进程。init的各个不同的子实例打开适当的终端,用于在文件0,1和2上进行输入和输出,如果必要,等待载波建立拨号连接。然后一个消息被打印出来,要求用户登录。当用户键入名字或其它标识,相应的init实例唤醒,接受登录行,和读取一个口令文件。如果用户的名字被找到,以及他能提供正确的口令,init改变到用户默认的当前目录,设置进程的用户ID为登录进入的人,并执行一个shell的execute。在这里,shell已准备好接受命令并且登录协议已经完成。

期间,init(所有后来成为shell自身的子实例的父进程)的主要路径是等待。如果子进程中之一终止,或是因为一个shell发现一个文件尾或因为一个用户键入了一个不正确的名字或口令,init的这个路径简单地重新创建已经死去的进程,它一次重复打开适当的输入和输出文件并且打印另一条登录消息。因此一个用户可以简单地在shell中,输入一个文件尾序列来注销。

6.7 其他shell程序

上面描述的shell被设计以允许用户完全访问系统的工具,因为它将以适当的保护模式,调用执行任何程序。然而,有时一个对系统的不同的界面被期望。并且这种特性容易获得。

回想一个用户通过提供名字和口令,成功登录后,init一般调用shell来解释命令行。用户在口令文件中的条目,可能包含登录后调用的程序名而不是shell。这个程序可以自由地,以它希望的任何方式解释用户信息。

例如,秘记编辑系统用户的口令文件条目,可能指定编辑器ed代替shell被使用。因此当这个编辑系统的用户登录,他们处于这个编辑器之内,并且可以马上工作;他们也可能被阻止调用不期望他们使用的程序。实际上,已经证明值得允许临时跳出编辑器,以执行格式化程序和其它工具。

系统上提供的几个游戏(例如,国际象棋、blackjack,3D tic-tac-toe)例证了一个更加严格受限的环境。对于它们每一个,口令文件中有一个条目指出调用适当的游戏程序,而不是shell。人们以这些游戏中的一个玩家身份登录,发现他们被限制于这个游戏,而不能把Uinx系统作为一个总体来研究(大概更有趣)。

VII. 陷入

PDP-11硬件发现了很多程序错误,比如引用不存在内存,未实现的指令,在需要偶数地址的地方使用了奇数地址。这些错误导致处理器陷入一个系统例程。除非已经做了其它安排,一个非法的动作导致系统终止处理,并把它的映像写入当前目录下的core文件。一个调试器可用于确定出错时的程序状态。

程序产生未期望输出,并进入循环,用户可以,输入“delete”字符,产生interrupt信号来终止它。除非采取了特殊行动,这个信号只是导致程序终止,不会产生一个core文件。也有一个quit信号用于强制产生一个映像文件。因此出乎意料发生循环的程序可以被终止,并且不需要预先安排,保持得到检查。

硬件产生的错误,和interrupt、quit信号能按照要求,被忽略或被一个处理捕获。例如,shell忽略quit,来阻止quit导致注销用户。编辑器捕获interrupt并返回到命令级。这可用于停止长时间的打印输出,而不用丢失正在进行的工作(编辑器修改它正在编辑的文件的一个备份)。在没有浮点硬件的计算机中,未实现的指令被捕获,对浮点指令进行解释。

VIII. 前景

或许荒谬的是,Unix系统的成功在很大程度上归因于,它没有被设计为符合任何预定义目标的事实。第一版编写于我们中的一位(Thompson)不满意现有的计算机工具,发现一台未被使用的PDP-7,并开始创建一个更加友好的环境。这个(本质上属个人的)努力非常成功,吸引了其它的创造者和同事,并在后来获得了PDP-11/20,特别支持一个文本编辑和格式化系统。当后来11/20系统过时了,这个系统证明了足够有用,以说服管理层购买PDP-11/45,后来的PDP-11/70和Interdata 8/32等机器。经过这些事情,它发展到它现在的样子。我们在这些努力中的目标是,在相关联的时候一直是在机器,和探索操作系统与其它软件的思想和发明间创建一个合适的关系。我们没有面临需要满足其它人的要求的情况,我们感激这种自由。

回顾起来,有三种考虑影响Unix的设计。

第一:因为我们是程序员,我们自然设计这个系统,让它易于编写、测试、和运行程序。对我们易于编程的渴望的最重要表达是,这个系统被计划用于交互使用,尽管最初版只支持一个用户。我们相信一个正确设计的交互系统,比一个“批处理”系统更高效和满足需要。而且这样一个系统,相当容易适用于非交互式使用,然后反过来却不成立。

第二:系统和软件总是有严格的大小限制。给出的部分对于合理的效率和表达能力的反对意见,这个尺寸限制不但鼓励经济性,而且也有一定的设计上的优雅。这可能有一点“苦难中获救”哲学的伪装版本。但是在我们的情况中,它可以工作。

第三:几乎从一开始,这个系统确实能自我维持。事实比看起来更重要。如果一个系统的设计者们,被迫使用那个系统,他们很快会知道它的功能和表面的不足,并怀有强烈的东西,在它变得太晚之前改正它们。因为所有源程序总是可用,并且很容易在线修改,我们愿意,修改和改写系统和它的软件,当新思想被发明、发现或由他人建议。

本文讨论的Unix的各个方面,清楚展示了至少其中开始的两个设计思想。例如,系统接口非常便于编程立场。最低可能的接口级别,被设计用于消除不同设备和文件,以及直接和顺序访问间的差异。没有大的“访问方法”例程被要求,用来把程序员和系统调用隔离开;事实上,所有用户程序直接调用系统,或者使用一个小于一页长的小型库程序,缓冲大量字符,并一次全部读入或写出它们。

另一个方便编程的重要的方面是,没有一个部分的维护和依赖于文件系统或其它系统调用的,复杂结构的“控制块”。一般而言,程序地址空间的内容由程序所有,并且我们尝试避免在那个地址空间的数据结构上添加限制。

假设所有程序应该对作为输入或输出的,任意文件或设备可用的要求,它也期望把设备相关性思想,引入操作系统本身。对所有程序唯一的替代看起来是,载入例程处理每个设备,它在空间花费上昂贵,或者依赖一些动态链接适合每个设备例程的方式,当它真的被需要,在管理和硬件上也是昂贵的。

同样,进程控制模式和命令接口都已证明是方便和有效的。因为shell的行为想一个普通、可交换的用户程序,它没有消耗系统正确性的线下空间,并且它不需要什么花费就可以被做成像需要那样功能强大。特别地,给定一个shell在其中作为一个进程执行的框架,产生其它进程来执行其它命令,I/O重定向的思想、后台进程、命令文件和用户选择的系统接口,对于实现来说全都从本质上变得微不足道。

影响

Unix的成功不在于多少新发明,而是对一套完全精心选择的富有创造力的思想的开发,尤其在展示它们对于一个小但是强大的操作系统的关键性。

fork操作,基本在我们实现它时,出现在GENIE时分系统。在很多方面,我们收到Multics的影响,它建议了I/O系统调用的特殊形式,shell的和它的通用函数的名字。Multics的早期设计也向我们建议,shell应该为每一个命令创建一个进程的思想,尽管在那个系统中,它因为效率原因被放弃。一个类似的模式被用于TENEX。

IX. 统计

下面出现的数字用于说明研究Unix活动的规模。我们的那些用户,没有参与文档准备,趋向使用系统进行程序开发,特别是语言工作。没有几个重要的“应用”程序。

总的来说,我们今天有:

       125    user population
        33    maximum simultaneous users
    1, 630    directories
   38, 300    files
  301, 700    512-byte secondary storage blocks used

有一个“后台”程序运行于最低可能的优先级;它用于吸收空闲CPU时间。它被用于产生一个接近常数e的百万数位,和其它不完全无限问题。并非计数这个后台工作,我们日常平均:

  13, 500   commands
      9.6   CPU hours
      230   connect hours
       62   different users
      240   log-ins

X. 致谢

对Unix的贡献者有,按照传统但在这里特别合适的措辞,太多了以致无法指出。肯定,总的感谢应该归于我们在计算机科学研究中心的同事。R. H. Canaday在文件系统的基本设计上贡献了很多。我们特别感谢R. Morris、M. D. McIlroy和J. F. Ossanna的发明、富有洞察力的批评和持续的支持。

参考

1.
L. P. Deutsch and B. W. Lampson, `An online editor,' J. Comm. Assoc. Comp. Mach. 10 12, December 1967 pp. 793-799, 803
2.
B. W. Kernighan and L. L. Cherry, `A System for Typesetting Mathematics,' J. Comm. Assoc. Comp. Mach. 18, pp. 151-157, March 1975.
3.
B. W. Kernighan, M. E. Lesk and J. F. Ossanna, `Document Preparation,' Bell Sys. Tech. J. 57 6 part 2, pp. 2115-2135, July-August 1978.
4.
T. A. Dolotta and J. R. Mashey, `An Introduction to the Programmer's Workbench,' Proc. 2nd Int. Conf. on Software Engineering, October 13-15, 1976, pp. 164-168.
5.
T. A. Dolotta, R. C. Haight, and J. R. Mashey, `The Programmer's Workbench,' Bell Sys. Tech. J. 57 6, pp. 2177-2200, July-August, 1978.
6.
H. Lycklama, `UNIX on a Microprocessor,' Bell Sys. Tech. J., 57 6, pp. 2087-2101. July-August 1978.
7.
B. W. Kernighan and D. M. Ritchie, The C Programming Language, Prentice-Hall, Englewood Cliffs, New Jersey, 1978. Second edition, 1988.
8.
Aleph-null, `Computer Recreations,' Software Practice and Experience, 1 2, April-June 1971, pp. 201-204.
9.
S. R. Bourne, `The UNIX Shell,' Bell Sys. Tech. J. 57 6, pp. 1971-1990, July-August 1978.
10.
L. P. Deutsch and B. W. Lampson, `SDS 930 time-sharing system preliminary reference manual,' Doc. 30.10.10, Project GENIE, Univ. Cal. at Berkeley, April 1965.
11.
R. J. Feiertag and E. I. Organick, `The Multics input-output system,' Proc. Third Symposium on Operating Systems Principles, October 18-20, 1971, pp. 35-41.
12.
D. G. Bobrow, J. D. Burchfiel, D. L. Murphy, and R. S. Tomlinson, `TENEX, a Paged Time Sharing System for the PDP-10,' Comm. Assoc. Comp. Mach., 15 3, March 1972, pp. 135-143.

Copyright ? 1996 Lucent Technologies Inc. All rights reserved.


(This Chinese translation isn't confirmed by the authors, and it isn't for profits.)

Translator : jhlicc@gmai1.c0m
Origin : http://cm.bell-labs.com/cm/cs/who/dmr/cacm.html