4.4BSD 操作系统的设计与实现
目录
2.1. 4.4BSD 的设计概述
2.1.1. 4.4BSD 功能和内核
4.4BSD 内核提供了四种基本功能:进程、文件系统、通信和系统启动。本节概述了本书中对这四种基本服务的描述位置。
进程构成地址空间中的控制线程。创建、终止和以其他方式控制进程的机制在第 4 章中描述。系统为每个进程复用单独的虚拟地址空间;第 5 章讨论了这种内存管理。
用户对文件系统和设备的界面是类似的;第 6 章讨论了共同的方面。文件系统是一组命名文件,组织在一个树状层次结构的目录中,以及用于操作它们的指令,如第 7 章所述。文件驻留在磁盘等物理介质上。4.4BSD 支持几种磁盘上的数据组织方式,如第 8 章所述。第 9 章介绍了访问远程机器上的文件。
传统 UNIX 系统提供的通信机制包括相关进程之间的单工可靠字节流(参见管道,第 11.1 节)以及异常事件的通知(参见信号,第 4.7 节)。4.4BSD 还具有通用进程间通信功能。此功能在第 11 章中进行了描述,它使用与文件系统不同的访问机制,但是,一旦建立连接,进程就可以像访问管道一样访问它。第 12 章讨论了一个通用网络框架,通常用作 IPC 功能的基础层。第 13 章详细描述了特定的网络实现。
任何真正的操作系统都有操作问题,例如如何启动它运行。启动和操作问题在第 14 章中进行了描述。
第 2.3 节到第 2.14 节介绍了与第 3 章到第 14 章相关的入门材料。我们将定义术语,提及基本系统调用,并探讨历史发展。最后,我们将说明许多主要设计决策的原因。
2.1.1.1. 内核
内核是系统中以保护模式运行的部分,它协调所有用户程序对底层硬件(例如 CPU、磁盘、终端、网络链路)和软件结构(例如文件系统、网络协议)的访问。内核提供基本系统功能;它创建和管理进程,并提供访问文件系统和通信功能的功能。这些功能称为系统调用,对用户进程来说就像库子程序一样。这些系统调用是进程访问这些功能的唯一接口。第 3 章给出了系统调用机制的细节,以及一些不作为进程执行系统调用的直接结果而执行的内核机制的描述。
在传统的操作系统术语中,内核是指一小部分软件,它只提供实现其他操作系统服务所需的最低限度功能。在当代研究型操作系统(如 Chorus [Rozier 等人]、Mach [Accetta 等人]、Tunis [Ewens 等人] 和 V 内核 [Cheriton])中,这种功能划分不仅仅是逻辑上的。文件系统和网络协议等服务被实现为内核或核心体的客户端应用程序进程。
4.4BSD 内核没有被划分为多个进程。这个基本的设计决策是在 UNIX 的最早版本中做出的。Ken Thompson 的前两个实现没有内存映射,因此没有在用户空间和内核空间之间进行硬件强制的区分 [Ritchie]。消息传递系统可以像实际实现的内核和用户进程模型一样容易地实现。单片内核的选择是为了简单性和性能。早期的内核很小;将网络等功能纳入内核增加了其大小。操作系统研究的当前趋势是通过将此类服务置于用户空间来减小内核大小。
用户通常通过命令语言解释器(称为shell)以及可能的其他用户应用程序与系统交互。此类程序和 shell 是用进程实现的。此类程序的细节超出了本书的范围,本书主要集中在内核上。
第 2.3 节和第 2.4 节描述了 4.4BSD 内核提供的服务,并概述了后者的设计。后面的章节描述了这些服务在 4.4BSD 中的详细设计和实现。
2.1.2. 内核组织
在本节中,我们将从两个方面来看待 4.4BSD 内核的组织
作为静态的软件主体,按构成内核的模块提供的功能进行分类
按其动态操作进行分类,根据提供给用户的服务进行分类
内核的大部分内容实现了应用程序通过系统调用访问的系统服务。在 4.4BSD 中,此软件已按以下方式组织
基本内核功能:计时器和系统时钟处理、描述符管理和进程管理
内存管理支持:分页和交换
通用系统接口:对描述符执行的 I/O、控制和多路复用操作
文件系统:文件、目录、路径名转换、文件锁定和 I/O 缓冲区管理
终端处理支持:终端接口驱动程序和终端行规程
进程间通信功能:套接字
网络通信支持:通信协议和通用网络功能,例如路由
类别 | 代码行数 | 内核百分比 |
---|---|---|
头文件 | 9,393 | 4.6 |
初始化 | 1,107 | 0.6 |
内核功能 | 8,793 | 4.4 |
通用接口 | 4,782 | 2.4 |
进程间通信 | 4,540 | 2.2 |
终端处理 | 3,911 | 1.9 |
虚拟内存 | 11,813 | 5.8 |
vnode 管理 | 7,954 | 3.9 |
文件系统命名 | 6,550 | 3.2 |
快速文件存储 | 4,365 | 2.2 |
日志结构文件存储 | 4,337 | 2.1 |
基于内存的文件存储 | 645 | 0.3 |
cd9660 文件系统 | 4,177 | 2.1 |
其他文件系统 (10) | 12,695 | 6.3 |
网络文件系统 | 17,199 | 8.5 |
网络通信 | 8,630 | 4.3 |
互联网协议 | 11,984 | 5.9 |
ISO 协议 | 23,924 | 11.8 |
X.25 协议 | 10,626 | 5.3 |
XNS 协议 | 5,192 | 2.6 |
这些类别中的大部分软件都是机器无关的,可以在不同的硬件架构之间移植。
内核的机器相关方面与主流代码隔离。特别是,没有一个机器无关的代码包含特定架构的条件代码。当需要架构相关的操作时,机器无关的代码会调用位于机器相关代码中的架构相关函数。机器相关的软件包括
低级系统启动操作
陷阱和故障处理
进程运行时上下文的低级操作
硬件设备的配置和初始化
I/O 设备的运行时支持
类别 | 代码行数 | 内核百分比 |
---|---|---|
机器相关头文件 | 1,562 | 0.8 |
设备驱动程序头文件 | 3,495 | 1.7 |
设备驱动程序源代码 | 17,506 | 8.7 |
虚拟内存 | 3,087 | 1.5 |
其他机器相关 | 6,287 | 3.1 |
汇编语言中的例程 | 3,014 | 1.5 |
HP/UX 兼容性 | 4,683 | 2.3 |
4.4BSD 内核中的机器无关软件 总结了构成 HP300 4.4BSD 内核的机器无关软件。第 2 列中的数字表示 C 源代码、头文件和汇编语言的行数。内核中的几乎所有软件都是用 C 编程语言编写的;不到 2%是用汇编语言编写的。如 4.4BSD 内核中的机器相关软件 中的统计数据所示,机器相关软件(不包括 HP/UX 和设备支持)仅占内核的 6.9%。
内核只有一小部分用于初始化系统。此代码在系统引导到操作时使用,负责设置内核硬件和软件环境(参见第 14 章)。一些操作系统(特别是那些物理内存有限的操作系统)会在执行完这些功能的软件后丢弃或覆盖执行这些功能的软件。4.4BSD 内核不会回收启动代码使用的内存,因为该内存空间在典型机器上仅占内核资源的 0.5%。此外,启动代码不会出现在内核中的一个位置——它散布在各个地方,并且通常出现在与其正在初始化的内容逻辑相关的位置。
2.1.3. 内核服务
内核级代码和用户级代码之间的边界由底层硬件提供的硬件保护功能强制执行。内核在独立的地址空间中运行,用户进程无法访问该地址空间。特权操作(例如启动 I/O 和停止中央处理单元 (CPU))仅对内核可用。应用程序通过系统调用请求内核的服务。系统调用用于使内核执行复杂的操作,例如将数据写入辅助存储器,以及简单的操作,例如返回当前时间。所有系统调用对应用程序来说都是同步的:当内核执行与系统调用相关的操作时,应用程序不会运行。内核可能会在返回后完成与系统调用相关的一些操作。例如,write 系统调用会在进程等待的同时将要写入的数据从用户进程复制到内核缓冲区,但通常会在内核缓冲区写入磁盘之前从系统调用返回。
系统调用通常实现为硬件陷阱,它更改 CPU 的执行模式和当前地址空间映射。用户在系统调用中提供的参数在被使用之前会由内核验证。这种检查确保了系统的完整性。所有传递到内核的参数都复制到内核的地址空间,以确保验证后的参数不会作为系统调用的副作用而更改。系统调用的结果由内核返回,要么在硬件寄存器中,要么通过将其值复制到用户指定的内存地址中。与传递到内核的参数一样,用于返回结果的地址必须经过验证,以确保它们是应用程序地址空间的一部分。如果内核在处理系统调用时遇到错误,它会向用户返回错误代码。对于 C 编程语言,此错误代码存储在全局变量errno中,并且执行系统调用的函数返回-1。
用户应用程序和内核彼此独立运行。4.4BSD 不会在应用程序的地址空间中存储 I/O 控制块或其他与操作系统相关的数据结构。每个用户级应用程序都拥有一个独立的地址空间来执行。内核使大多数状态更改(例如,在另一个进程运行时挂起一个进程)对所涉及的进程不可见。
2.1.4. 进程管理
4.4BSD 支持多任务环境。每个任务或执行线程都被称为进程。4.4BSD 进程的上下文包括用户级状态(包括其地址空间的内容和运行时环境)和内核级状态(包括调度参数、资源控制和标识信息)。上下文包含内核在为进程提供服务时使用的一切内容。用户可以创建进程、控制进程的执行并在进程的执行状态发生变化时接收通知。每个进程都被分配一个唯一值,称为进程标识符(PID)。内核在向用户报告状态更改时以及用户在系统调用中引用进程时使用此值。
内核通过复制另一个进程的上下文来创建进程。新进程被称为原始父进程的子进程。在进程创建中复制的上下文包括进程的用户级执行状态和内核管理的进程系统状态。内核状态的重要组件在第 4 章中进行了描述。
进程生命周期在进程生命周期中进行了描述。进程可以使用fork系统调用创建一个作为原始进程副本的新进程。fork调用返回两次:一次在父进程中,返回值是子进程的进程标识符;一次在子进程中,返回值为 0。父子关系在系统中的进程集上引入了一种层次结构。新进程共享其父进程的所有资源,例如文件描述符、信号处理状态和内存布局。
虽然在某些情况下,新进程旨在成为父进程的副本,但加载和执行不同的程序是一个更有用且更典型的操作。进程可以使用execve系统调用将其自身覆盖为另一个程序的内存映像,并将一组参数传递给新创建的映像。一个参数是文件的名称,其内容采用系统识别的格式——要么是二进制可执行文件,要么是导致执行指定的解释器程序来处理其内容的文件。
进程可以通过执行exit系统调用来终止,并向其父进程发送 8 位的退出状态。如果进程希望与其父进程通信超过一个字节的信息,则它必须要么使用管道或套接字建立进程间通信通道,要么使用中间文件。第 11 章广泛讨论了进程间通信。
进程可以使用wait系统调用挂起执行,直到其任何子进程终止,该系统调用返回终止的子进程的 PID 和退出状态。父进程可以安排在子进程退出或异常终止时通过信号得到通知。使用wait4系统调用,父进程可以检索导致子进程终止的事件的信息以及进程在其生命周期内消耗的资源的信息。如果一个进程因为其父进程在其完成之前退出而成为孤儿,则内核会安排将子进程的退出状态传递回特殊的系统进程init:参见第 3.1 节和 14.6 节)。
内核如何创建和销毁进程的详细信息在第 5 章中给出。
进程根据进程优先级参数进行调度执行。此优先级由基于内核的调度算法管理。用户可以通过指定一个参数(nice)来影响进程的调度,该参数对整体调度优先级进行加权,但仍然有义务根据内核的调度策略共享底层 CPU 资源。
2.1.4.1. 信号
系统定义了一组可以传递给进程的信号。4.4BSD 中的信号以硬件中断为模型。进程可以指定一个用户级子程序作为处理程序,信号应传递给该处理程序。当生成信号时,在处理程序捕获它时,会阻止其进一步发生。捕获信号涉及保存当前进程上下文并在其中构建一个新的上下文以运行处理程序。然后将信号传递给处理程序,处理程序可以中止进程或返回到正在执行的进程(可能在设置全局变量后)。如果处理程序返回,则信号将被解除阻塞,并且可以再次生成(并捕获)。
或者,进程可以指定要忽略信号,或者采取内核确定的默认操作。某些信号的默认操作是终止进程。此终止可能会伴随创建核心文件,该文件包含进程的当前内存映像,用于事后调试。
某些信号不能被捕获或忽略。这些信号包括SIGKILL(用于杀死失控进程)和作业控制信号SIGSTOP。
进程可以选择在特殊堆栈上交付信号,以便能够进行复杂的软件堆栈操作。例如,支持协程的语言需要为每个协程提供一个堆栈。语言运行时系统可以通过划分 4.4BSD 提供的单个堆栈来分配这些堆栈。如果内核不支持单独的信号堆栈,则必须将为每个协程分配的空间扩展到捕获信号所需的空间量。
所有信号都具有相同的优先级。如果多个信号同时处于挂起状态,则信号传递给进程的顺序是实现特定的。信号处理程序在阻止导致其调用的信号的情况下执行,但其他信号可能仍然会发生。提供了机制,以便进程可以保护代码的关键部分免受指定信号的发生。
信号的详细设计和实现在第 4.7 节中描述。
2.1.4.2. 进程组和会话
进程被组织成进程组。进程组用于控制对终端的访问并提供将信号分发到相关进程集合的方法。进程从其父进程继承其进程组。内核提供了机制允许进程更改其进程组或其后代的进程组。创建新的进程组很容易;新进程组的值通常是创建进程的进程标识符。
进程组中的进程组有时被称为作业,并由高级系统软件(如 shell)进行操作。shell 创建的一种常见作业是几个进程通过管道连接的管道,使得第一个进程的输出是第二个进程的输入,第二个进程的输出是第三个进程的输入,依此类推。shell 通过为管道的每个阶段分叉一个进程来创建这样的作业,然后将所有这些进程放入一个单独的进程组。
用户进程可以向进程组中的每个进程以及单个进程发送信号。特定进程组中的进程可能会收到影响该组的软件中断,导致该组挂起或恢复执行,或被中断或终止。
终端有一个分配给它的进程组标识符。此标识符通常设置为与终端关联的进程组的标识符。作业控制 shell 可以创建许多与同一终端关联的进程组;终端是这些组中每个进程的控制终端。只有当终端的进程组标识符与进程的进程组标识符匹配时,进程才能从其控制终端的描述符中读取。如果标识符不匹配,则进程在尝试从终端读取时将被阻塞。通过更改终端的进程组标识符,shell 可以协调多个不同作业之间的终端。这种协调称为作业控制,并在第 4.8 节中与进程组一起进行了描述。
就像一组相关进程可以收集到一个进程组中一样,一组进程组也可以收集到一个会话中。会话的主要用途是为守护进程及其子进程创建一个隔离的环境,以及将用户的登录 shell 和 shell 生成的作业收集在一起。
2.1.5. 内存管理
每个进程都有自己的私有地址空间。地址空间最初被划分为三个逻辑段:文本、数据和堆栈。文本段是只读的,包含程序的机器指令。数据段和堆栈段都是可读可写的。数据段包含程序的已初始化和未初始化数据部分,而堆栈段保存应用程序的运行时堆栈。在大多数机器上,堆栈段在进程执行时由内核自动扩展。进程可以通过进行系统调用来扩展或收缩其数据段,而进程只能在段的内容被文件系统中的数据覆盖或进行调试时更改其文本段的大小。子进程段的初始内容是父进程段的副本。
进程地址空间的全部内容不需要驻留才能执行进程。如果进程引用其地址空间中未驻留在主内存的部分,则系统将必要的页面信息调入内存。当系统资源稀缺时,系统使用两级方法来维护可用资源。如果可用内存适中,则系统将在进程未使用其资源时从进程中获取这些资源。如果资源严重短缺,系统将诉诸于将进程的整个上下文交换到辅助存储器。系统执行的按需分页和交换对进程来说是有效的透明的。但是,进程可以建议系统有关预期未来的内存使用情况,作为性能辅助。
2.1.5.1. BSD 内存管理设计决策
支持大型稀疏地址空间、映射文件和共享内存是 4.2BSD 的一项要求。一个名为mmap的接口被指定,它允许不相关的进程请求将文件的共享映射到它们的地址空间中。如果多个进程将同一个文件映射到它们的地址空间中,则一个进程对文件地址空间部分的更改将反映在其他进程映射的区域中,以及文件中本身。最终,由于需要提供其他功能(例如网络),4.2BSD 在发布时没有包含mmap接口。
在 4.3BSD 的开发过程中,mmap接口的开发工作仍在继续。超过 40 家公司和研究机构参与了讨论,最终形成了在伯克利软件体系结构手册中描述的修订后的体系结构[McKusick 等人]。一些公司已经实现了修订后的接口[Gingell 等人]。
再次,时间压力导致 4.3BSD 无法提供该接口的实现。虽然后者本可以构建到现有的 4.3BSD 虚拟内存系统中,但开发人员决定不将其包含在内,因为该实现已经近 10 年历史。此外,最初的虚拟内存设计是基于计算机内存小且昂贵,而磁盘是本地连接、快速、大型且廉价的假设。因此,虚拟内存系统的设计目标是节约内存使用,即使这会产生额外的磁盘流量。此外,4.3BSD 实现充斥着 VAX 内存管理硬件依赖性,阻碍了它向其他计算机体系结构的移植。最后,虚拟内存系统并非设计用于支持如今日益普遍且重要的紧密耦合多处理器。
尝试逐步改进旧实现似乎注定会失败。另一方面,全新的设计可以利用大型内存,节省磁盘传输,并有可能在多处理器上运行。因此,4.4BSD 中完全替换了虚拟内存系统。4.4BSD 虚拟内存系统基于 Mach 2.0 VM 系统[Tevanian],并结合了 Mach 2.5 和 Mach 3.0 的更新。它具有高效的共享支持、机器无关和机器相关功能的清晰分离,以及(目前未使用)的多处理器支持。进程可以将其地址空间中的任何位置映射文件。它们可以通过对同一文件进行共享映射来共享其地址空间的部分内容。一个进程所做的更改在另一个进程的地址空间中可见,并且也会写回文件本身。进程还可以请求文件的私有映射,这可以防止它们所做的任何更改对映射该文件的其他进程可见或写回文件本身。
虚拟内存系统的另一个问题是系统调用时将信息传递到内核的方式。4.4BSD 始终将数据从进程地址空间复制到内核中的缓冲区。对于传输大量数据的读或写操作,进行复制可能非常耗时。进行复制的替代方法是将进程内存重新映射到内核。4.4BSD 内核始终复制数据,原因如下:
用户数据通常未对齐到页面边界,也不是硬件页面长度的倍数。
如果从进程中获取页面,它将不再能够引用该页面。某些程序依赖于数据即使在写入这些数据后仍然保留在缓冲区中。
如果允许进程保留页面的副本(如当前 4.4BSD 语义中那样),则必须将页面设为写时复制。写时复制页面是指通过将其设为只读来防止其被写入的页面。如果进程尝试修改页面,内核将收到写故障。然后,内核将创建页面副本,进程可以修改该副本。不幸的是,典型的进程会立即尝试将其输出缓冲区写入新数据,从而强制数据无论如何都必须复制。
当页面重新映射到新的虚拟内存地址时,大多数内存管理硬件要求选择性地清除硬件地址转换缓存。缓存清除通常很慢。最终效果是,对于小于 4 到 8 KB 的数据块,重新映射比复制慢。
内存映射的最大诱因是访问大文件和在进程之间传递大量数据的需求。mmap接口提供了一种无需复制即可完成这两项任务的方法。
2.1.5.2. 内核内部的内存管理
内核通常会分配一些内存,这些内存仅在单个系统调用的持续时间内需要。在用户进程中,这种短期内存将在运行时栈上分配。由于内核的运行时栈有限,因此在上面分配中等大小的内存块是不可行的。因此,此类内存必须通过更动态的机制分配。例如,当系统必须转换路径名时,它必须分配一个 1 KB 的缓冲区来保存名称。其他内存块必须比单个系统调用更持久,因此即使有空间也不能在栈上分配。例如,协议控制块在网络连接的整个持续时间内都存在。
随着更多服务的添加,对内核中动态内存分配的需求也随之增加。通用内存分配器降低了编写内核代码的复杂性。因此,4.4BSD 内核有一个单一的内存分配器,系统任何部分都可以使用它。它具有类似于 C 库例程malloc和free的接口,这些例程为应用程序提供内存分配[McKusick & Karels]。与 C 库接口一样,分配例程采用一个参数,指定所需内存的大小。内存请求的大小范围不受限制;但是,物理内存已分配并且未分页。free 例程采用指向要释放的存储区的指针,但不要求要释放的内存块的大小。
2.1.6. I/O 系统
UNIX I/O 系统的基本模型是一系列字节,可以随机或顺序访问。在典型的 UNIX 用户进程中,没有访问方法,也没有控制块。
不同的程序期望不同的结构级别,但内核不会对 I/O 强加结构。例如,文本文件的约定是使用单个换行符(ASCII 换行符)分隔的 ASCII 字符行,但内核对此约定一无所知。对于大多数程序的目的,该模型进一步简化为数据字节流或I/O 流。正是这种单一的通用数据形式使得 UNIX 基于工具的方法得以实现[Kernighan & Pike]。来自一个程序的 I/O 流可以作为几乎任何其他程序的输入。(这种传统的 UNIX I/O 流不应与第八版流 I/O 系统或 System V,Release 3 STREAMS 混淆,这两者都可以作为传统的 I/O 流访问。)
2.1.6.1. 描述符和 I/O
UNIX 进程使用描述符来引用 I/O 流。描述符是从open和socket系统调用获得的小型无符号整数。open系统调用将文件名称和权限模式作为参数,以指定文件是否应打开以进行读取或写入,或同时进行读取和写入。此系统调用还可用于创建新的空文件。read或write系统调用可以应用于描述符以传输数据。close系统调用可用于释放任何描述符。
描述符表示内核支持的基础对象,并由特定于对象类型的系统调用创建。在 4.4BSD 中,三种类型的对象可以用描述符表示:文件、管道和套接字。
文件是至少有一个名称的线性字节数组。文件存在,直到其所有名称都被显式删除并且没有进程持有其描述符。进程通过使用open系统调用打开该文件的名称来获取该文件的描述符。I/O 设备作为文件访问。
管道是线性字节数组,与文件一样,但它仅用作 I/O 流,并且是单向的。它也没有名称,因此无法使用open打开。相反,它由pipe系统调用创建,该调用返回两个描述符,其中一个接受可靠地、无重复地并按顺序发送到另一个描述符的输入。系统还支持命名管道或 FIFO。FIFO 的属性与管道相同,只是它出现在文件系统中;因此,可以使用open系统调用打开它。希望通信的两个进程都打开 FIFO:一个打开它以进行读取,另一个打开它以进行写入。
套接字是一个用于进程间通信的临时对象;它仅在某些进程持有引用它的描述符时才存在。套接字由socket系统调用创建,该调用返回其描述符。有不同类型的套接字支持各种通信语义,例如数据的可靠传递、消息顺序的保持和消息边界的保持。
在 4.2BSD 之前的系统中,管道是使用文件系统实现的;当套接字在 4.2BSD 中引入时,管道被重新实现为套接字。
内核为每个进程维护一个描述符表,这是一个表,内核使用它将描述符的外部表示转换为内部表示。(描述符仅仅是此表中的一个索引。)进程的描述符表是从该进程的父进程继承的,因此对描述符所引用的对象的访问也会被继承。进程获得描述符的主要方式是打开或创建对象,以及从父进程继承。此外,套接字 IPC 允许在同一台机器上不相关的进程之间通过消息传递描述符。
每个有效的描述符都与一个文件偏移量相关联,该偏移量以字节为单位,从对象的开头算起。读写操作从该偏移量开始,并在每次数据传输后更新。对于允许随机访问的对象,也可以使用lseek系统调用设置文件偏移量。普通文件允许随机访问,某些设备也允许。管道和套接字不允许。
当进程终止时,内核会回收该进程正在使用的所有描述符。如果进程持有对对象的最后一个引用,则会通知对象的管理器,以便它可以执行任何必要的清理操作,例如文件的最终删除或套接字的释放。
2.1.6.2. 描述符管理
大多数进程在开始运行时都希望有三个描述符已经打开。这些描述符是 0、1、2,更常见地称为标准输入、标准输出和标准错误。通常,所有三个都由登录进程与用户的终端关联(参见第 14.6 节),并通过fork和exec由用户运行的进程继承。因此,程序可以通过读取标准输入来读取用户键入的内容,并且程序可以通过写入标准输出将输出发送到用户的屏幕。标准错误描述符也已打开以供写入,用于错误输出,而标准输出用于普通输出。
这些(以及其他)描述符可以映射到终端以外的对象;这种映射称为I/O 重定向,所有标准 shell 都允许用户执行此操作。shell 可以通过关闭描述符 1(标准输出)并打开所需的输出文件以生成新的描述符 1 来将程序的输出重定向到文件。它还可以通过关闭描述符 0 并打开文件来类似地将标准输入重定向到来自文件的输入。
管道允许一个程序的输出成为另一个程序的输入,而无需重写或重新链接任何一个程序。源程序的描述符 1(标准输出)不是设置为写入终端,而是设置为管道的输入描述符。类似地,接收程序的描述符 0(标准输入)设置为引用管道的输出,而不是终端键盘。生成的两个进程集和连接管道称为管道。管道可以是任意长的由管道连接的进程序列。
open、pipe和socket系统调用使用可用于描述符的最低未使用的数字生成新的描述符。为了使管道工作,必须提供某种机制将这些描述符映射到 0 和 1。dup系统调用创建指向相同文件表条目的描述符的副本。新的描述符也是最低未使用的描述符,但如果先关闭所需的描述符,则可以使用dup执行所需的映射。但是,需要小心:如果需要描述符 1,并且碰巧也关闭了描述符 0,则描述符 0 将是结果。为了避免此问题,系统提供了dup2系统调用;它类似于dup,但它带有一个额外的参数,指定所需描述符的编号(如果所需描述符已打开,dup2会在重用它之前关闭它)。
2.1.6.3. 设备
硬件设备具有文件名,用户可以通过与常规文件相同的系统调用访问它们。内核可以区分设备特殊文件或特殊文件,并且可以确定它指的是哪个设备,但大多数进程不需要进行此确定。终端、打印机和磁带驱动器都像 4.4BSD 磁盘文件一样被访问为字节流。因此,设备依赖性和特性尽可能地保留在内核中,甚至在内核中,大多数设备依赖性和特性也都在设备驱动程序中隔离。
硬件设备可以分为结构化或非结构化;它们分别称为块或字符设备。进程通常通过文件系统中的特殊文件访问设备。对这些文件的 I/O 操作由驻留在内核中的软件模块(称为设备驱动程序)处理。大多数网络通信硬件设备只能通过进程间通信设施访问,并且在文件系统名称空间中没有特殊文件,因为原始套接字接口提供了比特殊文件更自然的接口。
结构化或块设备以磁盘和磁带为例,包括大多数随机访问设备。内核支持对面向块的结构化设备进行读-修改-写类型的缓冲操作,以允许后者以完全随机的字节寻址方式读取和写入,就像普通文件一样。文件系统是在块设备上创建的。
非结构化设备是不支持块结构的设备。常见的非结构化设备包括通信线路、光栅绘图仪以及未缓冲的磁带和磁盘。非结构化设备通常支持大型块 I/O 传输。
非结构化文件称为字符设备,因为这些文件中的第一个实现是终端设备驱动程序。内核对这些设备驱动程序的接口被证明对其他非块结构的设备也很方便。
设备特殊文件由mknod系统调用创建。还有一个系统调用ioctl,用于操作特殊文件的底层设备参数。可以执行的操作因设备而异。此系统调用允许访问设备的特殊特性,而不是重载其他系统调用的语义。例如,磁带驱动器上有一个ioctl用于写入磁带结束标记,而不是存在write的特殊或修改版本。
2.1.6.4. 套接字 IPC
4.2BSD 内核引入了一种比管道更灵活的 IPC 机制,基于套接字。套接字是通信的端点,由描述符引用,就像文件或管道一样。两个进程可以分别创建一个套接字,然后连接这两个端点以生成可靠的字节流。连接后,进程可以读取或写入套接字的描述符,就像它们对管道执行的操作一样。套接字的透明性允许内核将一个进程的输出重定向到驻留在另一台机器上的另一个进程的输入。管道和套接字之间的主要区别在于,管道需要一个公共父进程来设置通信通道。套接字之间的连接可以通过两个不相关的进程建立,这些进程可能驻留在不同的机器上。
System V 通过 FIFO(也称为命名管道)提供本地进程间通信。FIFO 在文件系统中显示为一个对象,不相关的进程可以打开它并通过它发送数据,就像它们通过管道进行通信一样。因此,FIFO 不需要公共父进程来设置它们;它们可以在一对进程启动并运行后连接。与套接字不同,FIFO 只能在本地机器上使用;它们不能用于在不同机器上的进程之间进行通信。FIFO 在 4.4BSD 中仅实现是因为 POSIX.1 标准需要它们。它们的功能是套接字接口的一个子集。
套接字机制需要扩展传统的 UNIX I/O 系统调用以提供相关的命名和连接语义。开发人员没有重载现有的接口,而是最大程度地利用了现有的接口,直到它们在没有更改的情况下工作,并设计了新的接口来处理添加的语义。read和write系统调用用于字节流类型的连接,但添加了六个新的系统调用以允许发送和接收寻址消息(例如网络数据报)。用于写入消息的系统调用包括send、sendto和sendmsg。用于读取消息的系统调用包括recv、recvfrom和recvmsg。回想起来,每个类别中的前两个是其他类的特例;recvfrom和sendto可能应该分别作为库接口添加到recvmsg和sendmsg中。
2.1.6.5. 分散/聚集 I/O
除了传统的read和write系统调用之外,4.2BSD 还引入了执行分散/聚集 I/O 的功能。分散输入使用readv系统调用,允许将单个读取放置到多个不同的缓冲区中。相反,writev系统调用允许将多个不同的缓冲区写入单个原子写入中。进程不是像read和write那样传递单个缓冲区和长度参数,而是传递指向缓冲区和长度数组的指针,以及描述数组大小的计数。
此功能允许进程地址空间的不同部分中的缓冲区被原子地写入,而无需将它们复制到单个连续的缓冲区中。当底层抽象基于记录时,原子写入是必要的,例如在每次写入请求时输出磁带块的磁带驱动器。能够将单个请求读取到多个不同的缓冲区(例如将记录头读取到一个位置,将数据读取到另一个位置)也很方便。尽管应用程序可以通过将数据读取到一个大缓冲区中,然后将各个部分复制到其目标位置来模拟分散数据的功能,但在这种情况下,内存到内存的复制成本通常会使受影响应用程序的运行时间增加一倍以上。
就像send和recv可以作为sendto和recvfrom的库接口实现一样,也可以使用readv模拟read,使用writev模拟write。但是,read和write的使用频率要高得多,因此模拟它们的额外成本是不值得的。
2.1.6.6. 多文件系统支持
随着网络计算的扩展,支持本地和远程文件系统变得越来越必要。为了简化对多个文件系统的支持,开发人员在内核中添加了一个新的虚拟节点或vnode接口。从vnode接口导出的操作集看起来很像以前本地文件系统支持的文件系统操作。但是,它们可以由各种各样的文件系统类型支持。
基于本地磁盘的文件系统
使用各种远程文件系统协议导入的文件
只读CD-ROM文件系统
提供特殊用途接口的文件系统,例如
/proc
文件系统
一些4.4BSD的变体,例如FreeBSD,允许在文件系统第一次被mount系统调用引用时动态加载文件系统。vnode接口在第6.5节中描述;其辅助支持例程在第6.6节中描述;一些特殊用途的文件系统在第6.7节中描述。
2.1.7. 文件系统
普通文件是字节的线性数组,可以从文件中的任何字节开始读取和写入。内核在普通文件中不区分任何记录边界,尽管许多程序将换行符识别为区分行尾,并且其他程序可能会强加其他结构。与文件相关的系统信息不会保存在文件中本身,但文件系统会为每个文件存储少量的所有权、保护和使用信息。
文件名组件是最多255个字符的字符串。这些文件名存储在称为目录的文件类型中。目录中关于文件的信息称为目录项,除了文件名外,还包括指向文件本身的指针。目录项可以引用其他目录,也可以引用普通文件。因此形成了目录和文件的层次结构,称为文件系统;
一个小文件系统如图一个小文件系统所示。目录可以包含子目录,并且对目录嵌套的深度没有固有的限制。为了保护文件系统的完整性,内核不允许进程直接写入目录。文件系统不仅可以包含普通文件和目录,还可以包含对其他对象的引用,例如设备和套接字。
文件系统形成一棵树,其开头是根目录,有时也称为斜杠,用单个斜杠字符(/)拼写。根目录包含文件;在图2.2中的示例中,它包含vmunix
,这是内核可执行目标文件的副本。它还包含目录;在本例中,它包含usr
目录。在usr
目录中是bin
目录,其中主要包含程序的可执行目标代码,例如ls
和vi
文件。
进程通过指定文件的路径名来识别文件,路径名是由零个或多个用斜杠(/)字符分隔的文件名组成的字符串。内核将两个目录与每个进程关联起来,用于解释路径名。进程的根目录是进程可以访问的文件系统中的最高点;它通常设置为整个文件系统的根目录。以斜杠开头的路径名称为绝对路径名,内核从进程的根目录开始解释它。
不以斜杠开头的路径名称为相对路径名,并且相对于进程的当前工作目录进行解释。(此目录也以较短的名称当前目录或工作目录而闻名。)当前目录本身可以用名为点的名称直接引用,用单个句点(.
)拼写。文件名点点(..
)指的是目录的父目录。根目录是它自己的父目录。
进程可以使用chroot系统调用设置其根目录,并使用chdir系统调用设置其当前目录。任何进程都可以随时执行chdir,但只有具有超级用户权限的进程才能执行chroot。Chroot通常用于设置对系统的受限访问。
使用图2.2中所示的文件系统,如果一个进程的根目录为文件系统的根目录,并且其当前目录为/usr
,则它可以使用绝对路径名/usr/bin/vi
从根目录引用文件vi
,或者使用相对路径名bin/vi
从其当前目录引用它。
系统实用程序和数据库保存在某些众所周知的目录中。定义良好的层次结构的一部分包括一个目录,其中包含每个用户的主目录,例如图2.2中的/usr/staff/mckusick
和/usr/staff/karels
。当用户登录时,其shell的当前工作目录将设置为主目录。在他们的主目录中,用户可以像创建普通文件一样轻松地创建目录。因此,用户可以构建任意复杂的子层次结构。
用户通常只知道一个文件系统,但系统可能知道这个虚拟文件系统实际上是由几个物理文件系统组成的,每个文件系统都在不同的设备上。物理文件系统不能跨越多个硬件设备。由于大多数物理磁盘设备都分为几个逻辑设备,因此每个物理设备可能有多个文件系统,但每个逻辑设备最多只有一个。一个文件系统(所有绝对路径名的锚点文件系统)称为根文件系统,并且始终可用。其他文件系统可以挂载;也就是说,它们可以集成到根文件系统的目录层次结构中。对已挂载文件系统的目录的引用会被内核透明地转换为对已挂载文件系统的根目录的引用。
link系统调用获取现有文件的名称和要为该文件创建的另一个名称。成功执行link后,可以通过任一文件名访问该文件。文件名可以使用unlink系统调用删除。当文件的最后一个名称被删除(并且拥有该文件打开的最后一个进程关闭它)时,该文件将被删除。
文件在目录中分层组织。目录是一种文件类型,但与普通文件相比,目录具有系统强加的结构。进程可以像读取普通文件一样读取目录,但只有内核才能修改目录。目录由mkdir系统调用创建,并由rmdir系统调用删除。在4.2BSD之前,mkdir和rmdir系统调用是通过一系列link和unlink系统调用实现的。添加显式创建和删除目录的系统调用的原因有三个:
该操作可以是原子的。如果系统崩溃,目录不会被留下半构造状态,就像使用一系列link操作时可能发生的那样。
当运行网络文件系统时,需要以原子方式指定文件和目录的创建和删除,以便对其进行序列化。
当在磁盘的另一个分区上支持非UNIX文件系统(例如MS-DOS文件系统)时,其他文件系统可能不支持link操作。尽管其他文件系统可能支持目录的概念,但它们可能不会使用链接创建和删除目录,就像UNIX文件系统那样。因此,只有在提供显式的目录创建和删除请求时,它们才能创建和删除目录。
chown系统调用设置文件的拥有者和组,chmod更改保护属性。应用于文件名的stat可用于读回文件的此类属性。fchown、fchmod和fstat系统调用应用于描述符而不是文件名,以执行相同的操作集。rename系统调用可用于在文件系统中为文件提供新名称,替换文件的一个旧名称。与目录创建和目录删除操作类似,rename系统调用被添加到4.2BSD中,以便为本地文件系统中的名称更改提供原子性。后来,它被证明对显式地将重命名操作导出到外部文件系统和网络上很有用。
truncate系统调用被添加到4.2BSD中,以允许文件缩短到任意偏移量。添加此调用主要是为了支持Fortran运行时库,该库的语义是将随机访问文件的末尾设置为程序最近访问该文件的位置。如果没有truncate系统调用,缩短文件的唯一方法是将所需的部分复制到一个新文件,删除旧文件,然后将副本重命名为原始名称。除了此算法速度慢之外,库还可能在文件系统已满时失败。
一旦文件系统具有缩短文件的能力,内核就会利用该能力来缩短大型空目录。缩短空目录的优点是减少了在创建或删除名称时内核搜索它们的时间。
新创建的文件将被分配创建它们的进程的用户标识符和创建它们的目录的组标识符。为保护文件提供了三级访问控制机制。这三个级别指定了文件对以下对象的访问权限:
拥有该文件的用户
拥有该文件的组
其他所有人
每个访问级别都具有针对读取权限、写入权限和执行权限的单独指示器。
文件以零长度创建,并且在写入时可能会增长。当文件打开时,系统会维护一个指向文件的指针,该指针指示与描述符关联的文件中的当前位置。可以以随机访问方式在文件中移动此指针。通过fork或dup系统调用共享文件描述符的进程共享当前位置指针。由单独的open系统调用创建的描述符具有单独的当前位置指针。文件可能在其内部有空洞。空洞是文件中线性范围中的空区域,其中从未写入数据。进程可以通过将指针定位到当前文件末尾之后并写入来创建这些空洞。读取时,系统将空洞视为零值字节。
早期的UNIX系统每个文件名组件的限制为14个字符。此限制通常是一个问题。例如,除了用户希望为文件提供长描述性名称的自然愿望之外,形成文件名的常用方法是basename.extension
,其中扩展名(指示文件类型,例如C源代码的.c
或中间二进制对象的.o
)为1到3个字符,为basename保留10到12个字符。源代码控制系统和编辑器通常会为其目的在前面或后面再添加两个字符,剩下8到10个字符。在一个英文单词中使用10或12个字符作为basename(例如multiplexer
)很容易。
可以保持在这些限制内,但这很不方便甚至很危险,因为其他UNIX系统在创建文件时接受比限制更长的字符串,但随后会截断到限制。名为multiplexer.c
(已为13个字符)的C语言源文件可能有一个源代码控制文件,前面带有s.
,产生一个文件名s.multiplexer
,它与multiplexer.ms
的源代码控制文件无法区分,该文件包含C程序文档的troff
源代码。两个原始文件的内容很容易混淆,而源代码控制系统没有任何警告。仔细的编码可以检测到此问题,但4.2BSD中首次引入的长文件名实际上消除了它。
2.1.8. 文件存储
针对本地文件系统的操作定义分为两个部分。所有本地文件系统共有的特性包括分层命名、锁定、配额、属性管理和保护。这些特性独立于数据存储方式。4.4BSD 只有一个实现来提供这些语义。
本地文件系统的另一部分是存储介质上数据的组织和管理。文件内容在存储介质上的布局由文件存储负责。4.4BSD 支持三种不同的文件存储布局。
传统的 Berkeley 快速文件系统
基于 Sprite 操作系统设计的日志结构文件系统 [Rosenblum & Ousterhout]
基于内存的文件系统
尽管这些文件存储的组织方式完全不同,但这些差异对于使用文件存储的进程来说是无法区分的。
快速文件系统将数据组织成柱面组。根据文件在文件系统层次结构中的位置,可能一起访问的文件存储在同一个柱面组中。预计不会一起访问的文件会被移动到不同的柱面组中。因此,同时写入的文件可能会被放置在磁盘上的相距很远的位置。
日志结构文件系统将数据组织成日志。任何时间点正在写入的所有数据都会被收集在一起,并写入磁盘上的同一位置。数据永远不会被覆盖;相反,会写入文件的新的副本以替换旧的副本。旧文件会被垃圾回收进程回收,该进程在文件系统满且需要额外可用空间时运行。
基于内存的文件系统旨在将数据存储在虚拟内存中。它用于需要支持快速但临时数据的文件系统,例如 /tmp
。基于内存的文件系统的目标是尽可能紧凑地打包存储,以最大限度地减少虚拟内存资源的使用。
2.1.9. 网络文件系统
最初,网络用于将数据从一台机器传输到另一台机器。后来,它发展到允许用户远程登录到另一台机器。下一个合乎逻辑的步骤是将数据带给用户,而不是让用户去获取数据——网络文件系统由此诞生。本地工作的用户不会在每次按键时都遇到网络延迟,因此他们拥有更灵敏的环境。
将文件系统带到本地机器是首批主要的客户端-服务器应用程序之一。服务器是导出一个或多个文件系统的远程机器。客户端是导入这些文件系统的本地机器。从本地客户端的角度来看,远程挂载的文件系统就像任何其他本地挂载的文件系统一样出现在文件树命名空间中。本地客户端可以切换到远程文件系统上的目录,并且可以读取、写入和执行该远程文件系统中的二进制文件,其方式与它们对本地文件系统执行这些操作的方式完全相同。
当本地客户端对远程文件系统执行操作时,请求会被打包并发送到服务器。服务器执行请求的操作,并返回请求的信息或指示请求被拒绝的原因的错误。为了获得合理的性能,客户端必须缓存经常访问的数据。远程文件系统的复杂性在于维护服务器与其众多客户端之间缓存的一致性。
尽管多年来已经开发了许多远程文件系统协议,但 UNIX 系统中最普遍使用的协议是网络文件系统 (NFS),其协议和最广泛使用的实现是由 Sun Microsystems 完成的。4.4BSD 内核支持 NFS 协议,尽管实现是独立于协议规范完成的 [Macklem]. NFS 协议在第 9 章中进行了描述。
2.1.10. 终端
终端支持标准系统 I/O 操作,以及一组特定于终端的操作来控制输入字符编辑和输出延迟。在最低层是控制硬件终端端口的终端设备驱动程序。终端输入根据底层通信特性(如波特率)以及一组软件可控参数(如奇偶校验)进行处理。
在终端设备驱动程序之上是行规程,它提供不同程度的字符处理。当端口用于交互式登录时,会选择默认行规程。行规程在规范模式下运行;输入被处理以提供标准的面向行的编辑功能,并且输入以逐行的方式呈现给进程。
屏幕编辑器和与其他计算机通信的程序通常在非规范模式(也通常称为原始模式或逐字符模式)下运行。在此模式下,输入会立即传递给读取进程,无需解释。所有特殊字符输入处理都被禁用,没有删除或其他行编辑处理,所有字符都传递给正在从终端读取的程序。
可以在这两种极端情况之间配置终端的数千种组合。例如,想要异步接收用户中断的屏幕编辑器可能会启用生成信号的特殊字符并启用输出流控制,但在其他方面以非规范模式运行;所有其他字符都会未经解释地传递给进程。
在输出方面,终端处理程序提供简单的格式化服务,包括
将换行符转换为两个字符的回车-换行序列
在某些标准控制字符之后插入延迟
扩展制表符
将非图形 ASCII 字符以
^C
形式的两个字符序列显示(即,ASCII 脱字符字符后跟 ASCII 字符,该字符的值是从 ASCII@
字符偏移的)。
进程可以通过控制请求单独禁用这些格式化服务中的每一个。
2.1.11. 进程间通信
4.4BSD 中的进程间通信以通信域的形式组织。当前支持的域包括本地域(用于在同一台机器上执行的进程之间进行通信)、互联网域(用于使用 TCP/IP 协议族进行通信的进程之间进行通信,可能在互联网内)、用于需要运行它们的站点之间进行通信的 ISO/OSI 协议族,以及XNS 域(用于使用 XEROX 网络系统 (XNS) 协议进行通信的进程之间进行通信)。
在一个域内,通信发生在称为套接字的通信端点之间。如第 2.6 节所述,套接字系统调用创建一个套接字并返回一个描述符;其他 IPC 系统调用在第 11 章中进行了描述。每个套接字都有一个类型,用于定义其通信语义;这些语义包括可靠性、排序和防止消息重复等属性。
每个套接字都与一个通信协议相关联。此协议根据套接字的类型提供套接字所需的语义。应用程序在创建套接字时可以请求特定的协议,也可以允许系统选择适合正在创建的套接字类型的协议。
套接字可能绑定有地址。套接字地址的形式和含义取决于创建套接字的通信域。将名称绑定到本地域中的套接字会导致在文件系统中创建文件。
通过套接字传输和接收的普通数据是无类型的。数据表示问题是构建在进程间通信功能之上的库的责任。除了传输普通数据外,通信域还可以支持传输和接收特殊类型的称为访问权限的数据。例如,本地域使用此功能在进程之间传递描述符。
UNIX 上 4.2BSD 之前的网络实现通常通过重载字符设备接口来工作。套接字接口的目标之一是让简单的程序能够在流式连接上无需更改即可工作。此类程序只有在读取和写入系统调用保持不变的情况下才能工作。因此,原始接口保持不变,并使其在流式套接字上工作。为更复杂的套接字添加了一个新接口,例如用于发送数据报的套接字,其中必须在每个发送调用中提供目标地址。
另一个好处是新接口具有高度的可移植性。在伯克利提供测试版本后不久,套接字接口就被一个 UNIX 供应商移植到 System III(尽管 AT&T 直到 System V Release 4 发布才支持套接字接口,而是选择使用第八版流机制)。套接字接口也被移植到许多以太网板卡上运行,例如 Excelan 和 Interlan,这些公司正在向 PC 市场销售产品,这些机器太小,无法在主处理器中运行网络。最近,套接字接口被用作 Microsoft 用于 Windows 的 Winsock 网络接口的基础。
2.1.12. 网络通信
套接字 IPC 机制支持的一些通信域提供了对网络协议的访问。这些协议作为内核中套接字软件下方逻辑上独立的软件层实现。内核提供许多辅助服务,例如缓冲区管理、消息路由、协议的标准化接口以及网络接口驱动程序的接口,供各种网络协议使用。
在实现 4.2BSD 时,许多网络协议正在使用或开发中,每个协议都有其自身的优势和劣势。没有明确的优越协议或协议族。通过支持多种协议,4.2BSD 可以提供伯克利环境中可用的一组不同机器之间的互操作性和资源共享。多协议支持也为将来的更改提供了保障。如今为 10 到 100 兆比特/秒以太网设计的协议可能不适合未来的 1 到 10 吉比特/秒光纤网络。因此,网络通信层被设计为支持多种协议。新的协议被添加到内核中,而不会影响对旧协议的支持。旧应用程序可以继续使用旧协议在与运行较新网络协议的新应用程序相同的物理网络上运行。
2.1.13. 网络实现
在 4.2BSD 中实现的第一个协议族是 DARPA 的传输控制协议/互联网协议 (TCP/IP)。CSRG 选择 TCP/IP 作为第一个纳入套接字 IPC 框架的网络,因为来自 Bolt、Beranek 和 Newman (BBN) 的一个 DARPA 赞助项目公开了基于 4.1BSD 的实现。这是一个有影响力的选择:4.2BSD 实现是此协议套件使用极其广泛的主要原因。后来对 TCP/IP 实现的性能和功能改进也被广泛采用。TCP/IP 实现在第 13 章中进行了详细描述。
4.3BSD 的发布添加了 Xerox 网络系统 (XNS) 协议族,部分建立在马里兰大学和康奈尔大学完成的工作基础上。需要此套件来连接无法使用 TCP/IP 通信的隔离机器。
4.4BSD 的发布添加了 ISO 协议族,因为后者在美国内外越来越受到关注。由于为 ISO 协议定义的语义有些不同,因此套接字接口需要进行一些小的更改以适应这些语义。这些更改使得其他现有协议的客户端无法察觉。ISO 协议还需要对 4.3BSD 中内核提供的两级路由表进行大量添加。4.4BSD 大大扩展的路由功能包括具有可变长度地址和网络掩码的任意级别的路由。
参考文献
Accetta 等人,1986 年 “Mach:UNIX 开发的新内核基础” M.Accetta R.Baron W.Bolosky D.Golub R.Rashid A.Tevanian M.Young 93-113 USENIX 协会会议论文集 USENIX 协会 1986 年 6 月
Ewens 等人,1985 年 “Tunis:一个分布式多处理器操作系统” P.Ewens D. R.Blythe M.Funkenhauser R. C.Holt 247-254 USENIX 协会会议论文集 USENIX 协会 1985 年 6 月
Gingell 等人,1987 年 “SunOS 中的虚拟内存体系结构” R.Gingell J.Moran W.Shannon 81-94 USENIX 协会会议论文集 USENIX 协会 1987 年 6 月
Macklem,1994 年 “4.4BSD NFS 实现” R.Macklem 6:1-14 4.4BSD 系统管理员手册 O’Reilly & Associates, Inc. 1994 年加利福尼亚州塞巴斯托波尔
McKusick & Karels,1988 年 “4.3BSD UNIX 内核通用内存分配器的设计” M. K.McKusick M. J.Karels 295-304 USENIX 协会会议论文集 USENIX 协会 1998 年 6 月
McKusick 等人,1994 年 “伯克利软件体系结构手册,4.4BSD 版” M. K.McKusick M. J.Karels S. J.Leffler W. N.Joy R. S.Faber 5:1-42 4.4BSD 程序员补充文档 O’Reilly & Associates, Inc. 1994 年加利福尼亚州塞巴斯托波尔
Rosenblum & Ousterhout,1992 年 “日志结构文件系统的设计与实现” M.Rosenblum K.Ousterhout 26-52 ACM 计算机系统汇刊,10,1 计算机协会 1992 年 2 月
上次修改时间:2024 年 9 月 23 日 由 Fernando Apesteguía