Chinaunix首页 | 论坛 | 博客
  • 博客访问: 704346
  • 博文数量: 19
  • 博客积分: 5025
  • 博客等级: 大校
  • 技术积分: 290
  • 用 户 组: 普通用户
  • 注册时间: 2006-01-24 10:41
个人简介

null

文章分类

全部博文(19)

分类: 其他UNIX

2016-06-05 12:12:29


UNIX 实现

K. Thompson
Bell 实验室 Murray Hill, New Jersey 07974

翻译:寒蝉退士

译者声明:译者对译文不做任何担保,译者对译文不拥有任何权利并且不负担任何责任和义务。

摘要

本文用高层术语描述常驻的 UNIX 内核的实现。这些讨论分为三部分。第一部分描述 UNIX 系统如何看待进程、用户、和程序。第二部分描述 I/O 系统。最后一部分描述 UNIX 文件系统。


1. 介绍

UNIX 内核由大约 10,000 行 C 代码和大约 1,000 行汇编代码组成。这些汇编代码可进一步分割成 200 行是出于效能的缘故而被包含的(它们可以用 C 来写)和 800 行被用来完成不能用 C 写的硬件功能。

在组建成广义的“UNIX 操作系统”的所有代码中的这些内核代码占了 5 到 10 个百分点。内核是唯一不能依据用户的喜好来替换的 UNIX 代码。因此,内核应当尽可能少的做实际决定。这不意味着允许用户做同一件事情有无数种选择。而是意味着做一件事情只允许一种途径,但让这个途径成为可能已经提供了的所有选择(option)的最小公约数。

在内核中实现什么或不实现什么,体现了重大的责任和强大的力量二者。它是遵循“做事情所应有的途径”的一个“肥皂盒”平台。即使如此,如果“这个途 径”太根本(radical)了,则没有人会依从它。每个重要决定都被谨慎的权衡了。从始至终,高效性替代了简单性。只在可局部化其复杂性的条件下使用复 杂的算法。


2. 进程控制

在 UNIX 系统中,用户在叫做用户进程的环境中执行程序。在要求一个系统功能的时候,用户进程把系统作为一个子例程来调用。在这个调用中的某一点上,有一个明显的环 境切换。此后,这个进程被称为系统进程。在进程的正规定义中,用户进程和系统进程是同一个进程的不同阶段(它们从不同时执行)。出于保护,每个系统进程都 有一个自己的栈。

用户进程执行自一个只读的正文段,它可以被所有执行同一个代码的所有进程所共享。共享正文段没有带来功能上的利益。不需要把只读段交换出去的事实带 来了效能上的利益,因为在次级存储中的最初的复件(copy)仍然存在。这对在等待终端输入期间要被交换出去的交互程序有很大的好处。进而,如果两个进程 同时执行自一个只读段的相同的复件,则只有一个复件需要在主存中驻留。由于一个程序的同时执行不是常见的,所以这个效果是次要的。作为一个讽刺,这个效果 节省了主存的使用,但只在主存过于充足(overabundance)的时候发挥作用,此时有足够的内存来保持处于等待中的进程被装载着。

在系统中所有的当前只读正文段用正文表来维护。正文表条目持有这个正文段在次级存储中的位置。如果这个段被装载了,则这个表还持有主 存位置和共享这个条目的进程的记数。当这个记数减少到零的时候,这个条目与持有这个段的主存和次级存储一起释放。当一个进程首次执行一个共享正文段的时 候,分配一个正文条目并把这个段装载到次级存储上。如果第二个进程执行了已经分配了的一个正文段,简单的增加这个条目的引用记数。

用户进程在它自己的数据段中包含一些完全私有的读写数据。系统尽可能的不使用用户的数据段来持有系统数据。特别是,在用户地址空间中没有 I/O 缓冲区。

用户数据段有两个增长边界。一个边界由系统作为内存无效(fault)的结果而自动增加,它用于栈。第二个边界只能用显式的请求来增长(或缩小)。新近分配的主存的内容被初始化为零。

还有一个固定大小的系统数据段与进程有关联并一起被交换。这个段包含只在进程活跃的时候系统才需要的关于这个进程的所有数据。系统数据段中的包含的各种数据的例子有: 保存的中央处理器寄存器,打开的文件描述符,记帐信息,临时(scratch)数据区,和给这个进程的系统阶段的栈。系统数据段从用户进程是不可寻址的并以此来保护。

最后,有给每个进程一个条目的进程表。这个条目包含当这个进程不活跃的时候系统需要的所有数据。例如,进程的名字,其他段的位置,和调度信息。在建立进程的时候分配进程表条目,并在进程终止的时候释放。这个进程条目总是可以被内核直接寻址的。

图 1 展示了各种进程控制数据之间的联系。某种意义上,进程表是所有进程的定义(definition),因为从进程表条目出发可以访问与一个进程有关的所有数据。

2.1. 进程建立和程序执行

通过系统基本操作 fork 来建立进程。新建立的(子)进程是原始(父)进程的复件。在这两个进程之间没有可检测到的主存共享。(当然,如果父进程执行自一个只读正文段,则子进程将共享这个正文段。) 为子进程做出所有可写数据段的复本。在 fork 之前打开的文件在 fork 之后真正的被共享。把它们在这种关系中的位置通知进程,允许它们选择它们自己的(通常是不一致的)命运。父进程可以 wait (等待)任何它的子进程的终止。

进程可以 exec (执行)一个文件。这包括为这个文件中指定的新文本和数据段对换当前正文和数据。丢弃旧段。做 exec 不改变进程; 做 exec 的进程继续存在,但在 exec 之后它执行了一个不同的程序。在 exec 之前打开的文件在 exec 之后仍然打开。

如果一个程序,假定这个程序是一个编译器的第一遍,想要用另一个程序覆盖自己,假定那个程序是第二遍,则它可以简单的 exec 第二个程序。这是“跳转”的同义语。如果一个程序想要在 exec 另一个程序之后保持控制,则它应该 fork 一个子进程,让子进程 exec 这个程序,并让父进程 wait 子进程。这是“调用”的同义语。把调用分解成一个绑定和随后的一个转换类似于 SL-5 中的子例程连接。


2.2. 交换

需要时,把与一个进程有关的主要数据(用户数据段,系统数据段,和正文段)与次级存储来回交换。在连续的主存中保存用户数据段和系统数据段来减少交换延迟。(如果使用了低延迟设备,如 bubble、CCD、或 scatter/gather 设备,必须重新考虑这个决定)。进行主存和次级存储二者的分配使用相同的首次适配 (first-fit)算法。当一个进程增大的时候,分配一片新的主存。把旧内存中的内容复制到新内存中。释放旧内存并更改有关的表。如果没有足够的主存,则分配次级存储来替代。把这个进程交换到次级存储上,准备好以新的大小交换进主存。

交换进程是内核中一个独立的进程,它简单的把其他进程交换进出主存。它检查进程表找到已被交换出去并准备好了运行的一个进程。它为这个进程分配主存 并把它的段读取到主存中,这个进程将在主存中与其他载入的进程竞争中央处理器。如果没有可获得的主存,交换进程通过检查进程表找到可以交换出去的进程来得 到可使用的主存。它选择一个进程来交换出去,把它写到次级存储,释放主存,并回过头来查找要交换进来的进程。

这样交换进程就有两个特定算法。把交换出去的多个进程中的哪个交换进来? 这由次级存储驻留时间决定。 被交换出去时间最长的最先交换进去。对大进程有轻微的处罚。把装载的多个进程中的哪个交换出去? 首先挑出等待慢事件(比如,非当前运行或等待磁盘 I/O)的进程,用在主存中的年龄(age)来挑选,再次带有大小处罚。(如果没有的话)用相同的年龄算法检查其他进程,除非它们到了至少为规定的年龄,否则不取出它们。这为交换增加了滞后(hysteresis)并防止整体抖动(thrashing)。

这些交换算法是系统中最另人疑惑的。对于有限的主存,这些算法导致整体交换。这本身并不坏,因为交换不冲击驻留进程的执行。但是,如果交换设备还必须用于文件存储,交换流量严重的冲击文件系统流量。想要双重利用有限的磁盘资源的小系统确实如此。


2.3. 同步和调度

通过使进程等待事件来完成进程同步。事件用任意整数来表示。出于方便,事件被选择为与这些事件相关的表的地址。例如,等待某个子进程终止的一个进程将等待一个事件,这个事件是它自己的进程表条目的地址。 在进程终止的时候,它通告(signal)用它的父进程的进程表条目表示的这个事件。通告没有进程在等待的一个事件没有任何效果。类似的,通告有许多进程在等待的一个事件将把他们都唤醒。这与 Dijkstra 的 P 与 V 同步操作有相当的区别,这里没有与事件相关的记忆。所以不需要事件的分配先于它们的使用。 事件仅仅通过被使用而存在。

在相反的方面,因为没有有事件相关联的记忆,通过事件机制不能通告“有多少”的概念。例如,想要内存的进程可能等待有内存分配有关的一个事件。当任 何数量的内存变得可获得的时候,将通告这个事件。所有竞争的进程都将被唤醒来争夺这个新内存。(实际上,交换进程是唯一的等待主存变成可获得的进程。)

如果在一个进程决定等待一个事件的时刻和这个进程进入等待状态的时刻之间这个事件发生了,则这个进程将等待已经发生(并可能永不再次发生)的一个事 件。这种竞争条件偶尔出现的原因是没有与这个事件相关联的记忆来指示这个事件已经发生了;一个事件的唯一的动作是把一组进程从等待状态变更为运行状态。进 程切换只能通过对事件等待机制的显式的调用而在内核中发生,这个事实在相当大程度上解除了这个问题。如果了正在提及的这个事件由其他进程来通告,则没有问 题。但是如果这个事件由一个硬件中断来通告,则必须进行特别关照。在改变 UNIX 以适应多处理器配置的时候,这种同步竞争是暴露出的最大的问题。 

在内核中的事件等待代码好象是一个协同例程(co-routine)连接。在任何时候,除了一个之外所有的进程都调用了事件等待。剩下的进程是当前运行的那个进程。当它调用事件等待的时候,选择其等待的事件已经发生了的那个进程,并且这个进程从它的到事件等待的调用返回。

接着运行那个可运行的进程? 同每个进程有关的是特权级。系统进程的特权级由发起在一个事件上的等待的代码来赋予。这基本上等同于人们在这样一个事件上所希望的响应(次序)。 磁盘事件有高优先级,终端(teletype)事件有低优先级,当日时间(time-of-day)事件有非常低的优先级。(观测的结果是,在系统进程优 先级上的不同有很小甚至没有性能冲击。) 所有用户进程优先级都低于最低的系统优先级。用户进程优先级由一个算法来赋予,这个算法基于运算时间总量与这个进程消费的实际时间(real time)的当前比率。在上次的实时单元(unit)中使用了许多运算时间的进程被赋予低用户优先级。因为交互进程以低运算与实际时间比率为特征,(以此)维持交互响应而不用任何特殊安排。

调度算法简单的挑选有最高优先级的进程,所以首先挑出所有系统进程再挑出用户进程。每秒种更新一次运算-实际时间的比率。所以,所有其他东西是平等 的,以 1 秒为基本量(quantum)用轮转法调度围成圈的( looping)的用户进程。一个高优先级的进程醒来将抢占一个运行的低优先级进程。这个调度算法有一个非常吸引人的负反馈特征。如果一个进程使用它的高 优先级来独占计算机,则它的优先级将被降低。同时,如果一个低优先级进程长时间被忽略,则它的优先级将被提升。


3. I/O 系统

I/O 系统被分成两个完全独立的系统: 块 I/O 系统和字符 I/O 系统。回想起来,名字应当分别是“有组织的 I/O” 和“无组织的 I/O”;尽管术语“块 I/O”有些意义,“字符 I/O”却是完全的用词不当。

设备被特征化(characterize)为一个主设备号,一个次设备号,和一个类别(块或字符)。对于每一类别,有进入设备驱动程序的入口点的一 个数组。当调用一个特定设备驱动程序的代码的时候,用主设备号索引这个数组。次设备号作为一个参数而传递给这个设备驱动程序。次设备号除了驱动程序赋予它 的特性之外没有任何意义。通常,设备驱动程序使用次设备号来访问一些完全相同的物理设备中一个。

入口点的数组(配置表)作为系统代码和设备驱动程序之间的唯一连接的作用是非常重要的。这个系统的早期版本与驱动程序有一个非常不规范连接,所以手 工处置不同配置的系统非常困难。现在有可能在几个小时内建立一个新的设备驱动程序。在多数情况下,这个配置表由读取这个系统的部件清单的一个程序来自动建 立。


3.1. 块 I/O 系统

块 I/O 设备的模型由随机寻址的、每个 512 字节的次级存储块组成。这些块被唯一的编址为 0、1、... 直到这个设备的大小。块设备驱动程序的工作是在物理设备上仿真这个模型。

访问块 I/O 设备要通过一层缓冲区(buffer)软件。这个系统维护(典型的 10 到 70 个)缓冲区的一个链表,每个缓冲区都被赋予一个设备名字和一个设备地址。这个缓冲区池为块设备构成了一个数据缓存(cache)。对于一个读请求,缓存查 找想要的块。如果找到了这个块,请求者可获得这块数据而不用任何物理 I/O。如果这个块不在缓存中,重命名在缓存中最近最少使用的块,调用正确的设备驱动程序来填充这个被重命名的块,接着就可获得这块数据。以类似的方式处 理写请求。找到正确的缓冲区并且在必要时重新标注。通过标记这个缓冲区为“脏”来简单的完成写。物理 I/O 被推迟到这个缓冲区被重命名。

这个方案减少物理 I/O 的利益是相当可观的,特别是考虑到文件系统实现。但是有些缺点。这个算法的异步本质使错误报告和有意义的用户错误处理基本上不可能。UNIX 系统中对 I/O 错误的这种轻率的方式部分的归咎于块 I/O 系统的异步本质。一个次要的问题存在于延迟写中。如果系统异常停止,基本可以肯定在缓冲区中有许多逻辑上完成了,而物理上未完成的 I/O。有一个系统原语从缓冲区中刷新所有外出的(outstanding) I/O 活动。周期性的使用这个原语将有助于但不能解决这个问题。最后,缓冲区间的关联性可以让这个逻辑 I/O 顺序改变了物理 I/O 顺序。这意味着即使软件小心的以正确的次序进行 I/O,磁盘上的数据结构有时还是不一致的。在非随机设备上,尤其是磁带, 颠倒写的顺序是灾难性的。磁带的这个问题是通过每个驱动器只允许一个外出请求写来解决的。


3.2. 字符 I/O 系统

字符 I/O 系统由所有不能归入块 I/O 模型的设备组成。这包括“典型的”字符设备如通信线路、纸带、和行式打印机。还包括磁带和不按老套路使用的磁盘,例如,在磁带上的 80 字节物理记录和每次一磁道的磁盘复制。简短的说,字符 I/O 接口意味着“不是块的所有东西”。来自用户的 I/O 请求被发送到驱动程序而基本上不做变动。这些请求的实现理所当然的依赖于设备驱动程序。有帮助实现特定类型的设备驱动程序的准则和惯例。


3.2.1. 磁盘驱动程序

磁盘驱动程序被实现为带有一个事务(transaction)记录的队列。每个记录持有一个读写标志、一个主存地址、一个次级存储地址、和一个传输 字节记数。交换是通过把这样的一个记录传递给交换设备驱动程序来完成的。块 I/O 接口是通过传递带有要填充或清空系统缓冲区的请求的事务记录来实现的。到磁盘驱动程序的字符 I/O 接口建立直接指向用户区域的一个事务记录。建立这种记录的例程还确保用户在这次 I/O 事务期间不被交换出去。这样通过实现通用的磁盘驱动程序,可以把磁盘使用为块设备、字符设备、和交换设备。在普通磁盘驱动程序中唯一真正特定于磁盘的代码 是事务的预先排序,用以对一个特定设备最小化延迟,和 I/O 请求的实际上发起。


3.2.2. 字符列表

真正面向字符的设备可以使用处理字符链表的公用代码来实现。字符列表是一个字符的队列。一个例程把一个字符放入一个队列。另一个例程从一个队列取出 一个字符。还可能要求知道在一个队列中当前有多少个字符。在系统中所有队列的存储来自一个单一的公用池。在一个队列上放入一个字符将从这个公用池分配空间 并把这个字符连接到定义这个队列的数据结构上。从一个队列取出一个字符向这个公用池返还相应的空间。

一个典型的字符输出设备(例如纸带穿孔机)被实现为把来自用户的字符传递到一个字符队列上,直到这个队列上的字符到了某个最大数目。在这个队列上一 有东西就激励 I/O 启动,并且一旦启动,则由硬件完成中断来维持。每次有完成中断的时候,驱动程序从这个队列得到下一个字符并把它发送到硬件。检查在这个队列上的字符的数 目,若这个数目从一个中间水平上落了下来,则通告一个事件(队列地址)。从用户传递字符到这个队列的进程可以等待这个事件,并在事件发生时把这个队列重新 填充为它的最大数目。

以非常类似的方式处理一个典型的字符输入设备(例如纸带阅读器)。

另一类字符设备是终端。终端被表示为三个字符队列。两个输入队列(原始和规范(canonical))和一个输出队列。完全用上述描述的公用代码处理来到一个终端的输出队列上的字符。主要的区别是还有代码来把输出流解释为 ASCII 字符并进行一些转换,例如,为有所欠缺的终端做转义工作。另一个终端的公共特征是在特定控制字符后面插入实时延迟的代码。

终端上的输入有些不同。收集自终端的字符并放置到一个原始输入队列上。这里要处理一些设备相关的代码转换和转义解释。当在原始队列中完成了一行的时 候,通告(signal)一个事件。捕获这个信号(signal)接着从这个原始队列复制一行到一个规范队列的代码进行字符删除和行删除(kill)编 辑。读在终端上的请求的用户可以直接读取原始或者规范队列。


3.2.3. 其他字符设备

最后,有的设备不符合普通类型。这些设备被设置为字符 I/O 驱动程序。一个例子是把未映射的主存作为 I/O 设备来读写的驱动程序。一些设备作为偶尔的一个字符来对待太快了,但不适合磁盘 I/O 模型。例子有高速通信线路和高速行式打印机。这些设备要么有自己的缓冲区要么“借用”块 I/O 缓冲区一会并接着归还它们。

4. 文件系统

在 UNIX 系统中,文件是一个字节的(一维)数组。系统没有隐含其他的文件结构。文件被附加到目录层次上的任何(并可能多个)地方。目录是用户可以写的简单的文件。文件和目录的外部看法的进一步讨论请参见“unix 分时系统”。

UNIX 文件系统是完全通过块 I/O 系统访问的一个磁盘数据结构。如上所述,“磁盘”的规范看法是一个随机可寻址的 512 字节块的数组。文件系统把磁盘分成四个自我识别的区域(region)。第一块(地址 0)不被文件系统使用。它被留给引导过程。第二块(地址 1)包含“超级块”。这个块,包含磁盘的大小和其他区域的边界,还有其他东西。下一个区域是 i-list,它是文件定义的一个列表。每个文件定义是一个 64 字节的结构,叫做 i-node。一个特定 i-node 在 i-list 中的偏移量叫做它的 i-number。设备名字(主和从设备号)和 i-number 充当一个特定文件的唯一性的名字。在 i-list 之后,直到这个磁盘的结束,是可获得用做文件的内容的自由存储块。

磁盘上的空闲空间由可获得的磁盘块的一个链表来维护。在这个链中的每个块包含链中下一块的磁盘地址。剩余空间包含最多 50 个也是空闲的磁盘块的地址。这样对于一次 I/O 操作,系统获取 50 空闲块和一个找到更多块的指针。磁盘分配算法十分直截了当。因为所有分配都是固定大小块并且有确切数量的空间, 所以没有不需要紧缩和垃圾收集。但是,随着磁盘空间变得分散,延迟在逐步增加。一些安装选择不经常的紧缩磁盘空间来减少延迟。

i-node 包含 13 个磁盘地址。这些地址的前 10 个直接指向这个文件的前 10 个块。如果一个文件大于 10 块 (5,120 字节),则第 11 个地址指向包含这个文件的下 128 个块的地址一个块。如果文件还大于这个尺寸 (70,656 字节),则第 12 块指出最多 128 个块,每个块指出这个文件的 128 个块。更大的(8,459,264 字节)文件使用第 13 个地址用做 “三次间接”地址。这个算法结束于最大的文件大小 1,082,201,087 字节。

通过简单的增加一个新的文件类型来为这个平坦的物理结构增加上逻辑目录层次,这个文件类型就是目录。 目录可以完全作为普通文件来访问。它包含 16 字节的条目,其组成是一个 14 字节的名字和一个 i-number。层次的根有一个周知的 i-number (就是 2)。文件系统结构允许任意的目录有向图,并可以在这个图的任意位置上连接正规文件。实际上,非常早期的 UNIX 系统曾经使用这样一个结构。这样一个结构的管理是很混乱的所以后来的系统被限制为一个目录树。即使是现在,把正规文件多重连接到树中的任意位置,使统计空间成了问题。可能有必要限制整个结构为一个树,并允许一种新形式的连接服从于这个树结构。

文件系统允许轻易建立、删除、随机访问、和空间分配。由于多数物理地址局限于一个小的连续磁盘扇区中,复制、存储、和检查文件系统的一致性也是很容易的。 文件遭受到间接寻址,但是缓存防止了多数隐含的物理 I/O 而不增加许多执行。这个方案的空间开销特性非常好。例如,在一个特定的文件系统上,有 25,000 个文件包含着 130M 字节的数据文件内容。开销(i-node,间接块,和最后的块破碎)大约是 11.5M 。支持这些文件的目录结构有大约 1,500 个目录包含 0.6M 字节的目录内容,和在访问这些目录中的大约 0.5M 字节的开销。汇总到一起,为实际存储的数据带来了小于 10 个百分点的开销。许多系统单在添充的尾随的空白上就有更大的开销。


4.1. 文件系统实现

因为 i-node 定义了文件,文件系统实现的中心围绕着对 i-node 的访问。系统维护所有活跃 i-node 的一个表。在访问一个新文件的时候,系统定位相应的 i-node,分配一个 i-node 表条目,并把这个 i-node 读取到主存中。同在缓冲区缓存中一样,这个表条目被当作这个 i-node 的当前版本。对 i-node 的修改在这个表条目上进行。当对这个 i-node 的最后的访问结束,把这个表条目复制回到次级存储 i-list 并释放这个表条目。

在文件上的所有 I/O 操作在对应的 i-node 表条目的辅助下进行。文件的访问是前面提及的算法的直接实现。用户不用照管 i-node 和 i-number。按照这个目录树的路径来进行文件系统的引用。把一个路径名字转换成一个 i-node 表条目也是直接的。开始于某个已知的 i-node(根或者这个进程的当前目录),通过读这个目录来查找这个路径名的下一个成员。这给出(这个目录的)一个 i-number 和一个隐含的设备。所以可以访问下一个 i-node 表条目。如果它是这个路径名字的最后的成员,则这个 i-node 就是结果。如果不是,这个 i-node 是需要查找的这个路径名字的下一个目录,并重复这个算法。

用户进程用特定原语访问文件系统。其中最常用的是 opencreatereadwriteseek、和 close。维护的数据结构展示于图 2。

在与用户有关的系统数据段中,有留给(通常 10 到 50个)打开文件的空间。这个打开文件表由可以用来访问相应的 i-node 表条目的指针组成。与每个打开文件相关联的是一个当前 I/O 指针。这是在这个文件文件上下一次读/写操作的一个字节偏移量。系统把每个读/写请求做为随机操作,它带有对 I/O 指针隐含的定位(seek)。用户通常认为文件是一个序列,它带有的I/O 指针自动统计已经从文件读/写的字节数目。用户当然可以通过在读/写之前通过设置 I/O 指针来进行随机 I/O。

为了文件共享,必须允许有关的进程共享一个公共 I/O 指针,但让访问同一个的文件的无关进程有独立的 I/O 指针。出于这两个条件,I/O 指针不能驻留在 i-node 表或进程的打开文件列表中。为了持有 I/O 指针这个唯一功能而发明了一个新表(打开文件表)。共享同一个打开文件的进程(进行 fork 的结果)共享一个公共打开文件条目。同一个文件的单独打开将只共享 i-node 表条目,而有不同的打开文件表条目。

主要的文件系统原语实现如下。open 把一个文件系统路径转换成一个 i-node 表条目。在新近建立的打开文件表条目中放置到这个 i-node 表条目的一个指针。create 首先建立一个新的 i-node 条目,把这个i-number 写到一个目录中,并接着建立与 open 相同的结构。readwrite 只是访问上面描述的 i-node 条目。seek 简单的操纵 I/O 指针。不做物理的定位(seeking)。close 只是释放 opencreate 建立的结构。在打开文件表条目和 i-node 表中保持的引用记数,用来在最后的引用去掉之后释放这些结构。unlink 简单的减少指出给定 i-node 的目录的记数。当到一个 i-node 表条目的最后的引用被去掉的时候,如果没有目录指出这个 i-node,则删除这个文件并释放这个 i-node。文件的延迟删除防止了删除活跃文件所引发的问题。文件可以在仍然打开的时候被删除。在关闭这个文件的时候结果的未命名文件 消失。这是获取临时文件的一种方法。

有一种类型的未命名的 FIFO 文件叫做管道(pipe)。为了实现先入先出,管道的实现由在每次 readwrite 之前隐含的 seek 构成。还有检查和同步来防止写者对读者的过度产出和读者对写者的过度取用。


4.2. 挂装文件系统

UNIX 系统的文件系统开始于某个指定块设备、它被格式化为上面描述格式的来包含一个层次结构。这个结构的根是 UNIX 文件系统的根。其他格式化的块设备可以挂装到当前层次的任何叶子上。它在逻辑上延伸了当前的层次结构。挂装的实现是琐碎的。维护一个挂装表来持有成对的叶子 i-node 和块设备。在把一个路径名字转换成一个 i-node 的时候,做一个检查来看新 i-node 是否是一个指定的叶子。如果是,则用这个块设备的根的 i-node 来替换它。

文件的空间分配取自这个文件所在的设备上的空闲池。所以由多个挂装的设备组成的一个文件系统没有空闲次级存储空间的一个公共池。在不同的设备上的这种独立性0对于允许容易的卸装一个设备是必须的。

4.3. 其他系统功能

系统为用户做了一点其他事情,如记帐、跟踪/调试、和访问保护。其中多数未被良好的开发,因为在我们计算科学研究中使用的这个系统不需要这些东西。在一些应用中缺少一些特征,例如,良好的进程间通信。

UNIX 内核就应该是一个 I/O 复用器而不是一个完整的操作系统。出于这种观点,UNIX 内核中缺少在多数其他操作系统中可以见到的许多特征。例如,UNIX 内核不支持文件访问方法、文件部署、文件格式、文件最大大小、缓冲池、命令语言、逻辑记录、物理记录、逻辑文件名的分配、逻辑文件名、多个字符集、操作者 的控制台,操作者、log-in、或 log-out。其中的许多东西是征状(symptom)而不是特征。其中的多数东西在把内核用做工具的用户软件中实现。它们的一个最佳的例子是命令语 言。这个要点使每个用户都可以有他自己的命令语言。维护这种代码与维护用户代码一样容易。用普通用户原语(primitive)实现“系统”代码的想法直 接来在 MULTICS。


阅读(1409) | 评论(0) | 转发(0) |
0

上一篇:UNIX V7 I/O 系统(D.M.Ritchie著,中文翻译)

下一篇:没有了

给主人留下些什么吧!~~