清华大学ucore操作系统课笔记
操作系统
清华大学ucore操作系统课笔记
全文思维导图
1. 操作系统概述
1.1 什么是操作系统?
操作系统的定义
没有公认的精确定义
-
一个控制程序
- 一个系统软件
- 控制程序执行过程,防止错误和计算机的不当使用
- 执行用户程序,给用户程序提供各种服务
- 方便用户使用计算机系统
-
一个资源管理器
- 应用程序和硬件之间的中间层
- 管理各种计算机软硬件资源
- 提供访问计算机软件硬件资源的高效手段
- 解决资源访问冲突,保证资源公平使用
操作系统的地位
操作系统软件的组成
- shell--命令行接口
- GUI--图像用户接口
- Kernel--系统内部
- 执行各种资源管理的功能
ucore教学操作系统内核
组成
操作系统内核特征
-
并发
计算机同时存在多个运行的程序,需要OS管理和调度
-
共享
同时”访问“,互斥共享
-
虚拟
利用多道程序设计技术,让每个用户都觉得有一个计算机为他服务
-
异步
程序是走走停停的,推进速度不可知
只要运行环境相同,OS要确保程序运行结果的一致性
1.2 操作系统实例
三大家族
UNIX BSD
Linux
Windows
1.3 操作系统的演变
单用户系统
时间:1945-1955
操作系统=装载器+通用子程序库
问题:昂贵组件的低利用率
批处理系统
时间:1955-1965
顺序执行与批处理
多道程序系统
时间:1965-1980
保持多个工作在内存中并且在各个工作之间复用CPU,由顺序执行变成了多道程序的交替执行
多道系统只是为了让CPU一直处于工作状态,当目前程序出现I/O请求即暂时不再使用CPU时,CPU才去运行另一道程序。多道批处理系统的目的是为了解决人机矛盾及CPU和I/O设备速度不匹配的矛盾,提高系统的有效性(包括资源利用率和吞吐量),并不提供人机交互能力。
分时系统
时间:1970-
定时中断用于工作对CPU的复用
程序运行公平性更好,提高短作业的速度
分时系统中会将处理器的时间分成短的时间片,定时会切换不同的程序执行。这与多道批处理系统有本质上的区别。
分时系统是实现人机交互的系统。
个人电脑操作系统
个人计算机:每个用户一个系统
-
单用户
-
利用率不再是关注点
-
重点是用户界面和多媒体功能
-
很多旧的服务和功能不再存在
分布式操作系统
分布式计算机:每个用户多个系统
-
网络支持成为一个重要的功能
-
支持分布式服务
- 跨多系统的数据共享和协调
-
可能使用多个处理器
- 松、紧耦合系统
-
高可用性与可靠性的要求
计算机系统的演变
1.4 操作系统的结构
简单结构
MS-DOS
在最小的空间,设计用于提供大部分功能(1981-1994)
-
没有拆分模块
-
虽然MS-DOS在接口和功能水平没有很好地分离,主要用汇编汇编
-
每一种硬件对应一种操作系统,没有通用的
分层结构
-
操作系统分为很多层
- 每层建立在低层之上
- 最底层是硬件
- 最高层是用户界面
-
每一层仅使用更低一层的功能(操作)和服务
UNIX操作系统与C语言
-
设计用于UNIX操作系统的编码例程
-
”高级“系统编程语言创建可移植操作系统的概念
将操作系统的代码分为两部分:一部分与硬件平台无关的部分(高级语言),特定硬件平台相关的部分(汇编语言)。实现操作系统的可移植性。
ucore也是分层结构,本课程涉及到的部分(红线部分)
分层结构层次越来越复杂,会导致效率的下降
微内核结构(Microkernel)
尽可能把内核功能移到用户空间
用户模块间的通信使用消息传递
好处:灵活安全
缺点:性能下降
外核结构(Exokernel)
让内核分配机器的物理资源给多个应用程序,并让每个程序决定如何处理这些资源。
(原来操作系统的功能是由用户态的函数库来提供)
程序能链接到操作系统库(libOS)实现操作系统抽象
保护与控制分离
VMM(虚拟机管理器)
负责把真实的硬件虚拟成若干个虚拟的硬件,虚拟机管理器决定每个虚拟机可以使用哪些硬件资源
2. 启动、中断、异常和系统调用
2.1 启动
BIOS
- 计算机体系结构概述
- 计算机内存和硬盘布局
启动时计算机内存和磁盘布局
ROM只读存储
实模式只有20位,地址空间为1mb
加载程序的内存地址空间
BIOS系统调用
BIOS以中断调用的方式,提供了基本的I/O功能
- INT 10h: 字符显示
- INT 13h: 磁盘扇区读写
- INT 15h: 检测内存大小
- INT 16h: 键盘输入
只能在x86的实模式下访问
系统启动流程
CPU初始化
- CPU加电稳定后从0XFFFF0读第一条指令
- CS:IP = 0xf000:fff0
- 第一条指令是跳转指令
- CPU初始状态为16位实模式
- CS:IP是16位寄存器
- 指令指针PC = 16*CS +IP
- 最大地址空间是1MB
BIOS初始化
- 硬件自检POST
- 检测系统中内存和显卡等关键部件的存在和工作状态
- 查找并执行显卡等接口卡BIOS,进行设置初始化
- 执行系统BIOS,进行系统检测
- 检测和配置系统中安装的即插即用设备
- 更新CMOS中的扩展系统配置数据表ESCD(改写硬件配置表)
- 按指定启动顺序从软盘、硬盘或光驱启动(交出控制权)
主引导记录MBR格式
为了解决多分区启动问题,选择其中一个分区启动
最多四个分区
- 启动代码:446字节
- 检查分区表正确性
- 加载并跳转到磁盘上的引导程序
- 硬盘分区表:64字节
- 描述分区状态和位置
- 每个分区描述信息占据16字节
- 结束标志:2字节(55AA)
- 主引导记录的有效标志
分区引导扇区格式
- 跳转指令:跳转到启动代码
- 与平台相关代码
- 文件卷头:文件系统描述信息
- 启动代码:跳转到加载程序 (放在硬盘上的,可以改)
- 结束标注:55AA
加载程序(BootLoader)
系统启动规范
BIOS
- 固化到计算机主板上的程序
- 包括系统设置、自检程序和系统自启动程序
- BIOS-MBR(最多四个分区)、BIOS-GPT(全局分区表大于4个)、PXE(网络启动)
UEFI
统一可扩展固定接口
可信度检查
- 接口标准
- 在所有平台上一致的操作系统启动服务
2.2 中断、异常和系统调用
为什么需要中断、异常和系统调用?
-
在计算机运行中,内核是被信任的第三方
-
只有内核可以执行特权指令
-
方便应用程序
中断和异常希望解决的问题
- 当外设连接计算机时,会出现什么现象?
- 当应用程序处理意想不到的行为时,会出现什么现象?
系统调用希望解决的问题
- 用户应用程序是如何得到系统服务?
- 系统调用和功能调用的不同之处是是什么?
内核的进入与退出
定义
系统调用(system recall)
- 应用程序主动向操作系统发出的服务请求
异常(exception)
- 非法指令或者其他原因导致当前指令执行失败(如内存出错)后的处理请求
中断(hardware interrupt)
- 来自硬件设备的处理请求
三者比较
源头
- 中断:外设
- 异常:应用程序意想不到的行为(内核代码也可能出现问题)
- 系统调用:应用程序请求提供操作服务
响应方式
- 中断:异步(不会被感知)
- 异常:同步(必须处理异常)
- 系统调用:异步或同步
处理机制
- 中断:持续,对用户应用程序是透明的
- 异常:杀死或重新执行意想不到的应用程序指令
- 系统调用:等待或持续
中断处理机制
这里的中断是指三种形式的总称
硬件处理
- 在CPU初始化时设置中断使能标志(初始化时不进行处理)
- 依据内部或外部事件设置中断标志
- 依据中断向量调用相应中断服务例程
内核软件
- 现场保护(编译器)
- 中断服务处理(服务例程)
- 清楚中断标记(服务例程)
- 现场恢复(编译器)
中断嵌套
硬件中断服务例程可被打断
- 不同硬件中断源可能硬件中断处理时出现
- 硬件中断服务例程中需要临时禁止中断请求
- 中断请求会保持到CPU做出响应
异常服务例程可被打断
- 异常服务例程执行时可能出现硬件中断
异常服务例程可嵌套
- 异常服务例程可能出现缺页
系统调用
-
操作系统服务的编译接口
-
通常由高级语言编写(c/c++)
-
程序访问通常是通过高层次的API接口而不是直接进行系统调用
-
常见的应用程序编程接口(API)
Win32 API
用于windows
POSIX API
用于POSIX-based systems
(包括UNIX
,LINUX
,Mac OS X
的所有版本)Java API
用于JAVA
虚拟机(JVM
)
系统调用的实现
-
每个系统调用对应一个系统调用号
- 系统调用接口根据系统调用号来维护表的索引
-
系统调用接口调用内核态中的系统调用功能实现,并返回系统调用的状态和结构
-
用户不需要知道系统调用的实现
- 需要设置调用参数和获取返回结果
- 操作系统接口的细节大部分都隐藏在应用编程接口后
- 通过运行程序支持的库来管理
函数调用和系统调用的不同之处
- 系统调用
INT
和IRET
指令用于系统调用- 系统调用时,堆栈切换和特权级的转换
- 函数调用
CALL
和RET
用于常规调用- 常规调用时没有堆栈切换
三者开销
开销大于函数调用
中断、异常和系统调用开销
- 引导机制
- 建立内核堆栈
- 验证参数
- 内核态映射到用户态的地址空间
- 更新页面映射权限
- 内核态独立地址空间
TLB
系统调用示例
ucore
中库函数read()
的功能是读文件
user/libs/file.h: int read(int fd, void *buf, int length)
库函数read()
的参数和返回值
int fd
—文件句柄void *buf
—数据缓冲区指针int length
—数据缓冲区长度int return_value
: 返回读出数据长度
库函数read()
使用示例
in sfs_filtest1.c: ret = read(fd, data, len)
read函数实现
-
kern/trap/tranpentry.S: alltraps()
获取中断信息组成的数据结构 -
kern/trap/trap.c: trap()
tf->trapno == T_SYSCALL
(系统调用对应的中断向量) -
kern/syscall/syscall.c: syscall()
tf->tf_regs.reg_eax == SYS.read
(系统调用编号) -
kern/syscal/syscall.c: sys_read()
从
tf->sp
获取fd, buf, length
-
kern/fs/sysfile.c: sysfile_read()
读取文件
-
kern/trap/trapentry.S: trapret()
IRET
3. 内存管理
3.1 计算机体系结构/内存层次
计算机体系结构
内存层次
操作系统的内存管理方式
-
抽象
-
保护
-
共享
-
虚拟化
内存管理方式
- 重定位(relocation)一个进程占一个连续的地址空间(太大不好放置)
- 分段(segmentation)将程序分段,段内地址连续
- 分页(paging)
- 虚拟存储(virtual memory)
- 目前多数系统如linux 采用按需页式虚拟存储
实现高度依赖硬件
- 与计算机存储架构耦合
- MMU(内存管理单元):处理CPU存储访问请求的硬件
3.2 地址空间 & 地址生成
地址空间定义
物理地址空间——硬件支持的地址空间
- 起始地址0,直到\(MAX_{sys}\)
逻辑地址空间——在CPU运行的进程看到的地址
- 起始地址0,直到\(MAX_{prog}\)
逻辑地址生成
编译->汇编->链接->程序加载(重定位)
地址生成时机和限制
- 编译时
- 假设起始地址已知
- 如果起始地址改变,必须重新编译
- 加载时
- 编译时起始位置未知,编译器需生成可重定位的代码(relocatable code)
- 加载时,生成绝对地址
- 执行时
- 执行时代码可移动
- 虚地址转换(映射)硬件支持
地址生成过程
- CPU
- ALU:需要逻辑地址的内容
- MMU:进行逻辑地址和物理地址的转换
- CPU控制逻辑:给总线发送物理地址请求
- 内存
- 发送物理地址的内容给CPU
- 或接收CPU数据到物理地址
- 操作系统
- 建立逻辑地址LA和物理地址PA的映射
地址检查
3.3 连续内存分配
给进程分配一块不小于指定大小的连续的物理内存区域(重定位)
内存碎片
空闲内存不能被利用
外部碎片:分配单元之间的未被使用内存
内部碎片:分配单元内部的未被使用内存,取决于分配单元大小是否要取整
动态分配
动态分区分配
- 当程序被加载执行时,分配一个进程指定大小可变的分区(块,内存块)
- 分区的地址是连续的
操作系统需要维护的数据结构
- 所有进程的已分配分区
- 空闲分区(empty-blocks)
动态分区分配策略
最先匹配
找到第一个满足的就行
原理&实现
- 空闲分区列表按照地址顺序排序
- 分配过程中,搜索一个合适的分区(大于指定大小)
- 释放分区时,检查是否可与临近的空闲分区合并
优点
- 简单
- 在高地址空间有大块的空闲分区
缺点
- 外部碎片
- 分配大块时较慢
最佳匹配
遍历一个最佳的
原理&实现
- 空闲分区列表按照大小排序
- 分配时,查找一个合适的分区
- 释放时,查找并且合并临近的空闲分区(如果找到)
优点
- 大部分分配的尺寸较小时,效果很好
- 可避免大的空闲分区被拆分
- 可减小外部碎片的大小
- 相对简单
缺点
- 外部碎片
- 释放分区较慢
- 容易产生很多无用的小碎片
最差匹配
找到最大空闲空间
原理&实现
- 空闲分区列表按由大到小排序
- 分配时,选最大的分区
- 释放时,检查是否可与临近的空闲分区合并,进行合并,并调整空闲分区列表顺序
优点
- 中等大小的分配较多时,效果最好
- 避免出现太多的小碎片
缺点
- 释放分区较慢
- 外部碎片
- 容易破坏大的空闲分区,因此难以分配大的分区
碎片整理
通过调整进程占用的分区位置来减少或避免分区碎片
碎片紧凑(compaction)
- 通过移动分配给进程的内存分区,以合并外部碎片
- 碎片紧凑的条件
- 所有的应用程序可动态重定位
- 解决的问题
- 运行时不能搬动
- 开销
分区对换(swap in/out)
通过抢占并回收处于等待状态进程的分区,以增大可用内存空间
需要解决的问题?
- 交换哪个进程
开销大
3.4 伙伴系统
buddy system
整个可分配的分区大小为 \(2^u\),需要的分区大小\(s\)
- 若 \(2^{u-1} < s \leq 2^u\),把整个块分配给该进程
- 若 \(s \leq 2^{u-1}\),将当前空闲分区等分为两个大小相同的空闲分区,重复划分,直到大于0.5倍区域
伙伴系统的实现
数据结构
- 空闲块按大小和起始地址组织成二维数组
- 初始状态:只有一个空闲块
分配过程
- 由小到大在空闲块数组中找最小的可用空闲块
- 如空闲块过大,对可用空闲块进行二等分,直到得到合适的可用空闲块
释放过程
- 把释放的块放入空闲块数组
- 合并满足合并条件的空闲块
合并条件
- 大小相同\(2^i\)
- 地址相邻
- 起始地址较小的块的起始地址必须是\(2^{i+1}\)的倍数
用于做内核的分配
例子
3.5 非连续内存分配
连续内存分配缺点
- 分配给程序的物理内存必须连续
- 存在外碎片和内碎片
- 内存分配的动态修改困难
- 内存利用率低
设计目标
提高内存利用效率和管理灵活性
- 允许一个程序使用非连续的物理地址空间
- 允许共享代码与数据
- 支持动态加载和动态链接
实现
需要解决的问题
- 如何实现虚拟地址和物理地址的转换
- 软件实现(灵活,开销大)
- 硬件实现(够用,开销小)
- 非连续分配的硬件辅助机制
- 如何选择非连续分配中的内存分块大小
- 段式存储管理(segmentation)
- 页式存储管理(paging)
- 如何选择非连续分配中的内存分块大小
3.5 段式存储管理
段地址空间
进程的段地址空间由多个段组成
- 主代码段
- 子模块代码段
- 公用库代码段
- 堆栈段(stack)
- 堆数据(heap)
- 初始化数据段
- 符号表等
段式存储的目的:更细粒度和灵活的分离与共享
段内需要连续,段之间不连续
段访问机制
段的概念
- 段表示访问方式和存储数据等属性相同的一段地址空间
- 对应一个连续的内存“块”
- 若干个段组成进程逻辑地址空间
段访问
逻辑地址由二元组(s, addr)表示
- s: 段号
- addr: 段内偏移
段访问的硬件实现
3.6 页式存储管理
概念
页帧(帧、物理页面,Frame,Page Frame)
- 把物理地址空间划分为大小相同的基本分配单位
- 2的n次方,如512,4096,8192
- 物理内存被划分成大小相等的帧
- 内存物理地址的表示:二元组\((f, o)\)
- \(f\)——帧号(\(F\)位,共有\(2^F\)个帧)
- \(o\)——帧内偏移(\(S\)位,每帧有\(2^S字节\))
- 物理地址 \(= f * 2^S + o\)
计算实例
页面(页、逻辑页面,Page)
-
进程逻辑地址空间被划分为大小相等的页
-
页内偏移 = 帧内偏移
-
通常:页号大小不等于帧号大小
-
进程逻辑地址的表示:二元组 \((p, o)\)
- \(p\)——页号(\(P\)位,共有\(2^P\)个帧)
- \(o\)——页内偏移(\(S\)位,每帧有\(2^s字节\))
- 虚拟地址 \(= p * 2^S + o\)
页面到页帧
- 逻辑地址到物理地址
- 页表
- MMU/TLB
地址转换
- 页到帧的映射
- 逻辑地址中的页号是连续的
- 物理地址中的帧号是不连续的
- 不是所有的页都有对应的帧
转换过程
3.7 页表
页表结构
-
每个进程都有一个页表
- 每个页面对应一个页表项
- 随进程运行状态而动态变化
- 页表基址寄存器(PTBR:Page Table Base Register)(存储基地址位置)
- 页表项组成:
- 帧号: f
- 页表项标志:
- 存在位 resident bit
- 修改位 dirty bit
- 引用位 clock / reference bit
页表转换实例
性能问题
- 内存访问性能问题
- 访问一个内存单元需要2次访问
- 第一次访问:获取页表项
- 第二次访问:访问数据
- 访问一个内存单元需要2次访问
- 页表大小问题
- 页表可能非常大
- 64位机器如果每页1024字节,那么一个页表的大小会是多少?
- 如何处理?
- 缓存(Caching)(减少访存次数,快表)
- 间接(Indirection)访问(多级页表)
快表TLB
translation look-aside Buffer
缓存近期访问的页表项
-
TLB使用关联存储(associateive memory)实现,具备快速访问性能
-
如果TLB命中,物理页号可用很好被获取
-
未命中,对应的表项被更新到TLB中
多级页表
通过间接引用将页号分成k级
- 建立页表“树”
- 减少每级页表的长度
- 通过存在位,若不存在,则不构建下一级的页表
实例
反置页表
减少页表存储空间
大地址空间(64-bits)系统,多级页表变得繁琐
- 比如:5级页表
- 逻辑(虚拟)地址空间增长速度快于物理地址空间
页寄存器和反置页面的思路
- 不让页表与逻辑地址空间的大小相对应
- 让页表与物理地址空间的大小相对应
页寄存器
page registers
物理帧直接与页寄存器关联
每个物理帧与一个页寄存器(page register)关联,寄存器内容包括:
- 使用位:此帧是否被进程占用
- 占用页号:对应的页号p
- 保护位: 标记可读可写
页寄存器示例
- 物理内存大小:4096*4096 = 4 K * 4 KB = 16 MB
- 页面大小:4096 bytes = 4 KB
- 页帧数:4096 = 4 K
- 页寄存器使用的空间(假设每个页寄存器占8字节):
- 8 * 4096 = 32 KB
- 页寄存器带来的开销:
- 32 K / 16 M = 0.2%
- 虚拟内存的大小:任意
优点
- 页表大小相对于物理内存而言很小
- 页表大小与逻辑地址空间大小无关
缺点
- 页表信息对调后,需要依据帧号可找页号
- 在页寄存器中搜索逻辑地址的页号
页寄存器中的地址转换
-
对逻辑地址进行Hash映射,以减小搜索范围
-
需要解决可能的冲突
用快表缓存页表项后的页寄存器搜索步骤
- 对逻辑地址进行Hash变换
- 在快表中查找对应页表项
- 有冲突时遍历冲突项链表
- 查找失败时,产生异常
快表的限制
- 快表的容量限制
- 功耗限制(StrongARM上快表功耗占27%)
反置页表
所有的进程共同使用一张页表,这张页表中的条目的数量和内存中物理的页框的数量是一样的。
反置页表中每个条目拥有以下字段:
- 页号
- 进程ID
- 控制位
- 链接指针---如果出现进程共享内存的情况,就会用到链接指针
基于Hash映射值查找对应页表项中的帧号
- 进程标识与页号的Hash值可能有冲突
- 页表项中包括保护位、修改位、访问位和存在位等表示
3.8 段页式存储管理
段式存储在内存保护方面有优势,页式存储在内存利用和优化转移到存储方面有优势
在段式存储管理基础上,给每个段加一级页表
内存共享
通过指向相同的页表基地址,实现进程间的段共享
4. 虚拟存储
4.1 需求背景
程序规模的增长速度远远大于存储器容量的增长速度
理想的存储器
- 容量大,速度快,价格低的非易失性存储器
实际存储器
-
存储器层次结构
存储抽象
计算机系统时常出现内存空间不够用
-
覆盖
应用程序手动把需要的指令和数据保存在内存中
-
交换
操作系统自动把暂时不能执行的程序保存到外存中
-
虚拟存储
在有限容量的内存中,以页为单位自动装入更多更大程序
虚拟存储技术的目标
只把部分程序放到内存中,从而运行比物理内存大的程序
- 由操作系统自动完成
实现进程在内存与外存之间的交换,从而获取更多的空闲内存空间
- 在内存和外存之间只交换进程的部分内容
4.2 覆盖技术
在较小的可用内存中运行较大的程序
实现方法
依据程序逻辑结构,将程序划分为若干功能相对独立的模块,将不会同时执行的模块共享同一块内存区域。
- 必要部分的代码和数据常驻
- 可选部分放在其他程序模块中,在需要的时候装载
- 不存在调用关系的模块,可相互覆盖,共同用同一块内存区域
示例
不足
-
增加编程困难
-
增加执行时间
4.3 交换技术
增加正在运行或需要运行的程序的内存
实现方法
-
可将暂时不能运行的程序放到外存
-
换入换出的基本单位
- 整个进程的地址空间
-
换出
- 把一个进程的整个地址空间保存到外存
-
换入
- 将外存中某进程的地址空间读入到内存
问题
- 何时交换?
- 只当内存空间不够或有不够的可能时换出
- 交换区大小
- 存放所有用户进程的所有内存映像的拷贝
- 换入之后的重定位(地址位置可能发生了改变)
- 采用动态地址映射的方法
覆盖与交换的比较
覆盖
- 只能发生在没有调用关系的模块间
- 程序员需要给出模块间的逻辑覆盖结构
- 发生在运行程序的内部模块间
交换
- 以进程位单位
- 不需要模块间的逻辑覆盖结构
- 发生在内存进程间
4.4 局部性原理
principle of locality
程序在执行过程中的一个较短时期,所执行的指令地址和指令的操作数地址,分别局限于一定区域
-
时间局部性
一条指令或者数据的一次执行和下次执行(访问)都集中在一个较短的时期内
-
空间局部性
当前指令和临近的几条指令,都集中在一个较小的区域
-
分支局部性
一条跳转指令的两次执行,很可能跳到相同的内存位置
4.5 虚拟存储概念
将不常用的部分内存块暂存到外存
原理
-
装载程序时
只将当前指令执行需要的部分页面或段装入内存
-
指令执行中需要的指令和数据不在内存中称为缺页或缺段
处理器通知操作系统将相应的页面或段调入内存
-
操作系统将内存中暂时不用的页面或段保存到外存中去
实现方式
- 虚拟页式存储
- 虚拟段式存储
基本特征
- 不连续性
- 物理内存分配非连续
- 虚拟地址空间使用非连续
- 大用户空间
- 提供给用户的虚拟内存大于实际的物理内存
- 部分交换
- 虚拟存储只对部分虚拟地址空间进行调入和调出
支持技术
- 硬件
- 页式或短时存储中的地址转换机制
- 操作系统
- 管理内存和外存间页面或段的换入和换出
4.6 虚拟页式存储
在页式存储管理的基础上,增加请求调页和页面置换
思路
- 当用户程序装载到内存运行时,只装入部分页面,就启动程序
- 进程在运行中发现有需要的代码或数据不在内存时,则向系统发出缺页异常请求
- 操作系统在处理缺页异常时,将外存中相应的页面调入内存,使得进程能继续进行
虚拟页式存储中的地址转换
页表项结构
- 驻留位:表示该页是否在内存
- 1:在内存,页表项有效,可用
- 0:在外存,访问该页表项将导致缺页异常
- 修改位:表示在内存中的该页是否被修改过
- 回收该物理页面时。据此判断是否要把它的内容写回
- 访问位:表示该页面是否被访问过(读或写)
- 用于页面置换算法
- 保护位:表示该页的访问方式
- 可读可写等
虚拟页式存储示例
X86页表结构
4.7 缺页异常
处理流程
外存管理
- 何处保存未被映射的页?
- 方便找到外村中的页面内容
- 交换空间(磁盘或者文件)
- 采用特殊格式存储未被映射的页面
- 虚拟页式存储中的外存选择
- 代码段:可执行二进制文件
- 动态加载的共享库程序段:动态调用的库文件
- 其他段:交换空间
虚拟页式存储管理的性能
-
有效存储访问时间(effective memory access time EAT)
EAT = 访存时间*(1-p)+缺页异常处理时间 *缺页率 p
5. 置换算法
5.1 概念
当出现缺页异常,需调入新页面而内存已满时,置换算法选择被置换的物理页面
设计目标
-
尽可能减少页面的调入调出次数
-
把未来不再访问或短期内不访问的页面调出
页面锁定(frame locking)
- 描述必须常驻内存的逻辑页面
- 操作系统的关键部分
- 要求响应速度的代码和数据
- 页表中的锁定标注位 lock bit
评价方法
- 记录进程访问内存的页面轨迹
- 评价方法
- 模拟页面置换行为,记录产生缺页的次数
- 更少的缺页,更好的性能
分类
- 局部页面置换算法
- 置换页面的选择范围仅限于当前进程占用的物理页面内
- 最优算法、先进先出算法、最近最久未使用算法
- 时钟算法、最不常用算法
- 全局页面置换算法
- 置换页面的选择范围是所有可换出的物理页面
- 工作集算法、缺页率算法
5.2 局部页面置换算法
最优置换算法
置换在未来最长时间不访问页面
算法实现
- 缺页时,计算内存中每个逻辑页面的下次访问时间
- 选择未来最长时间不访问的页面
算法特征
- 缺页最少,是理想情况
- 无法预知访问时间,无法实现
- 作为置换算法的性能评价依据
算法示例
先进先出算法
First-In, First-out, FIFO
算法实现
- 维护一个记录所有位于内存中的逻辑页面链表
- 链表元素按驻留内存的时间排序,链首最长,链尾最短
- 出现缺页是,选择链首进行置换,新页面加到链尾
特征
- 实现简单
- 性能较差,调出的页面可能是经常访问的
- 进程分配物理页面数增加时,缺页并不一定减少(Belady现象)
- 很少单独使用
算法示例
最近最久未使用算法
Least Recently Used, LRU
算法思路
- 选择最长时间没有被引用的页面进行置换
- 如某些页面长时间未被访问,则再将来还可能会长时间不会被访问
算法实现
- 缺页时,计算内存中每个逻辑页面的上次访问时间
- 选择上一次使用到当前时间最长的页面
特征
- 最优置换算法的近似
算法示例
LRU算法的可能实现方法
页面链表
- 系统维护一个按最近一次访问时间排序的页面链表
- 链表首节点是最近使用的页面
- 链表尾节点是最久未使用的页面
- 访问内存时,找到相应的页面,并把它移到链表之首
- 缺页时,置换链表尾节点的页面
活动页面栈
- 访问页面时,将此号压入栈顶,并将栈内相同的页号抽出
- 缺页时,置换栈底的页面
特征
- 开销大
算法示例
时钟置换算法
Clock
仅对页面的访问情况进行大致统计
数据结构
- 在页表项增加访问位
- 页面组织成环形链表
- 指针指向最先调入的页面
算法
- 访问页面时,在页表项记录页面访问情况
- 缺页时,从指针处开始顺序查找未被访问的页面进行置换
特征
算法是LRU和FIFO的折中
算法实现
- 页面装如内存时,访问位初始化位0
- 访问页面时,访问位置1
- 缺页时,从指针当前位置顺序检查环形链表
- 访问位为0,则置换该页
- 访问位为1,则访问位置0,并指针移动到下一页面,直到找到可置换的页面
算法示例
改进的Clock算法
减少修改页的缺页处理开销
算法
- 在页面中增加修改位,并在访问时进行相应修改
- 缺页时,修改页面标志位,以跳过有修改的页面
算法示例
最不常用算法
LFU Least Frequently Used
缺页时,置换访问次数最少的页面
算法实现
- 每个页面设置一个访问计数
- 访问页面时,访问计算+1
- 缺页时,置换计算最小的页面
特征
- 算法开销大
- 开始时频繁使用,但以后不使用的页面很难置换
- 解决方法:计数定期右移
LRU和LFU区别
- LRU关注多久未访问,时间越短越好
- LFU关注访问次数,次数越多越好
算法示例
Belady现象
采用FIFO等算法时,可能出现分配的物理页面数增加,缺页次数反而升高的异常现象
原因
- FIFO算法的置换特征与进程访问内存的动态特征矛盾
- 被它置换出去的页面并不一定是进程近期不会访问的
思考
哪些算法没有此现象?
FIFO有,LRU没有此现象
FIFO算法示例
三个页,缺页次数9
四个页,缺页次数10
LRU、FIFO和Clock的比较
LRU算法和FIFO本质上都是先进先出的思路
- LRU依据页面的最近访问时间排序
- LRU需要动态调整顺序
- FIFO依据页面进入内存的时间排序
- FIFO的页面进入时间是固定不变的
LRU可退化成FIFO
- 所有页面进入内存后没有再次访问,最近访问和进入时间相同
LRU算法性能好,但系统开销较大
FIFO算法系统开销较小,会发生Belady现象
Clock算法是它们的折中
- 页面访问时,不动态调整页面在链表中的顺序,仅做标记
- 缺页时,再把它移动到链表末尾
对于未被访问的页面,Clock和LRU算法的表现以一样好
对于被访问过的页面,Clock算法不能记录准确访问顺序,而LRU可以
5.3 全局页面置换算法
工作集置换算法
给进程分配可变数目的物理页面
需要解决的问题
- 进程在不同阶段的内存需求是变化的
- 分配给进程的内存也需要在不同的阶段有所变化
- 全局置换算法需要确定分配给进程的物理页面数
CPU利用率与并发进程数的关系
CPU利用率与并发进程存在相互促进与制约的关系
- 进程数少时,提高并发进程数,可提高CPU利用率
- 并发进程导致内存访问增加
- 并发进程的内存访问会降低了访存的局部性特征
- 局部特征的下降会导致缺页率上升和CPU利用率下降
工作集
一个进程当前正在使用的逻辑页面集合,可表示位二元函数\(W(t, \Delta)\)
- \(t\)当前时刻
- \(\Delta\)称为工作集窗口,即一个定长的页面访问时间窗口
- \(W(t, \Delta)\)是指在当前时刻 $t $ 前的 $\Delta $ 时间窗口中所有访问页面所组成的集合
- \(|W(t, \Delta)|\)指工作集的大小,即页面数目
工作集的变化
-
进程开始执行后,随着访问新页面逐步建立较稳定的工作集
-
当内存访问的局部性区域的位置大致稳定时,工作集的大小也大致稳定
-
局部性区域的位置改变时,工作集快速扩张和收缩过渡到下一个稳定值
常驻集
在当前时刻,进程实际驻留在内存当中的页面集合
工作集与常驻集的关系
- 工作集是进程在运行过程中固有的性质
- 常驻集取决于系统分配给进程的物理页面数目和页面置换算法
缺页率与常驻集的关系
- 常驻集包含工作集时,缺页率小
- 工作集发生剧烈变动时,缺页多
- 进程常驻集大小达到一定数目后,缺页率也不会明显下降
思路
- 换出不在工作集中的页面
窗口大小x
- 当前时刻前x个内存访问的页引用是工作集,x被称为窗口的大小
实现方法
- 访存链表:维护窗口内的访存页面链表
- 访存时,换出不在工作集的页面;更新访存链表
- 缺页时,换入页面;更新访存链表
算法示例
缺页率置换算法
PFF,Page-Fault-Frequency
缺页率(page fault rate)
缺页次数/内存访问次数 或 缺页平均时间间隔的倒数
- 影响缺页率的因素
- 页面置换算法
- 分配给进程的物理页面数目
- 页面大小
- 程序的编写方法
通过调节常驻集大小,使每个进程的缺页率保持一个合理的范围内
- 若进程缺页率过高,则增加常驻集以分配更多的物理页面
- 若进程缺页率过低,则减少常驻集以减少物理页面
算法实现
- 访存时,设置引用标志位
- 缺页时,计算从上次缺页时间到现在时间的间隔
- 如果时间间隔大于给定时间T,则置换所有在间隔时间内没有被引用的页
- 如果时间间隔小于等于给定时间T,则增加缺失页到常驻集
缺页率置换算法示例
窗口大小为2
抖动和负载控制
抖动问题(thrashing)
- 进程物理页面太少,不能包含工作集
- 造成大量缺页,频繁置换
- 进程运行速度变慢
产生抖动的原因
- 随着驻留内存的进程数目增加,分配给每个进程的物理页面数不断减少,缺页率上升
操作系统需在并发水平和缺页率之间达到一个平衡
- 选择一个适当的进程数目和进程需要的物理页面数
负载控制
通过调节并发进程数(MPL)来进行系统负载控制
- \(\sum WSi\) = 内存的大小
- 平均缺页时间间隔(MTBF)= 缺页异常处理时间(PFST)
6. 进程
6.1 进程的概念
进程是指一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程
进程组成
进程包含了正在运行的一个程序的所有状态信息
- 代码
- 数据
- 状态寄存器
- 通用寄存器
- 进程占用系统资源
进程特点
- 动态性
- 并发性
- 进程可以被独立调度并占用处理机运行
- 独立性
- 不同的进程不会互相影响
- 制约性
- 因访问共享数据、资源或进程间同步而产生的制约
进程与程序的联系
- 进程是操作系统处于执行状态程序的抽象
- 程序 = 文件(静态可执行文件)
- 进程 = 执行中的程序 = 程序 + 执行状态
- 同一个程序的多次执行过程对应为不同进程
- 如命令 ls 的多次执行对应多个不同进程
- 进程执行需要的资源
- 内存:保存代码和数据
- CPU:执行指令
进程与程序的区别
- 进程是动态的,程序是静态的
- 程序是有序代码的集合
- 进程是程序的执行,进程有核心态、用户态
- 进程是暂时的,程序是永久的
- 进程是一个状态变化的过程
- 程序可长久保存
- 进程与程序的组成不同
- 进程的组成包括程序、数据和进程控制块
6.2 进程控制块
PCB,Process Control Block
操作系统管理控制进程运行所用的信息集合
- 操作系统由PCB来描述进程的基本情况以及运行变化的过程
- PCB是进程存在的唯一标志
- 每个进程都在操作系统中有一个对应的PCB
使用流程
- 进程创建
- 生成该进程的PCB
- 进程终止
- 回收它的PCB
- 进程的组织管理
- 通过对PCB的组织管理来实现
控制块内容
- 进程标识信息
- 处理机现场保护
- 进程控制信息
进程控制信息
- 调度信息和状态信息
- 进程和处理机使用情况调度
- 进程间通信信息
- 进程间通信相关的各种标识
- 存储管理信息
- 指向进程映像存储空间数据结构
- 进程所占资源
- 进程使用的系统资源,如打开文件等
- 有关数据结构连接信息
- 与PCB相关的进程队列
进程控制块的组织
链表
同一状态的进程其PCB成一链表,多个状态对应多个不同的链表
- 各状态的进程形成不同的链表:就绪链表、阻塞链表
索引表
同一状态的进程归入一个索引表(由索引指向PCB)
多个状态对应多个不同的索引表
- 各状态的进行形成不同的索引表:就绪索引表、阻塞索引表
6.3 进程状态
进程的生命周期划分
- 进程创建
- 进程执行
- 进程等待
- 进程抢占
- 进程唤醒
- 进程结束
进程切换
6.4 三状态进程模型
6.5 挂起进程模型
处于挂起状态的进程映像在磁盘上,目的是减少进程占用内存
- 等待挂起状态(Blocked-suspend)
- 进程在外存并等待某事件的出现
- 就绪挂起状态(Ready-suspend)
- 进程在外存,但只要进入内存,即可运行
- 挂起(Suspend):把一个进程从内存转到外存
- 等待到等待挂起
- 没有进程处于就绪状态或就绪进程要求更多内存资源
- 就绪到就绪挂起
- 当由高优先级等待(系统认为会很快就绪的)进程和低优先级就绪进程
- 运行到就绪挂起
- 对抢先式分时系统,当有高优先级等待挂起进程因事件出现而进入就绪挂起
- 等待到等待挂起
在外存时的状态转换
-
等待挂起到就绪挂起
- 当有等待挂起进程因相关事件出现
-
激活(Activate):把一个进程从外存转到内存
-
就绪挂起到就绪
没有就绪进程或挂起就绪进程优先级高于就绪进程
-
等待挂起到等待
当一个进程释放足够内存,并有高优先级等待挂起进程
-
状态队列
- 由操作系统来维护一组队列,表示系统中所有进程的当前状态
- 不同队列表示不同状态
- 就绪队列、各种等待队列
- 根据进程状态不同,进程PCB加入相应队列
7. 线程
7.1 为什么需要线程?
单进程播放
多进程实现
解决思路
在进程内部增加一类实体,满足以下特性:
- 实体之间可以并发执行
- 实体之间共享相同的地址空间
这种实体就是线程(Thread)
7.2 线程的概念
线程是进程的一部分,描述指令流执行状态。它是进程中的指令执行流的最小单元,是CPU调度的基本单位。
进程的资源分配角色:进程由一组相关资源构成,包括地址空间(代码段、数据段)、打开的文件等各种资源
线程的处理机调度角色:线程描述在进程资源环境中的指令流执行状态
进程和线程的关系
线程 = 进程 - 共享资源
线程的优点
- 一个进程中可以同时存在多个线程
- 各个线程之间可以并发执行
- 各个线程之间可以共享地址空间和文件资源
线程的缺点
- 一个线程崩溃,会导致其所属进程的所有线程崩溃
不同操作系统对线程的支持
路由器就是单进程多线程系统
进程与线程的比较
- 进程是资源分配的单位,线程是CPU调度单位
- 进程拥有一个完整的资源平台,而线程只独享指令流执行的必要资源,如寄存器和栈
- 线程具有就绪、等待和运行三种基本状态和状态间的转换关系
- 线程能减少并发执行的时间和空间开销
- 线程的创建时间比进程短
- 线程的终止时间比进程短
- 同一进程内的线程切换时间比进程短
- 由于同一进程的各线程间共享内存和文件资源,可不通过内核进行直接通信
7.3 用户线程
线程的三种实现方式
-
用户线程:在用户空间实现
POSIX Pthreads, Mach C-threads, Solaris threads
-
内核线程:在内核中实现
Window,Solaris, Linux
-
轻量级进程:在内核中实现,支持用户线程
Solaris (LightWeight Process)
概念
由一组用户级的线程库函数来完成线程的管理,包括线程的创建、终止、同步和调度等。
- 不依赖与操作系统的内核
- 内核不了解用户线程的存在
- 可用于不支持线程的多进程操作系统
- 在用户空间实现的线程机制
- 每个进程由私有的线程控制块(TCB)列表
- TCB由线程库函数维护
- 同一进程内的用户线程切换速度快
- 无需内核态和用户态切换
- 允许每个进程拥有自己的线程调度算法
不足
- 线程发起系统调用而阻塞时,则整个系统进入等待
- 不支持基于线程的处理机抢占
- 除非当前运行线程主动放弃,他所在进程的其他线程无法抢占CPU
- 只能按进程分配CPU时间
- 多个线程进程中,每个线程时间会变少
7.4 内核线程
进程由内核通过系统调用实现的线程机制,由内核完成线程的创建、终止和管理。
特征
- 由内核维护PCB和TCB
- 线程执行系统调用而被阻塞不影响其他线程
- 线程的创建、终止和切换相对较大
- 通过系统调用/内核函数,在内核实现
- 以线程为单位进行CPU时间分配
- 多线程的进程可获得更多CPU时间
7.5 轻权进程(LightWeight Process)
内核支持的用户线程。一个进程可有一个或多个轻量级进程,每个轻权进程由一个单独的内核线程来支持。
用户线程和内核线程的对应关系
8. 进程控制
8.1 进程切换
(进程切换)上下文切换
- 暂停当前运行进程,从运行状态变成其他状态
- 调度另一个进程从就绪状态变成运行状态
进程切换的要求
- 切换前,保存进程上下文
- 切换后,恢复进程上下文
- 快速切换
进程生命周期的信息
- 寄存器(PC,SP, )
- CPU状态
- 内存地址空间
进程控制块PCB:内核的进程状态记录
- 内核为每个进程维护了对应的进程控制块(PCB)
- 内核将相同状态的进程的PCB放置在同一队列
8.2 进程创建
Windows进程创建API:CreateProcess(filename)
-
创建时关闭所有在子进程里的文件描述符
CreateProcess(filename,CLOSE_FD)
-
创建时改变子进程的环境
CreateProcess(filename,CLOSE_FD,new_envp)
-
等等
Unix进程创建系统调用:fork/exec
-
fork()把一个进程复制成二个进程
parent(old PID),child(new PID)
-
exec()用新程序来重写当前进程
PID没有该变
fork的地址空间复制
fork使用示例
代码
8.3 进程加载
程序加载和执行系统调用exec()
- 允许进程,“加载”一个完全不同的程序,并从main开始执行(即_start)
- 允许进程加载时指定启动参数(argc, argv)
- exec调用成功时
- 是相同的进程
- 运行了不同的程序
- 代码段、堆栈和堆(heap)等完全重写
8.4 进程等待与退出
wait()系统调用用于父进程等待子进程的结束
- 子进程结束时通过exit()向父进程返回一个值
- 父进程通过wait()处理返回值
wait()系统调用的功能
-
有子进程存活时,父进程进入等待状态,等待子进程的返回结果
当某子进程调用exit()时,唤醒父进程,将exit()返回值作为父进程中wait的返回值
-
有僵尸子进程等待时,wait()立即返回其中一个值
-
无子进程存活时,wait()立刻返回
程序的有序终止exit()
- 进程结束执行时调用exit,完成资源回收
- exit系统调用的功能
- 将调用参数作为进程的“结果”
- 关闭所有打开的文件等占用资源
- 释放内存
- 释放大部分进程相关的内核数据结构
- 检查是否父进程是存活着的
- 如存活,保留结果的值直到父进程需要它,进入僵尸(zombie/defunct)状态
- 如果没有,它释放所有的数据结构,进程结束
- 清理所有等待的僵尸进程
- 进程终止是最终的垃圾收集(资源回收)
9. 处理机调度
9.1 处理机调度概念
CPU资源的时分复用
-
进程切换:CPU资源的当前占用者切换
- 保存当前进程在PCB中的执行上下文(CPU状态)
- 恢复下一个进程的执行上下文
-
处理机调度
- 从就绪队列中挑选下一个占用CPU运行的进程
- 从多个可用CPU中挑选就绪进程可使用的CPU资源
-
调度程序:挑选就绪进程的内核函数
调度策略:依据什么原则挑选进程/线程?
调度时机:什么时候进行调度?
调度时机
- 内核运行调度程序的条件
- 进程从运行状态切换到等待状态
- 进程被终结了
- 非抢占系统
- 当前进程主动放弃CPU时
- 可抢占系统
- 中断请求被服务例程响应完成时
- 当前进程被抢占
- 进程时间片用完
- 进程从等待切换到就绪
9.2 调度准则
调度策略
确定如何从就绪队列中选择下一个执行进程
调度策略要解决的问题
- 挑选就绪队列中的哪一个进程?
- 通过什么样的准则来挑选?
调度算法
- 在调度程序中实现的调度策略
比较调度算法的准则
- 哪一个策略/算法较好?
- CPU使用率
- CPU处于忙状态的时间百分比
- 吞吐量
- 单位时间内完成的进程数量
- 周转时间
- 进程从初始化到结束的总时间
- 等待时间
- 进程在就绪队列中的总时间
- 响应时间
- 从提交请求搭配产生响应所花费的总时间
- CPU使用率
处理机资源的使用模式
- 进程在CPU计算和I/O操作间交替
- 每次调度决定在下一个CPU计算时将哪个工作交给CPU
- 在时间片机制下,进程可能在结束当前CPU计算前被迫放弃CPU
吞吐量与延迟
-
调度算法的要求
- 希望更快的服务
-
什么是更快?
- 传输文件的高带宽、调度算法的高吞吐量
- 玩游戏的低延迟,调度算法的低响应延迟
- 这两个因素是独立的
-
与水管的类比
- 低延迟:喝水的时候想要一打开水龙头就留出来
- 高带宽:给游泳池充水希望从水龙头里同时流出大量的水,不介意是否存在延迟
处理机调度策略的响应时间目标
-
减少响应时间
及时处理用户的输入请求,尽快输出反馈给用户
-
减少平均响应时间的波动
在交互系统中,可预测性比高差异低平均更重要
-
低延迟调度改善了用户的交互体验
如果移动鼠标时,屏幕中的光标没动,用户可能会重启电脑
-
响应时间是操作系统的计算延迟
处理机调度策略的吞吐量目标
- 增加吞吐量
- 减少开销(操作系统开销,上下文切换)
- 系统资源的高效利用(CPU,I/O设备)
- 减少等待时间
- 减少每个进程的等待时间
- 操作系统需要保证吞吐量不受用户交互的影响
- 操作系统必须不时进行调度,即使存在许多交互任务
- 吞吐量是操作系统的计算带宽
处理机调度策略的公平性目标
- 公平的定义
- 保证每个进程占用相同的CPU时间
- 保证每个进程的等待时间相同
9.3 调度算法
先来先服务算法
FCFS:First Come First Served
依据进程进入就绪状态的先后顺序排列
- 进程进入等待或结束状态时,就绪队列中的下一个进程占用CPU
FCFS算法的周转时间
先来先服务算法的特征
-
优点
- 简单
-
缺点
-
平均等待时间波动较大
短进程可能排在长进程后面
-
I/O资源和CPU资源的利用率低
CPU密集型进程会导致I/O设备闲置时,I/O密集型进程也等待
-
短进程优先算法
SPN
选择就绪队列中执行时间最短进程占用CPU进入运行状态
- 就绪队列按预期的执行时间来排序
短剩余时间优先算法(SRT):可运行抢占CPU,只要执行时间更短
短进程优先算法具有最优平均周转时间
缺点
-
可能导致饥饿
连续的短进程流会使长进程无法获得CPU资源
-
需要预知未来
- 如何预估下一个CPU计算的持续时间
- 简单的解决方法:询问用户
- 用户欺骗就杀死进程
- 用户不知道
短进程优先算法的执行时间预估
- 用历史的执行时间来预估未来的执行时间
最高响应比优先算法
HRRN
选择就绪队列中响应比R值最高的进程
- 基于短进程优先算法基础上改进
- 不可抢占
- 关注进程的等待时间
- 防止无限制等待
时间片轮转算法
RR,Round-Robin
时间片
- 分配处理机资源的基本时间单元
算法思路
- 时间片结束时,按FCFS算法切换到下一个就绪进程
- 每隔(n-1)个时间片进程执行一个时间片q
算法示例
时间片长度
- RR算法开销
- 额外的上下文切换
- 时间片太大
- 等待时间太长
- 极限情况退化成FCFS
- 时间片太小
- 反应快,但产生大量上下文切换
- 大量上下文切换开销影响到系统吞吐量
- 时间片长度选择目标
- 选择一个合适的时间片长度
- 经验规则:维持上下文切换开销处于1%以内
多级队列调度算法
MQ
- 就绪队列被划分成多个独立的子队列
- 如:前台(交互)、后台(批处理)
- 每个队列拥有自己的调度策略
- 如:前台-RR、后台-FCFS
- 队列间的调度
- 固定优先级
- 先处理前台,然后处理后台
- 可能导致饥饿
- 时间片轮转
- 每个队列都得到一个确定的能够调度其进程的CPU总时间
- 如:80%CPU时间用于前台,20%CPU时间用于后台
- 固定优先级
多级反馈队列算法
MLFQ
- 进程可在不同队列间移动的多级队列算法
- 时间片大小随优先级级别增加而增加
- 如进程在当前的时间片没有完成,则降到下一个优先级
- MLFQ算法的特征
- CPU密集型进程的优先级下降很快
- I/O密集型进程停留在高优先级
公平共享调度算法
FSS,Fair Share Scheduling
FSS控制用户对系统资源的访问
- 一些用户组比其他用户组更重要
- 保证不重要的组无法垄断资源
- 未使用的资源按比例分配
- 没有达到资源使用率目标的组获得更高的优先级
总结
- 先来先服务算法(FCFS)
- 不公平,平均等待时间较差
- 短进程优先算法(SPN)
- 不公平,平均周转时间最小
- 需要精确预测计算时间
- 可能导致饥饿
- 最高响应比算法(HRRN)
- 基于SPN调度
- 不可抢占
- 时间片轮转算法(RR)
- 公平,平均等待时间较差
- 交互性很好
- 多级反馈队列算法(MLFQ)
- 多种算法的集成
- 公平共享调度算法(FSS)
- 公平是第一要素
9.4 实时调度和多处理器调度
实时操作系统
实时操作系统的定义
正确性依赖于其时间和功能两方面的操作系统
实时操作系统的性能指标
- 时间约束的及时性(deadlines)
- 速度和平均性能相对不重要
实时操作系统的特性
- 时间约束的可预测性
实时任务
- 任务(工作单元)
- 一次计算,一次文件读取,一次信息传递等等
- 任务属性
- 完成任务所需要的资源
- 定时参数
周期实时任务
一系列相似的任务
- 任务有规律地重复
- 周期 p = 任务请求时间间隔 (0 < p)
- 执行时间 e = 最大执行时间 (0 < e < p)
- 使用率 U = e / p
硬时限和软时限
- 硬时限(hard deadline)
- 错过任务时限会导致灾难性或非常严重的后果
- 必须验证,在最坏情况下能够满足时限
- 软时限(soft deadline)
- 通常能满足任务时限
- 有时不能满足,则降低要求
- 尽力保证满足任务时限
- 通常能满足任务时限
可调度性
可调度表示一个实时操作系统能够满足任务时限要求
- 需要确定实时任务的执行顺序
- 静态优先级调度
- 动态优先级调度
速率单调调度
(RM,Rate Monotonic)
- 通过周期安排优先级
- 周期越短优先级越高
- 执行周期最短的任务
最早截止时间优先算法
(EDF,Earilest Deadline First)
- 截止时间越早优先级越高
- 执行截止时间最早的任务
多处理器调度
特征
- 多个处理机组成的一个多处理机系统
- 多处理机间可负载共享
对称多处理器(SMP,Symmetric multiprocessing)调度
- 截止时间越早优先级越高每个处理器运行自己的调度程序
- 调度程序对共享资源的访问要进行同步
对称多处理器的进程分配
- 静态进程分配
- 进程从开始到结束都被分配到一个固定的处理机上执行
- 每个处理机有自己的就绪队列
- 调度开销小
- 各处理机可能忙闲不均
- 动态进程分配
- 进程在执行种可分配到任意空闲处理机执行
- 所有处理机共享一个公共的就绪队列
- 调度开销大
- 各处理机的负载是均衡的
9.5 优先级反置
priority inversion
-
操作系统中出现高优先级进程长时间等待低优先级进程所占用资源的现场
-
基于优先级的可抢占调度算法存在优先级反置
例如:有优先级为A、B和C三个任务,优先级A>B>C,任务A,B处于挂起状态,等待某一事件发生,任务C正在运行,此时任务C开始使用某一共享资源S。在使用中,任务A等待事件到来,任务A转为就绪态,因为它比任务C优先级高,所以立即执行。当任务A要使用共享资源S时,由于其正在被任务C使用,因此任务A被挂起,任务C开始运行。如果此时任务B等待事件到来,则任务B转为就绪态。由于任务B优先级比任务C高,因此任务B开始运行,直到其运行完毕,任务C才开始运行。直到任务C释放共享资源S后,任务A才得以执行。在这种情况下,优先级发生了翻转,任务B先于任务A运行。
优先级继承
Priority Inheritance
占用资源的低优先级进程继承申请资源的高优先级进程的优先级
- 只在占有资源的低优先级进程被阻塞时,才提高占有资源进程的优先级
优先级天花板协议
priority ceiling protocol
占有资源进程的优先级和所有可能申请该资源的进程的最高优先级相同
-
不管是否发生等待,都提升占用资源进程的优先级
-
优先级高于系统中的所有被锁定的资源的优先级上限,任务执行临界区时就不会被阻塞
10. 同步互斥
10.1 背景
独立进程:不和其他进程共享资源或状态
- 确定性 输入决定结果
- 可重现 能够重现起始条件
- 调度顺序不重要
并发进程:在多个进程间有资源共享
- 不确定性
- 不可重现
并发进程的正确性
- 执行过程是不确定性和不可重现的
- 程序错误可能是间歇性发生的
进程并发执行的好处
进程需要与计算机中的其他进程和设备进行协作
-
共享资源
- 多个用户使用一台计算机
- 银行账户存款余额在多台ATM机操作
- 机器人上的手臂和手的动作
-
加速
- I/O操作和CPU计算可用重叠(并行)
- 程序可划分成多个模块放在多个处理器上并行执行
-
模块化
- 将大程序分解成小程序
- 以编译为例,gcc会调用cpp,cc1,cc2
- 是系统易于复用和扩展
可能导致的错误
- 将大程序分解成小程序
原子操作
Atomic Operation
原子操作是一次不存在任何中断或失败的操作
- 要么操作完成
- 或者操作没有执行
- 不会出现部分执行的状态
操作系统利用同步机制在并发执行的同时,保证一些操作是原子操作
10.2 现实生活中的同步问题
操作系统和现实生活的问题类比
- 利用现实生活问题帮助理解操作系统同步问题
- 同时注意,计算机与人的差异
例如:家庭采购协调
如何保证家庭协调的成功和高效
- 有人去买
- 需要采购时,有人去买面包
- 最多只有一个人去买面包
方案一
- 使用便签来避免购买太多面包
- 购买之前留下一张便签
- 买完后移除
- 别人看到就不去购买
分析
面包还是买多了
方案二
先留便签,后检查面包和便签
会导致不会有人买面包
方案三
为便签增加标识,以区别不同人的便签
还是会导致有人不买
方案四
两个人采用不同的处理流程
- 有效,但复杂
- 很难验证其有效性
- A和B的代码不同
- 每个进程的代码也会略有不同
- 如果进程更多,怎么办?
- 当A在等待时,它不能做其他事
- 忙等待(busy-waiting)
方案五
利用两个原子操作实现一个锁(lock)
- Lock.Acquire()
- 在锁被释放前一直等待,然后获得锁
- 如果两个线程都在等待同一个锁,并且同时发现锁被释放了,那么只有一个能够获得锁
- Lock.Relese()
- 解锁并唤醒任何等待种的进程
进程的交互关系:相互感知程度
- 互斥(mutual exclusion)
- 一个进程占用资源,其它进程不能使用
- 死锁(deadlock)
- 多个进程各占用部分资源,形成循环等待
- 饥饿(starvation)
- 其他进程可能轮流占用资源,一个进程一直得不到资源
10.3 临界区
临界区(Critical Section):进程中访问临界资源的一段需要互斥执行的代码
进入区(entry section):
- 检查可否进入临界区的一段代码
- 如可进入,设置相应“正在访问临界区”标志
退出区(exit section):
- 清除“正在访问临界区”标志
剩余区(remainder section)
- 代码中其余部分
临界区的访问规则
- 空闲则入
- 没有进程在临界区,任何进程可进入
- 忙则等待
- 有进程在临界区,其他进程均不能进入临界区
- 有限等待
- 等待进入临界区的进程不能无限期等待
- 让权等待(可选)
- 不能进入临界区的进程,应释放CPU(如转换到阻塞状态)
临界区实现方法
- 禁用硬件中断
- 软件同步方法
- 更高级的抽象方法
不同的临界区实现机制的比较
- 性能:并发级别
10.4 禁用硬件中断
- 没有中断,没有上下文切换,因此没有并发执行
- 硬件将中断处理延迟到中断被启用之后
- 现代计算机体系结构都提供指令来实现禁用中断
- 进入临界区
- 禁止所有中断,并保存标志
- 离开临界区
- 使能所有中断,并恢复标志
缺点
- 禁用中断后,进程无法被停止
- 整个系统都会为此停下来
- 可能导致其他进程处于饥饿状态
- 临界区可能很长
- 无法确定响应中断所需的时间(可能存在硬件影响)
- 要小心使用
10.5 软件同步方法
第一次尝试
满足“忙则等待”,但是有时不满足“空闲则入”
- Ti不在临界区,Tj想要继续运行,但是必须等待Ti进入过临界区后
- Ti可能会被阻塞,所以Tj就会一直等待,不满足空闲则入
第二次尝试
不满足忙则等待
第三次尝试
满足忙则等待,不满足空闲则入
Peterson算法
- 满足线程Ti和Tj之间互斥的经典的基于软件的解决方法
Dekkers算法
N线程的软件方法
Eisenberg和McGuire
分析
- 复杂
- 需要两个进程间的共享数据项
- 需要忙等待
- 浪费CPU时间
10.5 更高级的抽象方法
硬件提供了一些同步原语
- 中断禁用,原子操作指令等
操作系统提供更高级的变成抽象来简化进程同步
- 例如:锁、信号量
- 用硬件原语来构建
锁(lock)
锁是一个抽象数据结构
-
是一个二进制变量(锁定/解锁)
-
Lock::Acquire()
锁被释放前一直等待,然后得到锁
-
Lock::Release()
释放锁,唤醒任何等待的进程
使用锁来控制临界区访问
原子操作指令
现代CPU体系结构都提供一些特殊的原子操作指令
- 测试和置位(Test-and-Set)指令
- 从内存单元中读值
- 测试该值是否为1(然后返回真或假)
- 内存单元值设置为1
-
交换指令(exchange)
- 交换内存中的两个值
自旋锁
使用TS指令实现自旋锁(spinlock)
线程在等待的时候消耗CPU时间
无忙等待锁
schedule()放弃占用CPU,让CPU调度其他进程
wakeup(t)唤醒线程,占用资源
原子操作指令锁的特征
优点
- 适用于单处理器或者共享主存的多处理器中任意数量的进程同步
- 简单并且容易证明
- 支持多临界区
缺点
- 如果是忙等待锁会占用CPU时间
- 可能导致饥饿
- 进程离开临界区时有多个等待进程的情况
- 死锁
- 拥有临界区的低优先级进程,请求访问临界区的高优先级进程获得处理器并等待临界区
总结
- 锁是一种高级的同步抽象方法
- 互斥可以使用锁来实现
- 需要硬件支持
- 常用的三种同步实现方法
- 禁用中断(仅限于单处理)
- 软件方法(复杂)
- 原子操作指令(单处理器或多处理器均可)
11. 信号量与管程
11.1 信号量
并发问题
- 多线程并发导致资源竞争
同步概念
- 协调多线程对共享数据的访问
- 任何时刻只能有一个线程执行临界区代码
确保同步正确的方法
- 底层硬件支持
- 高层次的编程抽象
基本同步方法
信号量的定义
semaphore:信号量
信号量是操作系统提供的一种协调共享资源访问的方法
- 软件同步是平等线程间的一种同步协商机制
- OS是管理者,地位高于进程
- 用信号量标识系统资源的数量
由Dijkstra在20世纪60年代提出,早期的操作系统的主要同步机制
- 现在很少用---但还是非常重要在计算机科学研究
信号是一种抽象的数据类型
- 由一个整型(sem)变量和两个原子操作P和V
- P()(Prolaag(荷兰语尝试减少)
- sem减一
- 如sem<0,进入等待,否则继续
- V()(Verhoog(荷兰语增加))
- sem加1
- 如sem<=0,唤醒一个等待进程
信号量与铁路的类比
- 2个站台的车站
- 2个资源的信号量
信号量是被保护的整数变量
- 初始化完成后,只能通过P()和V()操作修改
- 由操作系统保证,PV操作是原子操作
P()可能阻塞,V()不会阻塞
- P操作可能没有资源而进入等待状态
通常假定信号量是“公平的”
- 线程不会被无限期阻塞在P()操作
- 假定信号量等待按先进先出排队
自旋锁不能实现先进先出
信号量的实现
信号量的使用
信号量分类
- 二进制信号量:资源数目为0或1
- 资源信号量:资源数目为任何非负值
- 两者等价:基于一个可以实现另一个
信号量的使用
-
互斥访问
-
临界区的互斥访问控制
-
-
条件同步
-
线程间的时间等待
-
生产者-消费者问题
有界缓冲区的生产者-消费者问题描述
- 一个或多个生产者在生成数据后放在一个缓冲区里
- 单个消费者从缓冲区取出数据处理
- 任何时刻只能有一个生产者或消费者可访问缓冲区
问题分析
- 任何时候只能有一个线程操作缓冲区(互斥访问)
- 缓冲区空时,消费者必须等待生产者(条件同步)
- 缓冲区满时,生产者必须等待消费者(条件同步)
用信号量描述每个约束
- 二进制信号量 mutex
- 资源信号量 fullBuffers
- 资源信号量 emptyBuffers
PV操作不能调换,检查缓冲区是否有空地和占有缓冲区的操作不能调换
因为一旦先占有缓冲区,那就无法释放缓冲区,导致死锁现象
阻塞不代表会释放资源,资源的释放是通过对变量的加减来进行的。
使用信号量的困难
- 读开发代码比较困难
- 程序员需要能运用信号量机制
- 容易出错
- 使用信号量已经被另一个线程占用
- 忘记释放信号量
- 不能处理死锁问题(只能在写程序的时候解决)
11.3 管程
Moniter
管程是一种多线程互斥访问共享资源的程序结构
- 采用面向对象方法,简化了线程间的同步控制
- 任一时刻最多只有一个线程执行管程代码
- 正在管程中的线程可临时放弃管程的互斥访问,等待事件出现时恢复
管程的使用
- 在对象/模块,收集相关共享数据
- 定义访问共享数据的方法
管程的组成
-
一个锁
- 控制管程代码的互斥访问
-
0个或者多个条件变量
- 管理共享数据的并发访问
条件变量(Condition Variable)
- 条件变量是管程内的等待机制
- 进入管程的线程因资源被占用进入等待状态
- 每个条件变量表示一种等待原因,对应一个等待队列
- wait()操作
- 将自己阻塞在等待队列中
- 唤醒一个等待者或释放管程的互斥访问
- signal()操作
- 将等待队列中的一个线程唤醒
- 如果等待队列为空,则等同空操作
条件变量实现
wait操作中会释放对资源的占用,进程返回回来之后还会再次占据lock
生产者-消费者问题
因为wait会释放对资源的占用,所以检查缓冲区和资源的占用的顺序可用调换,不会造成死锁的现象
条件变量的释放处理方式
Hansen管程:主要用于真实OS、Java中
Hoare管程:主要见于教材中
11.4 经典同步问题
哲学家就餐问题
问题描述:
-
5个哲学家围绕一张圆桌而坐
- 桌子上放着5支叉子
- 每两个哲学家之间放一支
-
哲学家的动作包括思考喝进餐
- 进餐时需要同时拿到左右两边的叉子
- 思考时将两只叉子放回原处
-
如何保证哲学家们的动作有序进行?
如:不出现有人永远拿不到叉子?
方案一
方案二
方案三
读者-写者问题
问题描述
- 共享数据的两类使用者
- 读者:只读取数据,不修改
- 写者:读取和修改数据
- 对共享数据的读写
- 读-读允许:允许多个读者同时读
- 读-写互斥
- 写-写互斥
用信号量解决读者-写者问题
用信号量描述每个约束
- 信号量 WriteMutex
- 控制读写操作的互斥
- 初始化为1
- 读者计数 Rcount
- 正在进行读操作的读者数目
- 初始化为0
- 信号量 CountMutex
- 控制对读者计数的互斥修改
- 初始化为1
- 读者优先策略
- 只要有读者正在读状态,后来的读者都能直接进入
- 如读者持续不断进入,则写者就处于饥饿
- 写者策略
- 只要有写者就绪,写者应尽快执行写操作
- 如写者持续不断就绪,则读者就处于饥饿状态
用管程解决读者-写者问题
写者优先
判断AW+WW是因为采用了Hansen管程,在返回途中可能被写操作抢先了
12. 死锁和进程通信
12.1 死锁
由于竞争资源或通信关系,两个或更多线程在执行中出现,永远相互等待只能由其他进程引发的事件
死锁示例
进程访问资源的流程
- 资源类型\(R_1,R_2, ...,R_m\)
- CPU执行时间、内存空间、I/O设备
- 每类资源\(R_i\)有\(W_i\)个实例
- 进程访问资源的流程
- 请求/获取:申请空闲资源
- 使用/占用:进程占用资源
- 释放
资源分类
-
可重用资源
- 资源不能被删除且在任意时刻只能有一个进程使用
- 进程释放资源后,其他进程可重用
- 可重用资源示例
- 硬件:处理器、I/O通道、主和副存储器、设备等
- 软件:文件、数据库和信号量等数据结构
- 可能出现死锁
- 每个进程占用一部分资源并请求其他资源
-
消费资源
- 资源创建和销毁
- 消耗资源示例
- 在I/O缓冲区的中断、信号、消息等
- 可能出现死锁
- 进程间相互等待接收对方的消息
资源分配图
描述资源和进程间的分配和占用关系的有向图
出现死锁的必要条件
- 互斥
- 任何时刻只能有一个进程使用一个资源实例
- 持有并等待
- 进程至少保持一种资源,并正在等待获取其他进程持有的资源
- 非抢占
- 资源只能在进程使用后资源释放
- 循环等待
12.2 死锁处理方法
死锁预防(Deadlock Prevention)
- 确保系统永远不会进入死锁状态
死锁避免(Deadlock Avoidance)
- 在使用前进行判断,只允许不会出现死锁的进程请求资源
死锁检测和恢复(Deadlock Detection & Recover)
- 在检测到运行系统进入死锁状态后,进行恢复
由应用进程处理死锁
- 通常操作系统忽略死锁
- 大多数操作系统(包括UNIX)的做法
死锁预防
限制申请方式
预防是采用某种策略,限制并发进程对资源的请求,使系统在任何时刻都不满足死锁的必要条件。
- 互斥
- 把互斥的共享资源封装成可同时访问
- 持有并等待
- 进程请求资源时,要求它不持有任何其他资源
- 仅允许进程在开始执行时,一次请求所有需要的资源
- 资源利用率低
- 非抢占
- 如进程请求不能立即分配的资源,则释放已占有资源
- 只在能够同时获取所有需要资源时,才执行分配操作
- 循环等待
- 对资源排序,要求进程按顺序请求资源
死锁避免
利用额外的先验信息,在分配资源时判断是否会出现死锁,只在不会死锁时分配资源
-
要求进程声明需要资源的最大数目
-
限定提供与分配的资源数目,确保满足进程的最大需求
-
动态检查资源分配状态,确保不会出现环形等待
系统资源分配的安全状态
- 当进程请求资源时,系统判断分配后是否处于安全状态
- 系统处于安全状态
- 针对所有已占用进程,存在安全序列
安全状态与死锁的关系
12.3 银行家算法
银行家算法是一个避免死锁产生的算法。以银行借贷分配策略为基础,判断并保证系统处于安全状态
- 客户在第一次申请贷款时,声明所需最大资金量,在满足所有贷款要求并完成项目时,即使规划
- 在客户贷款数量不超过银行拥有的最大值时,银行家尽量满足客户要求
- 类比
- 银行家 → 操作系统
- 资金 → 资源
- 客户 → 申请资源的进程
数据结构
n = 线程数量,m = 资源类型数量
安全状态判断算法
当前的剩余资源是否可以满足其中一个线程的未来需要
遍历完成实际是找到一个安全序列
银行家算法
判断示例
不安全的
12.4 死锁检测
- 允许系统进入死锁状态
- 维护系统的资源分配图
- 定期调用死锁检测算法来搜索图中是否存在死锁
- 出现死锁时,用死锁恢复机制进行恢复
数据结构
- Available:长度为m的向量,每种类型可用资源的数量
- Allocation:一个n×m矩阵,当前分配给各个进程每种类型资源的数量
检测算法
检测示例
算法使用
- 死锁检测的时间和周期选择依据
- 死锁多久可能会发生
- 多少进程需要回滚
- 资源图可能有多个循环
- 难于分辨“造成”死锁的关键进程
死锁恢复:进程终止
- 终止所有的死锁进程
- 一次只终止一个进程直到死锁消除
- 终止进程的顺序:
- 进程的优先级
- 进程的已运行时间以及还需运行时间
- 进程已占用资源
- 进程完成需要的资源
- 终止进程的数目
- 进程是交互还是批处理
死锁恢复:资源抢占
- 选择被抢占进程
- 最小成本目标
- 进程回退
- 返回到一些安全状态,重启进程到安全状态
- 可能会出现饥饿
- 同一进程可能一直被选为被抢占者
12.5 进程通信
IPC,Inter-Process Communication
- 进程通信是进程进行通信和同步的机制
- IPC提供两个基本操作
- 发送操作:send(message)
- 接收操作:receive(message)
- 进程间通信
- 在通信进程间建立通信链路
- 通过send/receive交换信息
- 进程链路特征
- 物理(如共享内存,硬件总线)
- 逻辑(如逻辑属性)
通信方式
直接通信
- 进程必须正确的命名对方
- send(P,message)发送信息到进程P
- receive(Q,message)从进程Q接收消息
- 通信链路的属性
- 自动建立链路
- 一条链路恰好对应一对通信进程
- 每队进程之间只有一个链接存在
- 链接可以是单向和双向的
间接通信
-
通过操作系统维护的消息队列实现进程间的消息接收和发送
- 每个消息队列都有一个唯一的标识
- 只有共享了相同消息队列的进程,才能够通信
-
通信链路的属性
- 只有共享了相同消息队列的进程,才建立连接
- 连接可以是单向和双向的
- 消息队列可以与多个进程相关联
- 每队进程可以共享多个消息队列
-
通信流程
- 创建一个新的消息队列
- 通过消息队列发送和接收消息
- 销毁消息队列
-
基本通信操作
- send(A,message)发送消息到队列A
- receive(A,message)从队列A接收消息
阻塞与非阻塞通信
同步与异步通信
阻塞通信
- 阻塞发送:发送者在发送消息后进入等待,直到接收者成功收到
- 阻塞接收:接收方在请求接收消息后进入等待,直到成功收到一个消息
非阻塞通信
- 非阻塞发送:发送者在发送消息后,可立即进行其他操作
- 非阻塞接收:没有消息发送时,接收者在请求接收消息后,接收不到任何消息
通信链路缓冲
缓冲方式
- 0容量:发送方必须等待接收方
- 有限容量:通信链路缓冲队列满时,发送方必须等待
- 无限容量:发送方无需等待
12.6 信号和管道
信号
信号的定义
进程间的软件中断通知和处理机制
- 如:SIGKILL,SIGSTOP,SIGCONT等
信号的接收处理
- 捕获(catch):执行进程指定的信号处理函数被调用
- 忽略(Ignore):执行操作系统指定的缺省处理
- 例如:进程终止、进程挂起等
- 屏蔽(Mask):禁止进程接收和处理信号
- 可能是暂时的(当处理同样类型的信号)
不足
- 传送的信息量小,只有一个信号类型
信号的实现
使用示例
管道
进程间基于内存文件的通信机制
- 子进程从父进程继承文件描述符
- 缺省文件描述符:
0 stdin
1 stdout
2 stderr
进程不知道(或不关心)的另一端
- 可能从键盘、文件、程序读取
- 可能写入到终端、文件、程序
与管道相关的系统调用
-
读管道:
read(fd, buffer, nbytes)
scanf()
基于它实现的
-
写管道:
write(fd, buffer, nbytes)
printf()
基于它实现的
-
创建管道:
pipe(rgfd)
rgfd
是2个文件描述符组成的数组rgfd[0]
是读文件描述符rgfd[1]
是写文件描述符
管道示例
12.7 消息队列和共享内存
消息队列
消息队列是由操作系统维护的以字节序列为基本单位的间接通信机制
- 消息是一个字节序列
- 相同标识的消息组成按先进先出顺序组成一个消息队列(message queues)
消息队列的系统调用
-
msgget(key, flags)
获取消息队列标识
-
msgsnd(QID, buf, size, flags)
发送消息
-
msgrcv(QID, buf, size, type, flags)
接收消息
-
msgctl(...)
消息队列控制
共享内存
共享内存是把同一个物理内存区域同时映射到多个进程的内存地址空间的通信机制
进程
- 每个进程都有私有内存地址空间
- 每个进程的内存地址空间需明确设置共享内存段
线程
- 同一进程中的线程总是共享相同的内存地址空间
优点
- 快速方便地共享数据
不足
- 必须用额外的同步机制来协调访问数据
共享内存系统调用
-
shmget(key, size, flags)
创建共享段
-
shmat(shmid, *shmaddr, flags)
把共享段映射到进程地址空间
-
shmdt(*shmaddr)
取消共享段到进程地址空间
-
shmctl(...)
共享段控制
-
需要信号量等机制协调共享内存的访问冲突
13. 文件系统
13.1 文件系统的概念
文件系统和文件
文件系统是操作系统中管理持久性数据的子系统,提供数据存储和访问功能。
- 组织、检索、读写访问功能
- 大多数计算机系统都有文件系统
- Google也是一个文件系统
文件是具有符号名,由字节序列构成的数据项集合
- 文件系统的基本数据单位
- 文件名是文件的表示符号
文件系统的功能
-
分配文件磁盘空间
- 管理文件块(位置和顺序)
- 管理空闲空间(位置)
- 分配算法(策略)
-
管理文件集合
- 定位:文件及其内容
- 命名:通过名字找到文件
- 文件系统结构:文件组织方式
-
数据可靠和安全
- 安全:多层次保护数据安全
- 可靠
- 持久保存文件
- 避免系统崩溃,媒体错误,攻击等
文件属性
- 名称、类型、位置、大小、保护、创建者、创建时间、最近修改时间...
文件头:文件系统元数据中的文件信息
- 文件属性
- 文件存储位置和顺序
文件描述符
文件访问模式
- 进程访问文件数据前必须先“打开”文件
内核跟踪进程打开的所有文件
- 操作系统为每个进程维护一个打开文件表
- 文件描述符是打开文件的标识
操作系统在打开文件表中维护的打开文件状态和信息
- 文件指针:最近一次读写位置,每个进程分别维护自己的打开文件指针
- 文件打开计数:当前打开文件的次数,最后一个进程关闭文件时,将其从打开文件表中移除
- 文件的磁盘位置:缓存数据访问位置
- 访问权限:每个进程的文件访问模式信息
文件的用户视图和系统视图
文件的用户视图
- 持久的数据结构
系统访问接口
- 字节序列的集合(UNIX)
- 系统不关心存储在磁盘上的数据结构
操作系统的文件视图
- 数据块的集合
- 数据块是逻辑存储单元,而扇区是物理存储单元
- 块大小<>扇区大小(大小可能不一样)
用户视图到系统视图的转换
进程读文件
- 获取字节所在的数据块
- 返回数据块内对应部分
进程写文件
- 获取数据块
- 修改数据块中对应部分
- 写回数据块
文件系统中的基本操作单位是数据块
访问模式
- 操作系统需要了解进程如何访问文件
- 顺序访问:按字节一次读取
- 大多数的文件访问都是顺序访问
- 随机访问:从中间读取?
- 不常用,但重要
- 例如:虚拟内存中把内存页存储在文件
- 不常用,但重要
- 索引访问:依据数据特征索引
- 通常操作系统不完整提供索引访问
- 数据库是建立在索引内容的磁盘访问上
索引文件示例
文件内部结构
- 无结构
- 单词、字节序列
- 简单记录结构
- 分列
- 固定长度
- 可变长度
- 复杂结构
- 格式化文档(Word PDF)
- 可执行文件等
文件共享和访问控制
- 多用户系统中的文件共享是很必要的
- 访问控制
- 每个用户能够获得哪些文件的哪些访问权限
- 访问模式:读、写、执行、删除、列表等
- 文件访问控制列表(ACL)
- <文件实体,权限>
- 文件访问控制列表(ACL)
- <用户|组|所有人,读|写|可执行>
- 用户标识ID
- 识别用户,表明每个用户所允许的权限及保护模式
- 组标识ID
- 允许用户成组,并指定了组访问权限
语义一致性
- 规定多进程如何同时访问共享文件
- 与同步算法相似
- 因磁盘I/O和网络延迟而设计简单
- UNIX文件系统(UFS)语义
- 对打开文件的写入内容立即对其他打开同一文件的其他用户可见
- 共享文件指针允许多用户同时读取和写入文件
- 会话语义
- 写入内容只有当文件关闭时可见
- 读写锁(一些基本的互斥访问锁,用户进程选择)
- 一些操作系统和文件系统提供该功能
目录、文件别名和文件系统种类
- 文件以目录的方式组织起来
- 目录是一类特殊的文件
- 目录的内容是文件索引表<文件名,指向文件的指针>
- 目录和文件的树形结构
- 早期的文件系统是扁平的(只有一层目录)
目录操作
典型目录操作
- 搜索文件
- 创建文件
- 删除文件
- 列目录
- 重命名文件
- 遍历路径
操作系统应该只允许内核修改目录
- 确保映射的完整性
- 应用程序通过系统调用访问目录
目录实现
- 文件名的线性列表,包涵了指向数据块的指针
- 编程简单
- 执行耗时
- 哈希表——哈希数据结构的线性表
- 减少目录搜索时间
- 冲突——两个文件名的哈希值相同
- 固定大小
文件别名
两个或多个文件名关联同一个文件
硬链接:多个文件项指向一个文件
软链接:以”快捷方式“指向其他文件
- 通过存储真实文件的逻辑名称来实现
若删除文件,硬链接的文件还是会在,软链接的文件就不在了
文件目录中的循环
名字解析(路径遍历)
- 名字解析:把逻辑名字转换成物理资源
- 依据路径名,在文件系统中找到实际文件位置
- 遍历文件目录直到找到目标文件
-
当前工作目录(PWD)
-
每个进程都会指向一个文件目录用于解析文件名
-
允许用户指定相对路径来代替绝对路径
如,用PWD = ”/bin“ 能够解析 ”ls“
-
文件系统挂载
- 文件系统需要先挂载才能被访问
- 未挂载的文件系统被挂载在挂载点
文件系统种类
- 磁盘文件系统
- 文件存储在数据存储设备上,如磁盘
- 例如:FAT,NTFS,ext2/3,ISO9660等
- 数据库文件系统
- 文件特征是可被寻址(辨识)的
- 例如:WinFS
- 日志文件系统
- 记录文件系统的修改/事件
- 网络文件系统
- 例如:NFS,SMB,AFS,GFS
- 特殊/虚拟文件系统
网络/分布式文件系统
- 文件可以通过网络被共享
- 文件位于远程服务器
- 客户端远程挂载服务器文件系统
- 标准系统文件访问被转换成远程访问
- 标准文件共享协议
- NFS for UNIX,CIFS for Windows
- 分布式文件系统的挑战
- 客户端和客户端上的用户辨别起来很复杂
- NFS是不安全的
- 一致性问题
- 错误处理模式
- 客户端和客户端上的用户辨别起来很复杂
13.2 虚拟文件系统
文件系统的实现
- 分层结构
- 虚拟(逻辑)文件系统(VFS,Virtual File System)
- 特定文件系统模块
- 目的
- 对所有不同文件系统的抽象
- 功能
- 提供相同的文件和文件系统接口
- 管理所有文件和文件系统关联的数据结构
- 高效查询例程,遍历文件系统
- 与特定文件系统模块的交互
文件系统基本数据结构
- 文件卷控制块(Unix: "superblock")
- 每个文件系统一个
- 文件系统详细信息
- 块、块大小、空余块、计数/指针等
- 文件控制块(Unix: "vnode" or "inode")
- 每个文件一个
- 文件详细信息
- 访问权限、拥有者、大小、数据块位置等
- 目录项(Linux: "dentry")
- 每个目录项一个(目录和文件)
- 将目录项数据结构及树形布局编码成树形数据结构
- 指向文件控制块、父目录、子目录等
文件系统的存储结构
文件系统数据结构
- 卷控制块(每个文件系统一个)
- 文件控制块(每个文件一个)
- 目录节点(每个目录项一个)
持久存储在外存中
- 存储设备的数据块中
当需要时加载进内存
-
卷控制模块:当文件系统挂载时进入内存
-
文件控制块:当文件被访问时进入每次
-
目录节点:在遍历一个文件路径时进入内存
文件系统的存储视图
13.3 文件缓存和打开文件
多种磁盘缓存位置
数据块缓存
- 数据块按需读入内存
- 提供read()操作
- 预读:预先读取后面的数据块
- 数据块使用后被缓存
- 假设数据将会再次用到
- 写操作可能被缓存和延迟写入
- 两种数据块缓存方式
- 数据块缓存
- 页缓存:统一缓存数据块和内存页
页缓存
-
虚拟页式存储
- 在虚拟地址空间中虚拟页面可映射到本地外存文件中
-
文件数据块的页缓存
- 在虚拟内存中文件数据块被映射成页
- 文件的读/写操作被转换成对内存的访问
- 可能导致缺页和/或设置为脏页
- 问题:页置换算法需要协调虚拟存储和页缓存间的页面数
文件系统中打开文件的数据结构
- 文件描述符
- 每个被打开的文件都有一个文件描述符
- 文件状态信息
- 目录项、当前文件指针、文件操作设置等
- 打开文件表
- 每个进程一个进程打开文件表
- 一个系统级的打开文件表
- 有文件被打开时,文件卷就不能被卸载
打开文件锁
一些文件系统提供文件锁,用于协调多进程的文件访问
- 强制—根据锁保持情况和访问需求确定是否拒绝访问
- 劝告—进程可以查找锁的状态来决定怎么做
13.4 文件分配
文件大小
大多数文件都很小
- 需要对小文件提供很好的支持
- 块空间不能太大
一些文件非常大
- 必须支持大文件(64位文件偏移)
- 大文件访问需要高效
如何表示分配给一个文件数据块的位置和顺序
分配方式
- 连续分配
- 链式分配
- 索引分配
指标
- 存储效率:外部碎片等
- 读写性能:访问速度
连续分配
文件头指定起始块和长度
分配策略
- 最先匹配、最佳匹配
优点
- 文件读取表现好
- 高效的顺序和随机访问
缺点
- 碎片
- 文件增长问题
- 预分配?按需分配?
链式分配
文件以数据块链表方式存储
文件头包含了第一块和最后一块的指针
优点
- 创建、增大、缩小很容易
- 没有碎片
缺点
- 无法实现真正的随机访问
- 可靠性差
- 破坏一个链,后面的数据块也就丢了
索引分配
为每个文件创建一个索引数据块
- 指向文件数据块的指针列表
文件头包含了索引数据块指针
优点
- 创建、增大、缩小很容易
- 没有碎片
- 支持直接访问
缺点
- 当文件很小时索引开销
- 如何处理大文件?(需要多个索引块)
UFS多级索引分配
Unix File System
效果
- 提高了文件大小的限制阈值
- 动态分配数据块,文件扩展很容易
- 小文件开销小
- 只为大文件分配间接数据块,大文件在访问数据块时需要大量查询
13.5 空闲空间管理
跟踪记录文件卷中未分配的数据块
位图
链表
链式索引
13.6 冗余磁盘阵列RAID
磁盘分区
通常磁盘通过分区来最大限度减小寻道时间
- 分区是一组柱面的集合
- 每个分区都可视为逻辑上独立的磁盘(切换分区性能很差)
一个典型的磁盘文件系统组织
文件卷:一个拥有完整文件系统实例的外存空间
通常常驻在磁盘的单个分区上
多磁盘管理
使用多磁盘可改善
- 吞吐量(通过并行)
- 可靠性和可用性(通过冗余,数据存多份)
冗余磁盘阵列
RAID,Redundant Array of Inexpensive Disks
- 多种磁盘管理技术
- RAID分类,如 RAID-0, RAID-1, RAID-5
冗余磁盘阵列的实现
- 软件:操作系统内核的文件卷管理
- 硬件:RAID硬件控制器(I/O)
RAID-0
磁盘条带化
把数据块分成多个子块,存储在独立的磁盘中
- 通过独立磁盘上并行数据访问提供更大的磁盘带宽
RAID-1
磁盘镜像
向两个磁盘写入,从任何一个读取
- 可靠性成倍增长
- 读取性能线性增加
RAID-4
带校验的磁盘条带化
数据块级的磁盘条带化加专用的奇偶校验磁盘
- 允许从任意一个故障磁盘中恢复
提高可靠性和读写性能
RAID-5
带分布式校验的磁盘条带化
基于位和基于块的磁盘条带化
条带化和奇偶校验按字节或者位
- RAID-0/4/5:基于数据块
- RAID-3:基于位
可纠正多个磁盘错误的冗余磁盘阵列
RAID-6:每组条带快带有两个冗余块
- 允许两个磁盘错误
RAID嵌套
14. I/O 子系统
14.1 I/O 特点
设备类型
三种常见设备接口类型
- 字符设备
- 串口、键盘、鼠标等
- 访问特征
- 以字节为单位顺序访问
- I/O命令
- get(), put()等
- 通常使用文件访问接口和语义
- 块设备
- 磁盘驱动器、磁带驱动器、光驱等
- 访问特征
- 均匀的数据块访问
- I/O命令
- 内存映射文件访问
- 原始I/O或文件系统接口
- 网络设备
- 以太网、无线、蓝牙等
- 访问特征
- 格式化报文交换
- I/O命令
- send/receive网络报文
- 通过网络接口支持多种网络协议
同步与异步I/O
同步
异步
14.2 I/O 结构
CPU与设备的连接
- 设备控制器
- CPU与I/O设备间的接口
- 向CPU提供特殊指令和寄存器
- I/O地址
- CPU用来控制I/O硬件
- 内存地址或端口号
- I/O指令
- 内存映射I/O
- CPU与设备的通信方式
- 轮询、设备中断和DMA(IO直接到内存)
I/O指令和内存映射I/O
- I/O指令
- 通过I/O端口号访问设备寄存器
- 特殊的CPU指令
- out 0x21, AL
- 内存映射I/O
- 设备的寄存器/存储被映射到内存物理地址单元
- 通过内存load/store指令完成I/O操作
- MMU设置映射,硬件跳线或程序在启动时设置地址
内核I/O结构
I/O请求生存周期
14.3 I/O 数据传输
程序控制I/O(PIO, programmed I/O)
- 通过CPU的in/out或者load/store传输所有数据
- 特点
- 硬件简单、编程容易
- 消耗的CPU时间和数据量成正比
- 适用于简单的、小型的设备I/O
直接内存访问(DMA)
- 设备控制器可直接访问系统总线
- 控制器直接与内存互相传输数据
- 特点
- 设备传输数据不影响CPU
- 需要CPU参与设置
- 适用于高吞吐量I/O
实例
I/O设备通知操作系统的机制
- 操作系统需要了解设备状态
- IO操作完成时间
- IO操作遇到错误
- 两种方式
- CPU主动轮询
- 设备中断
轮询
- IO设备在特点的状态寄存器中放置状态和错误信息
- 操作系统定期检测状态寄存器
- 特点
- 简单
- IO操作频繁或不可预测时,开销大和延迟长
设备中断
- 设备中断处理流程
- CPU在IO之前设置任务参数
- CPU在发出IO请求之后,继续执行其他任务
- IO设备处理IO请求
- IO设备处理完成时,触发CPU中断请求
- CPU接收中断,分发到相应中断处理例程
- 特点
- 处理不可预测事件效果好
- 开销相对较高
- 一些设备可能结合轮询和设备中断
- 如 :高带宽网络设备
- 第一个传入数据包到达前采用中断
- 轮询后面的数据包直到硬件缓存为空
- 如 :高带宽网络设备
设备中断IO处理流程
14.4 磁盘调度
磁盘工作机制和性能参数
传输时间
磁盘调度算法
通过优化磁盘访问请求顺序来提高磁盘访问性能
- 寻道时间是磁盘访问最耗时的部分
- 同时会有多个在同一磁盘上的IO请求
- 随机处理磁盘访问请求的性能表现很差
先进先出(FIFO)算法
- 按顺序处理算法
- 公平对待所有进程
- 在有很多进程的情况下,接近随机调度的性能
最短服务时间优先(SSTF)
- 选择从磁臂当前位置需要移动最少的IO请求
- 总是选择最短寻道时间
扫描算法(SCAN)
-
磁臂在一个方向移动,访问所有未完成的请求,直到磁臂到达该方向上最后的磁道
-
调换方向
-
也称电梯算法
循环扫描算法(C-SCAN)
- 限制了仅在一个方向上扫描
- 当最后一个磁道也被访问过了后,磁臂返回到磁盘的另外一端再次进行
C-LOOK算法
- 磁臂先到达该方向上最后一个请求处,然后立即反转,而不是先到最后点路径上的所有请求
N步扫描算法(N-step-SCAN)
- 磁头粘着现象(Arm Stickiness)
- SSTF、SCAN和CSCAN的算法中,可能出现磁头停留在某处不动的情况
- 如:进程反复请求对某一磁道的IO操作
- N步扫描算法
- 将磁盘请求队列分成长度为N的子队列
- 按FIFO算法依次处理所有子队列
- 扫描算法处理每个队列
双队列扫描(FSCAN)算法
- FSCAN是N步扫描的简化
- 只分为两个队列
- FSCAN算法
- 把磁盘IO请求分成两个队列
- 交替使用扫描算法处理一个队列
- 新生成的磁盘IO请求放入另一个队列中,所有的新请求都将推迟到下一次扫描时处理
14.5 磁盘缓存
缓存
数据传输双方访问速度差异较大时,引入的速度匹配中间层
磁盘缓存是磁盘扇区在内存中的缓存区
-
磁盘缓存的调度算法很类似虚拟存储调度算法
-
磁盘的访问频率远低于虚拟存储中的内存访问频率
-
通常磁盘缓存调度算法会比虚拟存储复杂
单缓存与双缓存
单缓存(Single Buffer Cache)
双缓存(Double Buffer Cache)
访问频率置换算法
Frequency-based Replacement
问题
- 在一段密集磁盘访问后,LFU算法的引用计数变化无法反映当前的引用情况
算法思路
- 考虑磁盘访问的密集特征,对密集引用不计数
- 在短周期内使用LRU算法,而在长周期中使用LFU算法