首页 资讯频道 互联频道 智能频道 网络 数据频道 安全频道 服务器频道 存储频道

《UNIX操作系统设计》:详谈UNIX操作系统的体系结构

2020-03-11 10:34:08 来源 : 程序员书屋

当前,介绍UNIX系统的书籍很多,然而论述UNIX系统内部结构的专著却屈指可数。《UNIX操作系统设计》是其中非常引人注目的一本。本书作者Maurice J.Bach多年来在AT&T公司的贝尔实验室工作,对UNIX系统的设计思想有深刻了解,又有讲授UNIX系统的丰富经验。作者在回顾UNIX操作系统的发展演变的基础上,描述了UNIX系统Ⅴ内核内部的数据结构和算法,并对其做了深入浅出的分析。在每章之后,本书还给出了大量富有启发性和实际意义的题目。因而,本书不仅可用作大学本科高年级和研究生操作系统课程的教科书和参考书,也为从事UNIX操作系统的研究人员或UNIX实用程序开发人员提供了极有价值的参考资料。

主要目录结构:

第1章 系统概貌

第2章 内核导言

第3章 数据缓冲区高速缓冲

第4章 文件的内部表示

第5章 文件系统的系统调用

第6章 进程结构

第7章 进程控制

第8章 进程调度和时间

第9章 存储管理策略

第10章 输入/输出子系统

第11章 进程间通信

第12章 多处理机系统

第13章 分布式UNIX系统

样章试读:内核导言

上一章给出了对UNIX系统环境的高层次的看法。本章重点放在内核上,对内核的体系结构提出一个总的看法,勾画出它的基本概念和结构,而这些对于了解本书的其余部分是必不可少的。

2.1 UNIX操作系统的体系结构

Christian曾提出,UNIX系统支持文件系统有“空间”而进程有“生命”的假象(见[Christian 83)第239页)。文件和进程这两类实体是UNIX系统模型中的两个中心概念。图2-1给出了内核框图,显示出了各种模块及它们之间的相互关系,尤其是,它显示出了内核的两个主要成分:左边的文件子系统和右边的进程控制子系统。虽然在实际上,由于某些模块同其他模块的内部操作进行交互而使内核偏离该模型,但该图仍可作为观察内核的一个有用的逻辑视图。

图2-1让我们看到了三个层次:用户级、内核级及硬件级。系统调用与库接口体现了图1-1中描绘的用户程序与内核间的边界。系统调用看起来像C程序中普通的函数调用,而库把这些函数调用映射成进入操作系统所需要的原语。这在第6章中有更详细的叙述。然而,汇编语言程序可以不经过系统调用库而直接引用系统调用。程序常常使用像标准I/O库这样的一些其他库程序以提供对系统调用的更高级的使用。在编译期间把这些库连接到程序上,因此,就这里所讨论的目的来说,这些库是用户程序的一部分。下面的一个例子将阐明这一点。

图2-1把系统调用的集合分成与文件子系统交互的部分以及与进程控制子系统交互的部分。文件子系统管理文件,其中包括分配文件空间、管理空闲空间、控制对文件的存取以及为用户检索数据。进程通过一个特定的系统调用集合,比如,通过系统调用open(为了读或写而打开一个文件)、close、read、write、stat(查询一个文件属性)、chown(改变文件所有者)及chmod(改变文件存取许可权)等与文件子系统交互。这些及另外一些有关的系统调用将在第5章介绍。

文件子系统使用一个缓冲机制存取文件数据,缓冲机制调节在内核与二级存储设备之间的数据流。缓冲机制同块I/O设备驱动程序交互,以便启动往内核去的数据传送及从内核来的数据传送。设备驱动程序是用来控制外围设备操作的内核模块。块I/O设备是随机存取存储设备,或者说,它们的设备驱动程序使得它们对于系统的其他部分来说好像是随机存取存储设备。例如,一个磁带驱动程序可以允许内核把一个磁带装置作为一个随机存取存储设备看待。文件子系统还可以在没有缓冲机制干预的情况下直接与“原始”I/O设备驱动程序交互。原始设备,有时被称为字符设备,包括所有不是块设备的设备。

UNIX操作系统的体系结构

图2-1 系统内核框图

进程控制子系统负责进程同步、进程间通信、存储管理及进程调度。当要执行一个文件而把该文件装入存储器中时,文件子系统与进程控制子系统交互——进程子系统在执行可执行文件之前,把它们读到主存中。这些我们将在第7章看到。

用于控制进程的系统调用有fork(创建一个新进程)、exec(把一个程序的映象覆盖到正在运行的进程上)、exit(结束一个进程的执行)、wait(使进程的执行与先前创建的一个进程的exit相同步)、brk(控制分配给一个进程的存储空间的大小)及signal(控制进程对特别事件的响应)。第7章将介绍这些及其他系统调用。

存储管理模块控制存储分配。在任何时刻,只要系统没有足够的物理存储供所有进程使用,内核就在主存与二级存储之间对进程进行迁移,以便所有进程都得到公平的执行机会。第9章将描述存储管理的两个策略:对换与请求调页。对换进程有时被称为调度程序,因为它为进程进行存储分配的调度,并且影响到CPU调度程序的操作。然而,本书仍将称它为对换程序,以避免与CPU调度程序混淆。

调度程序(scheduler)模块把CPU分配给进程。该模块调度各进程依次运行,直到它们因等待资源自愿放弃CPU,或它们最近一次的运行时间超过一个时间量,从而内核抢占它们。于是调度程序选择最高优先权的合格进程投入运行;当原来的进程成为最高优先权的合格进程时,还会再次投入运行。进程间通信有几种形式,从事件的异步软中断信号到进程间消息的同步传输,等等。

最后,硬件控制负责处理中断及与机器通信。像磁盘或终端这样的设备可以在一个进程正在执行时中断CPU。如果出现这种情况,在对中断服务完毕之后内核可以恢复被中断了的进程的执行:中断不是由特殊的进程服务的,而是由内核中的特殊函数服务的,这些特殊函数是在当前运行的进程的上下文中被调用的。

2.2 系统概念介绍

本节将概述一些主要的内核数据结构,并且更详细地描述图2-1中给出的各模块的功能。

2.2.1 文件子系统概貌

一个文件的内部表示由一个索引节点(inode)给出,索引节点描述了文件数据在磁盘上的布局,并且包含诸如文件所有者、存取许可权及存取时间等其他信息。“索引节点”这一术语是index node的缩写,并且普遍地用于UNIX系统的文献中。每个文件都有一个索引节点,但是它可以有几个名字,且这几个名字都映射到该索引节点上。每个名字都被称为一个联结(link)。当进程使用名字访问一个文件时,内核每次分析文件名中的一个分量,检查该进程是否有权搜索路径中的目录,并且最终检索到该文件所对应的索引节点。例如,如果一个进程调用

open( “/fs2/mjb/rje/sourcefile”,1);

则内核检查“/fs2/mjb/rje/sourcefile”所对应的索引节点。当一个进程建立一个新文件时,内核分配给它一个尚未使用的索引节点。正如我们很快就会看到的那样,索引节点被存储在文件系统中,但是当操控文件时,内核把它们读到内存(in-core)[1]

索引节点表中。

内核还包含另外两个数据结构,文件表(file table)和用户文件描述符表(user file descriptor table)。文件表是一个全局核心结构,但用户文件描述符表对每个进程分配一个。当一个进程打开或建立一个文件时,内核在每个表中为相应于该文件的索引节点分配一个表项。这样一共有三种结构表——用户文件描述符表、文件表和索引结点表(inode table),用这三种结构表中的表项来维护文件的状态及用户对它的存取。文件表保存着文件中的字节偏移量——下一次读或写将从那里开始,并保存着对打开的进程所允许的存取权限。用户文件描述符表标识着一个进程的所有打开文件。图2-2表明了这三张表及它们之间的相互关系。对于系统调用open和系统调用creat,内核返回一个文件描述符(file descriptor),它是在用户文件描述符表中的索引值。当执行系统调用read和write时,内核使用文件描述符以存取用户文件描述符表,循着指向文件表及索引节点表表项的指针,从索引节点中找到文件中的数据。第4章和第5章将详细地描述这些结构,此刻,我们只要说使用这三张表可以实现对一个文件的不同程度的存取共享就够了。

UNIX操作系统的体系结构

图2-2 文件描述符表、文件表和索引节点表

UNIX系统把正规文件(regular file)及目录保存在诸如磁带或磁盘这样的块设备上。由于磁带和磁盘在存取时间上的差别,所以没有什么UNIX系统装置使用磁带实现它们的文件系统。今后,无盘工作站将用得很普遍。在无盘工作站中,文件被存放在一个远程系统上,并通过网络进行存取(见第13章)。然而,为简单起见,下面假设讨论的是有磁盘的系统。一套系统装置可以有若干物理磁盘设备,每个物理磁盘设备包含一个或多个文件系统。把一个磁盘分成几个文件系统可以使管理人员易于管理存储在那儿的数据。内核在逻辑级上只涉及文件系统,而不涉及磁盘,把每个文件系统都当作由一个逻辑设备号标识的逻辑设备(logical device)。由磁盘驱动程序实现逻辑设备(文件系统)地址与物理设备(磁盘)地址之间的转换。除非另有明确的说明,否则,本书在使用“设备”这一术语时总是意味着一个逻辑设备。

一个文件系统由一个逻辑块(logical block)序列组成,每个块都包含512、1024、2048 个字节或512个字节的任意倍数,这要依赖于系统实现。在一个文件系统中,逻辑块大小是完全相同的,但是在一个系统配置中的不同文件系统间逻辑块大小可以是不同的。使用大的逻辑块增加了在磁盘与主存之间的有效数据传送率,因为内核在每次磁盘操作中能传送较多的数据,所以只执行很少几次费时的操作。比如,一次从磁盘读1KB的读操作,会比读两次每次读512B的操作要快。然而,正如将在第5章中看到的那样,如果一个逻辑块太大,将失去有效的存储能力。为简单起见,本书将使用“块”这一术语表示一个逻辑块,并且它将假设一个逻辑块包含1KB数据,除非另有明确说明。

一个文件系统具有如下结构(图2-3):

UNIX操作系统的体系结构

图2-3 文件系统布局

引导块(boot block)占据文件系统的开头,典型地,是一个扇区。它可以含有被读入机器中起引导或初启操作系统作用的引导代码。虽然为了引导系统只需一个引导块,但每个文件系统都有一个(可能是空的)引导块。

超级块(super block)描述了文件系统的状态——如它有多大,它能存储多少文件,在文件系统的何处可找到空闲空间,以及其他信息。

索引节点表(inode list)是一张装有索引节点的表,它在文件系统中跟在超级块后面。当配置一个文件系统时,管理人员应指明索引节点表的大小。内核通过索引来访问索引节点表中的索引节点。有一个索引节点是文件系统的根索引节点(root inode):在执行了系统调用mount(见5.14节)之后,该文件系统的目录结构就可以从这个根索引节点开始进行存取了。

数据块(data block)在索引节点表结束后开始,并且包含文件数据与管理数据。一个已被分配的数据块,能且仅能属于文件系统中的一个文件。

2.2.2 进程

本节将更进一步介绍进程子系统:先描述一个进程的结构及用于存储管理的若干进程数据结构;然后给出进程状态图的初步看法,并考虑状态转换中的各种问题。

一个进程是一个程序的执行,它是由一系列有格式字节组成的,这些有格式字节被解释成机器指令[以下被称为“正文(text)”]、数据和栈区(stack)。当内核调度各个进程使之执行时,这些进程看起来像是同时执行的。而且,可以有几个进程是一个程序的实例。一个进程循着一个严格的指令序列执行,这个指令序列是自包含的,并且不会跳转到别的进程的指令序列上。它读或写自己的数据和栈区,但它不能读或写其他进程的数据和栈区。进程通过系统调用与其他进程及外界进行通信。

用实际的术语来说,UNIX系统上的进程是被系统调用fork所创建的实体。除了0进程以外,每个进程都是被另一个进程执行系统调用fork时创建的。调用系统调用fork的进程是父进程(parent process),而新创建的进程是子进程(child process)。每个进程都有一个父进程,但一个进程可以有多个子进程。内核用各进程的进程标识号(process ID)来标识每个进程,进程标识号简称为进程ID(或PID,见第6章)。0进程是一个特殊进程,它是在系统引导时被“手工”创建的;当它创建了一个子进程(1进程)之后,0进程就变成对换进程。正如在第7章所解释的那样,1进程被称为init进程,是系统中其他每个进程的祖先,并且享有同它们之间的特殊关系。

用户对一个程序的源代码进行编译以建立一个可执行文件,可执行文件由以下几部分组成:

描述文件属性的一组“头标(header)”;

程序正文;

数据的机器语言表示,它给出程序开始执行时的初值;一个空间指示,它指出内核应该为被称为bss[2]

的未初始化数据分配多大的空间(在运行时内核把bss的初值置为0);

其他段,诸如符号表信息。

对于图1-3中的程序,可执行文件的正文是函数main与copy所生成的代码,其中,变量version是初始化数据(放在本程序中仅仅是为让它有初始化数据),数组buffer是未初始化的数据。系统Ⅴ的C编译程序版本在缺省时创建一个分离的正文段,但它支持一种选择,该选择允许数据段中包含程序指令,这是在系统的较老的版本中使用的。

在系统调用exec期间,内核把一个可执行文件装入主存中,被装入的进程至少由被称为正文区、数据区及栈区的三部分组成。正文区和数据区相应于可执行文件中的正文段和数据bss段。但是栈区是自动创建的,而且它的大小在运行时是被内核动态调节的。栈区由逻辑栈帧(stack frame)组成,当调用一个函数时栈帧被压入,当返回时栈帧被弹出。一个称为栈指针(stack pointer)的特殊寄存器指示出当前栈深度。一个栈帧包含着用于函数调用的参数、它的逻辑变量及为恢复先前的栈帧所需要的数据——其中包括函数调用时程序计数器的值及栈指针的值。程序代码包含管理栈增长的指令序列,并且当需要时内核为栈区分配空间。在图1-3所示的程序中,当main被调用时(按惯例,在每个程序中被调用一次),函数main中的参数argc和argv、变量fdold和fdnew就会在栈区上出现。并且,无论何时函数copy被调用,copy中的参数old与new及变量count都在栈区上出现。

因为UNIX系统中的一个进程能在两种态——核心态(kernel mode)或用户态(user mode)下执行,所以UNIX系统中核心栈(kernel stack)与用户栈(user stack)是分开的。用户栈含有在用户态下执行时函数调用的参数、局部变量及其他数据。图2-4中的左半部表明一个进程在copy程序中做系统调用write时进程的用户栈。进程启动过程(此过程是包含在库中的)用两个参数调用函数main,并将第1帧压入用户栈;第1帧含有main的两个局部变量的空间。然后main用两个参数old与new调用copy,并将第2帧压入用户栈中,第2帧包含局部变量count的空间。最后,进程通过调用库函数write来引用系统调用write,每个系统调用都在系统调用库中有一个入口点;系统调用库按汇编语言编码并包含一个特殊的trap指令,当该指令被执行时,它引起一个“中断”,从而导致硬件转换到核心态。一个进程调用一个特定的系统调用库的入口点,正像它调用任何函数一样。对于库函数也要创建一个栈帧。当进程执行特定的指令时,它将处理机执行态转换到核心态,执行内核代码,并使用核心栈。

核心栈中含有在核心态下执行的函数的栈帧。核心栈上的函数及数据项涉及的是内核中的而不是用户程序中的函数和数据,但它的构成与用户栈的构成相同。当一个进程在用户态下执行时,它的核心栈为空。图2-4的右半部给出了一个在copy程序中执行系统调用write的进程的核心栈的表项。在以后的章节中对系统调用write进行详细讨论时,再叙述各算法名称。

UNIX操作系统的体系结构

图2-4 程序copy的用户栈及核心栈

每个进程在内核进程表(process table)中都有一个表项,并且每个进程都被分配一个u区[3]

,u区包含仅被内核操纵的私用数据。进程表包含(或指向)一个本进程区表(per process region table),本进程区表的表项指向区表(region table)的表项。一个区是进程地址空间中连续的区域,如正文区、数据区及栈区等。区表登记项描述区的属性,诸如它是否包含正文或数据,它是共享的还是私用的,以及区的“数据”位于主存的何处,等等。从本进程区表到区表的额外伺接级允许彼此独立的进程对区的共享。当一个进程调用系统调用exec时,在释放了进程一直在使用着的老区之后,内核为它的正文、数据和栈分配新区。当一个进程调用系统调用fork时,内核拷贝老进程的地址空间,在可能时允许进程对区共享,否则再建立一个物理拷贝。当一个进程调用系统调用exit时,内核释放进程使用过的区。图2-5展示了一个运行中的进程的有关数据结构:进程表指向本进程区表,本进程区表有指向该进程的正文区、数据区或栈区的区表表项的指针。

UNIX操作系统的体系结构

图2-5 进程的数据结构

进程表表项及u区包含进程的控制信息和状态信息。u区是进程表表项的扩展,第6章将介绍这两个表的区别。在后几章中讨论的进程表中的字段如下。

状态字段。

标识符——表示拥有该进程的用户(用户ID或UID,见第6章)。

当一个进程被挂起(在sleep状态)时的事件描述符集合。

u 区包含的是用来描述进程的信息,这些信息仅当进程正在执行时才是可存取的。重要的字段如下。

指向当前正在执行的进程的进程表项的指针。

当前系统调用的参数,返回值及错误码。

所有的打开文件的文件描述符。

内部I/O参数。

当前目录(current directory)和当前根(current root)(见第5章)。

进程及文件大小的限制。

内核能直接存取正在执行的进程的u区的字段,但不能存取其他进程的u区的字段。在其内部,内核引用结构变量u以存取当前正在运行的进程的u区,并且当另一进程执行时,内核重新安排它的虚地址空间,以使结构变量u引用的是新进程的u区。由于这一实现方式给出了从u区到它的进程表表项的指针,所以内核很容易识别出当前进程。

1.进程上下文

一个进程的上下文(context)包括被进程正文所定义的进程状态、进程的全局用户变量和数据结构的值、它使用的机器寄存器的值、存储在它的进程表项与u区中的值以及它的用户栈和核心栈的内容。操作系统的正文和它的全局数据结构被所有的进程所共享,因而不是进程上下文的一部分。

当执行一个进程时,系统被说成在该进程的上下文中执行。当内核决定它应该执行另一个进程时,它做一次上下文切换(context switch),以使系统在另一个进程的上下文中执行。正如将要看到的,内核仅允许在指定条件下进行上下文切换。当进行上下文切换时,内核保留足够信息,为的是以后它能切换回第一个进程,并恢复它的执行。类似地,当从用户态移到核心态时,内核保留足够信息以便它后来能返回到用户态,并从它的断点继续执行。在用户态与核心态之间的移动是态的改变,而不是上下文切换。再看一下图1-5,当它把上下文从进程A变成进程B时,内核做的是上下文切换;当发生从用户态到核心态或从内核态到用户态的改变时,所改变的是执行态,但仍在同一个进程(例如进程A)的上下文中执行。

内核在被中断了的进程的上下文中对中断服务,即使该中断可能不是由它引起的。被中断的进程可以是正在用户态下执行的,也可以是正在核心态下执行的。内核保留足够的信息以便它在后来能恢复被中断了的进程的执行,并在核心态下对中断进行服务。内核并不产生或调度一个特殊进程来处理中断。

2.进程状态

一个进程的生存周期能被划分为一组状态,每个状态都具有一定的用来描述该进程的特点。第6章将描述所有的进程状态,但现在了解如下状态是重要的:

(1)进程正在用户态下执行。

(2)进程正在核心态下执行。

(3)进程未正在执行,但是它已准备好运行——一旦调度程序选中了它,它就可以投入运行。很多进程可以处于这一状态,而调度算法决定哪个进程将成为下一个执行的进程。

(4)进程正在睡眠。当进程再也不能继续执行下去的时候,如正在等候I/O完成时,进程使自己进入睡眠状态。

因为任何时刻一个处理机仅能执行一个进程,所以至多有一个进程可以处在第一种状态和第二种状态。这两个状态相应于两种执行态:用户态与核心态。

3.状态转换

以上描述的进程状态给出了进程的一种静态观点,但是,实际上,各个进程是按照明确定义的规则连续地在各种状态间移动的。状态转换图是一个有向图,它的节点表示一个进程能进入的状态,而它的边表示引起一个进程从一种状态到另一种状态的事件。如果从第一种状态到第二种状态存在着一条边,则这两种状态之间的状态转换是合法的。可以从一种状态发出多个转换,但是,就处于某种状态的一个进程来说,依赖于所发生的系统事件,完成一个且只完成一个转换。图2-6给出了上述定义的进程状态的状态转换图。

UNIX操作系统的体系结构

图2-6 进程状态及转换

如前所述,在一个分时方式中,几个进程能同时执行,并且它们可能都要在核心态下运行。如果对它们在核心态下的运行不加以限制,则它们会破坏全局核心数据结构中的信息。通过禁止任意的上下文切换和控制中断的发生,内核可保护它们的一致性。

仅当进程从“核心态运行”状态转移到“在内存中睡眠”状态时,内核才允许上下文切换。在核心态下运行的进程不能被其他进程所抢占,因此内核有时被称为不可抢先(non-preemptive)的,虽然系统并不抢占处于用户态下的进程。因为内核处于不可抢先状态,所以内核可保持它的数据结构的一致性,从而解决了互斥(mutual exclusion)问题——保证在任何时刻至多一个进程执行临界区代码。

比如,让我们考虑图2-7中的示例代码。该代码段要把其地址在指针变量bp1中的数据结构,插入双向链表中地址在指针变量bp中的数据结构之后。如果当内核执行这一代码段时系统允许上下文切换,则会发生如下情形。假设直到注释出现之前内核执行该代码,然后做一个上下文切换,这时双向链表处于非一致性状态:结构bp1一半被插在该链表上,另一半在该链表外。如果进程沿着向前的指针,则它能在该链表上找到bp1;但如果沿着向后的指针,则它不能找到bp1(图2-8)。如果其他进程在原来的进程再次运行之前操控链表上的这些指针,则双向链表结构会被永久性地毁坏。UNIX系统通过一个进程在核心态下执行时不允许上下文切换来防止这种情况发生。如果一个进程进入睡眠从而允许上下文切换,则必须使内核算法的编码实现能够确保系统数据结构处于安全、一致的状态。

UNIX操作系统的体系结构

图2-7 创建双链表的示例代码

UNIX操作系统的体系结构

图2-8 由于上下文切换而造成的不正确链表

能引起内核数据的非一致性的有关问题是中断的处理。中断处理能改变内核状态信息。举例来说,如果内核正在执行图2-7中的代码,当执行到注释行时接收了一个中断,并且中断处理程序是如前所述的那样操纵指针,则中断处理程序就会破坏该链表中的信息。若规定在核心态下执行时系统禁止所有的中断,就可以解决这一问题。但是这可能会使中断的服务推迟,或者可能会损害系统吞吐量,为此,改为当进入代码临界区(critical region)时内核把处理机执行级提高,以禁止中断。如果任意的中断处理程序的执行会导致一致性问题的话,那么代码段是临界的。比如,如果一个磁盘中断处理程序操纵图中的缓冲区队列,则内核操纵缓冲区队列的那个代码段是关于磁盘中断处理程序的代码临界区。临界区应小且不经常出现,以便系统吞吐量不大会被它们的存在所影响。其他操作系统解决这一问题的方法是:规定在系统状态下执行时封锁所有的中断,或者采用完善的加锁方案以保证一致性。第12章将面对多处理机系统再回过头来讨论这一问题。这里所给出的解答在那时就不够了。

现在让我们回顾一下本节的内容:内核通过仅当一个进程使自己进入睡眠时才允许上下文切换,以及通过禁止一个进程改变另一个进程的状态来保护它的一致性。它还在代码临界区周围提高处理机执行级,以封锁其他能引起非一致性的中断。进程调度程序定期地抢占用户态下的进程执行,以使进程不能独占式地使用CPU。

4.睡眠与唤醒

一个在核心态下执行的进程在决定它对系统事件的反应上它打算做什么方面有很大的自主权。进程能互相通信并且“建议”各种可供选择的方法,但由它们自己做出最后的决定。正如我们将要看到的,存在着一组进程在面临各种情况时所应服从的规则,但是每个进程最终都是自主地遵循这些规则的。例如,当一个进程必须暂停它的执行(“进入睡眠”)时,它能自由地按自己的意图去做。然而,一个中断处理程序不能睡眠,因为如果中断处理程序能睡眠,就意味着被中断的进程会被投入睡眠。

进程会因为它们正在等待某些事件的发生而进入睡眠,例如:等待来自外围设备的I/O完成;等待一个进程退出;等待获得系统资源;等等。当我们说进程在一个事件上睡眠时,这意味着,直到该事件发生时,它们一直处于睡眠状态;当事件发生时它们被唤醒,并且进入“就绪”状态。很多进程能同时睡眠在一个事件上;当一个事件发生时,由于这个事件的条件再也不为真了,所以所有睡眠在这个事件上的进程都被唤醒。当一个进程被唤醒时,它完成一个从“睡眠”状态到“就绪”状态的状态转换,对于随后的调度来说,该进程就是个合格者了,但它并不立即执行。睡眠进程不耗费CPU资源;内核并不是经常去查看一个进程是否仍处于睡眠状态,而是等待事件的发生,那时把进程唤醒。

举例来说,一个在核心态执行的进程有时必须锁住一个数据结构,如果发生后来它进入睡眠的情况,其他企图操纵该上了锁的数据结构的进程必须检查上锁情况,并且因为别的进程已经占有该锁,则它们去睡眠。内核按如下方式实现这样的锁:

while(条件为真)

sleep(事件:条件变为假);

置条件为真;

它按如下方式解锁并唤醒睡眠在该锁上的所有进程:

置条件为假;

wakeup(事件:条件变为假);

图2-9描绘了三个进程A、B、C为一个上了锁的缓冲区进行竞争的情况。睡眠的条件是缓冲区处于上锁状态。在任一时刻只能有一个进程在执行,它发现缓冲区是上了锁的,就在缓冲区变为开锁状态的事件上等待。终于,缓冲区的锁解开了,所有的进程被唤醒并且进入“就绪”状态。内核最终选择一个进程(比如说B)执行。进程B执行“while”循环,发现缓冲区处于开锁状态,于是为缓冲区上锁,并且继续执行。如果后来进程B在为缓冲区解锁之前再次去睡眠(例如等候I/O操作的完成),则内核能调度其他进程去运行。如果它选择了进程A,进程A执行“while”循环,发现缓冲区处于上锁状态,那么它就再次去睡眠。进程C可以做同样的事情。最后,进程B醒来并为缓冲区解锁,允许进程A也允许进程C存取缓冲区。因此,“while-sleep”循环保证至多一个进程能获得对资源的存取。

第6章将极其详细地介绍睡眠与唤醒的算法。在此期间它们应被考虑成是“原子的”:一个进程瞬时地进入睡眠状态,并停留在那儿直至它被唤醒。在它睡眠之后,内核调度另一个进程去运行,并切换成后者的上下文。

UNIX操作系统的体系结构

图2-9 在一个锁上睡眠的多个进程

2.3 内核数据结构

大多数内核数据结构都占据固定长度的表而不是动态地分配空间,这一方法的优点是内核代码简单。但是它限制了一种数据结构的表项的数目,即为系统生成时原始配置的数目。如果在系统操作期间,内核用完了一种数据结构的表项,则它不能动态地为新的表项分配空间,而是必须向发出请求的用户报告一个错误。此外,如果内核被配置得具有不可能用完的表空间,则因不能用于其他目的而使多余的表空间浪费了。然而,一般都认为内核算法的简单性比挤出主存中每一个仅有的字节的必要性更重要一些。算法通常使用简单的循环来寻找表中的空闲表项,这是一个较易于理解的方法,而且有时比复杂的分配方案更为有效。

2.4 系统管理

管理进程可以非严格地归入为用户团体的公共福利提供各种功能的那类进程。这些功能包括磁盘格式化、新文件系统的创建、修复被破坏的文件系统、内核调试及其他。从概念上说,管理进程与用户进程没有区别:它们都使用为一般用户团体可用的相同的一组系统调用。它们仅在被允许的权限与特权上区别于一般用户进程。例如,文件存取权限允许管理进程操纵对一般用户来说禁止进入的文件。在内部,内核把一个称为超级用户(superuser)的特殊用户区别出来,赋予它特权,这一点我们即将看到。通过履行一次注册-口令序列或通过执行特殊程序可使一个用户成为超级用户。超级用户特权的其他用途将在随后的章节中研究。简而言之,内核不识别一个分离的管理进程类。

2.5 本章小结

本章描述了内核的体系结构:它的两个主要成分是文件子系统与进程子系统。文件子系统控制用户文件中数据的存储与检索。文件被组织到文件系统中,而文件系统被看作一个逻辑设备。像磁盘这样的一个物理设备能包含几个逻辑设备(文件系统)。每个文件系统都有一个用来描述文件系统的结构和内容的超级块,并且文件系统中的每个文件都由索引节点描述,索引节点给出了文件的属性。操控文件的系统调用通过索引节点来实现其功能。

进程有各种状态,并且按照明确定义的转换规则在这些状态之间转移。特别之处在于,在核心态下执行的进程能暂停它们的执行而进入睡眠状态,但是没有哪一个进程能把另一进程投入睡眠状态。内核是不可被抢占的,这意味着,一个在核心态下执行的进程将连续执行,直至它进入睡眠状态或直至它返回到用户态下执行时为止。内核通过实施不可抢占策略以及通过在执行代码临界区时封锁中断来维护它的数据结构的一致性。

本章的其余部分详细描述了图2-1所示的子系统及它们的交互作用。从文件子系统开始,继之以进程子系统。下一章将涉及高速缓冲问题,并描述在第4章、第5章和第7章要介绍的算法中所使用的缓冲区分配算法。第4章考查文件系统的内部算法,包括索引节点的操控、文件的结构及路径名到索引节点的转换。第5章解释若干系统调用,例如,系统调用open、close、read及write,这些系统调用使用了第4章中的算法来访问文件系统。第6章论述进程上下文的基本思想及其地址空间;第7章涉及有关进程管理及使用第6章的算法的系统调用;第8章介绍进程调度;第9章讨论存储管理算法;第10章讲的是设备驱动程序,直到这时终端驱动程序与进程管理之间的相互关系才能被解释;第11章介绍了进程间通信的某些形式;最后两章涉及若干高级专题,包括多处理机系统与分布式系统。

2.6 习题

1.考虑如下命令序列:

grep main a.c b.c c.c > grepout &

wc −1 < grepout &

rm grepout &

每一命令行尾部的“&”都通知shell在后台运行这些命令,并且它能并行地执行每个命令。为什么这不等价于如下的命令行?

grep main a.c b.c c.c| wc −1

2.考虑图2-7中的内核代码示例。假设当代码到达注释处时发生上下文切换,并且假设另一进程通过执行如下代码而从链表中摘掉一个缓冲区:

remove(qp)

struct queue *qp;

{

qp— >forp — >backp = qp — >backp;

qp— >backp— >forp=qp — >forp;

qp— >forp=qp— >backp = NULL;

}

考虑三种情况:

进程从链表中摘掉bp结构;

进程从链表中摘掉bp1结构;

进程摘掉链表上当前跟在bp1之后的结构。

这三种情况中哪种是原来的进程执行完注释以后的代码时链表的状态?

3.如果内核试图唤醒睡眠在一个事件上的所有进程,但是在唤醒时没有进程睡眠在那个事件上,那么会发生什么情况?

[1] “core”这一术语指的是机器的原始存储,不是指硬件技术。

[2] bss这一名字来自IBM 7090机的汇编伪运算符,它代表“block started by symbol”。

[3] u区中的u代表用户。u区的另一个名称是ublock;本书则总是称它为u区。

UNIX操作系统设计(各大网店已上架)

莫里斯·J.,巴赫(Maurice J.Bach) 著,陈葆钰,王旭,柳纯录,冯雪山 译

Linux之父Linux Torvalds曾捧读的经典著作

UNIX操作系统经典著作,畅销多年

深度剖析UNIX操作系统内核的内部数据结构、算法和UNIX系统的问题

本书以UNIX系统为背景,全面、系统地介绍了UNIX操作系统内核的内部数据结构和算法。本书首先对系统内核结构做了简要介绍,然后分章节描述了文件系统、进程调度和存储管理,并在此基础上讨论了UNIX系统的问题,如驱动程序接口、进程间通信与网络等。在每章之后,还给出了大量富有启发性和实际意义的题目。

相关文章

最近更新