NOTE:UNIX Time Sharing System一文的作者为Dennis M. Ritchie 和 Ken Thompson,最初发表于ACM的Communications于1974年
本人在这里只是参考网上的资料,对原文进行了下翻译。本文翻译的原文来自于Sasank的博文: The UNIX Time-Sharing System - Sasank’s Blog,对于本人有疑惑的翻译,我在一旁放了原文。
摘要 #
UNIX是一个通用的,多用户,交互式操作系统,为Digital Equipment公司的PDP-11/40和11/45计算机所设计。它提供了许多即使在大型操作系统中也很少见的功能,包括:
- 包含可卸卷的分层级的多层文件系统
- 兼容的文件,设备和内部处理I/O
- 初始化异步进程的能力
- 对每个用户都可选的系统命令行语言
- 用多种语言实现的超过100个子系统
本文讨论文件系统和用户命令界面的实质和实现。
介绍 #
UNIX有三个版本。最早的版本(大约在1969–1970年)运行在Digital Equipment公司的PDP-7和PDP-9计算机上。第二个版本运行在不受保护的PDP-11 / 20计算机上。本文仅描述了PDP-11 / 40和PDP-45系统[1],因为它更现代,比起较早的UNIX,针对对一些重新设计时发现的不足或缺少的功能进行了修改。
PDP-11 UNIX 在1971年2月后可用,最初大约安装了40套;它们都比我们这里描述的系统要小。其中大部分用于处理非常具体的事务,例如处理和格式化专利申请,或其他的文字材料,对大量基于Bell系统的交换机进行故障数据的收集和处理,记录和检查电话服务命令。我们自己安装的则主要用于操作系统、编程语言、计算机网络及其他计算机科学方面的研究,当然也用来处理文档。
或许UNIX所达成的一个最重要的目的,是证明一个强大的交互式操作系统不需要那么昂贵的设备或人力维护。UNIX能够在不到40,000美元的硬件设备上运行,并且系统软件上的投入不到2人年。UNIX包含了许多在大型系统中少见的特性。可以预料的是,用户将会发现,这些重要的特性在UNIX系统中的实现是简单的、优雅的、易用的。
除了系统本身,UNIX上可主要的可用程序包括:汇编器,基于QED[2]的文本编辑器,链接加载器,符号调试器,如BCPL[3]一样具有类型和结构(C)的语言的编译器,BASIC方言的解释器,文本格式化程序,Fortran编译器,Snobol语言解释器,自顶向下的编译程序编译器(TMG)[4],自底向上的编译程序的编译器(YACC),表单字母生成器,宏处理器(M6),还有一个索引排序程序。
还有许多可维护的,实用的,娱乐性的和全新的程序。这些程序全都是在本地编写的。同时值得注意的是这个系统是完全自给自足的。所有的UNIX程序都在UNIX下维护,同样的,UNIX的相关文档也是通过UNIX编辑器编写和文字格式化程序格式化的。
硬件与软件环境 #
用于安装我们的UNIX系统的PDP-11/45是一个16位字长(8-bit byte),具有144K字节(byte)核心内存的计算机;UNIX占用了其中的42Kb(K bytes)。然而这个系统包括了大量的设备驱动并且为I/O缓冲区和系统表分配了足够的空间;一个能够运行上述软件的最小系统,最小总共只需要50K字节的内核。
PDP-11有一个1Mb(M bytes)的固定磁头磁盘,用于文件系统的存储和交换,4个可移动磁头磁盘驱动器,均可在移动磁盘盒中提供2.5Mb存储,还有一个单独的,具有可移动磁头,可以使用可移除的40Mb磁盘的驱动器。还有一个高速打孔纸带阅读器,9磁道磁带,以及D-tape(各种磁带设施,可以处理和重写单个记录)。除了控制台键盘输入,还有用于假脱机输出到公用行打印机的14个可变速通讯接口,附加在100系列数据集和201数据集接口。还有一些设备如可视电话接口,音频反馈单元,音频同步器、照相排字机、数字交换网络、和在一个随PDP-11/20产生向量、曲线、字符并在Texktronix 611展现的存储显像管。
大部分的UNIX软件是使用上面提到的C语言[6]编写的的。早期版本的操作系统是使用汇编语言编写的。但是在1973年的夏天,用C语言重写了一遍。新系统的体积比老版本大3倍。由于新的系统不仅更易于理解和修改,还包括了许多函数的改进,如多道程序(multiprogramming/多任务处理)和多用户程序之间共享可重入代码的功能,所以我们认为由此带来的体积增长是完全可接受的。
文件系统 #
UNIX中最重要的的工作是提供了一个文件系统。从用户的角度看,一共有三种类型的文件:普通磁盘文件,目录和特殊文件。
普通文件 #
一个文件可以包含用户放置的任何类型的信息,例如符号或者二进制(对象)或程序。不需为系统指定特定的数据结构。文本文件包含简单的、由换行符分隔段落的字符串。二进制文件是当程序开始执行时,将会在出现在核心内存中的,按顺序存放的字的序列。少数用户程序以更多的结构操作文件:汇编生成器和加载器需要特定格式的对象文件。即便这样,文件的结构也是由使用它们的程序控制的,而不是系统。
目录 #
目录提供了文件名和文件本身之间的映射,从而在整个文件系统上引入了一种结构。每个用户都有一个自己文件的目录;他还可以方便地创建包含文件群组的子目录。目录的行为与普通文件完全相同,不同之处在于目录无法由非特权程序写入,只有系统可以控制目录的内容。但是,具有合适权限的任何人都可以像读取其他任何文件一样读取目录。
系统维护几个目录供自己使用。其中之一是root目录。通过跟踪目录链中的路径直到找到所需的文件,可以找到系统中的所有文件,此类搜索的起点通常是根目录。另一个系统目录包含所有通常需要使用的程序,即所有的命令。但是就如将看到的,程序不必驻留在此目录中即可执行。
文件以14个或更少字符的序列命名。将文件名指定给系统后,它可以采用路径名的形式,该路径名是一系列目录名称,由斜杠/分隔,并以文件名结尾。如果序列以斜杠开头,则搜索将从根目录开始。名称/alpha/beta/gamma
使系统在根目录中搜索目录alpha
,然后在alpha中搜索beta
,最后在beta
中找到gamma
。 gamma
可以是普通文件,目录或特殊文件。作为限制,名称/
表示根目录本身。
不以/
开头的路径名会使系统开始在用户当前目录中进行搜索。因此,名称alpha/beta
在当前目录的子目录alpha
中指定了名为beta
的文件。最简单的名称(例如alpha
)是指本身在当前目录中找到的文件。作为另一种限制情况,如果文件名为空,则是指当前目录。
同一非目录的文件可能会以不同的名称出现在多个目录中,此功能称为链接,文件的目录条目有时称为链接。 UNIX与其他允许链接的系统不同,它与文件的所有链接都具有相同的状态。也就是说,文件在特定目录中不存在。文件的目录条目仅由其名称和指向实际描述文件的信息的指针组成,因此,文件实际上独立于任何目录条目而存在,尽管实际上会使该文件与最后一个链接一起消失。
每个目录始终至少有两个条目(.
和..
),每个目录的名称均指对应的目录。因此,程序在不知道完整路径名时可以读取当前目录为.
。而名称..
按照惯例是指向当前目录的父目录,即当前目录被创建时所在的目录。
目录结构被限定为为有根树的形式。除特殊条目 .
和 ..
外,每个目录必须作为另外一个目录(即其父目录)的条目出现,这是为了简化访问目录结构子树的程序的编写,更重要的是避免层次结构各部分的分离。如果允许链接到任意目录,则很难检测到从根到目录的最后一次连接何时断开。
特殊文件 #
特殊文件构成UNIX文件系统最不寻常的功能。 UNIX支持的每个I / O设备都与至少一个这样的文件相关联。特殊文件的读取和写入与普通磁盘文件一样,但是请求读取或写入会导致关联设备的激活。每个特殊文件的条目都位于目录/dev中,尽管可以像普通文件一样链接到其中一个文件。因此,例如要打孔纸带,可以在文件/dev/ppt上写。每个通信线路,每个磁盘,每个磁带驱动器以及物理核心内存都存在特殊文件。当然,活动磁盘和核心专用文件受到保护,不会受到任意访问。
以这种方式处理I / O设备具有三重优势:文件和设备I / O尽可能相似;文件名和设备名具有相同的语法和含义,因此可以将以文件名作为参数的程序传递给设备名。最后,特殊文件受与常规文件相同的保护机制。
可移除的文件系统 #
尽管文件系统的根目录始终存储在同一设备上,但是不必将整个文件系统层次结构都驻留在该设备上。有一个挂载系统请求,其中包含两个参数:现有普通文件的名称,以及直接访问的特殊文件的名称,该文件的关联存储卷(例如磁盘包)应具有包含其自身的独立文件系统的结构目录层次结构。 “ mount”的作用是使对以前普通文件的引用改为引用可移动卷上文件系统的根目录。实际上,mount用一个全新的子树(存储在可移动卷上的层次结构)替换层次结构树(普通文件)的叶子。挂载之后,可移动卷上的文件与永久文件系统上的文件之间几乎没有区别。例如,在我们的安装中,根目录位于固定头磁盘上,包含用户文件的大磁盘驱动器由系统初始化程序安装,四个较小的磁盘驱动器可供用户安装磁盘包。通过在其对应的特殊文件上写入来生成可安装文件系统。可以使用实用程序来创建一个空文件系统,或者可以仅复制一个现有文件系统。
对不同设备上的文件进行相同处理的规则只有一个例外:一个文件系统层次结构与另一个文件系统层次结构之间可能不存在链接。实施该限制是为了避免繁琐的簿记工作,否则将需要这些簿记工作,以确保在最终卸下可移动的卷时要卸下链接。特别是,在所有文件系统的根目录(可移动或不可移动的根目录)中,名称..均指目录本身而不是其父目录。
文件保护 - Protection #
尽管UNIX中的访问控制方案非常简单,但是它具有一些不寻常的功能。系统的每个用户都分配有一个唯一的用户标识号。当创建文件后,将会使用文件所有者的用户ID对其进行标记,还会为新文件提供一个七比特的保护位,其中六个指定文件所有者和所有其他用户的独立读取,写入和执行权限。
如果第七位打开,则每当文件作为程序执行时,系统都会暂时将当前用户的用户标识更改为文件创建者的标识。用户ID的这种更改仅在调用它的程序执行期间有效。设置用户ID功能提供特权程序,这些特权程序可能使用其他用户无法访问的文件。例如,一个程序可能会保存一个记录文件,除了程序自己,其他程序都不应该有阅读和修改该文件的权限。如果该程序的set-user-identification位为on,则它可以访问文件,尽管该访问可能被给定程序的用户调用的其他程序禁止。由于任何程序的调用者的实际用户ID始终可用,因此set-user-ID程序可以采取所需的任何措施来满足自己的调用者凭据。此机制用于允许用户执行精心编写的调用特权系统条目的命令。例如,有一个系统条目只能由“超级用户”(如下)调用,从而创建一个空目录。如上所述,目录应具有的条目有.
和..
。创建目录的命令由超级用户拥有,并具有set-user-ID位集。在检查其调用者的授权以创建指定目录后,它会创建该目录并设置进出方式为 .
和 ..
。
由于任何人都可以在自己的文件中设置“设置用户ID”位,因此通常无需管理即可使用此机制。例如,这种保护方案可以轻松解决[7]中提出的MOO计费问题。
系统将一个特定的用户ID(“超级用户”的ID)识别为不受通常文件访问限制的用户;因此,可以编写程序在解除保护系统不必要的干扰的情况下,转储和重新加载文件系统。
I/O请求 - I/O Calls #
系统的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指定的字节数组之间最多传输字节数。返回值n是实际传输的字节数。在写情况下,n与计数相同,除非在特殊情况下,例如I / O错误或特殊文件上的物理介质结尾。但是,在读取中,n可以毫无错误地小于count。如果读取指针非常靠近文件的末尾,以至于读取计数字符会导致读取超出末尾,则仅传输足够的字节才能到达文件的末尾;同样,类似键盘输入的设备绝不会返回多行输入。当读调用返回的n等于零时,它指示文件的结尾。对于磁盘文件,这在读取指针变得等于文件的当前大小时发生。通过使用取决于所用设备的转义序列,可以从打字机生成文件结尾。
写在文件上的字节仅影响写指针位置和计数所隐含的字节。文件的其他部分均未更改。如果最后一个字节位于文件末尾之外,则文件将根据需要增长。
要执行随机(直接访问)I / O,仅需要将读取或写入指针移动到文件中的适当位置:
location = seek(filep, base, offset)
取决于基数,与filep关联的指针从文件的开头,指针的当前位置或文件的结尾移到位置偏移字节。偏移量可能为负。对于某些设备(例如,纸带和键盘),寻线被忽略。从指针开始移动到的文件开头的实际偏移量返回到位置。
其他I/O调用 #
还有其他一些与I / O和文件系统有关的系统条目,将不再讨论。例如:关闭文件,获取文件状态,更改保护模式或文件所有者,创建目录,建立指向现有文件的链接,删除文件。
文件系统的实现 #
正如上面文件系统-目录中提到的,一个目录条目包含一个关联到文件的名字和一个指向自身的指针。这个指针是一个叫i-number
的整型数。当文件被访问时,它的i-number
被用作系统表(i-list
)的索引——系统表存储在目录所在设备的已知部分。由此找到的条目(文件的i-node
)包含文件的以下描述信息:
- 所有者;
- 保护位;
- 文件内容的物理磁盘或磁带的地址;
- 大小;
- 上次修改时间;
- 到文件的链接数,也就是文件在目录中出现的次数;
- 一个表示文件是否是目录的bit;
- 一个表示文件是否属于特殊文件的bit;
- 一个表示文件是“文件”还是“小文件”的bit;
‘open’或‘create’系统调用的目的是,通过查询显式或隐式命名的目录把用户指定的路径名称转换为’i-number‘。文件一旦被打开,它所在的设备、i-number
、读/写指针都被存储在系统表中。系统表通过open和create返回的文件描述符索引。因此文件描述符能够很容易的与随后可能用到的read、write系统调用访问文件时所需的信息关联。
创建一个新文件时会分配一个i-node
给新文件,同时创建一个包含文件名和i-node编号的目录条目。建立一个到现有文件的链接时,会用新文件名创建一个新的目录条目,从源目录条目拷贝i-number
,增加i-node
中的link-count值。删除文件时,会减少目录条目对应的i-node
中的link-count,然后抹去目录条目。如果link-count减为0,文件的所有磁盘块都被释放,i-node
也被重新分配。
所有固定或可移除磁盘的空间都被划分为512字节的块,地址编号从0到设备本身的容量上限。每个文件的i-node中都为8个设备地址留有空间。一个小文件(非特殊文件)适合于8个或更少的块,在此情况下块的地址本身被存储。对于大文件(非特殊文件),8个设备地址中的每一个, 都指向一个256个块地址组成的文件本身。 这些文件可以大到8256512=1,048,576(2的10次方)字节。
前面的讨论都是针对普通文件的。当对一个i-node
显示是特殊文件的I/O请求时,其他的7个设备地址字就不重要了。列表被解释为组成内部设备名称的字节。这些字节确定了各自的设备类别,并由此决定由哪个系统程序来处理该设备的I/O。子设备编号选择了例如附加在一个指定的控制器上的1个磁盘驱动器,或多个相同的键盘输入接口之一。
在此情况下,mount
系统调用的实现非常简单。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避免了这个问题。
文件系统的效率 #
为了提供对Unix和文件系统的指示,我们分析一个7621行的汇编程序的时间。该汇编程序独自在机器上运行,总体的时钟时间是35.9秒,每秒运行212行。时间被按照如下方式划分:63.5%的时间用于汇编的执行,16.5%的时间是系统开销,20%的时间是磁盘的等待时间。我们不视图解释这些数字,也不会去和其他系统对比,只是说我们总体上对于这样的系统开销是满意的。
进程和映像 - Process and image #
映像(image)是一个计算机执行环境。它包括核心映像,通用寄存器值,打开的文件状态,当前目录,以及与此相似的东西。印象是一个伪计算机的当前状态。
进程是一个映像的执行。当处理器代表一个进程去执行时,印象要驻留在核心内存中。在其他进程的执行过程中,它仍然驻留在核心内存中,除非一个激活的,更高优先级的进程强制性的把它从内存中交换到固定磁头的磁盘驱动器上。
一个映像的用户核心部分在逻辑上分成3段。程序文本段从虚拟地址空间的0开始。在执行过程中,这个段是写保护的,而且它的单一拷贝被所有执行相同程序的进程所共享。从最初开始,程序文本段的8K字节处,是一个可写的、非共享的数据段。这个段的体积可以通过系统调用扩展。在虚拟地址的最高处开始的是栈段,当栈指针变动时,它自动向下增长。
进程 #
除了Unix引导它自身进入运行中,只能通过使用fork系统调用创建一个新的进程。
processid = fork(label)
当一个进程执行fork系统调用,它会被分离成两个独立的执行进程。这两个进程有各自独立的源印象的拷贝,并共享所有打开的文件。新进程唯一不同于父进程的是:在父进程中,控制从fork直接返回,在子进程中,控制被传递给label.fork系统调用返回的processid是相对于其他进程的标识。
因为父进程和子进程的返回点不同,fork后的每个印象都可以决定它是父进程还是子进程。
管道 #
进程可以与相关的进程用和文件系统相同的read/write系统调用方式通信。
filep = pipe()
这个系统调用返回一个文件描述符filep,并创建一个叫做pipe的进程间通道。这个通道就像打开的文件一样,在印象中通过fork调用从父进程传递到子进程。read调用使用管道文件描述符,等待其他进程使用同样的管道文件描述符写入。在这点上,数据是在两个进程的印象之间传递的。进程不需要了解管道比普通文件的区别,只需要调用它。
程序的执行 #
另一个主要的系统原语通过一下方式调用
execute(file,arg1,arg2,...,argn)
该调用请求系统读入并执行名为file
的程序,并传递字符串类型的参数arg1,arg2,...argn
。一般的arg1
与file
相同,因此程序可以确定被调用的名称。用execute
执行的进程中所有的代码和数据,都被file所替代。但是打开的文件、当前目录、进程间的关系是不变的。只有当调用失败时,例如找不到file
对应的程序文件,或由于这个文件的执行许可位(execute-permission bit)没有被置位,才会从execute
原语返回。这很像机器指令中的"jump",而不是子程序的调用。
进程同步 #
另一个进程控制的系统调用
processid = wait()
这个调用导致它的调用者将被挂起,直到它的某个子进程执行结束。wait返回终止进程的processid。如果调用的进程没有后代进程,会返回error。子进程的某些状态也是可用的,wait可以获取到孙子或更远祖先进程的状态。
进程终止 #
最后,
exit(status)
这个系统调用终止一个进程,销毁其印象,关闭打开的文件,抹去这个进程。当父进程通过wait原语被通知,参数status所指示的状态就可用了。如果父进程已终止,其状态对祖父进程可用,以此类推。进程也可能由于一些非法动作或用户产生的信号终止。
Shell #
对于大部分用户,与Unix的沟通都是通过一个叫做Shell的程序辅助完成的。Shell是一个命令行解释器:它读取用户输入的行,然后将他们解释为需要请求执行的程序。在最简单的情况下,一个命令行由命令名和跟随的参数组成,命令名和参数之间通过空格分隔。
command arg1 arg2 ... argn
Shell将命令名和参数分割为独立的字符串,这样就能找到名为command的文件。command可能是一个包含"/“的路径名以指出系统中的任何文件。如果command被找到,它将被引入到核心内存中被执行。Shell收集到的参数对于command是可访问的。Shell重新回到自己的执行当中,并立刻准备接受用户输入的下一条命令。
如果没有找到command对应的文件,Shell会自动在command前添加/bin/,来试图从/bin目录下再次寻找。/bin目录中包含了常用的命令。
标准I/O #
前面讨论过的I/O表明每个程序用到的文件都必须通过程序打开或创建以获取文件描述符。通过Shell执行的程序,是以两个打开的文件描述符为0和1的文件开始的。这样的程序开始执行时,文件1用于写,可以理解为标准的输出文件。除了以下情况,即文件是用户的打印机。程序希望通过文件描述符1写入有用的,或调试用的信息。相反的,文件0用于读取,程序希望读取用户输入的信息。
Shell能修改标准分配的键盘或打印机文件描述符。如果command的一个参数是以”>“开头,文件描述符1在command执行中就会被替换为>后面跟随的文件名。例如:
ls
上诉一般会在打印机上列出当前目录中的文件。
下面的命令:
ls > there
会创建一个there文件,然后把文件列表作为文件内容填进去。因此参数 “>“表示把输出放到there中。
另一面
ed
一般会进入编辑器,通过用户的键盘输入获取请求。命令:
ed < script
把script作为编辑器命令的文件解释。"<script"意味着,从script获取输入。
尽管”<",">“后面跟随的文件名作为命令参数出现,事实上它会被Shell作为命令解释,而不会作为参数传递给前面的命令。因此,没有在每个command中处理重定向的必要。命令只要在适当的时候使用标准的文件描述符0和1即可。
过滤器 - Filters #
标准I/O概念的一个扩展,是将一个命令的输出用做另一个的输入。1个用竖线分隔的命令序列会使Shell同时执行所有的命令,并同时把每个命令的输出作为下一个命令的输入。在命令行中:
ls | pr –2 | opr
ls列出当前目录中的所有文件名;它的输出被传递给pr,这个命令可以把输入分页,并在页眉显示日期。参数“-2”表示显示2列。同样的,pr的输出作为opr的输入。这个命令把它的输入假脱机到一个离线打印的文件上。
这个过程也可以通过一个更笨拙的方式实现
ls >temp1
pr –2 <temp1 >temp2
opr <temp2
在没有重定向input和output的能力时,一个更笨拙的办法是接受用户的请求来分页其输出,把它按照多列打印,然后将输出传输到脱线。事实上期望ls
命令的作者提供如此之多的命令选项,这会让人很诧异,而且既不高效也不明智。
类似pr
的程序把它的标准输入拷贝到标准输出的操作,叫做过滤器(filter)。过滤器在我们处理字符串的直译、输入的排序、编码和解码上是很有用的。
命令分隔符:多任务 #
Shell提供的另一个功能则相对简单:命令不必位于不同的行,并且它们可以用分号来分隔。
ls ; ed
首先将列出当前目录的内容,然后进入编辑器。
一个相关的功能更有趣。如果命令后跟&,那么命令行管理程序将不等待命令完成再提示,而是准备立即执行新命令,例如:
as source >output &
source文件将会被as程序编译,输出的结果将放入output文件;无论as花费多长时间,Shell都会立即返回。当前命令行管理程序不会等待as命令完成,将显示运行该命令的进程的标识(Pid),该标识可以用于等待命令完成或终止它。 &可以在一行中多次使用:
as source >output & ls >files &
同时在后台进行as编译命令和ls命令。在上面使用&的示例中,提供了除键盘输入之外的output文件;如果不这样做,那么各种不同的命令输出结果将会混合在一起。
Shell还允许在上述操作中加上括号,例如:
(date; ls)>x &
在文件x
上打印当前日期和时间,后跟当前目录列表。Shell也将会立即返回并准备运行其他请求。
Shell作为命令:命令文件 #
Shell本身是一个命令,可以递归调用。假设文件tryout
包含以下几行
as source
mv a.out testprog
testprog
mv
命令使文件a.ou
t重命名为testprog
。 a.out
是汇编器的(二进制)输出,准备用于执行。因此,如果在控制台上键入以上三行,则将用汇编器编译source
,生成名为testprog
的程序,并执行testprog
。当输入行为tryout
,那么命令:
sh < tryout
将会使Shellsh
按顺序执行以上命令。
Shell还具有其他功能,包括替换参数以及从目录中文件名的指定子集构造参数列表的能力,也可以根据字符串比较或给定文件的存在,从而有条件地执行命令,并在已归档的命令序列内执行控制转移。
Shell的实现 #
现在可以了解Shell的操作概述。在大多数情况下,命令行管理程序正在等待用户键入命令。键入换行符后的换行符,Shell的read
调用返回。命令行管理程序将分析命令行,并将参数放入适合execute
(执行)的形式。然后调用fork
。子进程(其代码当然仍然是Shell的代码)会尝试使用适当的参数进行execute
(执行)。如果成功,它将引入并开始执行给出名称的程序。同时,由fork
产生的另一个进程(即父进程)wait
(等待)子进程死亡。发生这种情况时,命令行管理程序就会知道命令已完成,因此它会键入提示符并读取键盘输入以获取另一个命令。
在这种框架下,后台流程的实现非常简单。每当命令行包含&
时,命令行管理程序就不会等待它创建的执行命令的进程。
幸运的是,所有这些机制都与标准输入和输出文件的概念很好地结合在一起。当由fork
原语创建一个进程时,它不仅继承其父级的核心映像,而且还继承其父级中当前打开的所有文件,包括那些文件描述符为0和1的文件。Shell当然会使用这些文件来读取命令行并编写其提示和诊断信息,在通常情况下,其子级(命令程序)会自动继承它们。但是,给定带有<
或>
的参数时,子进程将在执行执行之前使标准I / O文件描述符0或1分别引用命名文件。这很容易,因为根据协议,在打开(或创建)新文件时会分配最小的未使用文件描述符。仅需要关闭文件0(或1)并打开命名文件。因为命令程序运行的过程只是简单地终止,所以在<或>之后指定的文件与文件描述符0或1之间的关联将在该过程结束时自动结束。因此,命令行管理程序不需要知道文件的实际名称,它们是它自己的标准输入和输出,因为它不需要重新打开它们。
Filters是标准I/O重定向的直接扩展,使用的是管道(pips)而不是文件(files)。
在通常情况下,命令行管理程序的主循环永远不会终止。 (主循环包括属于父进程的fork返回的分支;即,该分支先进行等待,然后读取另一条命令行。)导致Shell终止的一件事是发现结尾输入文件的-file条件。因此,当使用指定的输入文件将Shell作为命令执行时,如下所示:
sh < comfile
comfile
中的命令将一直执行到comfile
结束为止;那么sh
调用的Shell实例将终止。由于此Shell进程是Shell另一个实例的子级,因此将返回在后者中执行wait
(等待),并可以处理另一个命令。
初始化 #
用户向其键入命令的Shell实例本身就是另一个进程的子级。 UNIX初始化的最后一步是创建单个进程,并调用(通过执行)名为init的程序。 init的作用是为每个打字机通道创建一个进程,用户可以拨打该进程。 init的各种子实例为输入和输出打开适当的打字机。由于调用init时没有打开文件,因此在每个进程中,打字机键盘将分别接收文件描述符0和打印机文件描述符1。每个进程都会键入一条消息,要求用户登录并等待阅读打字机,以获取回复。首先,没有人登录,因此每个进程都挂起了。最后,有人键入他的名字或其他身份证明。适当的init实例将被唤醒,接收登录行,并读取密码文件。如果找到了用户名,并且能够提供正确的密码,则init会更改为用户的默认当前目录,将进程的用户ID设置为登录用户的ID,然后执行命令行管理程序。此时,命令行管理程序已准备就绪,可以接收命令,并且登录协议已完成。
同时,init
的主流路径(其自身所有子实例的父级,后来将成为Shell)都在wait
(等待)。如果子进程之一终止,要么是因为命令行管理程序找到了文件的末尾,要么是因为用户键入了错误的名称或密码,因此,此初始化路径只是重新创建了已终止的进程,这反过来又重新打开了相应的输入和输出文件及类型另一个登录消息。因此,用户可以简单地通过键入文件结尾序列来代替对Shell的命令来注销。
其他作为Shell的程序 #
如上所述的Shell被设计为允许用户完全访问系统的设施,因为它将以适当的保护模式调用任何程序的执行。但是,有时需要与系统使用不同的接口,并且可以轻松安排此功能。
回想一下,在用户通过提供其名称和密码成功登录后,init
通常会调用Shell解释命令行。用户在密码文件中的输入可能包含登录后要调用的程序的名称,而不是Shell。该程序可以自由地以其希望的任何方式解释用户的消息。
例如,秘书编辑系统用户的密码文件条目指定使用编辑器ed
而不是命令行管理程序。因此,当编辑系统用户登录时,他们位于编辑器内部并且可以立即开始工作。同样,可以防止它们调用不适合其使用的UNIX程序。在实践中,事实证明,允许暂时退出编辑器以执行格式化程序和其他实用程序是合乎需要的。
在UNIX上可用的几种游戏(例如国际象棋,二十一点,3D井字游戏)要求更为严格的限制环境。对于每个密码,密码文件中都有一个条目,指定要调用适当的游戏程序而不是Shell。以其中一款游戏的玩家身份登录的人发现自己仅限于该游戏,无法整体上研究可能更有趣的UNIX产品。
自陷 - Traps #
PDP-11硬件检测到许多程序错误,例如对不存在的内存的引用,未实现的指令以及在需要偶数地址的地方使用的奇数地址。此类故障会导致处理器陷入系统例程。当发现非法行为时,除非做出其他安排,否则系统会终止该过程,并将用户的映像写入当前目录的文件核心中。调试器可用于确定发生故障时程序的状态。
循环的程序会产生不需要的输出,或者引起用户重新思考的循环程序可以通过使用interrupt
信号来停止,该中断信号是通过键入“delete”字符生成的。除非采取了特殊措施,否则该信号只会导致程序停止执行而不会生成核心映像文件。
还有一个quit
信号,用于强制生成核心图像。因此,可能会暂停意外循环的程序,并且无需预先安排即可检查核心映像。
硬件生成的故障以及中断和退出信号可以通过请求被过程忽略或捕获。例如,命令行管理程序忽略退出以防止退出使用户注销。编辑器捕获中断并返回其命令级别。这对于在不丢失正在进行的工作的情况下停止较长的打印输出很有用(编辑器将操纵它正在编辑的文件的副本)。在没有浮点硬件的系统中,将捕获未实现的指令,并解释浮点指令。
透视 - Perspective #
也许自相矛盾的是,UNIX的成功很大程度上归因于它并非旨在满足任何预定义的目标。当我们中的一个人(汤普森/Thompson)对可用的计算机设施不满意,发现了一个很少使用的系统PDP-7并着手创建一个更友好的环境时,就编写了第一版。这种本质上的个人努力非常成功,引起了其余作者和其他人的兴趣,后来又证明了购买PDP-11 / 20的合理性,特别是支持文本编辑和格式化系统。后来11/20的数量已不多了,事实证明UNIX足以说服管理层投资PDP-11 / 45。我们在努力中的目标,即使是明确表达的,也始终与建立与机器的舒适关系以及探索操作系统中的思想和发明有关。我们没有面临满足别人要求的需求,对于这种自由,我们深表感谢。
现在回想起来,影响UNIX设计的三个因素如下:
首先,由于我们是程序员,所以我们自然而然地设计了该系统以使其易于编写,测试和运行程序。对编程便利性的渴望的最重要表达是该系统被安排用于交互使用,即使原始版本仅支持一个用户。我们认为,设计合理的交互式系统比“批处理”系统更具生产力和使用满意度。而且,这样的系统相当容易适应于非交互使用,而反之则不成立。
其次,系统及其软件始终存在相当严格的大小限制。考虑到局部性对合理效率和表达能力的渴望,尺寸约束不仅鼓励了经济性,而且还鼓励了设计的某种优雅。这可能只是“通过苦难得救”哲学的一个变相的变体,但在我们看来,它是有效的。
第三,几乎从一开始,系统就能够并且确实自我维护。这个事实比看起来更重要。如果系统的设计者被迫使用该系统,他们会很快意识到其功能和表面上的缺陷,并会极力主动地进行纠正,以免为时已晚。由于所有源程序始终可用,并且可以轻松地在线修改,因此我们愿意在他人发明,发现或提出新想法时修改和重写系统及其软件。
本文讨论的UNIX方面至少清楚地显示了这些设计考虑因素中的前两个。例如,从编程的角度来看,文件系统的接口非常方便。最低的接口级别旨在消除各种设备和文件之间以及直接访问和顺序访问之间的区别。不需要大型“访问方法”例程即可使程序员与系统调用隔离开来;实际上,所有用户程序要么直接调用系统,要么使用一个小型的库程序,该程序只有几十条指令,该程序可以缓冲许多字符并一次读取或写入所有字符。
编程便利性的另一个重要方面是,没有“控制块”具有复杂的结构,该结构部分地由文件系统或其他系统调用维护,并受其依赖。一般来说,程序地址空间的内容是程序的属性,我们试图避免对该地址空间内的数据结构施加限制。
考虑到所有程序都应可与任何文件或设备一起用作输入或输出的要求,从节省空间的角度出发,也希望将与设备有关的注意事项推入操作系统本身。唯一的选择似乎是使用所有程序加载用于与每个设备打交道的例程,这在空间上是昂贵的,或者取决于在实际需要时动态链接到适合于每个设备的例程的某种方式,这或者是昂贵的在开销或硬件中。
同样,过程控制方案和命令界面已被证明既方便又有效。由于Shell作为普通的可交换用户程序运行,因此它不会占用系统适当的有线空间,并且可以以很少的成本实现所需的强大功能,特别是考虑到Shell作为框架执行时所需要的框架。产生其他进程来执行命令的进程,I / O重定向,后台进程,命令文件和用户可选的系统接口的概念,在实现上都变得微不足道。
影响 #
UNIX的成功不仅仅在于它是一个全新的操作系统,还在于对精心选择的丰富思想的充分利用,尤其是表明它们可以成为实现小型而强大的操作系统的关键。
伯克利分时系统[8]中存在分叉操作,本质上是我们执行时的操作。在许多方面我们受到了Multics的影响,Multics提出了I/O系统调用的特殊形式[9]以及Shell的名称及其常规功能。Shell应该为每个命令创建一个进程的概念Multics的早期设计也向我们建议了此功能,尽管后来出于效率原因将其删除。TENEX [10]使用了类似的方案。
统计数字 #
以下展示了有关UNIX的统计信息,用于显示当前系统的使用规模以及在哪些方面被使用。我们那些不在文档准备工作中被包含的用户倾向于将系统用于程序开发,尤其是语言的工作。很少有重要的“应用程序”程序。
总体情况 #
数量 | 描述 |
---|---|
72 | 使用者 |
14 | 最大并行用户 |
300 | 目录 |
4400 | 文件 |
34000 | 512字节的辅助存储块 |
每天(每天24小时,每周7天) #
有一个“后台”进程以最低的优先级运行;它用于吸收任何空闲的CPU时间。它已用于产生常数e – 2的百万位近似值,并且现在正在生成复合伪素数(基数2)。
180 | Commands |
---|---|
4.3 | CPU 小时(aside from background) |
70 | 连接小时 |
30 | 不同用户 |
75 | 登陆 |
命令CPU使用率(截止为1%) #
比例 | 程序 | 比例 | 命令 |
---|---|---|---|
15.7% | C 编译 | 1.7% | Fortran 编译 |
15.2% | users’ programs | 1.6% | remove file |
11.7% | editor | 1.6% | tape archive |
5.8% | Shell (used as a command including command times) | 1.6% | file system consistency, check |
5.3% | chess | 1.4% | library maintainer |
3.3% | list directory | 1.3% | concatenate/print files |
3.1% | document formatter | 1.3% | paginate and print file |
1.6% | backup dumper | 1.1% | print disk usage |
1.8% | assembler | 1.0% | copy file |
命令访问(截止到1%) #
15.3% | editor | 1.6% | debugger |
---|---|---|---|
9.6% | list directory | 1.6% | Shell (used as a command) |
6.3% | remove file | 1.5% | print disk availability |
6.3% | C compiler | 1.4% | list processes executing |
6.0% | concatenate/print file | 1.4% | assembler |
6.0% | users’ programs | 1.4% | print arguments |
3.3% | list people logged on system | 1.2% | copy file |
3.2% | rename/move file | 1.1% | paginate and print file |
3.1% | file status | 1.1% | print current date/time |
1.8% | library maintainer | 1.1% | file system consistency check |
1.8% | document formatter | 1.0% | tape archive |
1.6% | execute another command conditionally |
可靠性 #
我们关于可靠性的统计数据比其他数据更为主观。以下结果是我们合并后的记录中最好的,时间跨度超过一年,年份非常早,年份为11/45。
由于软件无法应对重复的电源故障而导致奔溃的硬件问题,导致文件系统丢失了一次(五个磁盘中的一个磁盘)。该磁盘上的文件已备份三天。
“崩溃(crash)”是指计划外的系统重新引导或停止,每隔一天大约发生一次奔溃,其中约三分之二是由硬件问题引起,例如电源中断和莫名其妙的随机位置的处理器中断,其余是软件故障。最长的不间断运行时间约为两周。服务呼叫平均每三周进行一次,但每次的群集非常密集,总的正常运行时间约为我们计划中的每天24小时,共365天中的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.
最后修改于2020-11-26,今后会继续修改