引论
操作系统
- 作为扩展机器的操作系统
- 操作系统的一个主要任务是隐藏硬件,呈现给程序(程序员)良好、清晰,优雅,一致的抽象。
- 操作系统的实际客户是应用程序。
- 作为资源管理者的操作系统
- 资源管理包括以下两种方式实现多路复用(共享)资源
- 时间上的复用:当一种资源在时间上复用时,不同的程序或客户轮流的使用它
- 空间上的复用:每个客户都得到资源的一部分,从而取代了客户排队
- 资源管理包括以下两种方式实现多路复用(共享)资源
操作系统所处的位置
- 多数计算机由两种运行模式:内核态(管态)和用户态(目态)。软件中最基础的部分是操作系统,它运行在内核态,在这个模式中,操作系统具有对所有硬件的完全访问权,可以执行机器能够运行的任何指令。软件的其余部分运行在用户态下,只使用机器指令中的一个子集。用户接口程序(shell 或 GUI)处于用户态程序中的最低层次,允许用户运行其他程序。操作系统运行在裸机之上,为所有其他软件提供基础的运行环境。操作系统中的程序由硬件进行保护,防止用户试图对其进行修改。
多道程序设计
- 将内存分为几部分,每一部分存放不同的作业。当第一个作业等待I/O操作完成时,另一个作业可以使用CPU。
- 采用多道程序设计可以提高CPU利用率。
- CPU利用率 = 1 - p ^ n,p为一个进程等待I/O操作的时间与其停留在内存中的时间比,n为多道程序设计的道数
- 如果内存中可以同时存放足够多的作业,则CPU利用率可以接近100%
假脱机SPOOLing
- 同时的外部设备联机操作
- 提高了I/O速度
- 设备没有分配给任何进程
- 实现了虚拟设备功能
- 若有进程要求对它打印输出时,SPOOLing系统并不是将这台打印机直接分配给进程,而是在共享设备(磁盘或磁鼓)上的输出SPOOLing存储区中为其分配一块存储空间,进程的输出数据以文件形式存放于此。各进程的数据输出文件形成了一个输出队列,由输出SPOOLing系统控制这台打印机进程,依次将队列中的输出文件实际打印输出。在SPOOLing 系统中,实际上并没有为任何进程分配,而只是在输入井和输出井中,为进程分配一存储区和建立一张I/O请求表。这样,便把独占设备改造为共享设备。
系统调用
- 用户程序和操作系统之间的接口
- 任何单 CPU 计算机一次只能执行一条指令。如果一个进程正在用户态运行一个用户程序,并且需要一个系统服务,比如从一个文件读取数据,那么它就必须执行一个陷阱(trap)或系统调用(system call)指令,将控制转移到操作系统。操作系统接着通过参数检查找出所需要的调用进程。然后,它执行系统调用,并把控制返回给在系统调用后面跟随着的指令。在某种意义上,进行系统调用就像进行一个特殊的过程调用,但是只有系统调用可以进入内核,而过程调用则不能。
- 在内核模式下执行
- 计算机系统运行状态
- 管理态,内核态
- 用户态
- Trap 指令
- 用户态到内核态
- 库过程
- 封装 trap 指令
- 在用户态下执行
- 分类
- 用于进程管理的系统调用
- 创建进程,终止进程
- 获取过程属性
- 用于文件管理的系统调用
- 创建文件,删除文件,读,写
- 获取、设置文件属性
- 用于设备管理的系统调用
- 请求设备,释放设备,读,写
- 套接字
- 打开连接,接受连接,读取信息,写信息,关闭连接
- 信息维护
- 获取当前日期,操作系统版本,等等…
- 用于进程管理的系统调用
进程与线程
进程的概念
- 进程本质上就是一个正在执行程序的实例。
- 与每个进程相关的是地址空间,这是从某个最小值的存储位置(通常是零)到某个最大值的存储位置的列表。在这个地址空间中,进程可以进行读写。
- 一个(挂起的)进程包括:进程的地址空间(往往称作磁盘映像),以及对应的进程表项。
- 停留在后台处理诸如电子邮件、Web网页、新闻、打印之类活动的进程称为守护进程。
进程运行状态
- 运行态(该时刻进程实际占用CPU)
- 就绪态(可运行,但因为其他进程正在运行而暂时停止)
- 阻塞态(除非某种外部事件发生,否则进程不能运行)
进程的创建
- 系统初始化
- 正在运行的程序执行了创建进程的系统调用
- 用户请求创建一个新进程
- 一个批处理作业的初始化
进程的终止
- 正常退出(自愿的)
- 出错退出(自愿的)
- 严重错误(非自愿)
- 被其他进程杀死(非自愿)
进程的实现
- 为了实现进程模型,操作系统维护着一张表格(一个结构数组),即进程表。每一个进程占用一个进程表项(进程控制块)。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态必须保存的信息。
线程
- 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
- 人们需要多线程的主要原因:在许多应用中同时发生着多种活动,其中某些活动会随着时间的推移被阻塞。通过将这些应用程序分解成可以准并行运行的多个顺序线程,程序设计模型会变得更简单。
- 需要多线程的理由
- 并行实体拥有共享同一个地址空间和所有可用数据的能力。对于某些应用而言,这种能力是必须的,而这正是多进程模型所无法表达的。
- 线程比进程更轻量级,所以他们更容易创建和销毁。
- 若存在着大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠进行,从而会加快应用程序执行的速度。
- 在多CPU系统中,多线程是有益的,在这样的系统中,真正的并行有了实现的可能。
线程与进程的区别
- 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
- 资源拥有:同一进程内线程共享本进程的资源,如内存、I/O、CPU等,但是进程之间的资源是独立的。
- 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃,整个进程死掉。所以多进程要比多线程健壮。
- 进程创建、切换时,消耗资源都远远大于线程。
- 进程是资源分配的最小单位,线程是程序执行的最小单位。
- 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
并发与并行
- 并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
- 并行:指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
进程间通信
- 竞争条件:两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精准时序。
- 互斥:以某种手段确保当一个进程在使用一个共享变量或文件时,其他进程不能做同样的操作。
- 临界区:对共享内存进行访问的程序片段。
- 忙等待:连续测试一个变量直到某个值出现为止
- 自旋锁:用于忙等待的锁
临界区互斥访问解决的四个条件
- 任何两个进程不能同时处于临界区
- 不应对CPU的速度和数量作任何假设
- 临界区外运行的进程不得阻塞其他进程
- 不得使用进程无限期等待进入临界区
忙等待的互斥
- 屏蔽中断
- 每个进程刚刚进入临界区后,屏蔽所有的中断。
- 若多CPU结果,只能屏蔽本CPU的中断。
- 锁变量
- 读取锁变量,若锁变量为0,则将他设为1并进入临界区。
- 读和写是两个操作,无法保证中间连续执行。
- 严格轮换法
- 整型变量turn,初始值为0,记录当前进入临界区的进程,其他进程不停访问turn,直到turn为0。
- 避免了互斥,但造成了忙等待。
- Peterson 算法
#define FALSE 0
#define TRUE 1
#define N 2 /*number of processes*/
shared int turn; /*whose turn is it?*/
shared int interested[N]; /*all values initially 0*/
void enter_region(int process)
{
int other;
other=1-process;
interested[process]=TRUE;
turn=process;
while(turn == process && interested[other] == TRUE);
}
void leave_region(int process) {
interested[process]=FALSE;
}
- TSL指令
- 需要硬件支持的方案,设计一条指令:TSL RX,LOCK,称为测试并加锁,保证读和写不可分割。
信号量 semaphore
- 一个整型变量来累计唤醒次数
- 一个信号量取值可以为0或整数
- down操作:检查其值是否大于0,若大于0,将其值减1,并继续。若其值为0,则进程将睡眠。
- up操作:对信号量值加1。
- down和up都是原子操作。
互斥量
- 互斥量是一个可以处于两态之一的变量:解锁和加锁。
管程monitor
- 管程是一种高级同步原语。是一个由过程、变量及数据结构等组成的一个集合,它们组成一个特殊的模块或软件包。进程可在任何需要的时候调用管程中的过程,但它们不能再管程之外声明的过程中直接访问管程内的数据结构。
- 管程有一个很重要的特性,即任一时刻管程中只能有一个活跃进程,这一特性使管程能有效地完成互斥。而进入管程时的互斥由编译器负责,一般的解决方法是引入条件变量,wait 和 signal 操作。
消息传递
- send(destination, &message);
- receive(source, &message);
进程调度
- 何时调度
- 创建一个新进程时,决定是执行子进程还是继续执行父进程。
- 当一个进程退出时
- 当一个进程在I/O和信号量上或其他原因阻塞时
- 在一个I/O中断发生时
- 非抢占式调度算法:挑选一个进程,让其运行直至阻塞
- 抢占式调度算法:让某个进程运行至某个固定时段的最大值
- 调度算法分类
- 批处理:非抢占式算法或对每个进程都有长时间周期的抢占式算法。
- 交互式:抢占式算法
- 实时:抢占有时是不需要的
- 调度算法的目标
- 所有系统
- 公平
- 策略强制执行
- 平衡
- 批处理系统
- 吞吐量
- 周转时间
- CPU利用率
- 交互式系统
- 响应时间
- 均衡性
- 实时系统
- 满足截止时间
- 可预测性
- 所有系统
调度算法
- 批处理系统中的调度
- 先来先服务
- 最短作业优先
- 最短剩余时间优先
- 交互式系统中的调度
- 轮转调度
- 每个进程被分配一个时间段,称为时间片,即允许该进程在该时间段中运行。如果该时间片结束时该进程还在运行,则将剥夺CPU并分配给另一个进程。如果该进程在时间片结束前阻塞或结束,则CPU立即进行切换。
- 优先级调度
- 多级队列
- 最高优先级运行一个时间片,次高优先级运行两个时间片,再次一级运行四个…
- 最短进程优先
- 根据进程过去的行为进程预测,并执行估计运行时间最短的那个,可以和之前的运行时间做加权和来预测
- 保证调度
- 确保n个进程中每个进程占用CPU的时间约为1/n
- 彩票调度
- 反应迅速,所有进程都是平等的,但是可以给更重要的进程额外的彩票
- 公平分享调度
- 以进程的所有者均分CPU时间而不论进程数目
- 轮转调度
实时系统中的调度
- 实时系统通常分类
- 硬实时:必须满足绝对的截止时间。
- 软实时:虽不希望偶尔错失截止时间,但是可以容忍。
- 实时系统中的事件按响应方式
- 周期性:以规则的时间间隔发生
- 非周期性:发生时间不可预知
- 实时系统
- 可调度
- 不可调度
- 实时系统的调度算法
- 静态调度:在系统开始运行前作出调度决策
- 动态调度:在运行过程中进行调度决策
死锁
资源
资源可以是硬件设备(如蓝光驱动器)或是一组信息(如数据库中一个加锁的记录)。
- 可抢占资源:可以从拥有他的进程中抢占而不会产生任何副作用。存储器就是一类可抢占资源。
- 不可抢占资源:在不引起相关计算失败的情况下,无法把它从占有它的进程处抢占过来。
使用一个资源所需要的事件顺序可以用抽象的形式表示如下: - 请求资源
- 使用资源
- 释放资源
死锁简介
如果一个进程集合中的每一个进程都在等待只能由该进程集合中的其他进程才能引发的事件,那么,该进程集合就是死锁的。
大多数情况下,每个进程所等待的事件是释放进程集合中其他进程所占有的资源,这种死锁被称为资源死锁。
死锁的条件
- 互斥条件:每个资源要么已经分配给了一个进程,要么就是可用的。
- 占有和等待条件:已经得到了某个资源的进程可以请求新的资源。
- 不可抢占条件:已经分配给一个进程的资源不能再强制性的被抢占,它只能被占有它的资源释放。
- 环路等待条件:死锁发生时,系统中一定有由两个或两个以上进程组成的一条环路,该环路中的每个进程都在等待着下一个进程所占有的资源。
死锁发生时,以上四个条件一定是同时满足的。如果其中任何一个条件不成立,死锁都不会发生。
处理死锁的策略
- 忽略该问题
- 检查死锁并恢复
- 仔细对资源的分配,动态地避免死锁
- 通过破坏引起死锁的四个必要条件之一,防止死锁的产生
从死锁中恢复
- 利用抢占恢复
- 利用回滚恢复
- 通过杀死进程恢复
安全状态与不安全状态
- 安全状态
- 不安全状态
- 不安全状态并不是死锁,在不安全状态系统还能运行一段时间,甚至还有一些进程可以运行完成。
- 安全状态和不安全状态的区别是:从安全状态出发,系统能够保证所有进程都能完成;而从不安全状态出发,就没有这样的保证。
单个资源的银行家算法
- Dijkstra 提出了一种能够避免死锁的调度算法,称为银行家算法,这是上面通过检测死锁算法的扩展。
- 单个资源的银行家算法:Has 表示已有数量,Max 表示最大需求,下图中 a,b安全,c 不安全
- 银行家算法就是对每一个请求进行检查,检查如果满足这一请求是否会达到安全状态。若是,那么就满足该需求;否则,就推迟对这一请求的满足。
- 为了检查状态是否安全,银行家需要考虑他是否有足够的资源满足某一客户。如果可以,那么这笔贷款是能够收回的,并且接着检查最接近最大限额的一个客户,以此类推。如果所有投资最终都能被收回,那么该状态是安全的,最初的请求可以批准。
多个资源的银行家算法
多个资源的银行家算法:左矩阵表示已分配资源,右矩阵表示仍然需要的资源,E 表示现有资源,P 表示已分配资源,A 表示可用资源
检查一个状态是否安全的算法如下:
- 查找右边的矩阵中是否有一行,其没有被满足的资源数均小于A。如果不存在这样的行,那么系统将会死锁。
- 假如找到这样的行,那么假定它获得所需的资源并运行结束,将该进程标记为终止,并将其资源加到A上。
- 重复以上两步,或者直到所有进程都被标记为终止,其初始状态是安全的;或者所有进程的资源都得不到满足,此时就是发生了死锁。
死锁预防
- 破坏互斥条件:假脱机
- 破坏占有并等待条件:
- 规定所有进程在开始执行前请求所需的全部资源。
- 当一个进程请求资源时,先释放其当前占用的资源,然后再次尝试一次获得所需的全部资源。
- 破坏不可强占条件
- 破坏环路等待条件
有关死锁的其他问题
- 两阶段加锁
- 在第一阶段,进程试图对所有所需的记录进行加锁,一次锁一个记录。如果第一阶段加锁成功,就开始第二阶段,完成更新然后释放锁。在第一阶段并没有做实际的工作。
- 如果在第一阶段某个进程需要的记录已经被加锁,那么该进程释放它所有加锁的记录,然后重新开始第一阶段。
- 通信死锁
- 资源死锁是最普遍的一种类型,但不是唯一的一种。
- 通信死锁发生在通信系统(e.g. 网络)中,一个普遍的情形是:进程 A 向进程 B 发送请求信息,然后阻塞直至 B 回复,但是请求信息在网络中丢失,A 将阻塞以等待回复,而 B 也会阻塞等待一个向其发送命令的请求,因此发送死锁。
- 可以设置适当的超时机制来解决通信死锁。
- 活锁
- 在某些情况下,当进程意识到它不能获取所需要的下一个锁时,就会释放已经得到的锁,等待1ms,然后再尝试一次,但是,如果另一个进程在相同的时刻做了相同的操作,那么两个进程就像两个人在一条路相遇并同时给对方让路一样,相同的步调将导致双方都无法前进。
- 保持饥饿:SJF
- 一些管理资源的策略可能使一些进程永远得不到服务,比如最小作业优先策略,可以使用先进先出的分配策略来避免饥饿。
内存管理
分层存储器体系
- 高速缓存
- 内存
- 磁盘存储
- 可移动存储装置
存储管理器
- 操作系统中管理分层存储器体系的部分称为存储管理器。
- 任务:有效地管理内存,即记录哪些内存是正在使用的,哪些内存是空闲的;在进程需要时为其分配内存,在进程使用完释放内存。
交换技术
有两种处理内存超载的通用方法。
- 交换技术:即把一个进程完整调入内存,使该进程运行一段时间,然后把它存回磁盘。
- 虚拟内存:能使程序在只有一部分被调入内存的情况下运行。
交换在内存中产生了多个空闲区(hole,也称为空洞),通过把所有的进程尽可能向下移动,有可能将这些小的空闲区合成一大块。该技术称为内存紧缩。
如果进程在运行时内存需要增长,为了减少因内存不够而引起的进程交换和移动所产生的开销,当换入或移动进程时可以为它分配一些额外的内存。
- 为可能增长的数据段预留空间;
- 为可能增长的数据段和堆栈段预留空间。
空闲内存管理
- 使用位图的存储管理
- 使用链表的存储管理
- 首次适配:每次从头开始搜索,找到足够大的空闲区。
- 下次适配:从上一次结束位置开始找,内存空间使用均匀。
- 最佳适配:找到能容纳进程的最小空闲区。
- 最差适配:每次找最大的空闲进行切割。大程序难以执行。
- 快速适配:为常用大小的空闲区维护单独的链表。
虚拟内存
- 基本思想:每个程序拥有自己的地址空间,这个空间被分割成多个块,每一块称作页或页面。每一页有连续的地址范围。这些页被映射到物理内存,但并不是所有的页面都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立即立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。
分页
地址通过索引,基址寄存器、段寄存器或其它方式产生。
由程序产生的这些地址称为虚拟地址,他们构成了一个虚拟地址空间。在使用虚拟内存的情况下,虚拟地址不是被直接送到内存总线上,而是被送到内存管理单元(MMU),MMU把虚拟地址映射为物理地址。
虚拟地址空间按照固定大小划分成被称为页面的若干单元,在物理内存中对应的单元称为页框。
页面置换算法
- 最优页面置换算法
- 替换未来最长时间不被访问的页
- 不可能实现
- 最近未使用页面置换算法
- 先进先出页面置换算法
- 第二次机会页面置换算法
- 为了避免FIFO算法将重要的页换出内存,Second Chance算法提供了一些改进。Second Chance算法在将页面换出内存前检查其使用位(使用位前文有介绍),如果其使用位为1,证明此页最近有被使用,猜测它还可能被使用,于是不把它置换出内存,但是把其使用位置为0,随后检查下一个页面,直到发现某页的使用位为0,将此页置换出内存。
- 时钟页面置换算法
- 将第二次机会页面置换算法中的链表改为环形链表
- 最近最少使用页面置换算法(LRU,Least Recently Used)
- 需要在内存中维护一个所有页面的链表,最近最多使用的页面在表头,最近最少使用的页面在表尾,每次访问内存时都必须更新整个链表。(找到一个页面,删除它,然后把它移动到表头)
- 最近未使用页面置换算法(NRU,Not Recently Used)
- 检查所有页面的 R 和 M 位,分为4类:(每次时钟中断清除 R 位)
- 0:没有被访问,没有被修改。
- 1:没有被访问,被修改。
- 2:被访问,没有被修改。
- 3:被访问,被修改。
- 随机选择编号最小的非空类中的一个页面淘汰。
- 主要优点:易于理解和能够有效地被实现。
- 检查所有页面的 R 和 M 位,分为4类:(每次时钟中断清除 R 位)
- 老化算法(修改后的 NRU,可以模拟 LRU)
- 与 LRU 的区别:
- 在一个时钟滴答内无法区分先后。
- 计数器只有有限位数。
- 与 LRU 的区别:
- 工作集页面置换算法(解决抖动)
- 请求调页:页面是在需要时调入的,而不是预先装入的。
- 工作集:一个进程当前正在使用的页面的集合。
- 颠簸:每执行几条指令程序就发生一次缺页中断。
- 工作集模型:不少分页系统都会设法跟踪进程的工作集,以确保在让进程运行以前,它的工作集就已在内存中了。
- 预先调页:在进程运行前就预先装入其工作集页面。
- 工作集是随着时间变化的。
- 工作集时钟页面置换算法(WSClock)
- 工作集算法有合理的性能,但它的实现开销较大。工作集时钟算法是它的一种变体,不仅具有良好的性能,并且还能高效地实现。
局部性原理
局部性原理: CPU访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中。
- 时间局部性:如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。
- 空间局部性:在最近的将来将用到的信息很可能与现在正在使用的信息在空间地址上是临近的。
分段
每个段由一个从0到最大的线性地址序列构成。各个段的长度可以是从0到某个允许的最大值之间的任何一个值。
每个段构成了一个独立的地址空间,它们可以独立的增长或减小而不会影响到其他的段。
一个段可能包括一个过程,一个数组,一个堆栈,一组数值变量,但它一般不会同时包含多种不同类型的内容。
文件系统
文件的实现
- 连续分配
- 优势
- 实现简单:记录每个文件只需记住第一块的磁盘地址和文件的块数。
- 读写性能好:单个操作就可以从磁盘上读出整个文件。只需要一次需要(第一个块),之后不再需要寻道和旋转延迟。
- 不足:随着时间的推移,磁盘会变得零碎。
- 适用于CD-ROM 这样的文件系统,所有的文件大小都事先知道,而且在后续使用中不会再改变。
- 优势
- 链表分配
- 优点
- 充分利用了每个磁盘块
- 顺序读文件很方便
- 缺点
- 指针占去了一些字节,每个磁盘块存储数据的字节不再是2的整数次幂,降低了系统运行效率。
- 优点
- 采用内存中的表进行链表分配
- 内存中这样一个表格称为文件分配表(File Allocation Table,FAT)。
- 这种方法的主要缺点是必须把整个表都存放在内存中
- i节点
- 相对于在内存中采用表的方式而言,这种机制具有很大的优势,即只有在对应的文件打开时,其 i 节点才在内存中。如果每个 i 节点占有 n 个字节,最多允许 k 个文件同时打开,那么只需在内存中提前保留 nk 个字节。
- 相对于在内存中采用表的方式而言,这种机制具有很大的优势,即只有在对应的文件打开时,其 i 节点才在内存中。如果每个 i 节点占有 n 个字节,最多允许 k 个文件同时打开,那么只需在内存中提前保留 nk 个字节。
目录的实现
目录中提供了查找文件磁盘块所需要的信息:
- 整个文件的磁盘地址(连续分配方案)
- 第一个块的编号(两种链表分配方案)
- i 节点号
存放文件属性: - 把文件属性直接存放在目录项中。
- 文件属性存放在 i 节点中而不是目录项中。目录项只有文件名和 i 节点号。
在目录中处理长文件名的两种方法:a)在行中;b)在堆中
目录项的实现:
- 线性表
- 散列表:查找迅速,但是需要复杂的管理。
共享文件
- 硬链接:使用两个文件名指向同一个内部数据结构来代表一个文件。
- 符号链接:是一类特殊的文件,这个文件包含了另一个文件的路径名(绝对路径或者相对路径)。
- 在对符号文件进行读或写操作的时候,系统会自动把该操作转换为对源文件的操作,但删除链接文件时,系统仅仅删除链接文件,而不删除源文件本身。符号链接的操作是透明的:对符号链接文件进行读写的程序会表现得直接对目标文件进行操作。某些需要特别处理符号链接的程序(如备份程序)可能会识别并直接对其进行操作。一个符号链接文件仅包含有一个文本字符串,其被操作系统解释为一条指向另一个文件或者目录的路径。它是一个独立文件,其存在并不依赖于目标文件。如果删除一个符号链接,它指向的目标文件不受影响。如果目标文件被移动、重命名或者删除,任何指向它的符号链接仍然存在,但是它们将会指向一个不复存在的文件。这种情况被有时被称为被遗弃。
- 符号链接的优点在于它能够跨越磁盘的界限,甚至可以命名在远程计算机上的文件,不过符号链接的实现不如硬链接那样有效率。
I/O
四种I/O访问机制
- 程序控制I/O
- 以打印一个字符串为例:首先,数据被复制到内核空间。然后,操作系统进入一个密闭的循环,一次输出一个字符。在输出一个字符之后,CPU 要不断地查询设备以了解它是否就绪准备接收另一个字符。这一行为通常称为轮询或忙等待。
- 程序控制 I/O 十分简单但是有缺点:直到全部 I/O 完成之前要占用 CPU 的全部时间。
- 中断驱动I/O
- 这种允许 CPU 在等待打印机变为就绪的同时做某些其他事情的方式就是使用中断。
- 当打印字符串的系统调用发出时,字符串缓冲区被复制到内核空间,并且一旦打印机准备好接收一个字符时就将第一个字符复制到打印机中。这时,CPU 要调用调度程序,并且某个其他进程将运行,请求打印字符串的进程将被阻塞,直到整个字符串打印完。
- 当打印机将字符打印完并且准备好接收下一个字符时,它将产生一个中断。这一中断将停止当前进程并且保存其状态。然后,打印机中断服务过程将运行。如果没有更多的字符要打印,中断处理程序就采取某个操作将用户进程解除阻塞。否则,它将输出下一个字符,应答中断,并且返回到中断之前正在运行的进程,该进程将从其停止的地方继续运行。
- 使用DMA的I/O
- 中断驱动 I/O 的一个明显缺点是中断发生在每个字符上。中断要花费时间,所以这一方法将浪费一定数量的 CPU 时间。这一问题的一种解决方法是使用直接存储器 DMA。
- DMA 控制器能够独立于 CPU 而访问系统总线。
- 让 DMA 控制器一次给打印机提供一个字符,而不必打扰 CPU。本质上,DMA 是程序控制 I/O,只是由 DMA 控制器而不是主 CPU 做全部工作。这一策略需要特殊的硬件(DMA 控制器),但是使 CPU 获得自由从而可以在 I/O 期间做其他工作。
- DMA 的重大成功是将中断的次数从打印每个字符一次减少到打印每个缓冲区一次。
- 使用通道的 I/O
程序题
读写者问题
typedef int semaphore;
semaphore mutex=1; // 控制对 rc 的访问
semaphore db=1; // 控制对数据库的访问
int rc=0; // 正在读或者即将读的进程数目
void reader(void){
while(TRUE){
down(&mutex);
rc=rc+1; // 现在多了一个读者
if(rc==1) down(&db); // 如果这是第一个读者
up(&mutex);
read_data_base();
down(&mutex);
rc=rc-1; // 现在少了一个读者
if (rc==0) up(&db); // 如果这是最后一个读者
up(&mutex);
use_data_read();
}
}
void writer(void){
while(TRUE){
think_up_data();
down(&db);
write_data_base();
up(&db);
}
}
理发师问题
#define CHAIRS 5
typedef int semaphore;
semaphore customers=0;
semaphore barbers=0;
semaphore mutex=1;
int waiting =0;
void barber(void){
while(TRUE){
down(&customers);
down(&mutex);
waiting=waiting-1;
up(&barbers); // 如果有顾客,理发师醒来
up(&mutex);
cut_hair();
}
}
void customer(void){
down(&mutex);
if(waiting<CHAIRS){
waiting=waiting+1;
up(&customers);
up(&mutex);
down(&barbers);
get_haircut();
}else{
up(&mutex);
}
}