首页 操作系统原理与设计(下)

操作系统原理与设计(下)

举报
开通vip

操作系统原理与设计(下)220CH13 操作系统UNIX 13.1 UNIX操作系统概述 220 13.1.1 UNIX系统的结构 220 13.1.2 UNIX系统的运行描述 221 13.2 UNIX操作系统的进程管理 222 13.2.1 UNIX操作系统的进程 222 13.2.2 UNIX操作系统的进程调度 229 13.2.3 UNIX操作系统的进程通信 231 13.2.4 UNIX中的进程控制 232 13.3 UNIX操作系统的存储管理 235 13.3.1 存储管理部件 235 13.3.2 UNIX进程映像的虚、实...

操作系统原理与设计(下)
220CH13 操作系统UNIX 13.1 UNIX操作系统概述 220 13.1.1 UNIX系统的结构 220 13.1.2 UNIX系统的运行描述 221 13.2 UNIX操作系统的进程管理 222 13.2.1 UNIX操作系统的进程 222 13.2.2 UNIX操作系统的进程调度 229 13.2.3 UNIX操作系统的进程通信 231 13.2.4 UNIX中的进程控制 232 13.3 UNIX操作系统的存储管理 235 13.3.1 存储管理部件 235 13.3.2 UNIX进程映像的虚、实地址空间 237 13.3.3 UNIX存储管理及分配释放策略 239 13.4 UNIX操作系统的文件管理 240 13.4.1 UNIX文件系统的基本工作原理 240 13.4.2 UNIX文件系统的数据结构综述 241 13.4.3 UNIX文件的物理结构 247 13.4.4 UNIX文件系统的资源管理综述 248 13.4.5 子文件系统的装卸 250 13.4.6 文件的共享 252 13.4.7 文件卷的动态安装 254 13.4.8 UNIX文件操作的系统调用 255 13.5 UNIX操作系统的设备管理 259 13.5.1 块设备管理的数据结构 260 13.5.2 缓存控制块buf的各种队列 264 13.5.3 字符设备管理的数据结构 266 13.5.4 字符缓存的管理 267 13.5.5 块设备的读写操作 268 13.5.6 磁盘中断处理 273 13.5.7 块设备I/O操作与文件读写关系 276 13.6 UNIX命令语言shell 277 13.6.1 shell命令语言 277 13.6.2 shell程序设计语言 280 CH13 操作系统UNIX 13.1 UNIX操作系统概述 13.1.1 UNIX系统的结构 UNIX是一个通用的、多用户分时、交互型的操作系统,是美国贝尔实验室的汤普逊(K. Thompson)和里奇(D. M. Ritchie)于70年代初共同研制成功的。在经历了开发、发展、不断完善和广泛应用的过程之后,影响日益扩大。目前,UNIX系统已经成为世界上最为著名的分时操作系统之一,我们这里所介绍的,是以PDP-11/40机上的第六版为依据。UNIX系统的结构,可如图13-1所示,下面对此做些说明。 图13-1 UNIX系统结构示意图 (1) UNIX系统基本上可分成核心与核外程序两部分,通常所说的UNIX操作系统多是指核心程序,它大致由存储管理、进程管理、设备管理和文件系统管理等几部分组成。进程管理还可以进一步分成高级进程管理和低级进程管理两部分。前者主要包括:进程创建、终止;进程间的高级通信;进程在内、外存之间的转移;信号机构和进程间的跟踪控制等。后者主要包括:进程调度;进程间的低级通信机构等。 (2) 由于UNIX产生于七十年代,当时才刚刚开始进行结构程序设计方法的研究,因此UNIX系统的结构并不是很理想的,它的整个核心属于模块结构,模块间的关系极为复杂。不过由于设计者具有的丰富经验,悉心研究和精心构思,使得核心程序不光代码紧凑,系统效率高,而且对模块间的关系做了较好的处置和安排,所以从模块间的调用关系上看,除某些模块外,大多数模块之间仍然可以划分成一定的层次。譬如从图1可以看出,文件系统的大部分模块都在设备管理的上面,而设备管理又处在存储管理之上。 (3) UNIX操作系统源程序由三大部分组成: · C语言程序文件; · C语言全局变量和符号常数文件; · 汇编语言程序文件。 每个C语言程序文件包括若干个子程序,这些文件都可以独立编译,UNIX操作系统所具有的功能基本上由这些文件反映出来。C语言全局变量和符号常数文件中包含有UNIX操作系统使用的重要数据结构的说明,它与C语言程序文件的区别在于它不能单独编译,只能和C语言文件一起编译。汇编语言程序文件大约有1000条左右的汇编语句,大部分反映了与机器硬件直接关联的功能,也有的是为追求系统效率而设置的。 (4) 如果把UNIX核心看作是分层的,那么核心的最外层是“系统调用”。系统调用命令是UNIX向用户提供的第一种界面——面向用户程序的界面,即用户在编写程序时可以使用的界面,这是用户程序获得操作系统为其服务的必由和唯一的途径。 很多操作系统也都为用户提供系统调用这种界面,但这只能做到汇编语言这一级上(即用户在编写汇编语言源程序时,可在源程序中使用操作系统提供的系统调用命令)。但是UNIX不仅在汇编语言,而且也在程序设计语言C中向用户提供这种界面,用户可以把系统调用命令视为C语言的一部分去加以应用,去编写自己的C语言源程序。 (5) UNIX是一个多用户分时、交互式的操作系统,用户会经常通过终端发出命令与系统进行交往:一方面控制自己的作业在系统中的运行,另一方面也可对系统采取的某些动作作出及时地响应。这种要求操作系统进行某种工作的通信语言,在UNIX系统中形成了向用户提供的第二种界面——面向用户终端的界面,即命令语言shell。UNIX系统提供的shell,不仅是一种键盘命令语言,而且也含有许多高级语言所具备的复杂的控制结构与变量运算能力。因此,用户除了能使用shell在终端上与UNIX系统会话,直接参与管理和控制计算机工作,还可以把它做为一种程序设计语言,用来编写出所谓的shell过程,以文件形式存入系统,需要时经shell调入内存,按用户进程的形式执行。一般地,把shell提供的界面简称为用户界面,把系统调用命令提供的界面简称为系统调用。 13.1.2 UNIX系统的运行描述 如图13-2,在UNIX系统生成后,系统的所有程序都是以文件的形式存放在磁盘上,系统开始初启,引导程序把系统的核心(UNIX操作系统)放入内存空间的低地址部分,随之初启程序为系统建立起进程0。 进程0是系统调度进程,其主要任务是负责把在盘上的准备运行的所有进程换入内存。 进程1是系统初启时由进程0创建的。随之,它为每一个终端建立一个相应的shell进程,这些进程都执行shell命令解释程序。每个终端的shell进程都等待用户敲入命令,在接到命令后,相应进程的shell解释程序就开始工作。它根据命令名去查找存放在文件系统中的这个命令文件,并将它调入内存,建立一个子进程来执行这个命令,以完成shell命令的功能。这个子进程执行完后就被撤销,回到相应shell进程,以接收下一个shell命令。 图13-2 UNIX系统运行描述图 这里要注意两点:第一,进程0和进程1是在系统初启时在核心态下直接创建的两个进程,它们与UNIX系统中的其它进程不同,其它进程都是通过系统调用命令fork而建立起来的。UNIX操作系统能否正常运行,在很大程度上取决于进程和进程1的工作。第二,在一般的操作系统中,命令解释语言都是作为操作系统的一部分而常驻内存的(譬如MS-DOS的COMMAND.COM文件),但UNIX的shell却采用了独特的处理方法,它把实现shell命令的程序都以文件的形式存放在文件系统中,每一个终端的shell进程根据用户键入的命令,找到相应的命令程序,并为之建立一个子程序在用户态下运行。这样的好处,不光使UNIX操作系统显得短小精悍,节省内存,而且能很方便地扩充和更改shell的功能,使得用户能根据自己的需要,灵活地构造出与UNIX系统的用户界面。 13.2 UNIX操作系统的进程管理 13.2.1 UNIX操作系统的进程 1、进程映像与UNIX进程 从前面进程的基本概念可知,描述一个进程应由程序、数据集合以及进程控制块PCB三者来完成。 程序——表示该进程所要完成的操作,允许多个进程共享一个程序,这时的程序应编制成为“可重入”的结构,即程序的代码在执行过程中不能被修改。 数据集合——这是程序运行时所需要的数据以及工作区(如栈区等),是附属于该进程的私有物。 进程控制块——表示进程存在的唯一标志,它记录下有关进程的重要信息。譬如进程的状态,保留进程运行暂停时刻的各通用寄存器,控制寄存器,程序状态字的当前值等。 可以看出,在整个进行的生命周期里,这三部分的瞬息变化如同电影中的一幅幅镜头似地,描绘出了该进程的执行过程。因此,UNIX中就称这三部分为进程映像,把进程定义为是“进程映像的执行”。 2、UNIX进程的两种运行状态 在有的操作系统里,把执行操作系统程序的进程称作系统进程,譬如作业调度进程、存储分配进程等;而把执行用户程序的进程称为用户进程。UNIX对此有不同的处理:在UNIX里,进程既可以执行操作系统程序,也可以执行用户程序,操作系统程序的任务是管理资源,控制系统中的各种活动,用户程序则应在操作系统的管理和控制下,完成自己的任务。 为了能区分开一个进程在两种不同环境下的运行情况,UNIX采取的措施为: 第一,让操作系统程序和用户程序构成各自的虚拟地址空间,运行时两者在内存占有不同的存储区域,使用两套不同的映射寄存器组,来分别实现它们的虚拟地址空间到内存地址空间的映射。 第二,为进程设置了两种运行状态:核心态和用户态,这实际上也是处理机的两种工作状态,体现在处理机状态字PS里(图13-3)。 图13-3 PDP-11机的处理机状态字PS 所谓核心态,即表示进程在执行操作系统程序,此时它的活动地址空间为核心空间;所谓用户态,即表示进程在执行用户程序,此时它的活动地址空间为用户态空间。这两种运行态将按照需要在一定时机进行转换。 3、UNIX进程的映像 UNIX把进程映像分成:进程控制块、数据段、以及共享正文段三大部分,下面加以分别介绍。 1) 进程控制块 UNIX的进程控制块由基本控制块proc结构和扩充控制块user结构两部分组成。在proc结构里存放着关于一个进程的最基本、最必需的信息,因此它常驻内存;在user结构里存放着只有进程运行时才用到的数据和状态信息,因此为了节省内存空间,当进程暂时不在处理机上运行时,就把它放在磁盘上的对换区中,进程的user结构总和进程的数据段在一起。下面行介绍proc结构。 系统中维持一张proc表它有50个表目,每个表目为一个proc结构,供一个进程使用,因此UNIX中最多同时可存在50个进程。创建进程时,在proc表中找一个空表目,以建立起相应于该进程的proc结构。每个进程对应的proc结构,包含有22个字节,用C语言表示,下面列出其中最常用的几项加以说明。 struct proc { char p-stat; 进程状态 char p-flag; 进程特征 char p-pri; 进程优先数 char p-sig; 软中断号(送至进程的信号数) char p-uid; 进程所属的用户标识数,指向终端用户的tty char p-time; 进程在内存或外存的驻留时间 char p-cpu; 用于调度优先数计算 char p-nice; 用于调度优先数计算 int p-ttyp; 指向对应终端的tty结构 int p-pid; 进程标识数 int p-ppid; 父进程的标识数 int p-addr; 进程数据段起始地址(在内存或盘上) int p-size; 数据段大小 int p-wchan; 标识进程睡眠的原因 int * p-textp; 指向本文的文本结构 } proc [NPROC] 其中, p-stat 表示进程状态码 NULL 0 表示进程结构尚未分配任何进程 SSLEEP 1 进程处于高优先级睡眠状态 SWAIT 2 进程处于低优先级睡眠状态 SRUN 3 逻辑上可能运行,已获得处理机为运行态,否则为就绪态,等待处理机 SIDL 4 父进程正在创建子进程 SZOMB 5 表示进程终止态,等待父进程最后消灭它(使成为NULL) SSTOP 6 等待父进程发送跟踪命令 p-flag 进程特征标志码 SLOAD 01 表示进程映象已在内存中 SSYS 02 该进程是调度进程、不能换出内存 SLOCK 04 表示本进程映象不能换出内存 SSWAP 010 表示本进程映象不能换出内存 STRC 020 子进程已发出跟踪请求 SWTED 040 另一跟踪标志 进程诸状态之间的转换,如图13-4所示。 图13-4 UNIX的进程状态变迁图 (1) 运行状态SRUN 一个进程占有处理机时就处于运行状态。这时其ppda的起址为核心态第七页KISA6所指,即proc [i].p-addr就是KISA6所指的地址。 (2) 准备就绪状态 一个进程如等待占用处理机,则处于准备就绪状态,其p-stat也为SRUN,它与运行状态的主要区别是,第一,它的ppda的起始地址(或p-addr)尚未在KISA60。第二,可能进程映象尚未在内存,这时SLOAD尚未被置位,调度程度sched ( )按进程优先级从高到低将所有准备就绪进程调入内存。 (3) 睡眠状态 一个运行进程由于下列原因之一,放弃处理机而进入睡眠状态。 ① 进程使用系统调用wait ( )或sleep ( ),自身主动停止,以实现进程间的同步或进程自身定时间歇。 ② 进程在运行过程中要求使用某种系统资源,由于系统资源不足,不能立即满足其要求,它被迫进入睡眠状态,等待其它进程释放这类资源。 ③ 保护程序运行的临界区。例如,当进程A对进程通信文件pipe进行读写时,要对其它进程封锁。若此时有进程B企图使用pipe时,要迫使进程B进入睡眠状态。 ④ 等待I/O操作结束。 ⑤ 调度进程,即进程0在一次调度结束后进入睡眠状态。 进入睡眠状态的进程,系统按其睡眠的原因,赋予它当被唤醒时所具有的调度优先数分别有高低优先级睡眠状态SSLEEP和SWAIT。 (4) 停止状态SSTOP 这是一种特殊的睡眠状态,被用于跟踪机构,表明子进程已接收信号等待父进程对它进行跟踪处理。由stop ( )程序把其p-stat置成SSTOP。以后由setrun ( )程序将停止状态进程转入就绪状态。 (5) 等待善后处理状态SZOMB 一个进程用系统调用exit ( )自我终止,并未立即放弃proc结构,相应的user结构也暂留起来。这时p-stat被置成SZOMB状态,并唤醒其父进程对它进行善后处理。这是一个进程在消亡之前,等待父进程对其作某些处理的一个短暂状态。 ·P-flag进程标志 进程标志实际上是对进程状态的进一步说明,它只有一个字节,利用该字节中各位的0和1取值,表示进程的映像是否在内存、是否可交换到磁盘的对换区上去,等等。共有六个标志位(图13-5),一个进程的标志可以是这六种标志的组合: 图13-5 UNIX的进程标志字节 SLOAD:该位为1,表示本进程的映像全在内存中; SSYS:该位为1,表示本进程为进程0,应常驻内存; SLOCK:该位为1,表示本进程映像因某种原因不能调出内存; SSWAP:对换标志; STRC:该位为1,表示本进程已向父进程提出跟踪请求; STWED:该位为1,表示父进程已接受了进程跟踪的请求。 ·P-pri进程优先数 进程优先数表示一个进程的优先权,其值越小,则该进程的优先权越高。优先数的取值范围为-100~+127。 ·P-time 进程映像调入内存后,在内存的驻留时间,或进程映像调出到盘对换区后,在盘对换区的驻留时间。 ·P-addr 进程数据段的起始地址,也就是除正文段外,进程非常驻内存部分的起始地址(ppda)。 ·P-size 数据段的长度。 ·P-wchan 标明处于睡眠状态的进程的睡眠原因。 ·P-textp 是一个指向text结构指针,在那里存放着与该进程正文段有关的信息。 ·P-cpu 记录进程使用处理机的程序,值越大意味着进程使用处理机的程度越高。 2) 数据段 UNIX进程映像中的数据段由如下三部分顺序排列组成(图13-6): 图13-6 UNIX的数据段 USER数据结构的C语言表示如下: struct user { int u-rsav [2]; 保存r5, r6,用于程序的正常或非正常返回 int u-fsav [2]; 用于保留浮点寄存器 char u-segflg; I/O目的地址在用户空间或系统空间的标志 char u-error; 返回出错号码 char u-uid; 有效用户标识数 char u-ruid; 有效用户小组标识数 char u-rgid; 实际用户小组标识数 int u-procp; 指向本进程proc结构的指针 char * u-base; 指向读写缓冲存储器的起始地址 char * u-count; 用来存放读写字节数nbytes char * u-offset[2]; 在文件中进行读、写的起始位置 int * u-cdir; 当前工作目录文件的inode的指针 char u-dbuf [DIRSIZ]; 用于存放当前工作文件的路径名DIRSIZ=14 char * u-dirp; 指向当前目录文件名的指针,路径名最多14个字符 struct {构成文件的目录项,由文件所在节点的节点号ino和文件的路径 int u-ino; 名组成 char u-name [DIRSIZ]; } u-dent; int * u-pdir; 指向父目录项i节点的指针 int u-uisa [16]; 本进程用户空间地址寄存器样本,由16个组成 int u-uisd [16]; 本进程用户空间说明寄存器样本,由16个组成 int u-ofile [NOFILE]; 用户打开文件表,由指向系统打开文件表的表项指针 组成,NOFILE=15,共有15项 int u-arg [5]; 用于存放当前系统调用所带的参数 int u-tsize; 本进程的文本段的块数 int u-dsize; 本进程的数据段的块数 int u-ssize; 本进程用户栈的块数 int u-sep; 指令空间I(文本段)和数据空间D(数据段)是否分离的标志 int u-qsav [2]; 保存r5, r6 int ussav [2]; 保存r5, r6 int u-signal [NSIG]; 软中断处理程序入口表,NSIG=20,共20项 int u-utime; 本进程用户态运行时间 int u-stime; 本进程核心态运行时间 int u-cutime [2]; 子进程用户态运行时间之和 int u-cstime [2]; 子进程核心态运行时间之和 int * u-aro 中断保留区中保留R0的栈内单元的地址 int u-prof [4]; 用于保存程序直方图计算的参数 char u-intflg; 表示系统调用执行完成与否的标志 } u; ·进程系统数据区,通常称作ppda,它位于数据段的前面,进程proc结构中的P-addr指向这个区域的首址。该区共有1024个字节,由两块内容组成:最前面的289个字节为进程的扩充控制块user结构,剩下的734个字节为核心栈,当进程运行在核心态时,这里是它的工作区。 ·用户数据区,这时通常存放程序运行时用到的数据,如果进程运行的程序是非享的,那么这个程序也放于此地。 ·用户栈区,当进程运行在用户态时,这里是它的工作区。 对于数据段,做如下几点说明: 第一,在user结构里,总共包含35项内容,涉及到现场保护、内存管理、系统调用、文件管理、文件读写、时间信息、进程映像位置等方面。譬如,u-procp是指向本进程基本控制块proc的指针,于是u-procp与proc结构中的p-addr就形成了互相勾链。又譬如在进程的user结构里劈了两个数组,一个为uisa,一个为uisd,这们是该进程用户态虚地址空间八个页面的相对虚、实地址映照表。再譬如,在user结构里有一个整形数组u-ofile[15],共有15个元素,称作为该进程的打开文件表,进程每打开一个文件,就在此表里做一登记,所以每个进程可同时最多打开十五个文件。 第二,一个进程的proc结构是常驻内存的,当该进程运行时,进程映像的其余部分也都全部在内存。但当处理机转去运行别的进程时,为了节省内存,就可能会把该进程映像的绝大部分换出送到磁盘上去。由于进程运行的程序可能是共享的,这种共享正文段此时就不能交换出去。所以一个进程的数据段不能包含共享正文段,它是进程映像的非常驻内存部分。在UNIX里,进程映像的非常驻内存部分在内、外存之间的对换操作是经常进行的,磁盘上被开辟专门存放进程非常驻内存部分的那部分区域,在UNIX里称为盘对换区。 图13-7 UNIX进程映像的基本结构 第三,在实行内、外存之间的进程映像对换时,被对换的非常驻内存部分应是一个整体,即要调出则一起调出,要调入则一起调入,这样管理起来比较便利。 3) 共享正文段 这是可以被多个进程共享的可重入程序和常数,如果一个进程的程序是不被共享的,那么它的进程映像中就不出现这一部分。若一个进程有共享正文段,那么当把该进程的非常驻内存部分调入内存时,应该关注共享正文段是否也在内存,如果发现不在内存,则要将它 调入;当把该进程的非常驻内存部分调出内存时,同样要关注它的共享正文段目前被共享的情况,只要还有一个别的共享进程的映像全部在内存,那么这个共享正文段就不得调出去。 由于共享正文段在进程映像中的特殊性,为了便于对它们的管理,UNIX系统在内存中设置了一张正文段表。该表共有40个表目,每一个表目都是一个text结构,用来记录一个共享正文段的属性(位置、尺寸、共享的进程数等),有时也把这种结构称为正文段控制(信息)块。text结构的内容如下所列: x-daddr:正文段在盘对换区中的起址; x-caddr:已被装入内存的正文段在内存中的起址; x-size:正文段的长度; x-count:共享本正文段的所有进程数; x-ccount:共享本正文段、且进程映像全部在内存的进程数; x-iptr:指向正文段所有文件的;节点的指针。 要说明的是,如果一个进程有共享正文段,那么该共享正文段在正文段表里一定有一个text结构与之相对应,而在该进程的基本控制块proc里,p-textp就指向这一个text结构。 综上所述,在UNIX进程映像的三个组成部分中,proc、user和text这三个数据结构是最为重要的角色,图7反映了这三者之间的基本关系。 13.2.2 UNIX操作系统的进程调度 1、进程切换调度及其时机 UNIX系统中的进程0主要是由两个程序组成:一是进程切换调度程序swtch,一是进程映像传送程序sched。swtch使运行进程释放处理机,然后选择另一个进程占用处理机,所以UNIX中是通过进程切换调度程序swtch来实现对CPU的具体分配而完成进程调度任务的。 为了处理机分配给一个进程使用,swtch来查看整个proc表,在处于就绪状态(SRUN)且进程映像在内存(SLOAD)的进程里,挑选出一个优先数最小(即优先权最高)的进程来投入运行。如果找不到满足条件的进程,则进程0将处于等待中断状态,一旦发生中断请求,处理机响应并处理后,进程0就会再次去搜索proc表。这种作法,能使系统以最快的速度找到可运行的进程,提高CPU的利用率。 进程将在以下两类情况下进行切换调度: (1) 当前占用处理机的进程不再具备继续占用处理机的条件而自愿让出处理机。譬如它已完成了任务而进入SZOMB状态;它因等待某事件的发生而进入SSLEEP或SWAIT状态;它申请内存资源得不到满足,只能先将自己调出到盘对换区上,等内存空间够用时再调入,这时该进程仍然处于SRUN状态,但不带SLOAD标志;如此等等。 (2) 当系统中出现比运行进程更适宜占用处理机的进程时,强迫现运行进程让出处理机。譬如,由于某事件的出现使处于SSLEEP或SWAIT状态的进程变为SRUN状态,且优先权比现运行进程的高,这时就要强迫现运行进程让出处理机;又如中断发生时,现运行进程在用户态下工作,则中断处理结束后,要看一下是否有因中断而出现了具有更高优先权的进程,如果出现,就要强迫现运行进程(它被中断打断)让出处理机;如此等等。 2、进程切换调度算法 由上可知,UNIX在进程切换调度时使用的是优先数算法,即根据一个进程的优先数大小来确定它是否能获取处理机,优先数越小,获得处理机的权利越高。 UNIX中进程的优先数是动态变化的,确定一个进程优先数,有设置和计算两种方法。设置方法用于当一个进程从运行状态变为睡眠状态时,系统将根据进程不同的睡眠原因,赋予它们不同的优先数(存放在其proc结构里的p-pri里),这个优先数在进程唤醒后发挥作用。如果赋给的优先数为负值,则这是所谓的高优先权睡眠状态,这时进程proc中的p-stat被设置为SSLEEP;否则是所谓的低优先权睡眠状态,这时进程proc的p-stat被设置为SWAIT(参见图9-10)。计算方法是使用如下公式来算出进程的优先数p-pri: p-pri=min {127, (p-cpu/16+PUSER+P-nice)} 其中p-nice是一个用户可以通过系统调用命令来设置的量(记录在进程proc结构里),各种用户可按执行任务的紧急程度来给相应进程赋上不同的p-nice值;PUSER为常数100;P-CPU在进程的proc结构里已见过,它反映了进程使用处理机的程度。进程优先数p-pri在数值127和(p-cpu/16+PUSER+p-nice)两者中取其小的值。 这时最关键的是p-cpu,它按照如下办法来改变:系统时钟中断处理程序每20ms做一次,每做一次就将现运行进程的p-cpu加1。每到1秒时它就依次检查系统中所有进程的p-cpu一次,如果被查进程的p-cpu<10,这表时该进程在这一秒内所使用处理机的时间不超过200ms,于是把这个p-cpu置为0;如果被查进程的p-cpu>10,这表明该进程在这一秒内所使用处理机的时间超过200ms,于是把p-cpu减10。这种改变办法的效果是: (1) 如果一个进程连续占用处理机较长时间,那么它的p-cpu值就增大,从而计算出的p-pri也增大,于是优先数下降。当实行进程切换调度时,这种进程被调度到的可能性就减少。 (2) 如果一个进程长时间未被调用到或者虽频繁调度到,但每次占用的时间都小于200ms,那么p-cpu就较小甚至为0,从而计算出的p-pri也较小,于是优先权上升。当实行进程切换调度时,这种进程被调度到的可能性就增加。 这种称之谓“负反馈”的效果(见图13-8),就能使用户态下运行的进程能有较为均衡地获取CPU时间的权利。 图13-8 p-cpu变化带来的负反馈效果 UNIX系统计算优先数的时机有二:一是对所有优先数大于100的进程,系统每秒钟重新计算一次它的优先数;二是每次系统调用命令处理完毕之时,就重新对现进程的优先数进行计算。 3、进程映像的调入和调出 进程切换调度程序swtch选择运行进程的条件之一是该进程的映像要全部在内存。因此,把进程映像在内存和盘对换区之间进程调入和调出的传送工作,为切换调度创造了前提条件,也就为swcth提供了尽可能多的候选者。如前所提及的,这一调入、调出的传送任务是由进程0中的进程映像传送程序sched来完成。 1) 谁是调入者?当一个就绪进程的映像不在内存(即处于SRUN状态、但SLOAD示被设置)时,只有把它的进程映像调入内存才能让它真正参与对CPU的竞争。每个进程proc结构中的p-time记录了该进程调入内存或调出到磁盘对换区后所经历的时间,sched程序根据现在进程映像在盘对换区里的就绪进程的p-time,依照先长后短的原则逐一把它们的进程映像调入内存,直到内存没有跷的空闲区时为止。 2) 谁是调出者?如果sched程序实行调入时,内存根本无空闲可以接纳调入者,那么就应先实施调出某一个进程映像的非常驻内存部分,以腾出空间让更具运行条件的进程调入。 在UNIX中,有两种类型的进程映像不允许调出内存,一种是proc结构里p-flag标志字节中的SSYS被置位的进程,这是系统进程,整个系统中只有进程0才具备这一条件;另一种是标志字节中的SLOCK被置位的进程,它表示该进程因为某种原因而要求将其进程映像暂时保留在内存中。除这两类进程外,其余进程映像在内存的进程全是调出的候选者,其调出的先后顺序是: · 调出低优先权睡眠状态进程(SWAIT)和暂停状态进程(SSTOP); · 调出高优先权睡眠状态进程(SSLEEP); · 按在内存中驻留时间的长短,调出就绪状态进程(SRUN)。 13.2.3 UNIX操作系统的进程通信 UNIX操作系统为进程之间的通信提供了三种工具,这三种工具或使用的场合不同,谨、或完成的功能有异: (1) 基本通信工具sleep和wakeup,适用于解决操作系统中程序间的同步和互斥; (2) 利用“软中断”实现同一用户诸进程之间少量信息的传输和同步; (3) 文件系统提供的pipe工具,实现同一用户两个进程之间大量信息的传输。 1、基本通信工具sleep和wakeup 在UNIX操作系统的程序实现过程中,有很多的同步和互斥问题需要解决,这里没有使用惯常的信号量和P、V操作,而是根据不同情况做了不同的处理。不过在一旦判定需要转入睡眠去取得同步或互斥时,就统一地调用sleep程序,使调用进程进入睡眠状态,在睡眠原因消失以后,就统一地调用wakeup程序,以便将有关进程唤醒。 2、“软中断”——进程间的简单通信方式 在UNIX系统里,设置有系统调用命令Signal和kill,用于信号的设置和发送之用。 1) signal (sig, func)——信号设置 其功能是建立起信号sig与功能func之间的关系,即若收到了sig信号,则去执行由func指出的相应动作。 2) kill (pid, sig)——信号发送 其功能是将信号sig送给同一用户的一个或几个进程:如果pid为非0,则它就是接收信号的进程的标识数;如果pid为0,则要将信号sig送给所有进程。 当一个进程接获某一信号后,处理方式与硬中断非常类似,即它立即暂停自己正在执行的程序,转去执行该信号事先规定的信号处理程序func,完成后再返回原先正在执行的程序(图13-9),所不同的只是信号的设置、检查都是由软件实施的,所以被称作“软中断”。 图13-9 “软中断”处理示意图 进程运行时,会在下列地点或时刻检查是否收到了什么信号: (1) 在系统执行各类中断处理程序(除I/O中断)的末尾,检查被中断的进程是否接收到了信号。 (2) 现运行进程执行系统调用处理程序过程中,可能因为种种原因调用到sleep程序,在要进入低优先权睡眠状态之前和被唤醒之后,都要检查是否接收到了信号。 (3) 若时钟中断了在用户态下运行的进程,则每隔一秒钟,时钟中断处理程序就去检测现运行进程是否接到了信号。 3、pipe——进程间大量的信息通信 利用系统提供的系统调用命令pipe,在文件系统的支持下,就可以为同一用户的两个进程传送大量的信息。 通过系统调用命令pipe,可以建立起一个pipe通信机构,它由两个文件组成,一个在pipe机构中只能读,也就是说它是该通信机构中的信息接收端;另一个在pipe机构中只能写,也就是说它是该通信机构的信息发送端。这样,发布pipe命令的进程可以通过信息接收端进行写操作,而另一进程可以通过信息发送进行读操作。UNIX规定一次最多可以读/写4096个字节的信息。若写的内容超出这个数量,写进程就要在一次写4096字节后睡眠等待,在读进程取走这部分信息之后,唤醒写进程继续写。 13.2.4 UNIX中的进程控制 1、UNIX启动及进程树形成 由系统引导程序,把UNIX核心装到内存的最低部分,系统进入启动程序start。系统首先建立整个系统的调度进程proc [0],接着建立进程proc [1],由它通过系统调用“exec ( )”执行一个叫做“/etc/init”的文件,为每个终端生成一个子进程,系统等待用户在终端上注册。用户在终端上输入命令,每个命令都对应于一个可执行文件。Shell命令解释程序解释此命令,找到相应的可执行命令文件,用系统调用fork ( )创建子进程,由此子进程调用系统调用exec ( )执行此命令文件;父进程(即终端进程)处于等待状态,子进程执行文件结束,调用系统调用exit ( )自我终止,唤醒父进程,善后处理,并再次发出提示信号,准备接收下一命令。 此过程如图13-10所示。 图13-10 UNIX进程树形成过程 在这里要指出: · proc [0]是整个系统的调度进程,其功能是执行程序段sched ( )不断把外存中的准备就绪的进程调入内存。proc [0]一旦存在,就不会消亡,要么在执行调度功能,要么处于睡眠状态。 · proc [1]是整个系统中除proc [0]以外的所有进程的祖先。由它产生各终端子进程,子进程又可产生子进程,等等。形成一进程树。 2、进程控制 这一小节要讨论的是关于进程的创建,执行和自我终止的问题,它由三个基本的系统调用fork ( ), exec ( ), exit ( )来实现。 fork ( )的功能是创建一个子进程。所创建的子进程是fork ( )调用者进程(即父进程)的复制品,即它的进程映象,除了进程的标识数物进程特性有关的一些参数以外,与父进程相同。并与父进程共享文本段和打开的文件。当子进程要运行时,通过系统调用exec ( )按指定文件中的文本段和数据段取代自己的映象,并按指定的参数执行文本段,这相当于一个动态链接。因此exec ( )的功能是改变进程映象使之执行一个指定的(新的)目的文件。exit ( )系统调用的功能是进程的自我终止,释放资源并通知父进程。此时,它处于SZOMB状态,等待父进程作善后处理。父进程用系统调用wait ( )等待一个子进程的终止或进入暂停状态SSTOP。如果在使用wait ( )之前已经有一个子进程结束了,则对它作简短的善后处理:主要是释放子进程占用的proc结构,使之成为空闲项,把子进程的有关时间统计项u-utime, u-cutime, u-stime, u-cstime分别加到父进程的u-cutime, u-cstime上去。子进程在请求父进程跟踪时,处于暂停状态SSTOP。 fork ( ), exec ( ), wait ( ), exit ( )这一组系统调用,在Shell命令执行过程中使用的情况如图11-10所示。它们也可在一般程序中使用,其形式往往为: if (fork ( )) { 父进程程序实体 } else { 子进程程序实体 } 父进程程序实体一般包含有wait ( ) 以等待子进程结束,子进程一般以exit ( )自行结束。下面给出一个具体例子: / * Example for system calls fork, wait, exit*/ main ( ) { int i; int l; int j; if (fork ( )) { i =wait ( ); l =getpid ( ); printf (“ I AM PARENT PROCESS, ID num. %d\n”, l); printf (“The child process, ID number %d, is finished. \n”, i); } else { printf (“ I am child process. \n”); j =getpid ( ); printf (“ j =$d\n”, j); exit ( ); } } 其运行结果为 192 I am child process. j =122 I AM PARENT PROCESS, ID num. 121 The child process, ID number 122, is finished. 13.3 UNIX操作系统的存储管理 13.3.1 存储管理部件 PDP-13/40的字长为16位,因此可直接寻址的范围为64K字节。这就是说,如果没有任何硬件的支持,每个进程可用的最大程序地址空间为64K字节。然而处理机的总线寻址能力为18位,即内存的物理地址空间是256K字节。为了能把16位的字节地址映射到18位的字节地址上,硬件必须配置存储管理部件,它成为UNIX操作系统存储管理的基础。 1、虚地址 在存储管理部件的支持下,16位的字节地址不再被直接解释为物理地址,而被看作是一个虚地址,虚地址的组成如下图13-11所示。 图13-11 虚地址的形式 把64K字节的虚地址空间分成8页,每页分成128块,每块为64个字节,于是每页最多可达到8K字节。由于这里的页面大小是可变的,故可称为“可变长的页式管理”。要注意的是,为了充分利用内存空间,系统是按虚页实际的页长来分配内存空间的。 2、活动页寄存器(APR) 虚存页在内存中的实际位置,需要有一张映象表加以反映,这个映象表在PDP-11中是由两组活动页寄存器组成的,一组用于核心态,一组用于用户态。每组有8个32位长的寄存器,每一个又一分为二:一个是页地址寄存器(PAR),一个是页说明寄存器(PDR),如图13-12所示。 图13-12 活动页寄存器 1) 页地址寄存器(PAR) 16位的页地址寄存器如图9-18(a)所示。PDP-11中,把内存按64个字节为单位划分成块,共有4096=212块,编号为0~(212—1),内存分配以块为单位。 据此,每个PAR只使用其低12位,用来记录虚地址空间相应各页在内存的起始块号。 2) 页说明寄存器(PDR) 16位的页说明寄存器如图13-13(b)所示,在它里面记录着虚地址空间相应各页的存取权限、扩展方向以及该页的长度。 图13-13 活动页寄存器的组成 · PDR寄存器的第1、2位表明对相应页的存取权限; · PDR寄存器的第3位(ED)表明该页的扩展方向。当一个虚页实际长度小于128内存块时,由ED这一位说明该页空白部分在低地址一边还是在高地址一边。这样,如果该虚页要扩展,就可以确定向何方向延伸。具体地说,ED=0,表示空白部分在高地址一边,扩展时就向高地址方向延伸;ED=1,表示空白部分在低地址一边,扩展时就向低地址方向延伸。通常,程序占用页的ED=0,堆栈占用页的ED=1。 · PDR寄存器的第8~14位为该虚页的实际长度,用含多少块来表示。由于一虚页最长为128块,故由七个二进制位表示。 3、虚——实地址的转换 在得到一个形如图13-14所示的虚存空间地址之后,按照如下过程就可实现从虚地址到实地址的转换: 图13-14 虚地址到实地址的转换示意图 第一步,根据处理机状态字PS的第14、15位,确定处理机现在所处的运行状态,由此选择相应的活动页寄存器组; 第二步,由虚地址中的页号得到该组中相应的页地址寄存器(PAR)主页说明寄存器(PDR); 第三步,由页地址寄存器中的“本页在内存起始块号”和虚地址中的“页内块号”构成内存中的物理块号,并由页说明寄存器检查该块号的合法性(如是否越界等); 第四步,由物理块号和虚地址中的“位移量”构成这个虚地址所对应的物理地址。 13.3.2 UNIX进程映像的虚、实地址空间 UNIX的存储管理是建立在上述PDP-11存储管理部件基础上的,为此它为每一个进程提供两个虚存,一个是核心态下的虚存,它使用该态下的那组活动页寄存器来实现其虚、实地址的映射;一个是用户态下的虚存,它使用该态下的那组活动页寄存器来实现其虚、实地址的映射。这两个状态下的虚存,最大都是64K字节。 1、进程在核心态下的虚地址空间 在核心态下,进程运行的是操作系统程序,要使用核心栈区,要用到该进程的user结构,此态下的虚地址空间的组成如图13-15所示。 图13-15 UNIX进程的虚地址空间 其中,0~5页存放UNIX代码,这里面包括诸如常驻内存的进程proc结构;第7页存放与系统输入、输出相关的内容,称输入、输出页;第6页放现运行进程的ppda区,即user结构和核心栈,实际只用1024个字节。对所有进程来说,第0~5页和第7页共七页的内容都是相同的。 2、进程在用户态下的虚地址空间 在用户态下,进程运行时涉及共享正文段,数据区(里面包括非共享的程序),以及用户栈区,此态下的虚地址空间的安排原则为: (1) 由低往高安排先共享正文段,再数据区;由高往低放置栈区; (2) 以页为分配单位,不满一页按页找齐。如图14(a)所示,若共享正文段长度为2.5页,则在虚地址空间中占据0~2页;数据区为2.25页,则在虚地址空间中紧接共享正文段占据3~5页;栈区为0.5页,则占据虚地址空间的第7页,且向低地址方向延伸。这时虚空间的第6页为空白,未被占用。 3、进程映像在内存中的实际分布 虽然每个进程都有一个核心态的虚地址空间,但它们中的第0~5以及第7页的内容是完全一样的。此外,即使是每个进程的用户态虚拟空间,也不能按其分配情况原封不动地照搬到内存中去,否则会造成内存空间的巨大浪费。因此,各个进程映像在内存的真实分布情况是这样的(见图13-16); 图13-16 进程映像在内存中的实际分布 (1) 把核心态虚地址空间的0~5页装在内存0地址开始的低地址处,常驻内存; (2) 把核心态虚地址空间的第7页装在内存最高地址处的8K空间里,常驻内存; (3) 根据当前存储分配情况,以内存块为单位把共享正文段放在一个连续区内; (4) 根据当前存储分配情况,以内存块为单位把核心态虚地址空间中的1024字节的ppda内容、用户态虚地址空间中的数据区、以及用户栈区放在一个连续区内,构成进程映像的数据段。由于这恰是进程的非常驻内存进程映像,所以这种存放有利于它们对盘对换区的调入和调出; (5) 将进程映像全部在内存的 进程的共享正文段的起始块号送入相应text结构的x-caddr保存,将数据段的起始块号送入proc结构的p-addr保存。 (6) 如果现在调度到进程pa运行,则第一,要把该进程的ppda地址(在proc结构的p-addr里)送入核心态活动页寄存器组的APR6里;第二,根据共享正文段起址x-caddr、数据段起址p-addr、以及user里数组uisa和uisd记录的相对虚、实地址映照表内容,得到该进程用户态虚地址空间八个页的实际虚、实地址映照表,并送入用户态活动页寄存器的APR0~APR7保持。至此,就能保证该进程的正常运转了。 13.3.3 UNIX存储管理及分配释放策略 UNIX存储管理的对象为两类:一是内存中除了UNIX代码以及输入、输出页所占区域之外的部分;一是在磁盘上专门开辟的对换区。它们实际上都是为了存放进程映像非常驻内存部分的,因此对这两部分资源,UNIX采用了同样的分配和释放算法,差别只是在于对于内存,分配和释放以内存块大小为单位,每个内存块为64个字节;对于盘对换区,分配和释放以磁盘块为单位,每个磁盘块为512个字节。 1、可用存储区表 为了管理内存和盘对换区的可用存储区,UNIX系统设置了内存可用存储区表coremap和盘对换区可用存储区表swapmap,每个表均含有50个表目,每个表目由两部分内容组成: · m-size记录了一个空闲内存或磁盘可用区域的大小; · m-addr记录了一个空闲内存或磁盘可用区域的起始地址。 因此,每个表目反映了一个内存或磁盘可用区域的情况。 这两个表按照可用存储区起始地址由小到大进程登记,未使用的表目用m-size为0来标识成为空白项,它们显然都集中在表的后半部分。为了便于管理,表中最后一个表目项必须为空白项。 2、存储区的分配——malloc UNIX在接到一个存储申请时,采用最先适应策略来进行存储空闲区的分配。方法是从相应可用存储区表的第一个表目开始,用申请的尺寸与表目中的m-size比较,第一个大于或等于申请尺寸的m-size即为所求。在按单位分配后,要对可用存储区表进行调整,如果尺寸与所申请量恰好相等,则表中随后各表目都要随之上移,整个分配工作是由malloc程序实行和完成的。 3、存储区的释放——mfree 在对存储区进行释放时,先根据释放存储区的起始地址,在相应可用存储区表中找到它的插入位置,然后根据插入位置处的表目以及前一个表目的信息,判别被释放的存储区是否可以与它们合并成一个可用存储区。随之根据不同情况调整相应可用存储区表,以完成释放工作。整个释放工作是由mfree程序完成的。 这里要注意的是,存储区的分配和释放都要在相应的可用存储区表上进行操作。也就是说,两个可用存储区表coremap和swapmap是分配和释放的共享变量,所以在malloc和mfree两个程序里,涉及到对coremap和swapmap操作的那部分程序段就是临界段,这两部分的执行必须要保证互斥。 13.4 UNIX操作系统的文件管理 13.4.1 UNIX文件系统的基本工作原理 1、UNIX文件的逻辑结构及分类 从总体上看,文件的逻辑结构可以分成两种形式,一是记录式文件,一是无结构的流式文件。记录式文件是一种有结构的文件,这种文件由记录组成,每个记录又包含若干数据项,它们彼此之间具有一定的关系。如果文件中每个记录的长度都相等,则称为定长记录文件,否则称为变长记录文件。所谓无结构的流式文件,即是把文件视为一个无内部结构的字符流。UNIX系统中文件的逻辑结构就是采用这种形式,并把它们分为: · 一般文件:这是通常意义下的磁盘文件,系统总把它视为无结构、无记录概念的字符流,文件的长度可以动态地增长。作为用户,可以根据自己的需要,在处理过程中把它们构造成有结构的文件。系统软件、用户自己编写的各种原程序以及可以执行的二进制目标程序等都属此列。 · 目录文件:由文件的目录组成的文件称为目录文件。一般来讲,文件的目录应该包括:文件名、文件长度、文件在存储设备上的物理位置、文件类型、存取权限、管理信息等内容。在UNIX里,为了加快文件目录的搜索速度,便于实施文件共享,而把这些内容划分为两部分:一部分称为该文件的文件控制块(或索引节点)inode,它包含了文件的长度、物理位置、文件组、文件类型、存取权限、共享信息、管理住处等内容;另一部分仍称为该文件的目录,它只含文件名以及相应inode节点的编号(见图13-17)。因此,UNIX的目录文件虽也是由文件的目录组成,但相比之下要比通常所说的目录文件简单许多。 · 特殊文件:在UNIX系统中,把块存储设备(如磁盘)和字符设备(如终端机)都视为文件。但为了与前两类文件有区别,故统称它们为特殊文件。考虑到检查和处理方便起见,系统把所有特殊文件都放在名为“dev”的目录文件中。例如,行式打印机特别文件可写成/dev/lp,终端机特别文件可写成/dev/con (con=console)。 图13-17 UNIX文件目录的 格式 pdf格式笔记格式下载页码格式下载公文格式下载简报格式下载 2、基本文件系统及可装卸的子文件系统 UNIX文件系统可分成基本文件系统和可装卸的子文件系统两部分(见图13-18)。 图13-18 UNIX文件系统的基本结构 1) 基本文件系统 基本文件系统固定在根存储设备上,是整个文件系统的基础。通常把硬盘做为根存储设备,系统一旦启动运行,基本文件系统就不能脱卸。 2) 可装卸的子文件系统 存储在可装卸存储介质(如软盘)上的文件系统为可装卸的子文件系统,它可以随时更换。每个用户都可以把自己的文件存放在软盘上,使用时插入软件驱动器,然后通过系统调用命令将其与基本文件系统勾连在一起,也可以用系统调用命令使子文件系统与基本文件系统脱勾。 UNIX文件系统的这样一种结构,使其使用灵活,便于扩充和更改。 3) 文件系统的目录结构 在UNIX文件系统里,基本文件系统和子文件系统都独立采用树型带勾连的目录结构。所谓树型,即它们各自都有一个根目录文件,在根目录文件中所列的文件,可以是一个目录文件,也可以是一个一般文件或特殊文件。这样一层层地发展下去,就形成了一个通常意义下的树型文件目录结构。在这种结构下,叶节点为一般文件或特殊文件,中间节点为目录文件。图13-19是UNIX文件系统的目录结构图,我们以方框代表目录文件,圆圈代表一般文件或特殊文件。 图13-19 UNIX文件系统的目录结构 目录文件中的每一个文件都有一个文件名,最大长度为14个字符(即14个字节)。 UNIX在树型结构的基础上增加交叉连接部分,以达到文件共享的目的。在UNIX系统中,是通过文件的inode节点来实现文件共享勾连的,并且只允许勾连到代表一般文件的叶节点上去。由图13-19可知,wang和lee共享文件f2。 还要注意的是,在基本文件系统和子文件系统之间不能使用这种方式来共享文件。 13.4.2 UNIX文件系统的数据结构综述 在UNIX文件系统的实施过程中,涉及到多种数据结构。有一类数据结构用于对文件的静态管理,因此都分布在文件所在的存储设备上,它们包括外存文件控制块inod、目录,以及存储资源管理信息块filsys三种;另一类数据结构用于文件打开时的管理,因此都出现在内存,它们包括内存文件控制块inode、打开文件控制块file、以及进程打开文件表三种。下表分别简介之。 1、外存文件控制块inode 由前知,文件存储设备上的每一个文件,都有一个文件控制块inode与之对应,这些inode被集中放在文件存储设备上的inode区。文件控制块inode对于文件的作用,犹如进程控制块proc、user对于每个进程的作用,这集中了这个文件的属性及有关信息,找到了inode,就获得了它所对应的文件的一切必要信息。 每一个inode结构理用32个字节,共九项内容,反映出一个文件的如下信息:文件长度及在存储设备上的物理位置、文件主的各种标识、文件类型、存取权限、文件勾连数、文件访问和修改时间、以及inode节点是否空闲。下面给出与我们讲述有关的三项。 (1) i-mode 文件的各种属性 用各位来表示文件的不同属性,譬如此索引节点是否被占用,此索引节点对应的是何种类型的文件,以及对该文件的存取权限等。 (2) i-nlink 文件勾连数 这项表明在文件目录结构中,该文件具有的路径名个数。由于在UNIX中,只允许对叶节点产生交叉连接,因此一个文件系统从根出发到任何中间节点(目录文件),只能存在一条路径,但从根出发到叶节点则可能出现多条路径。于是,对应于某个一般文件的inode中的i-nlink,反映了对该文件的共享情况。 (3) i-addr[8] 存放文件所在物理块号的基本索引表 UNIX文件的物理结构,根据文件规模的不同而采取索引结构或多级索引结构。i-addr[8]是一个有八个元素的数组,它构成了文件逻辑块号和物理块号的基本对照表。如果文件规模超出八个物理块,则可在它的基础上扩展成一级间接、二级间接的索引表,详情见后面关于UNIX文件物理 结构部分的讲述。 2、目录和目录文件 1) 目录 UNIX中的每个文件都有一个目录项,其格式如图9-22所示,目录项中记录了文件的名字以及该文件对应的外存inode的编号。文件名是一个文件的外部标识,而这个文件的外存inode编号,则是它的内部标识。可以看出,文件的目录项建立起了文件内、外部标识之间的对应关系:根据文件名找到它的目录项,由目录项的外存inode编号找到文件控制块inode,从而获得该文件的信息。 2) 目录文件 UNIX视每张目录表为一目录文件。作为一个文件,它有自己的名字以及对应的外存inode。要注意的是,每个文件系统(基本的或子文件系统)都有一个根目录文件,它的外存inode总是放于文件存储设备上inode区中的第一个,于是保证很容易从它出发,到达树型目录结构上的任一节点。另外还要注意的是,由于每一个目录项需要16个字节的存储空间,每个盘块的容量为512字节,因此存放目录文件的盘块中,每一盘块可以存放32个文件的目录。有了这些,UNIX文件目录的树型结构可以细化成如图13-20所示。 图13-20 UNIX树型目录结构的细化 3) 文件目录中的勾连 为了实现文件共享,UNIX允许对一般文件节点实行交叉连接,这称为勾连。它是通过在同一文件系统中的两个不同目录项里填入同一个外存inode节点编号来实现的,图9-25中的虚线处反映的正是这种勾连。譬如说,一般文件的原有路径名为/a31,则在目录文件中就应有一目录项,其文件名为a31,对应的外存inode编号为n。如果再给它另起一个路径名/a0/bx31,则在目录文件a0中应该新设置一个目录项,它的文件名部分填入bx31,它的外存inode编号仍填为n。另外,在编号为n的外存inode里,将i-nlink的值加1。于是文件系统中就有两个目录项同时指向这一个inode,实现了对文件a31的不同名共享。 3、存储资源管理信息块 文件系统所在存储设备上的存储资源,主要有两个用途,一是用来存放外存文件控制块inode,一是用来存放文件信息或扩展的地址索引表(当文件是大型的巨型的时,就需要这样做)。为了能对盘块的使用情况加以管理,UNIX将这些管理信息集中放在一个数据结构——存储资源管理信息块filsys中。filsys总是固定在1#盘块上,这一盘块通常称作该文件系统的管理块。这样,整个磁盘空间的安排就如图13-21所示。 图13-21 文件系统磁盘存储区的分布图 要注意的是,为了使用方便,基本文件系统以及与基本文件系统装连上的子文件系统都要申请一个缓冲存储区,将自己的管理块filsys复制到里面,以便内存中有它们的副本。 由前知,每一个文件的inode节点占用32个字节,因此每一个inode块包含16个文件控制块。这些inode顺序编号,一个文件占用了某inode,则其编号就成为这个文件的内部标识,第1号inode是专门用于根目录文件的。 数据结构filsys共有12项内容,下面给出与我们讲述有关的六项。 (1) s-isize inode区占用的盘块数; (2) s-fsize 盘块总数; (3) s-nfree 直接管理(也就是s-free[100]指向)的空闲块数; (4) s-free[100] 空闲块索引表 (5) s-ninode 直接管理的空闲inode节点数; (6) s-sinode[100] 空闲inode节点索引表。 至于如何通过filsys来对空闲inode和空闲盘进行具体管理,详情见后面关于UNIX文件系统资源管理综述部分。 4、内存文件控制块inode和内存inode表 外存inode记录了一个文件的属性和有关信息。可以想象,在对某一文件的访问过程中,会频繁地涉及到它,于是它就要不断来回于内、外存之间,这当然是极不经济的。为此,UNIX在系统占用的内存区里开辟了一张表——内存inode表(或活动文件控制块表、活动索引节点表),该表共有100个表目,每个表目称为一个内存文件控制块inode,当需要使用某文件的信息,而在内存inode表中找不到其相应的inode时,就申请一个内存inode,把外存inode的大部分内存拷贝到这个内存inode中,随之就使用这个内存inode来控制磁盘上的文件。在最后一个用户关闭此文件后,内存inode的内容被写到外存inode,然后释放以供它用。 内存inode的结构基本上与外存inode相同,仅略有增减。增加的有关项目有: · i-count:内存inode访问计数。若为0,表示此节点为空闲,某文件被打开时,其内存inode里的此项就加1。只有所有用户都关闭了此文件,以使i-count为0后,这个文件才被真正关闭。 · i-number:与此内存inode相对应的外存inode编号。 5、打开文件控制块file和file表 一个文件可以被同一进程或不同进程、用同一路径名不同路径名、具有同一特性或不同特性(读、写、执行)同时打开。做为内存inode,基本上只包含文件的静态信息(如文件的物理结构、勾连情况、存取权限等),没有记录下一个文件被打开时的动态信息。为此,UNIX又在系统占用的内存区里开辟一张表)——打开文件控制块表,简称file表,共有100个表目,每一个表目称做一个打开文件控制块file。主要内容有 · f-flag打开文件的特性,指明对文件的读、写要求 · f-count共享该file的进程数 · f-inode指向对应于此打开文件的内存inode 当打开一个文件时,都要在该表里申请分配一个file,形成该打开文件的控制块。 6、进程打开文件表 在进程的user结构里,设有一个整型数组u-ofile[15],它被称为该进程的打开文件表。在进程打开一个文件时,分配到一个打开文件控制块file,就把这个控制块的地址填入u-ofile[ ]的一个元素里。于是,这个打开文件就和它在u-ofile[ ]中占据的位置形成一个对应关系,这个位置就是打开文件号。图13-22给出了UNIX文件系统中各数据结构之间的关系。 图13-22 UNIX文件系统各数据结构间的关系 当某进程欲打开一个文件时,首先在自己user结构的打开文件表里申请一个打开文件号fd(即找到一个u-ofile[ ]中的空表目),随之在系统打开文件控制块表里申请一个file结构,且将其位置fp填入u-ofile表目中。然后在内存inode表里找到该文件的内存inode,或申请一个内存inode,并将位置信息填入相应file中的f-inode, i-count加1。这样,如果该文件的内存inode原先就在内存inode表中,那么现在这个进程则又一次通过同一路径名不同路径名将它打开。由于它们都有自己的file结构,所以它们对这个文件可以持有不同的操作要求,拥有各自的读、写指针,从而形成了通过不同的file结构,使用一个内存inode的共享形式。这种共享在file结构里的f-count都为1,但因大家都指向同一个内存inode,故内存inode里面的i-count则大于1(有几个file结构指向它,它的i-count就为几),图13-23描述了这一情形。 图13-23 不同进程共享打开文件示意图 共享打开文件的另一种情形是由父进程创建子进程引起的。在UNIX中,当父进程创建一个子进程时,先要继承父进程的u-ofile表的全部内容。这样它和父进程使用同一个file结构,对这个文件有相同的操作要求和读写指针。所以,这种共享打开文件表现为通过共享同一个file结构来体现,图13-24描述了这一情形。由此可知,在UNIX里提供了两种文件共享的方式,第一种是在目录结构里通过勾连,对同一文件提供不同路径名,以达到能够异名共享的目的;第二种是在打开文件结构里,通过共享同一个file结构或共享同一个内存inode而实现对打开文件的共享。 图13-24 父、子进程共享打开文件示意图 13.4.3 UNIX文件的物理结构 前面已经提及,UNIX文件采用的是索引式物理结构,并且也知道有关文件物理位置的索引信息是由该文件外存inode中的八个数组元素i-addr[8]给出的。然而UNIX在具体的实施中,并没有受一般索引结构的限制,它把自己的文件根据长度的不同分为小型文件、大型文件、以及巨型文件三种,分别通过一级索引、二级索引和三级索引来加以实现。 1、小型文件的索引结构 文件存储设备以盘块为单位进行存取,每块512个字节。当文件长度在1~8个盘块之间时,称为小型文件,数组i-addr[ ]就是通常意义下的地址索引表,它里面的内容就是文件在盘中的物理块号。因此,小型文件是通过i-addr[ ]的一级索引而找一盘文件的。 2、大型文件的索引结构 当文件长度在9~7(256个盘块之间时,称为大型文件,此时的数组i-addr只使用七个元素i-addr[0]~ i-addr[6],形成一个间接索引表,每一个指向一个盘块,它们才是真正的地址索引表。由于一个盘块包含256个字,所以通过这七盘块的索引,可最多得到7(256个盘块。由此可见,大型文件是通过二级索引而找到盘文件的。 3、巨型文件的索引结构 当文件长度在(7(256+1)~(7(256+256(256)个盘块之间时,称为巨型文件,这时i-addr前七个元素的作用不变,而把i-addr[7]用来进行扩充。即把它指向的盘块作为间接索引表,再指向的256个盘块才形成真正的地址索引表,所以巨型文件有一部分是通过三级索引而找到盘文件的。 13.4.4 UNIX文件系统的资源管理综述 为了实施文件系统,需要涉及众多的资源。综前述,这些资源有如下几种:系统打开文件控制块表(file)、系统内存inode表,用户打开文件表u-ofile、外存inode区、以及外存一般存储块区。对于前三种资源的管理比较简单,都是采用线性搜索分配法,这里着重介绍后两种资源的管理方法。有关这两种资源的管理信息,都集中放在文件存储设备的存储资源管理信息块filsys中(它总是固定存放在1#盘块内,并且在内存中都有各自的副本)。 1、外存inode区的管理 在filsys里,s-isize记录了inode区所占用的盘块数。由于每个inode占用32个字节,因此可以得知在该inode区里共有多少个inode节点。由于存储设备上创建一个文件就需要有一个inode节点与之对应,删除一个文件时,它占用的inode节点就被系统收回,所以inode区中空闲inode的数量是动态变化的。 系统按照如下规定来实现对空闲inode的管理: (1) 在filsys里,开辟一个空闲inode索引表:s-inode[100]。它是一个具有100个元素的数里,每个元素可指向一个空闲inode,这里系统直接管理的空闲inode。至于当前该数组里究竟含有多少个空闲inode,则由filsys里的s-ninode加以记录。 (2) 把s-inode[ ]视为一个栈来使用。按照C语言的约定,数组下标总是从0开始,所以s-nnode的值恰好是一个可以使用的索引表目的下标。当需要分配inode 时,如果s-ninode不为0,则将s-inode[--s-ninode]里指示的inode 节点分配出去;如果释放回一个inode 节点,则把该节点指针送入s-inode[s-ninode++]中。 (3) 如果s-inode[ ]已无直接管理的空闲区了(s-ninode=0),则搜索inode 区,将找到的空闲inode 依次登入,直至表满或搜索完整个inode 区。如果s-inode[ ]已经直接管理了100个空闲inode,则对再释放的inode不作任何处理,让这个空闲的inode散布在inode区里。 2、外存一般存储块区的管理 由上述可知,系统对文件存储设备上inode区里的空闲inode,通过s-ninode和s-inode[100]只直接管理最多100个空闲inode,置其它空闲inode而暂时不顾。 在filsys里,对一般存储块区也开辟了两个项目: ·s-free[100] 空闲块索引表 ·s-nfree 直接管理的空闲块数目 形式上,它们与s-inode[100]、s-ninode相似,但实际上却采用了不同的管理方法——分组链接法。 1) “分组链接”法的基本思想 把一般存储块区里的空闲块归成若干组,第一组为99个空闲块,从第二组起均为100个空闲块,最后一组可能会不足100个空闲块。为了进行链接,规定每个后组的第一个空闲块的开头101个字用来记录其前组的空闲块数目以及每一空闲块的位置。由于最后一组不可能还有后组,故将最后一组的有关信息存放在管理块filsys的s-nfree和s-free[100]中。于是就形成了如图13-25所示的空闲块分组链式索引(那里假定最后一组共有52个空闲块)。 图13-25 文件存储设备空闲块分组链式索引 2) 空闲块的分配 分配总是在filsys中的空闲块索引表s-free[ ]中进行,并把它视为一个栈结构来使用。如前面空闲inode管理中所说,这里的f-nfree之值也恰是下个可以使用的索引表目的下标。因此,当s-nfree不为0时,总是把s-free[--s-nfree]里指示的空闲块分配出去。这里要注意的是,如果在——s-nfree操作后s-nfree为0,则表明是把s-free[0]中指示的当前这组的最后一个空闲盘块分配出去,而这一盘块里却存放有它前面一组空闲块的信息。因此在把它分配出去之前,应该先把它前面的101个字中的内容移入s-nfree和索引表s-free[100]中。 另外还要注意的是,第二组的n-free=99, free[0]=0。这两个值是判定空闲块全部分配完的重要信息。设想现在已把第三组及其后面各组管理的空闲块都分配出去了,于是filsys中的s-nfree=99, s-free[0]=0, s-free[1]~s-free[99]将指向第一组的99个空闲块。若把这99个空闲块也全都分配出去了,那么此时s-nfree=1, s-free[0]=0。这时如果再提出空闲块的申请,按上面的办法去做时,s-nfree减1为0,于是可以发现s-ree[0]=0。这就表明已到空闲块的分组链尾,没有空闲块可分配了。 3) 空闲块的释放 释放时的动作总是把释放的空闲块位置登入索引表的s-free[s-nfree++]表目中。这里要注意的是,如果在登入前发现索引表已满,那么意味着在此前已收集到了100个空闲块,可以形成一个新的链组。而现在释放的空闲块是下一组的开始,因此应先把filsys里s-nfree的值和索引表s-free[100]中的内容复写到新释放的这一空闲块的前101个字中,然后将此释放块的地址填入filsys的s-free[0]中,置s-nfree=1。 13.4.5 子文件系统的装卸 在UNIX中,每个用户都可以建立自己的子文件系统(或称文件卷),并可以把它装配到基本文件系统上去而融为一棵更大的树。不用时,也可以把装配上去的文件卷完整地拆卸下来,因此UNIX的文件系统显得十分灵活。 子文件系统与基本文件系统的这种装配连接,是通过所谓的装配块来进行的,它在这两者之间架起了联系的桥梁。一旦完成了这种装配连接,装配块所起的作用对用户而言是完全透明的。 1、装配块及装配块表 装配块是系统专为子文件系统装配到基本文件系统上去而开设的数据结构,它共有三项内容: (1) m-dev 子文件系统所在存储设备号; (2) m-bufp 指向子文件系统管理块filsys在内存的副本; (3) m-inodp 指向基本文件系统中对应于子文件系统根目录文件的内存inode。 这样的装配块总共有五个,形成了装配块表mount[5]: mount[0]~mount[4]。在系统初启时,总是把mount[0]用于基本文件系统,将根设备的信息登在其中,也就是说,mount[0]中的m-dev记录根设备号,m-bufp指向根设备上1#盘块中管理块filsys在内存的副本,m-inodp指向与根设备上inode区的第一个inode相对应的内存inode。余下的四个装配块mount[1]~mount[5]可用于子文件系统的装配,所以UNIX最多可以允许同时装配四个文件卷于基本文件系统之上。 2、子文件系统的装配 子文件系统通过装配块而与基本文件系统连接,可以划分为两部分:一是装配块与基本文件系统的连接,一是装配块是子文件系统的连接。 1) 与基本文件系统的连接 对于一个文件系统而言,最关键的是得到它的根目录文件,因为从它出发,就可以得到该文件系统的全部目录结构。因此,要把一个子文件系统装配到基本文件系统上去,最重要的是在基本文件系统连接处的目录文件中设置一个目录项,该目录项对应于与其装接的子文件系统名,并指向一个inode,这个inode对应于一个内存inode。把内存inode的i-flag设置成“已装配”标志,说明它是用来连接某个子文件的。然后在装配块表中获得一个装配体,使其m-inodp指向这个内存inode。这样,装配块就与基本文件系统连接上了。 2) 与子文件系统的连接 把子文件系统的管理块filsys复制到某个缓冲存储区中,用装配块里的m-bufp指向它,另外把子文件系统所在设备号填入到m-dev,这样就实现了装配块与子文件系统的连接。 5、文件搜索过程 若给出了某个文件的路径全名(即从基本文件系统根目录出发),开始的搜索一切如常。只是当遇到该子文件系统名对应的内存inode时,发现它的i-flag标志有“已装配”信息,表明它是用来与苛子系统装接的,于是转到装配块表,去从中寻找出哪一个装配块的m-inodp也是指向这个内存inode。找到的装配块正是用来与子文件系统连接的那块。根据装配块中的m-dev可找到子文件系统所在的设备,从而获得其根目录文件的inode。这样,由这个inode出发就可得到子文件系统所在的设备,从而获得其根目录文件的inode。这样,由这个inode出发就可得到子文件系统的全部目录结构。通过使用由m-bufp指出的filsys,可以对该子文件系统的物理资源加以管理。图13-26给出了示意图 图13-26 利用装配块连接卷1的示意图 13.4.6 文件的共享 所谓文件共享,就是不同用户或不同进程共同使用同一个文件,文件共享有时不仅为不同用户完成共同的任务所必须,而且还可以节省大量的外存空间,减少由于文件复制而增加的访问外存次数。 文件共享可以有多种形式。在UNIX系统中,允许多个用户静态地共享,或动态地共享同一个文件,下面就介绍UNIX系统的这两种共享文件方式。 1、文件的静态共享 在UNIX系统中,两个或多个用户可以通过对文件连接达到共享一个文件的目的。比如图7.10中,user 下的sun共享liu的文件d2。共享时,可以用原名,也可以换另外的名字,如D2,来访问同一文件。由于这种共享关系不管用户是否正在使用系统,其文件的连接关系,都是存在的,因此,我们称它为静态共享。用文件连接来代替文件的复制可以提高文件资源的利用率,节省文件的物理存储空间。 为了实现文件的连接,只要把不同目录的索引节点编号,指定为同一文件的索引节点即可。但为了反映共享同一文件的用户数,在每个索引节点中设立了一个变量i_link。当文件第一次创建时,i_nlink等于“1”。以后,每次连接时就把i_nlink加“1”。当用户每次删除文件(对于文件主)或解除连接(对其它用户)时,就把该计数值减“1”,直到发现其结果为零时,才释放文件的物理存储空间,从而真正删除这个文件。 2、文件的动态共享 所谓文件的动态共享,就是系统中不同的用户进程或同一用户的不同进程并发地访问同一文件。这种共享关系只有当用户进程存在时才可能存在,一旦用户的进程消亡,其共享关系也就自动消失。 在UNIX系统中,文件打开以后,对应的索引节点就在活动索引节点表中,活动索引节点表是整个系统公用的。为了使用户掌握他们当前使用文件的情况,系统在各个进程的user结构中设立了一个用户打开文件表,并通过它与各自打开文件的活动索引节点相联系。 由于UNIX系统中的文件是无结构的字符流序列,因此文件的每次读写都要由一个读/写位移指针指出要读写的位置。现在的问题是:若一个文件可以为多个进程所共享,那么应让多个进程共用同一个读/写位移,还是应让各个进程具有各自的读写位移呢?下面分两种情况进行讨论。 在UNIX系统中,进程可以动态地创建,被创建的进程完全继承了父进程的一切资源,包括user结构中用户打开文件表中指出的打开文件。通常同一用户的父、子进程往往要协同完成同一任务,若使用同一读/写位移,那么一个进程改变它时,另一个进程能够感觉到它的变化。这样,使父、子进程更容易同步地对文件进行操作。因此,该位移指针宜放在相应文件的活动索引节点中。此时,当用系统调用fork建立子进程时,父进程的user结构被复制到子进程的user结构之中,使两个进程的打开文件表同指向同一活动的索引节点,达到共享同一位移指针的目的。 另一方面,若一个文件为两个以上的用户所共享,必然是每个用户都希望能独立地读、写这个文件,彼此不相干扰。显然,这时不能只设置一个读写位移指针,而必须为每个用户进程分别设置一个读、写位移指针。因此,位移指针应放在每个进程用户打开文件表的表目中。这样,当一个进程读、写文件,并修改位移指针时,另一个进程的位移指针不会随之改变,从而使两个进程能独立地访问同一文件。 可见,在上述两种动态共享方式中,对读写位移指针的设置位置和数目是不同的。 为了解决这一矛盾,同时满足这两种共享要求,系统又设置了一个“系统打开文件表”。该表共有100个表目,为整个系统所公用。在该表的每个表目中,除了含有读写位移指针f_offset之外,还有文件的访问计数f_count,读写标志f_flag和指向对应文件的活动索引节点指针f_inode,其中f_count指出使用同一系统打开文件表目的进程数目,它是系统打开文件表目资源能否释放的标志。f_flag指出打开文件是为了读还是写,它为进行访问权限验证之用,这一点以后还要进一步介绍。用户打开文件表、系统打开文件表和活动索引节点表之间的关系如图13-27所示。 图13-27 用户打开文件表、系统打开文件表和活动索引节点表之间的关系 图中,进程A和它的子进程通过同一个系统打开文件表表目共享文件d2,因而其f_count为“2”。而进程B自己独立使用一个系统打开文件表表目,因而其f_count为“1”,但它却通过同一个活动索引节点与进程A及其子进程共享文件d2。图中用户打开文件表表目的序号称为文件描述字,以后进程就是通过文件描述字对相应的文件进行读、写操作。下面看一下系统打开文件表目的申请和释放过程。 当一个进程要求打开一个文件时,系统首先要为它申请一个系统打开文件表表目,并建立该表目与相应文件活动索引节点的联系,然后把用户打开文件表表目中的一个空闲项指向它。如果在这之后,用户进程通过系统调用fork建立一个子进程,系统自动把用户打开文件表目所指的系统打开文件表目中f_count加“1”。反之,当一个进程关闭一个文件时,系统不能简单地释放系统打开文件表目,而必须首先判断f_count的值是否大于1。如果大于1,说明还有进程共享相应的系统打开文件表目,此时只须把f=count减1即可。由上述过程可知,进程之间到底采用哪一种方式动态地共享同一文件,主要是由执行“fork”和“打开”系统调用而打开同一名字的文件,则系统分别为它所分配不同的系统打开文件表目,并指向同一活动索引节点,这时,这两个进程独立地使用各自的读、写位移量指针。但如果进程在打开一个文件之后,又用fork系统调用建立了一个子进程,由于子进程的用户打开文件表表目是由父进程复制得到的,那么它自然会指向父进程使用的系统打开文件表表目,因此这两个进程就共用同一个位移量指针来共享一个文件。 13.4.7 文件卷的动态安装 在一般的文件系统中,总的文件存储空间是不能动态变化的。而在UNIX系统中,则可以动态地装卸文件卷。 在系统初启时,系统中只有一个安装的文件卷,即所谓根文件卷,其上的文件是保证系统正常运行的最小文件集。如汇编语言和C语言编译程序、文件卷构造程序、终端命令解释程序等等。这个根文件卷在系统运行过程中是不可卸下的,所以它是系统的基本部分。 图28 文件卷的安装 其它的文件卷可以根据需要,作为子系统动态地安装到一个“已存”文件卷的某一个节点上。通常,这个节点是为文件卷安装而特地创建的一个空目录。而“已存”文件卷是指根文件卷,或者是已安装的别的文件卷。因为每个文件卷本身有一个完整独立的目录树结构,所以文件卷的安装就如同树的“嫁接”一样,使安装的文件卷目录树结构与原有的文件卷树形结构融为一体。这一过程如图28所示。 当然,也可以把“安装”上去的文件卷整个地“拆卸”下来,从而恢复“安装”前的状态。 文件卷的装卸不同于存储介质(盘、带)的装卸,因为文件卷卸是从逻辑意义上说的,与文件卷所在的存储介质本身的装卸有本质的不同。即使物理介质本身在工作,但若其上的文件卷没有安装好,系统也无法存取其中的信息。 一旦文件卷安装好,就要把这个文件卷的专用块复制到内存专用块中。以后,当文件操作要进行资源分配和释放时,就要使用这个内存区。因此,即使多个文件卷组成了一个统一的树型结构,但分配和释放资源仍然是各自独立进行的。 文件卷的这种动态装卸有以下优点: (1) 可以随时扩充文件系统的内存空间。例如用户又买了一个磁盘时,可以把它构造之后安装到文件树结构中。 (2) 可以加强对文件的保护。为了防止一个文件卷上的信息受到有意或无意的破坏,可以把暂时不用的文件卷卸下,使用户不能访问其上的信息。 13.4.8 UNIX文件操作的系统调用 到目前为止,我们已经介绍了文件的组织管理及文件系统的结构。在本节中,我们通过UNIX系统的使用,来进一步加深理解前述的概念和技术。这些系统调用主要包括文件的打开和关闭,文件的创建和删除,文件的连接和解除连接,文件的读和写,以及文件的随机访问。用户可以通过文件系统提供的系统调用在其程序中对文件进行上述操作。另外,有关文件操作的实用程序,实际上也是通过上述系统调用来实现的。 1、文件的创建和删除 当文件还不存在时,即文件还未在文件卷中登记时,需要创建;或者文件原来已经存在,有时需要重新创建。注意这同文件的打开是完全不同概念。文件打开是指当文件已经存在,需要使用时先执行打开,以便建立用户进程与文件的联系。相应地,文件不再有人需要,则要删除之,以便腾出存储空间。这同文件“关闭”也是完全不同的概念。文件关闭是指用户暂时不使用文件时,通过关闭操作切断用户进程与文件的联系。 1)文件的创建 文件创建首先是要求文件系统为新的文件建立一个新目录项和相应的索引节点,以便随后的写操作为这个新文件输入信息。该系统调用的C语言格式为: int fd, mode; char * filenamep; fd = creat (filenamep, mode); 其中参数filenamep是指向要创建的文件路径名字符串指针;参数mode是文件创建者提出的该文件应该具有的存取权限。在文件成功地创建之后,这个权限就记录在相应索引节点的i_mode之中。变量fd中的内容是当创建成功之后,系统返回给用户的文件描述字,即用户打开文件表项的编号。由此可见,creat兼有文件的“打开”功能,于是随后的文件写系统调用,就可使用这个fd进行操作。 例如,用户文件的路径名是/usr/lib/d2,则用户可用如下的C语言程序调用creat: char * dp; int fdlib, fmode; de = “/usr/lib/d2”; fmode = 0775; fdlib = creat (dp, fmode); 或用更简单的方式 int fdlib; fdlib = creat (“/usr/lib/d2”, 0775); 下面简述这一系统调用的执行过程,这里假定文件是首次创建,即在执行之前,文件还未存在: ① 首先为新文件d2分配索引节点和活动索引节点,并把索引节点编号与文件分量名d2组成一个新的目录项,记到目录/usr/lib中。在这一过程中,需要执行以前介绍过的目录检索程序。 ② 在文件d2所对应的活动索引节点中置初值,包括把存取权限i_mode置为0775,连接计数i_nlink置为“1”等等。 ③ 为文件分配用户打开文件表项和系统打开文件表项,置系统打开文件表项的初值。包括在f_flag中置“写”标志,读写位移f_offset清“0”等等。然后,把用户打开文件表项,系统打开文件表项及d2所对应的活动索引节点用指针连接起来,最后把用户打开文件表项的序号,即文件描述字返回给调用者。 由于在上述步骤中,也执行了文件“打开”功能,因此在以后操作中,不用再执行“打开”操作。 2) 文件的删除 删除的主要任务是把指定文件从所在的目录文件中除去。如果没有连接的用户,即如果在执行删除之前i_link 为“1”,还要把这个文件占用的存储空间释放。文件删除系统调用的形式为:unlink (filenamep); 其中参数与creat中的意义相同。在执行删除时,必须要求用户对该文件具有“写”操作权。这一系统财用的处理步骤比较简单,读者可自己设想一下它的执行步骤。 2、文件的连接和解除连接 1) 文件的连接 在文件共享一节中,已介绍文件连接的意义,它的调用方式为: chat * oldnamep, * newnamep; link (oldnamep, newnamep); 其中oldnamep和newnamep分别为指向已存在文件名字符串和文件别 名字符串的指针。这一系统调用的执行步骤如下: ① 检索目录 找到oldnamep所指向文件的索引节点编号。 ② 再次检索目录 找到newnamep所指文件的父目录文件,并把已存文件的索引节点编号与别名构成一个目录项,记入到该目录中去。 ③ 把已存文件索引节点的连接计数i_nlink加“1”。 从上述过程可知,所谓连接,实际上是共享已存文件的索引节点。 2) 文件的解除连接 其调用形式与文件删除相同:unlink (namep); 实际上解除连接与文件删除执行的是同一系统调用程序。删除文件是从文件主角度讲的,而解除文件连接是从共享文件的其它用户角度讲的。不论删除还是解除连接,都要除去目录项,把i_nlink减“1”,不过,只有当i_nlink减为“0”时,才真正删除文件。 3、文件的打开和关闭 文件在使用前,必须先“打开”,以建立用户进程与文件的联系。其主要任务是把文件夹的索引节点复制到活动索引节点表中,以便加速文件夹的访问。 另一方面,活动索引节点表的大小,受到内存容量的限制,这就要求用户一旦当前不再对文件操作时,应及时释放相应的活动索引节点,以便让其它进程使用。这就是“关闭”文件的主要功能。 1)文件的打开 其调用方式为:int fd, mode; char * filenamep; fd = open (filenamep, mode); 其中mode是打开的方式,它表时打开后的操作要求,如读(0)、写(1)或又读又写(2)。其余参数的意义与creat中的相同。open的执行过程如下: ① 检索目录:一般来说,要求打开的文件应该是已经创建的文件,因此它应该在文件目录中登记,否则就算错。在检索到指定文件之后,就把它的索引节点复制到活动索引节点表中。 ② 把参数mode提出的打开方式与活动索引节点中在创建文件时记录的文件访问权限相比较,如果非法,则这次打开失败。 ③ 当“打开”合法时,为文件分配用户打开文件表项和系统打开文件表项,并为系统打开文件表项设置初值。然后通过指针建立这些表项与活动索引节点之间的联系。在完成上述工作之后,把文件描述字,即用户打开文件表中相应文件表项的序号返回给调用者。 需要指出,如果在执行这一调用之前,别的用户已打开了同一文件,则活动索引节点表中已有了这个文件的索引节点,于是在执行这一调用时,不用执行第(1)步中复制索引节点的工作。而仅把活动索引节点中点的计数器i_count加“1”即可。这里i_count反映了通过不同的系统打开文件表项来共享同一活动索引节点的进程组数目。它是以后执行文件关闭时,活动索引节点能否释放的依据。例如,图7-13的情况,活动索引节点中的i_count应为“2”。 2) 文件的关闭 文件使用完毕,就应该执行close6系统调用把它关闭,从而切断用户进程与文件之间的联系。其调用方式为: int fd; close (fd); 显然,要关闭的文件应该是已经打开的,所以文件描述字fd一定存在。close的执行过程如下: ① 根据fd找到用户打开文件表项,继而找到系统打开文件表项。把用户打开文件表项释放。 ② 把对应的系统打开文件表项中的f_count减“1”,如果不为“0”,说明进程族中还有子程序正在共享这一系统打开文件表项,所以不用释放系统打开文件表项,而直接返回;否则释放这个系统打开文件表项,并找到与之连接的活动索引节点。 ③ 把上述活动索引节点中的i_count减“1”,若不为“0”,表明还有其它用户进程正在使用该文件,所以不用释放该活动索引节点而直接返回,否则在把该活动索引节点中的内容复制回文件卷的相应索引节点之后,释放该活动索引节点。 由上述过程可见,f_count和i_count分别反映了进程动态地共享一个文件的两种方式。前者反映了不同进程通过同一个系统打开文件表项共享一个文件的情况;而后者反映了不同的进程或进程族通过同一个活动索引节噗共享一个文件的情况。通过这两种方式,进程之间既可以用相同的位移指针f_offset,也可以用不同的f_offset来动态地共享同一个文件。 4、文件的读和写 文件的读和写是文件夹的最基本操作。“读”是指文件的内容读入到用户进程的变量区中,“写”是指把用户进程变量区中的信息写入到文件存储区中。从文件的什么逻辑位置读入数据,或把数据写入文件的什么逻辑位置均由系统打开文件表中的f_offset决定。 1) 读文件 该系统调用的形式为: int nr, fd, count; char buf [ ] nr = read (fd, buf, count); 这里fd是打开系统调用执行之后返回给用户程序的文件描述字;buf是读出信息应送入的用户内存区首地址;count是本次要求传送的字节数,nr是这个系统调用执行之后返回的实际读入字节数。即使在正常情况下,nr指出的字节数也可能小于count要求的字节数。例如,一旦读到文件末尾时,系统调用就返回,而不管是否已读了用户要求的字节数。如果文件的位移时指针已指向文件末尾,又使用了read系统调用,则返回“0”值。 假定我们通过打开系统调用打开了文件/usr/lib/d2,与它有关的用户打开文件表项,系统打开文件表项和活动索引节点见图29所示的关系。 图29 用户打开文件表项,系统打开文件表项和活动索引节点的关系 现要求读文件d2的1500个字符到指针bufp指向的用户内存区中,number用来存放实际传送的字节数,则可按如下方式调用read: number = read (fdlib, bufp, 1500); 在执行read系统调用的过程中,系统首先根据f_flag中记录的信息,检查读操作的合法性,如果合法,则根据当前位移量f_offset的值,要读出的字节数,以及活动索引节点中i_addr指出的文件物理块存放地址,把相应的物理块读到块设备缓冲区中,然后再送到bufp指向的用户内存区中。由此可见,在执行read的过程中,一定要用到块设备管理中的读程序。 2) 写文件 该系统调用的形式为: nw = write (fd, buf, count); 其中,fd, count和nw的意义类似于read,只是buf是信息传送的源地址,即把buf所指向的用户内存区中的信息,写入到文件存储区中。只要情况正常(中间无差错),nw一定与count相等。 5、文件的随机存取 在文件初次“打开”时,文件的位移量f_offset总是清为零。如果不特别指明,以后的文件读写操作总是根据offset的当前值,顺序地读写文件。为了支持文件的随机访问,文件系统提供了系统调用lseek,它允许用户在读、写文件之前,事先改变f_offset的指向。这一系统调用的形式为: long lseek; long offset; int whence, fd; lseek (fd, offset, whence); 其中,文件描述字fd必须指向一个用读或写方式打开的文件,当whence是“0”时,则f_offset被置为offset,当whence是“1”时,则f_offset被置为文件当前位置加上offset。 13.5 UNIX操作系统的设备管理 在UNIX系统中,外部设备按其信息组织和传送单位的不同而被分成两类,一是存储设备,主要代表为磁盘。由于在这类设备上存储的信息,在物理往往是按盘块为单位组织和传输的,所以称作为块设备;一是输入/输出设备,主要代表为终端键盘和行式打印机。由于在这类设备上的信息往往是以字符为单位组织和传输的,因此称作为字符设备。 为了能提高CPU和块设备工作的并行度,为了能解决CPU和字符设备之间的速度不匹配问题,UNIX在设备管理时,采用了完善的缓冲技术。 本节主要讲述UNIX采用的各种缓冲技术以及对缓冲的管理。至于I/O管理,由于涉及具体设备,琐碎且繁杂,加之篇幅所限,就省略不介绍了。 UNIX系统的设备管理具有下述特点: 1、文件和外部设备的一致性。所有设备都作为文件来处理——一种特殊文件。它们驻留在目录/dev下,每一类设备都有一个文件名,对每一类设备除了具体的输入/输出处理外,其余处理都和普通文件一样。特定设备的特性都放到了各自的设备驱动程序里,用户无须知道有关的细节。因此使用I/O设备如同读写一个普通文件,可同样使用读写系统调用: read (fd, buffer, count) write (fd, buffer, count) 2、从设备管理的层次结构上,块设备管理和字符设备管理是相似的。尽管这两类设备在功能上和物理上都有很大的差别,但在管理控制方法、数据结构和层次结构上都相似。管理上可以分三个层次:(1) 低层都设置了一个公用的缓冲池及相应的管理;(2) 中间层是设备驱动程序,都设置了设备表和设备开关表;(3) 高层是设备管理与文件系统的接口,包含有读、写、中断处理过程等。 3、读写文件除了一般的读写方式外,还提供了“提前读”、“异步写”和“延迟写”方式。从盘中读文件时,通常都是顺序地读出一系列逻辑块中的信息,但为了加快信息读出,可采用“提前读”方式。这时,一方面把指定的“当前块”中的信息读入内存,同时把“当前块”的下一块的信息提前读出,为下次读提前作好准备,从而提高了读出速度。在写的过程中,写过程须等待传送完成才能返回,而在“异步写”时,进程不必等待传送完成即可返回,这样提高了运行进程和磁盘I/O传送的并行程度。采用“延迟写”方式是为了尽量减少不必要的I/O操作,节省磁盘空间。因为当前要写的信息,可能以后还要用,那么就让它驻留在缓冲区内,延迟一段时间再写入磁盘,如果此时有进程要读该信息,就不必从盘中去读,而是直接从缓冲区中去读,这样就减少了对该文件的磁盘I/O操作。 4、尽可能用公用代码处理设备的相似部分。例如磁盘可以用作一般的文件存贮器,又可作为进程对换的外存区(swap区),还可以作为非块设备的字符设备。在使用中,把内存地址、盘块号、传送字节数、读写特性等各自事先处理好,然后再去调用公共处理程序。又如,打印机、终端等字符设备有公共的处理字符的I/O加工程序,只要花很少代码去处理它们之间的差别就行。这样不仅使整个设备处理代码大为减少,也为以后增加设备打下良好的基础,因为只需增加与增加设备特别有关的部分代码,其它则可利用已有的公用代码。 13.5.1 块设备管理的数据结构 1、缓冲存储区与缓存控制块 1) 缓冲存储区 为了便于内存和块设备之间信息的传输,在内存设置了十五个缓冲存储区(以下简称缓存),每个缓存为514个字节大小;其中512个字节恰能放入一个盘块的有效信息,另外两个字节供它用。这十五个缓存构成了一个缓冲池。 缓存主要用于文件信息的读、写。在磁盘和处理机之间设置这个缓冲池,就能发挥它们之间的并行工作能力,协调速度不匹配的予盾。 2) 缓存控制块buf 缓存只是用来存放有效信息的,为了对缓存的使用情况加以管理,为了能记录具体的输入/输出要求,系统相应地设置了十五个缓存控制块buf。每一个buf对应于一个缓存,它们在物理上并不相邻,而是在buf里有指针记录下对应缓存的起始地址。此外在buf中还有关于相应缓存的使用情况以及队列指针等信息。因此缓存控制块buf和它所对应的缓冲存储区之间的关系,恰如进程控制块PCB与进程之间的关系似地,系统运行过程中感知的是buf。正是buf在各种队列中排队,得到一个buf,就会获得它所对应的缓存及其使用情况。 每一个buf结构有十二项内容,buf是一个结构类型的数据。其形式如下: struct buf { int b_flags; 各种标记 struct buf * b_forw; 设备缓冲区(b)链向前指针 struct buf * b_back; 设备缓冲区(b)链向后指针 struct buf * av_forw; 空闲缓冲区(av)链向前指针 struct buf * av_back; 空闲缓冲区(av)链向后指针 int b_dev; 与缓冲区相连的设备号 int b_wcount; 传送字数 char * b_addr; 缓冲区内存地址低16位 char * b_xmen; 缓冲区内存地址高6位 char * b_blkno; 块设备上的物理块号 char b_error; char * b_resid; } buf [NBUF]; /*flags*/ 各种标记如下: # define B_WRITE 0 写标记 # define B_READ 01 读标记 # define B_DONE 02 传送结束 # define B_ERROR 04 传送有错 # define B_BUSY 010 缓冲区正被使用不在av链上 # define B_PHYS 020 # define B_MAP 040 # define B_WANTED 0100 有进程正在等待使用该缓冲区 # define B_RELOC 0200 # define B_ASYNC 0400 异步I/O操作,无需等待 # define B_DELWRI 01000 延迟写 ·b-addr和b-xmen 这是存放对应信息内存起址的地方,b-addr为低16位,b-xmen为高16位。 ·b-dev 这是存储设备的设备号。设备号由主设备号d-major和次设备号d-minor组成。主设备号是设备的类型号,位于b-dev的高节字处,次设备号是设备在同类设备中的编号,位于b-dev的低字节处。 ·b-blkno 这是存放块设备上物理块序号的地方。 由buf中的(1)~(3)信息就建立起了一个缓存和某块设备上的一个盘块之间的对应关系。 ·b-wcount 这是输入/输出请求传输的字数,如果是传送一盘块,则是256。 ·b-error 这是输入/输出出错时返回的出错信息。 ·b-resid 这是由于输入/输出出错而未能传输的剩余字数。 ·b-flags 这是标志字。一是用来标明相应缓存的使用情况,二是反映输入/输出方式,主要有: B-WRITE 0 写(将缓存中的信息写到盘块上去) B-READ 01 读(将盘块中的信息送到缓存中) B-DONE 02 I/O操作结束 B-ERROR 04 I/O因出错而中止 B-BUSY 010 相应缓存正在使用 B-WANTED 0100 有进程正在等待使用该buf管理的缓存,请B-BUS时, 唤醒此进程 B-ASYNC 0400 异步I/O,不需等待其结束 B-DELWR 11000 延迟写,只有在相应缓存要移作它用时,才将其内容写 到盘块上 ·b-forw和b-back 当buf位于某个块设备的设备buf队列时,b-forw为队列的前向指针,b-back为队列的后向指针。 ·av-forw和av-back 当buf空闲时,它将排在自由buf队列,av-forw为队列的前向指针,av-back为队列的后向指针。由于buf不仅反映了缓存使用的有关信息,也记录了具体的输入/输出要求和执行结果(和上面的b-wcount, b-error, b-reisid, b-flags)。因此对某一块设备的I/O请求,也可以通过它体现出来,即用buf构成对一个设备的输入/输出请求队列。由于一个buf排在设备输入/输出请求队列时,绝对不可能排在自由buf队列里,所以buf中的av-form和av-back也借用来形成设备输入/输出请求队列的前向、后向指针。 2、块设备表devtab UNIX为各类设备(也就是b-major不同的设备)设置了块设备表,以便对该类设备的输入/输出请求队列以及与该类设备相关的设备buf队列进行管理。 每一类块设备有一张块设备表,每个devtab结构有六项内容,其结构为: struct devtab { char d_active; 相应设备已被启动进行I/O操作标记 char d_errcnt; I/O操作出错记数,对盘而言,错误次数小于10 次,重执行I/O操作 struct buf * b_forw; 该设备缓冲器队列的头指针 struct buf * b_back; 该设备缓冲器队列的尾指针 struct buf * d_actf; 该设备I/O缓冲器队列的头指针 struct buf * d_actl; 该设备I/O缓冲器队列的尾指针 } 列置如下: (1) d-active 该类设备的忙/闲标志。 (2) d-errcnt 出错重试次数。对磁盘而言,若出错次数(10,则重复执行该I/O请求。 (3) b-forw和b-back 该类设备的buf队列的前向指针和后向指针。 (4) d-actf和d-actl 该类设备的输入/输出请求队更的首指针和尾指针。 3、块设备开关表 为了组织设备的输入、输出,各类块设备都有各自的一套子程序,例如打开、关闭、启动等子程序。为了能方便地寻找到这些子程序的入口,系统为每类设备设置了一数据结构,用于存放这些子程序入口,这个数据结构被称为设备开关,起名bdevsw。每类块设备的设备开关集中在一起,就称作块设备开关表。块设备开关表也称为处理程序入口表,其结构如下: struct bdevsw { int ( * d_open) ( ); 打开子程序入口地址 int ( * d_close) ( ); 关闭子程序入口地址 int ( * d_strategy) ( ); 启动子程序入口地址 int * d_tab; 对应设备表地址 } bdesw [ ] bdevsw结构共有四项内容,列置如下: (1) d-open 打开子程序的入口地址; (2) d-close关闭子程序的入口地址; (3) d-strategy启动子程序的入口地址; (4) d-tab该块设备的设备表地址。 譬如,由于磁盘没有“打开”和“关闭”子程序,故在相应位置处填入&nulldev,在启动程序位置填上&rkstrategy,在设备控制表处填上&rktab。 在系统生成时,根据设备配置的情况形成如下开关表: int ( * bdevsw [ ]) ( ) { & nulldev, & nulldev, & rkstrategy, & rktab, /*rk*/ & nodev, & nodev, & nodve, 0, /*rr*/ & nodev, & nodev, & nodve, 0, /*rf*/ & nodev, & nodev, & nodve, 0, /*tm*/ & nodev, & nodev, & nodve, 0, /*tc*/ & nodev, & nodev, & nodve, 0, /*hs*/ & nodev, & nodev, & nodve, 0, /*hp*/ & nodev, & nodev, & nodve, 0, /*ht*/ 0 } 这里,&表示指向变量或函数(子程序)的地址。 & nulldev表示无此种设备(空操作)。 & nodev表示无此类设备。 一般UNIX系统中只配备了一台块设备即磁盘rk,其打开子程序和关闭子程序是空操作,它的启动子程序是rkstrategy,对应设备表为rktab。如果用户需增加块设备,则根据所增加设备的特性填上相应各项。 13.5.2 缓存控制块buf的各种队列 由于buf记录了与缓存有关的各种管理信息(包括具体的I/O要求),所以系统总是通过管理buf来使用缓存的。 1、自由buf队列和自由buf队列控制块bfreelist 一个可被分配它用的buf,一定位于自由buf队列,这意味着该buf所对应的缓存现在是自由的,此种buf标志字b-flags中的B-BUSY标志均被清除。 每个处于自由buf队列中的buf结构相互使用av-forw和av-back形成双向勾链。 为了能真正管理起自由buf队列,显然需要对队列的首、尾加以管理,这将由自由buf队列控制块bfreelist来完成。为了整齐化一,系统让它也具有和一般buf相同的结构,只是用它里面的av-forw做自由buf队列的首指针,而av-back做自由buf队列的尾指针。于是,自由buf队列可如图13-30所示。 图13-30 自由buf队列 对自由buf队列,系统采用FIFO管理算法,一个缓存被释放时,相应于它的buf就被送入自由buf队列之尾;当请求一个缓存时,就从自由buf队列之首摘下一个buf,它所对应的缓存即被分配出去。 2、输入/输出请求队列 通过缓存进行输入/输出时,具体的I/O请求全都包含在buf结构中,它们是:操作类型(读或写)、信息源和目的地的地址、以及数据的传输量等。因此,buf既是缓存的控制块,又是I/O的请求块。也就是说,向主设备号相同的各设备提出的I/O请求所构成的输入/输出队列也由buf组成。由于位于I/O请求队列的buf肯定不会出现在自由buf队列中,故在I/O请求队列里的buf就借用av-forw来构成一个单向链,该队列的队首、队尾指针是该类设备的块设备表中的d-actf和d-actl。于是某块设备的输入/输出请求队列如图13-31所示。 图13-31 块设备的I/O请求队列 对于块设备的I/O请求队列,系统仍采用FIFO管理算法。要注意的是,系统中也有不通过缓存进行的输入和输出。回忆一下UNIX存储管理,在那里曾提及在磁盘上专门开辟了进程映像对换区,用以存放调出的进程映像非常内驻内存部分。这部分存储资源归UNIX存储管理部分进行分配和释放,但它提出的输入/输入却要由设备管理部分来负责完成。由于它的I/O请求是指把对换区盘块中的内容送到内存的用户区指定处,与缓存没有关系,也以为了能提交这种不通过缓存进行的I/O请求,系统专门开设了一个名为swbuf的I/O请求块。swbuf的结构与buf结构一样,它也排列在块设备的I/O请求队列中,使用av-forw与它的前、后buf勾链。不同的是它里面的b-addr和b-xmen给出的不是哪个缓存的内存起址,而是分配给它的内存区起址。顺便再强调一下,swbmf和bfreelist是使用buf结构的二种特殊情况,它们和缓存没有关系。 3、设备buf队列 整个系统的缓存资源是有限的,为了实现对它的充分共享,UNIX做了如下考虑: 第一,一旦某缓存被释放,相应的buf就应立即进入自由buf队列,以作它用。 第二,一个已经位于自由队列的buf,在尚未移作它用之前,它里面的信息仍保持着缓存与某设备上盘块的对应关系。若此时系统根据需要能够退回来按原状继续使用它,那么就可省去重复的、且十分耗费时间的设备I/O操作过程,从而提高文件系统的工作效率。为了能方便地做这种“进”(移作它用)或“退”(继续按原状使用),UNIX在自由buf队列和I/O请求队列之外,又设置了一种新队列:设备buf队列。 每类块设备都有一个设备buf队列,队首和队尾分别是相应块设备表devtab中的b-forw和b-back。队列中的buf结构用其指针b-forw和b-back进行双向勾链。一个缓存被分配用来对某类块设备的某一盘块进行读、写时,与之对应的buf就进入了该类块设备的设备buf队列。即使在完成了I/O请求而被释放后(这时进入了自由buf队列),它仍然呆在这个设备buf队列里,除非再次分配用于别类块设备时止。 每当一个buf进入设备buf队列时,总是在该队列之首进行插入。 这里要注意的一个问题是,在系统初启时,每一个buf都没有分给任何实际的块设备,为此,系统增设了一个想象的设备NODEV,其首、尾指针就用bfreelist中的b-forw和b-back。于是NODEV队列和某类设备的设备buf队列如图13-32的所示。 图13-32 设备buf队列的初始态和演变 由于一个buf建立起某类设备的一个盘块与一个缓存之间的联系之后,它就一直呆在该类设备的设备buf队列里,直到这个缓存重分它用,因此对于一个buf来讲,任何时候都处在上述三个队列的两个之中:在自由buf队列和设备buf队列(包括NODEV队列的情形),或在I/O请求队列和设备buf队列。 13.5.3 字符设备管理的数据结构 1、字符缓存和自由字符缓存队列 为字符设备设置字符缓冲的主要出发点是解决它们和CPU之间的速度不匹配问题。由于字符设备的共同特点是:工作速度慢,一次输入/输出请求的字符数量少且不固定,因此系统为字符设备设置了一个使用比较灵活、占用内存较小的缓冲机构。 系统在内存总共设置了100个字符缓存cblock,它们形成一个能够共享的缓总池cfree。每个字符缓存为8个字节,前两个字节为字符缓存指针c-next,后六个字节为该缓存的信息区info。 未被使用的字符缓存通过各自的c-next勾链成一个单向的自由字符缓存队列,其队首由一个名为cfreelist的指针指出,处于队列之尾的字符缓存的c-next取值null,图13-33给出了自由字符缓存队列的示意。 图13-33 自由字符缓存队列 系统视自由字符缓存队列为一栈,分配和释放均在队首进行。这里要注意的是,由于对字符缓存的管理比较简单,因而没有必要再设置专门的缓存控制块。 2、字符设备表和I/O字符缓存队列 和块设备类似,每个字符设备也都有一张设备表。但由于各类字符设备的特性判别很大,不可能设置统一形式的设备表。譬如说在UNIX系统里,纸带机的设备表仅三项内容,而终端机的设备表则含有十三项内容。 但它们共同之处是都包含有输入/输出字符缓存队列的控制块,该控制块均为clist结构,里面含三项内容。 (1) c-cc队列中可用字符计数; (2) c-cf指向队列中的第一个字符; (3) c-cl指向队列的最后一个字符。 于是,一个字符设备的输入/输出字符缓存队列的一般形式如图13-34所示。 图13-34 I/O字符缓存队列 由此可知,在整个系统中存在着多个I/O字符缓存队列。如果某一字符设备既有输入功能,又有输出功能,则它的设备表中就会有两个字符缓存队列控制块,一个控制输入字符缓存队列,一个控制输入字符缓存队列。 3、字符设备开关表 每个字符设备也有自己的设备开关,这些开关集合在一起,就形成字符设备开关表cdevsw。开关表中的每一表目与一个字符设备相对应,是该设备的开关。它包含五项内容,前四项分别是打开、关闭、读和写子程序的入口地址,第五项是专门为终端设置的,为设置并获得终端机特性子程序的入口地址,其余字符设备均没有此项。 13.5.4 字符缓存的管理 为了从字符缓存中取字符或往字符缓存中送字符时能够较为容易地判断出是否要释放字符缓存或申请字符缓存,UNIX系统在对cfreelist进行初始化时,采取了一定的措施,使每一个字符缓存的起始地址的最后三位为0。这实际上只需要确保第一个字符缓存cfree[0]的起始地址最后三位为0即可,因为每个字符缓存都由8个字节构成,这样排列下来,每个字符缓存起始地址的最后三位就都为0了(这也是每个字符缓存长度取为8个字节的原因之一)。 现在来看一下取字符和释放字符缓存的问题以及送字符和申请字符缓存的问题。 以图34为基础,根据c-cf指点,逐一从字符缓存里取出字符e和f,c-cc做计数调整,c-cf也依次下移。若发现c-cf的最低三位已为0时,这说明该字符缓存中的字符已全部取完,c-cf应指向下一字符缓存的第一个字符,即要把释放的这一个字符缓存中的c-next加工送给c-cf,这时图13-34就成为图13-35所示。 图13-35 取出e、f字符后的I/O字符缓存队列 若在图13-35的基础上,根据c-cl指点往字符缓存里送字符。显然,当c-cl加2而获取下一存放字符的位置时,会发现该地址的最后三位为0,这表示目前的字符缓存已经存满,需要先申请一个新的字符缓存才能把字符存入。于是去cfreelist申请一个新的字符缓存,链入到I/O字符缓存队列之尾,然后再根据调整后的c-cl送入字符。此时图35就成为图13-36所示。 图13-36 送入S后的I/O字符缓存队列 13.5.5 块设备的读写操作 通过缓冲技术,块设备的读写有三种方式、五个过程: 1、一般读、写方式,有二个过程: bread (dev, blkno)——一般读过程,把物理块中的信息读入缓冲区 bwrite (bp)——一般写过程、把bp指定的缓冲区中的信息写到磁盘上,要等待写完成。 图13-37、13-38给出了一般读写方式的流程图。 图11-37 bread (dev, blkno) 流程 图13-38 bwrite (bp) 流程图 下面介绍在上述流程中所调用到的几个过程: 1)rkstrategy (bp) 这一过程的功能是把bp所指定的缓冲区排列在磁盘设备缓冲区I/O队列末尾。若设备不忙,则启动磁盘,传送该队列中第一个缓冲区的内容。 过程的流程图如图13-39所示。 图13-39 rkstrategy (bp) 流程图 2)rkstart ( ) 它由rkstrategy (bp)调用,共主要功能是从磁盘设备缓冲区I/O队列中取出第一个缓冲区控制块,调用devstart (bp, devloc,devblk, hbcom)过程,启动磁盘进行I/O操作。主要流程图13-40所示。 图13-40 rkstart ( ) 流程图 3)devstart (bp, devloc, devblk, hbcom) 此过程由rkstart ( )调用。功能是装配和填写各个磁盘寄存器,启动磁盘。其中的四个参数的意义为: bp——要传送信息的缓冲区首部地址,缓冲区首部中包含着本次传送的所有控制信息。 devloc——给出要启动的设备的寄存器组织的起始地址。 devblk——指定的外设所要求的块设备地址格式。 hbcom——命令码高位。 下面我们以RK磁盘为例加以说明,但这个过程不仅仅适用于RK磁盘。 RK磁盘控制器的寄存器组由六个寄存器组成: RKDS 存放驱劝器状态的寄存器 RKER 存放传送出错信息的寄存器 RKCS 存放控制状态信息的寄存器 RKWC 存放传送字数的补码的寄存器 RKBA 存放内存数据地址的寄存器 RKDA 存放磁盘数据地址的寄存器 在每次磁盘传送之前,驱动程序必须按下面的规定填写: RDKA为: RKBA要填入传送数据在内存的地址的低16位。 RKWC传送字数的补码,如同缓冲区首部中的b_wcount。 RKCS是控制状态寄存器,其上信息一旦填入就立即启动磁盘。 第0位(GO):此位置1则启动磁盘实现第1~3位指出的传送。规定如下: 第321位 传送方式 000 控制复位RESET 001 从内存写到磁盘上 010 从磁盘读到内存中 第5-4位:内存传送地址共占用18位,RKBA中存放低16位,高2位存放于此。 第6位(IENABLE):此位为1表示开放中断。 第7位(CTLRDY)由硬件设置,为1表示设备已准备好接收下一传输命令。 第15位(RKER)由硬件设置,表示传送结果,如为1,则传送出错。 磁盘传送完成之后,其中包含有传送错误与否的信息,由RKDS和RKER中的信息表明。 图13-41给出了devstart ( )的流程图。 图13-41给出了devstart ( )的流程图。 2、“提前读”和“延迟写”方式,有二个过程: breada (adev, blkno, rablkno)——提前读,先把指定物理块blkno中的信息读入缓冲区,接着再把“提前”要读的块rablkno的信息读出。 bdwrite (bp)——延迟写,系统只把信息写到指定缓冲区,并把该缓冲区写标记B-DELWRI置1,但并不启动磁盘把它写到盘上,待以后有进程申请缓冲区,且申请到该缓冲区并判该缓冲区是“延迟写”,则调用bwrite过程,以异步写方式将该缓冲区信息写到磁盘上。 设置延迟写是为了减少不必要的I/O重复和节省磁盘空间。有些情况下,这些输出的信息可能不久又要将它读入内存。在延迟写方式下,只要将其它进程未申请到该缓冲区,其上的信息便暂时不会写入磁盘,以后需要再使用这些信息时,就可直接从该缓冲区读入,而不必从盘上去读,从而避免了把信息输出后又马上要读入的I/O反复操作。另外,在信息较短的情况下,为了节省磁盘空间,一个磁盘块上可装几个这种短信息,因此相应的缓冲区在未写满信息时,并不把它立刻写入磁盘块中,而只是将该缓冲区又挂到av链上,当下次又有短信息要写入盘时,则从av链上摘下此缓冲区,写入新的短信息,然后又挂到av链上,如此反复,直到该缓冲区写满或有其他进程申请此缓冲区时,才启动磁盘写入。这样,既避免了过于频繁的I/O操作,也节省了磁盘空间。 异步写bawrite (bp) :异步写过程与一般写过程基本相同,但它不必等待传送完成即可返回。bawrite (bp)调用者进程先把异步写标志位B_ASYNC置1,然后调用bwrite (bp)并立即返回。这样,进程一方面在进行其他操作,同时在作写操作,因而提高了进程运行和I/O操作的并行度。 13.5.6 磁盘中断处理 当磁盘I/O传送结束,产生I/O结束中断,CPU响应中断,进入中断处理。它首先检查相应设备表中的d_active项是否为1,即磁盘是否真正被启动,若未被启动,则不作中断处理而立即返回。若是在启动状态中,取出其缓冲区I/O队列的第一缓冲控制块,置d_active = 0,表明本次传送结束。然后根据状态寄存器RKCS的第15位是否为“1”,判传送出错否。若出错,则打印出错信息(由RDER和RKDS的内容决定)。由于磁盘设备传送出错几率很高,因此UNIX中并不是一出错便立即停止传送,而是重复执行10次传送(最多10次),若仍出错,那么才置本次缓冲区传送错误标志B_ERROR,放弃本缓冲区的I/O操作,转向缓冲区I/O队列的下一个缓冲区信息的传送。中断处理rkintr ( )的流程图如图13-42所示。 上面我们已比较详细地介绍了块设备的管理,目的是希望对块设备管理了解得更具体一些,以供用户自行增加某种设备时参考,为了让读者对块设备的读写过程有一完整的概念,我们给出读过程的源程序及其流程图。 bread (dev, blkno) { register struct buf * rbp rbp = getblk (dev, blkno); if (rbp->b_flags & B_DONE) return (rbp); rbp->b_flags = | B_READ; rbp->b_wcount = -256; (*bdevsw [dev.d-major]. d_strategy) (rbp); iowait (rbp); return (rbp); } 图13-42 rkintr ( ) 的流程图 它的功能是从块设备dev上将一指定的字符块blkno上的内容读到缓冲存贮器。由源程序可知,这一读过程由下列步骤构成: 1、根据dev, blkno在dev设备的缓冲区队列中找到指定的缓冲区,由于其内容就是bread ( )所需的,不必再进行读操作,而直接返回缓冲区的指针。 2、在dev的缓冲区队列中找不到相应的缓冲区,则要从空闲缓冲区队列中分配一个缓冲区。 3、把所得的缓冲区挂到设备dev的I/O缓冲区队列,并进行读操作。如I/O缓冲区队列中无其他的I/O操作,则立即读取该块。否则,先读其他的请求读取的信息块。每读一块,将由中断处理程序来启动执行下一块的读取。图13-43给出了它的流程图。 图13-43 bread (dev, dlkno) 流程图 13.5.7 块设备I/O操作与文件读写关系 下面进一步说明块设备读操作bread (dev, blkno)与文件系统中的读文件系统调用read (fd, base, count)的关系。 读文件read (fd, base, count)的过程如下(参见图13-44): 图13-44 read (fd, base, count) 工作流程 1、用户程序请求操作系统为其服务,读取一文件。通过trap处理进入读文件系统调用入口read ( )。这时进程由用户态进入核心态。 2、read ( )调用rdwr (FREAD),而由rdwr (FREAD)执行; 根据文件描述符fd,通过用户打开文件表项确定系统打开文件表项及内存活动i节点,并确认读或写操作的合法性,置有关工作单元初值,调用readi ( )。 3、readi ( )执行: (1) 确定是块设备文件,还是字符设备文件,是后者则通过字符设备开关表转到特别文件处理。是块设备文件则转下述的读处理。 (2) 由读写位移u. u_off_set得到文件逻辑块号,本次实际传送字节数。并调用映象处理程序bmap ( )把逻辑块号转换成物理块号。 (3) 确定一般方式读还是提前读,调用bread ( )或bread ( ),执行读取一块到缓冲区。 (4) 调用imove ( )程序,把已读入缓冲区的信息移至内存,并准备读取下一块。 (5) 调用brelse ( ),释放缓冲区。 (6) 全部文件块读完或出现错误时,则返回;否则继续读下一块文件信息。 由上可知,块设备的读(写也一样)操作(即I/O操作),就是文件读或写系统调用的一个内部过程。面向用户的只是文件读写的系统调用,设备的I/O操作对用户则是完全透明的。 13.6 UNIX命令语言shell 由图13-1示出的UNIX系统结构图可知,命令语言shell是用户与UNIX系统交往的界面。虽然shell是一种命令语言,但它不仅仅是一些简单有效的、能为直接运行程序提供便利的交互命令,它也是一种命令级程序设计语言,能够用其来编制复杂的命令程序。因此,UNIX的shell是命令语言、命令级程序设计语言及两者的解释程序的总称,形成了UNIX系统的重要特点之一。 13.6.1 shell命令语言 1、简单命令 简单命令是shell命令语言的基础,其一般形式为: command arg1 arg2 … argn 其中,command是命令名,arg1、arg2、… agrn是该命令执行时需要携带的参数,命令名和各参数之间用空格分开。简单命令可以没有参数,有些参数是可选项。例如: pwd 是一个简单命令,意思是打印出工作目录(或称当前目录)的名字,它没有带任何参数。又如: cp disarm/ * newlit 也是一个简单命令,意思是把子目录disarm里的全部文件拷贝到子目录newlit里,它带有两个参数disarm/ *和newlit。下面列出一些简单命令及含义。 命令名 含 义 cat 连接并显示文件 cc 调用C编译程序编译文件 cd 改变工作目录 cp 拷贝文件 date 设置日期和时间 ed 进行文本编辑 kill 终止一个进程 lpr 在行式打印机上打印文件 ls 列出目录内容 mkdir 建立一个目录 ps 显示进程的状态信息 pwd 打印工作目录名 rm 删除文件 rmdir 删除目录 who 列出使用系统的用户名 这不是简单命令的全部,只是通过这几个命令,你可以悟出shell命令语言中简单命令的各种功能。简单命令中的参数是用来向所执行的程序传递信息用的,如果参数中有任选项,则应列在命令名的后面。在UNIX系统里,习惯上在可选参数的前面都加上一个连字符“—”。譬如说,单纯的ls命令是列出目录内所含文件的名字和类型,但若带有任选项“-1”,即ls-1,则除列出文件名和类型外,还要列出诸如文件主名,各类用户的存取权限,文件长度等更为详细的信息。 2、后台命令——& 有时你要运行一个需花很长时间才能结束的程序,在它运行期间并不需要从终端做什么输入,输出在最后产生。于是你只得耐心地等待它,直到运行完毕。shell有一个特殊的功能:在启动一个这样的程序之后,可以随它去做,而你则继续输入你的shell命令。我们称前面的那个程序是在后台运行,你随之输入的shell命令则是运行在前台,后台进程和前台进程同时在做。 为了实现这一功能,只需你在某个shell命令之后放上一个“&”符号(之间应有一个空格)。这样,shell就在后台开始做这一命令(执行这个程序),同时允许你输入别的命令。 例如,你打算运行一个叫做acct的程序,它要花费较长时间,这时你可以键入命令 acct & 于是shell就在后台启动acct运行,并显示出acct这个进程的标识数。随后,你就可以在终端键入别的shell命令了。譬如说,你想在编译prog1.c的同时,对prog2.c进行编辑,那么你可以发如下命令: cc prog1.c & ED prog2.c shell在接到这一命令序列后,先调用C的编译程序,让它在后台对prog1.c进行编译,并打印出执行这一任务的进行标识数。然后就转入前台,来对prog2.c进行编辑。很明显,这种前、后台的工作方式,大大提高了系统和用户的工作效率。 3、输入、输出重定向 计算机终端是计算机和用户之间的基本通信设备,如果不做特别说明,shell命令的输入总是发往终端显示器,输入总是来自终端键盘,因此终端键盘和显示器是shell命令的 标准 excel标准偏差excel标准偏差函数exl标准差函数国标检验抽样标准表免费下载红头文件格式标准下载 输入和标准输出。例如,PS、LS、SHO、PWD等命令程序都是使用标准输出为你提供它们的信息,而交互程序(如shell和编辑程序)则从标准输入读得你的各种命令,并通过标准输入告知它们对命令的响应。 1) 输入重定向 由于标准输入和标准输出是由shell建立起来的,因此可以通过shell来重新指定标准输入和标准输出。这种输入、输出重定向,是UNIX系统最重要的特性之一。 例如,一般地输入命令PS,则PS程序就把进程状态信息在标准输入——你的终端上示出,然而,若输入命令: ps>file1 那么结果就和上面略有不同。PS程序虽然仍是把进程状态信息写到标准输出上去,但由于后面的特殊符号“>file1”,shell就把标准输出与名为file1的一个普通磁盘文件连接起来,进程状态住处被写入了文件file1,终端屏幕上看不到什么输出。 “>”是一个特殊的shell符号,它意味着命令的输入将被送往它后面指出的一般文件。要注意的是,如果file1文件里原先就有信息,那么执行PS>file1之后,就会对它进行重写,以致原有内容丢失。如果你希望把输出送到这个文件的末尾而不造成原先信息的丢失,那么可发命令: ps>>file1 2) 输出重定向 标准输入也可以被重新指定,它使用特殊的shell符号“<”,意味着输入源来自于符号右端给出的一般文件。 例如,wc是一个统计文件中有的字符数、字数和行数的简单命令,如果只发WC,那么它就统计工作目录中文件里的字数、字符数和行数,但如果发: wcfile1 则意味着把WC统计出的file2中的字符数、字数和行数送入文件file1保存,而不在屏幕上加以显示。 4、管道命令——“|” 管道(pipe)把一个程序(命令)的标准输出连接到另一个程序(命令)的标准输入上。管道与前面所讲的输入/输出重新定向不同,输入重定向是把程序的输入写到指定的文件上,输入重定向是从指定文件获取输入的内容,而管道则直接地把一个程序的输入作为另一个程序的输入,中间无需通过文件来介绍。例如,下面的一组shell命令: cat file1 file2>file3 wc ”、“,”、“|”等都属于元字符。如果在某种场合下需要去除它们的特殊含义而按一般字符对等,那么就应该在这一元字符前加一个反斜杠“\”即可。例如,\*就是通常意义下的星号,而不再是通配符。要注意的是,出现在两个单引号之间的字符串中若包含元字符,一律按一般字符处理。 13.6.2 shell程序设计语言 UNIX系统的shell,既是一个交互命令的解释程序,也是一个命令级程序设计语言的解释程序。某些计算机系统具有简单而有效的交互命令解释程序,但却缺少编制精巧命令程序的能力;另一些计算机系统灵便的命令级程序设计语言,虽然功能完备有力,但却未能提供程序运行的简便手段。UNIX则把这两种能力全部归入了shell,成为shell的一大特色。 前面提及的shell的典型交互式应用,例如简单命令(“ls”),文件名生成手段(“ls *.doc”),I/O重定向(“ls>myfile”)以及管线(“ls|wc-1”)等,功能不仅强,而且非常有用,但它们仅是shell能力的很小一部分。 1、执行一个shell程序 把shell命令序列存放在一个文件里,就构成了一个shell程序或一个命令文件。通常,当该文件里只包含shell命令的简单序列时,就使用命令文件这一术语;当文件里对命令有更为复杂的安排(如使用了shell的条件命令等)时,就使用shell程序这一术语。 执行一个shell程序的最一般的形式是: sh programname arg1 arg2 … argn 这里,sh为命令名,programname为所要执行的shell程序名,arg1、arg2、…、argn则是shell程序执行时需要的参数。 2、shell变量 shell程序设计语言用变量来存放值,shell变量只能存放字符串,使用一个赋值命令就可为一个变量置值。如: ux = u.UNIX 这个赋值命令把值u.UNIX赋给了名为un的shell变量。 一个shell变量的名字必须以一个字母打头,然后可以包含字母、数字或下划线。由于ux是一个变量,因此对它施行另一个赋值: un = PCDOS 就会立即将其值加以变更。 在你要使用存放在一个变量里的值时,必须在该shell变量名前放上一个‘$’符号,该符号告知shell,随后的名字涉及的是一个变量而不是一个文件。 3、shell程序中的控制语句 做为一种程序设计语言,shell中提供了各种程序结构的控制语句,它们是: · if语句; · while语句; · for语句; · case语句; · break和continue语句等等。 这里不可能把shell做为一种程序设计语言时所具有的功能全都讲述出来,但通过粗略的介绍就可以知道,shell作为一种程序设计语言,其功能确实是很强的。 � EMBED Word.Picture.8 ��� PAGE I _1025721509.doc
本文档为【操作系统原理与设计(下)】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
该文档来自用户分享,如有侵权行为请发邮件ishare@vip.sina.com联系网站客服,我们会及时删除。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
下载需要: 免费 已有0 人下载
最新资料
资料动态
专题动态
is_388936
暂无简介~
格式:doc
大小:1MB
软件:Word
页数:63
分类:互联网
上传时间:2018-09-04
浏览量:51