├── 导论.md
├── 文件和IO.md
├── 内存.md
└── 进程与线程.md
/导论.md:
--------------------------------------------------------------------------------
1 | ### 操作系统基础(一)导论
2 |
3 | [toc]
4 |
5 | #### 概述
6 |
7 | 操作系统是指控制和管理整个计算机系统的硬件和软件资源,并且合理地组织调度计算机工作和资源的分配,以提供给用户和其他软件方便接口和环境的程序集合。
8 |
9 |
10 | 从底层到上层分别是:
11 |
12 | 硬件->操作系统->计算机程序->用户
13 |
14 | 
15 |
16 | #### 操作系统的特征
17 |
18 | **并发**:两个或多个事件在同一时间间隔内发生
19 |
20 | **共享**:系统中的资源可供内存中多个并发执行的进程共同使用
21 |
22 | 并发和共享是操作系统两个最基本的特征
23 |
24 | **虚拟**:把物理实体变为若干个逻辑的对应
25 |
26 | **异步**:进程的执行走走停停,以不可预知的速度向前推进
27 |
28 | #### 操作系统的目的和功能
29 |
30 | 功能:处理机管理、存储器管理、设备管理、文件管理以及提供接口给用户。
31 |
32 | 操作系统为用户提供操作计算机硬件系统的接口
33 |
34 | ##### 命令接口
35 |
36 | 联机控制方式(适用于分时、实时操作系统):说一句做一句
37 |
38 | 脱机控制方式(适用于批处理):写在单子上,按单子一个一个做
39 |
40 | ##### 程序接口
41 |
42 | 程序接口由一组**系统调用命令**组成,在程序中使用这些调用命令来请求操作系统为其提供服务
43 |
44 | 系统调用只能用过用户程序间接使用
45 |
46 |
47 |
48 | #### 操作系统的发展
49 |
50 | ##### 1. 手工操作阶段
51 |
52 | ##### 2. 批处理阶段
53 |
54 | 单道批处理系统:内存中始终保持一道作业
55 | **特征:自动性、顺序性、单道性**
56 |
57 | 多道批处理系统:允许多个程序同时进入内存并运行
58 | **特征:多道、宏观并行、微观串行**
59 |
60 | 批处理的缺点是缺少交互性
61 |
62 | ##### 3. 分时操作系统
63 |
64 | 采用分时技术,把处理器的运行时间分为很短的时间片,按照时间片轮转算法把处理器分配给各个联机作业使用。
65 |
66 | 分时操作系统可以让多个用户通过终端连接同一个主机,用户之间互不干扰。
67 |
68 |
69 |
70 | **特征:同时性、交互性、独立性、及时性**
71 |
72 | ##### 4. 实时操作系统
73 |
74 | 为了完成紧急任务而不需要时间片排队,通常采用抢占式的优先级高者优先调度
75 |
76 | 实时系统必须在被控制对象规定时间内处理来自外部的事件
77 |
78 | 通常运用场景是一些需要立即反应的场合,比如股票、订票、机床控制什么的
79 |
80 |
81 |
82 | **特征:及时性、可靠性**
83 |
84 | ##### 5. 网络操作系统和分布式计算机系统
85 |
86 | 网络中各种资源的共享以及各台计算机之间的通信
87 |
88 | **特征:分布性、并行性**
89 |
90 |
91 |
92 |
93 | #### 操作系统的运行环境
94 |
95 | ##### 内核态与用户态
96 |
97 | 程序分为内核程序和外层应用程序,他们能执行的指令的权限不一样,所以操作系统划分为了用户态(目态)和核心态(管态)来严格区分这两种程序
98 |
99 |
100 |
101 | 操作系统内核运行在核心态,用户程序运行在用户态
102 |
103 | 内核包括:
104 |
105 | ###### 1. 时钟管理
106 |
107 | ###### 2. 中断机制
108 |
109 | ###### 3. 原语
110 |
111 | 处于最底层、运行具有原子性、运行时间较短,有这些特点的程序被称为原语
112 |
113 | ###### 4. 系统控制的数据结构及处理
114 |
115 | ##### 运行机制
116 |
117 |
118 |
119 | ##### 中断和异常
120 |
121 | ###### 中断:外中断,与当前运行的程序无关,比如设备的中断请求、时钟中断等等
122 |
123 | ###### 异常:内中断,指令内部导致中断
124 |
125 | 中断处理一定会保存程序状态子寄存器
126 |
127 | 外部中断,PC值由中断隐指令自动保存,通用寄存器的内容由操作系统保存
128 |
129 | ##### 系统调用
130 |
131 | 按照供功能可以分为设备管理、文件管理、进程控制、进程通信、内存管理等
132 |
133 | 
134 |
135 | 从用户态转到核心态会用到访管指令,访管指令在用户态使用,所以它不可能是特权指令
136 |
137 | 用户态指令:
138 |
139 | 核心态指令:输入输出指令
140 |
141 | 从用户态转换到核心态,这是由硬件完成的
142 |
143 | 置时钟指令只能在核心态下完成
144 |
145 | 中断发生后,进入中断处理的程序在核心态执行,是操作系统程序
146 |
147 | 广义指令(系统调用指令),必然在核心态执行,调用都可以
148 |
149 | 输入输出必然在核心态执行
150 |
151 | 分清楚调用和执行,有不少都是用户态可以调用但是执行必须在核心态的
152 |
153 | CPU处于核心态,除了访管指令,其余所有指令都可以调用
154 |
155 | 导致用户从用户态切到内核态的操作有:某个东西导致了异常中断、I/O等等
156 |
157 | #### Tips
158 |
159 | - 操作系统不关心高级语言编译器
160 | - 单处理机系统中,进程与进程不能并行
161 | - 用户可以使用命令接口和系统调用来使用计算机
162 | - 计算机开机后,操作系统最终被加载到RAM上(内存中的系统区)
163 | - 提高单机资源利用率的关键技术是多道程序设计技术
164 | - 提到多道批处理就往提交若干作业(清单)上靠,提到分时就往用户靠,提到实时就往响应速度靠
165 | - 通用操作系统使用时间片轮转算法
166 | - 通道技术是一种硬件技术
167 | - 进程调度不需要硬件的支持
168 | - 异常处理后不一定会返回到发生异常的地方继续执行,比如除以0会直接跳过
169 |
--------------------------------------------------------------------------------
/文件和IO.md:
--------------------------------------------------------------------------------
1 | ### 文件管理与I/O
2 |
3 | 
4 |
5 | #### 文件基本概念
6 |
7 | 文件是记录在外存上相关信息的具名集合,对于用户而言文件是逻辑外存最小的分配单位,文件是一组有意义信息的集合
8 |
9 | 在系统运行时,计算机以进程为基本单位进行资源的调度和分配,而在用户进行输入输出时,则以文件为基本单位。
10 |
11 | 需要系统提供一个文件管理系统来让用户管理文件,文件系统由一组文件和目录结构组成
12 |
13 | ##### 文件属性
14 |
15 | 文件名、标志符(唯一标签,用户不可读)、类型、位置、大小、保护信息、时间、日期和用户标识
16 |
17 | 文件属于抽象数据类型
18 |
19 | ##### 文件基本操作
20 |
21 | (操作系统应该向上提供的文件操作功能)
22 | 创建文件、写文件、读文件、在文件内重定位、删除文件(完全删除)、截短文件(删除内容不删属性)
23 |
24 | ##### 文件的打开与关闭
25 |
26 | 要读一个文件首先要用open系统调用打开该文件,open中的参数包括文件路径名和文件名,
27 |
28 | read只需要使用open返回的文件描述符,不使用文件名作为参数
29 |
30 | 每个打开文件有以下信息:
31 | - 文件指针
32 | - 文件打开计数器
33 | - 文件磁盘位置
34 | - 访问权限
35 |
36 | ##### 文件的逻辑结构
37 |
38 | 文件的逻辑结构是**从用户观点出发看到的文件内部的组织形式**
39 |
40 | 文件按照逻辑结构分为无结构文件(流式文件)和有结构文件(记录式文件)
41 |
42 | 无结构文件(流式文件):以字节为单位
43 |
44 | 有结构文件(记录式文件):分为顺序文件、索引文件、顺序索引文件、散列文件
45 |
46 | 文件的目录结构(文件之间的组织形式)
47 | 文件存放在外存中(文件的物理结构)
48 |
49 | ##### 访问方法
50 |
51 | 1. 顺序访问
52 |
53 | 2. 直接访问
54 |
55 | ##### 目录结构
56 |
57 | 引入**文件控制块(FCB)** 这个数据结构
58 |
59 | FCB包括基本信息、存取控制信息和使用信息
60 |
61 | 目录相关操作有:搜索文件、创建文件、删除文件、遍历目录、重命名文件、跟踪文件系统
62 |
63 | ##### 保护
64 |
65 | 信息需要被保护,不受物理损坏(可靠性)和非法访问(保护)
66 |
67 | 口令、存取控制和用户权限表都是常见的文件保护方法
68 |
69 | 可靠性通常靠文件备份来提供
70 |
71 | 访问类型:读、写、执行、添加、删除、列表清单
72 |
73 | 为每个文件添加一个访问控制列表(ACL)
74 |
75 | 加密保护和访问控制:加密保护安全性更高,访问控制灵活性更好。访问控制需要由系统实现来保证安全性
76 |
77 | 
78 |
79 |
80 | ##### 错题归纳
81 |
82 | - 从用户的角度看,引入文件系统的目的是实现对文件的按名存取;从系统角度看,文件系统则负责对文件储存空间进行组织分配、负责文件储存并对存入的文件进行保护和检索
83 | - FCB的有序集合称为文件目录,一个FCB就是一个文件目录项
84 | - 逻辑结构的组织形式取决于用户,物理结构的组织形式取决于储存介质特性
85 | - 文件目录项(FCB)不包括FCB的物理位置
86 | - 对一个访问的限制,常由用户访问权限和文件属性共同限制,与优先级无关
87 | - 一个文件被用户首次打开的过程中,操作系统需要将文件控制块读到内存中,不是文件内容
88 |
89 |
90 | #### 文件系统实现
91 |
92 | ##### 文件系统层次结构
93 |
94 | 
95 |
96 | 1. 用户调用接口:给用户文件操控的接口
97 | 2. 文件目录系统:管理文件目录
98 | 3. 存取控制模块:实现文件保护
99 | 4. 逻辑文件系统与文件信息缓冲区:根据逻辑结构将用户要读写的逻辑记录转换成文件逻辑结构内的相应块号
100 | 5. 物理文件系统:把相应块号转成实际物理地址
101 | 6. 辅助分配模块:管理辅存空间,辅存空间的分配和回收
102 | 7. 设备管理模块:管理相关设备
103 |
104 | 单层结构目录、双层结构目录、树状结构目录、无环图目录(同一文件可以在不同目录中)、通用图目录
105 |
106 | 物理块是分配和传输的基本单位
107 |
108 | ##### 目录实现
109 |
110 | 线性链表、哈希表
111 |
112 | ##### 外存分配方式
113 |
114 | 1. 连续分配
115 |
116 | 每个文件在磁盘上占有一组连续的块
117 |
118 | 分配方式可以用第一块磁盘的地址和连续块数量来确定
119 |
120 | 优点:实现简单、存取速度快
121 | 缺点:不方便动态增加,容易产生外部碎片
122 |
123 | 2. 链接分配(隐式、显式)
124 |
125 | 以离散分配的方式,消除外部碎片
126 |
127 | 隐式:除最后一个块外,每个块都有一个指向下一个盘块的指针,目录包括第一个指针和最后一个指针
128 |
129 | 显式:显式地存放在内存的一张链接表中,文件分配表(File Allocation Table, FAT)
130 |
131 | 3. 索引分配(一级、二级、多级)
132 |
133 | 每个文件设置一个索引块,是一个磁盘块地址的数组
134 |
135 | 
136 |
137 | 
138 |
139 |
140 | ##### 错题归纳
141 |
142 | - 文件储存空间的管理实质上是对外存空闲区的管理和组织
143 |
144 |
145 | #### 磁盘结构
146 |
147 | 概念:磁盘由表面涂有磁性物质的圆形盘片组成,每个盘片被划成一个个磁道,每个磁道被划成一个个扇区
148 |
149 | 如何读写:磁头移动到目标位置,盘片旋转,对应扇区划过磁道,完成读写
150 |
151 | 磁盘的物理地址依靠柱面号、盘面号、扇区号来确定
152 |
153 | 磁盘分类:
154 | 活动头磁盘、固定头磁盘
155 | 可换盘磁盘、固定盘磁盘
156 |
157 | ##### 磁盘调度算法
158 |
159 | 一次读/写操作需要的时间包括
160 | 寻找时间:读写之前磁头移动到磁道花费的时间,$T_S = T_{启动磁头臂} + m_{跨越一个磁道耗时}*n_{需要跨越的磁道数}$
161 | 延迟时间:通过旋转磁盘让磁头定位到目标扇区所需要的时间,平均延迟时间$T_R = \frac{1}{2} * \frac{1}{r}$ (r为转速)
162 | 传输时间:从磁盘读出或向磁盘写入数据所经历的时间,转速为r,读写的字节数b,每个磁道字节数N,传输时间$T_t = \frac{1}{r} * \frac{b}{N}$
163 |
164 | 延迟时间和传输时间是硬件决定的,操作系统只能通过调度算法优化寻找时间
165 |
166 |
167 | 磁盘调度算法:
168 | 先来先服务(FCFS):按顺序处理
169 | 最短寻找时间(SSTF):先处理离目前磁头最近的请求,贪心算法(局部最优不一定是全局最优),可能导致饥饿
170 | 扫描算法(SCAN):只有移动到最内侧磁道才能往外侧移动,只有移动到最外侧磁道才能往内侧移动
171 | LOOK调度算法:扫描算法的升级,在移动方向上没有请求了就可以改变方向
172 | 循环扫描算法(C-SCAN):扫描算法的变形,每次扫描的移动方向一样,扫到底了直接快速移到开头,中间不管,这样解决了SCAN算法两侧与中间不均的问题
173 | C-LOOK:C-SCAN的升级,相比C-SCAN头尾不需要是磁盘的最前面和最后面,只要有请求的最前面和最后面
174 |
175 | 题目里的SCAN都默认是对应的LOOK算法
176 |
177 | ##### 减少
178 |
179 | 交替编号
180 | 错位命名
181 |
182 | #### I/O管理
183 |
184 | ##### I/O设备分类
185 |
186 | 按照使用特性:
187 | 人机交互外部设备:比如鼠标键盘(数据传输最慢)
188 | 储存设备:比如移动硬盘(数据传输最快)
189 | 网络通信设备:比如路由器
190 |
191 | 按照传输速率:
192 | 低速设备、中速设备、高速设备
193 |
194 | 按照信息交换的单位:
195 | 块设备(传输快、可以寻址)、字符设备(不可寻址,通常用中断驱动控制)
196 |
197 | ##### I/O控制方式
198 |
199 | 程序直接控制方式:CPU不断轮询检查是否设备就绪,简单,CPU利用率低
200 |
201 | 中断驱动方式:CPU先让IO进程阻塞,之后IO设备向CPU发出中断让其执行,CPU利用率提高,但频繁中断浪费CPU时间
202 |
203 | DMA(直接存储器存取)方式:在IO与内存之间开辟新的数据通路,仅在开始和结束时才需要CPU干预,数据传输不需要经过CPU,可以传数据块
204 |
205 | 通道控制方式:可以看成专门处理IO的阉割版CPU,完成后才向CPU发送中断
206 |
207 | 上面四个方式从上往下CPU干预程度越来越低,CPU利用率越来越高
208 |
209 | ##### I/O子系统的层次结构
210 |
211 | 用户层I/O软件:比如库函数printf
212 | 设备独立性软件
213 | 设备驱动程序
214 | 中断处理程序
215 | 硬件设备
216 |
217 | 中间三层属于I/O核心子系统
218 |
--------------------------------------------------------------------------------
/内存.md:
--------------------------------------------------------------------------------
1 | ### 操作系统基础(三)内存
2 |
3 | 
4 |
5 |
6 | #### 背景
7 |
8 | ##### 程序装入和链接
9 |
10 | - 编译:编译程序将源代码编译成若干目标模块
11 | - 链接:链接程序把各个目标模块,包括外部库函数链接在一起
12 | - 装入:由装入程序将装入模块装入内存中运行
13 |
14 | 其中链接分为三种:
15 | 静态链接
16 | 装入时动态链接:装入时边装入边链接
17 | 运行时动态链接:执行时才链接
18 |
19 | 装入也分为三种:
20 | 绝对(静态)装入:在编程阶段就把物理地址计算好
21 | 可重定位装入:有一个逻辑地址到物理地址的映射,之后不能变
22 | 动态重定位装入:运行时才转换,之后可以变
23 |
24 | ##### 地址绑定
25 |
26 | 将指令和数据绑定到内存地址有三种情况:
27 | - 编译时:生成绝对代码,编译时就知道了进程在内存中的驻留地址,所生成的编译代码就可以从该位置往后扩展(开始位置发生变化就要重新编译)
28 | - 加载时:生成可重定位代码
29 | - 执行时:绑定延迟到执行时才进行
30 |
31 | 物理地址: 内存地址寄存器中的地址,又叫绝对地址、实地址
32 |
33 | 逻辑地址: CPU生成的地址,又叫相对地址、虚地址(用户程序在经过汇编后目标代码常用,把首地址设为0,后面是相对地址)
34 |
35 | 编译和加载时的地址绑定方法生成相同逻辑地址和物理地址,而执行时的地址绑定方案生成不同的逻辑和物理地址,这种情况通常称逻辑地址为虚拟地址
36 |
37 | 运行时从虚拟地址到物理地址的映射由**内存管理单元**来完成
38 |
39 | ##### 内存映射与保护
40 |
41 | 内存通常分为两个区域:用于驻留操作系统和用于用户进程
42 |
43 | 为输入队列中需要调入内存的进程分配内存空间,采用连续内存分配,每个进程位于一个连续的区域
44 |
45 | 逻辑地址通过界限地址寄存器(判断是否合理)、重定位寄存器(加一个基地址)映射到物理地址,定位到内存
46 |
47 | 内存保护:
48 |
49 | (1)界地址保护
50 | 1. 设置地址上界寄存器、下界寄存器的地址检查机制
51 | 2. 基址、限长寄存器和动态地址转换机构
52 |
53 | 
54 |
55 |
56 | CPU所能访问的存储器只有内存和CPU内的寄存器,因此执行指令以及指令使用的数据必须在这些可访问的存储设备中,否则就要在CPU使用前把数据移到内存中
57 |
58 | 确定进程只能访问其合法范围,通过基地址寄存器和界限地址寄存器可以做到:
59 |
60 | 基地址寄存器:a
61 | 界限地址寄存器:b
62 | 那么程序可以访问a~a+b的所有地址
63 |
64 |
65 | 寄存器 $\to$ cache $\to$ 内存 $\to$ 外存
66 |
67 | ##### 交换
68 |
69 | 进程可以暂时从内存中交换到辅存上(换出),需要再次执行时再调回内存中(换入)
70 |
71 | 处于阻塞或者优先级低,会被换出,优先级更改会被换入
72 |
73 | #### 连续内存分配
74 |
75 | ##### 内存分配
76 |
77 | 单一连续分配:分为系统区用户区,简单但是只适用于单用户
78 | 固定分区分配:用户空间划分好多个分区(分为大小均相同分区和大小不相同分区两种)
79 | 动态分区分配:用户空间一开始不划分,而是在装入内存时根据进程大小动态建立分区
80 |
81 | 以下都是讨论动态分区(可变分区):
82 |
83 | 从一组可用孔中选择一个合适的空闲孔的方法:
84 | - 首次适应:分配第一个足够大的孔(空闲分区链以地址递增顺序链接)
85 | - 最佳适应:分配最小的足够大的孔(空闲分区链按长度递增顺序链接)
86 | - 最差适应:分配最大的孔
87 |
88 | 首次适应被认为既是最简单的,也是最好最快,最佳和最差容易生成小碎片(不要被最佳的名字忽悠,这家伙性能很差,会产生最多的外部碎片)
89 |
90 | 分区式存储管理优缺点:
91 | 优点:
92 | 1. 便于动态申请内存
93 | 2. 便于共享内存
94 | 3. 便于动态链接
95 |
96 | 缺点:
97 | 1. 碎片问题,内存利用率不高
98 |
99 | ##### 碎片
100 |
101 | 内部碎片:固定分区方法会有内部碎片,程序所占空间小于分给它的空间,内部空间的间隙为内部碎片
102 |
103 | 外部碎片:动态分区随着进程装入和移出内存,空闲内存被分为小片段,中间产生的空隙为外部碎片,首次适应和最佳适应都会有这种外部碎片问题
104 |
105 | 解决外部碎片的方法:
106 | 1. 紧缩,仅在重定位是动态并在运行时可用,把所有的孔移到一段,开销较大(可重定位分区)
107 | 2. 允许物理地址非连续,只要有物理内存就能为进程分配,有两种互补的实现技术:分页、分段、段页
108 |
109 |
110 |
111 |
112 |
113 | #### 分页
114 |
115 | 以上是连续内存分配管理方式,接下来的分页和分段是非连续内存分配管理方式
116 | 连续分配方式,比如说一个需要1GB的程序,就必须要去内存找一块连续的1G空闲区间分配给它,而非连续分配方式可以分散地给这段程序分配内存空间
117 |
118 | 分页不会产生外部碎片,每个进程平均产生半个块大小的内部碎片(很小的)
119 |
120 | 把物理内存分为固定大小的块,称为帧,也叫物理块
121 | 把逻辑内存分为同样大小的块,称为页
122 |
123 | 每个地址分为页号p和偏移d,前面的页号经过页表变成内存块号,后面的页内偏移直接对过去
124 |
125 | 页表:为每个进程建立一张页表,将进程的每一页离散地储存在内存的物理块中(页号→块号)
126 |
127 | 举个例子:
128 | | 32 - 12 | 11 - 0 |
129 | | ------- | ------- |
130 | | 页号P | 偏移量W |
131 |
132 | 说明总共有220页(也就是1M页),每页212大小(也就是4KB)
133 |
134 | 
135 |
136 | 
137 |
138 |
139 | 采用分页技术不会产生外部碎片,但是会有内部碎片,产生的原因是进程所需内存不是页的整数倍,最后一页可能装不满最后一个帧(物理块),导致产生页内碎片
140 |
141 | 分页的一个重要特点是用户视角的内存和实际物理内存的分离,用户看到的是一整块内存用于一个进程,但实际的物理内存则是分散的,逻辑地址到物理地址的映射用户不知道。
142 |
143 | 用户进程不能访问该进程非占用的内存,即无法访问页表规定之外的内存
144 |
145 | 页表的初始地址放在寄存器中
146 |
147 | ##### 快表
148 |
149 | 快表(TLB)是一种高速缓冲寄存器,加速地址变换过程,通常采用TLB + 页表的形式
150 |
151 | 每次要访存两次,为了提高速度,加入TLB(快表),本质是一个cache,可以看成是页表的子集,里面不是连续存储
152 |
153 | TLB由键和值组成,包括页表中的一小部分条目,逻辑地址的页号提交给TLB,如果命中那么直接得到帧号,否则就访问页表(TLB失效),同时把页号和帧号加入到TLB中
154 |
155 | 快表的有效性基于局部性原理
156 |
157 | 
158 |
159 |
160 | 页号在TLB中被查找到的几率称为命中率,有效内存访问时间要按加权计算
161 |
162 |
163 |
164 |
165 |
166 | 保护:每个帧有相关联的保护位
167 |
168 | 共享页:可以共享公共代码
169 |
170 | 二级页表和多级页表:把页表再一步离散化,用页表再映射页表
171 |
172 |
173 |
174 | #### 分段
175 |
176 | 支持用户视角的内存管理方案,逻辑地址空间由一组段组成,分段地址中的地址包括段号和段内偏移,用户通过段号和段内偏移来指定地址 \
177 |
178 | 为每个段分配一个连续的分区
179 |
180 | 段表可以把二维数组定位到一维物理地址,每个段在段表中占一个表项,记录了该段在内存中的起始地址和段的长度(防止越界),段内地址不能大于段长
181 |
182 | 段表示例:
183 | | 段号 | 段首地址 | 段长 |
184 | | ---- | -------- | ---- |
185 | | 0 | 50k | 20k |
186 | | 0 | 100k | 60k |
187 | | ... | ... | ... |
188 |
189 | 
190 |
191 |
192 | 硬件支持:一对寄存器(段表基址寄存器和段表长度寄存器)
193 |
194 | 也可以加快表,逻辑和页表类似
195 |
196 | ##### 分页和分段的比较
197 |
198 | 页是信息的物理单位,是出于系统管理的需要;
199 | 页的大小固定且由系统确定,由硬件实现,系统中的页只能有一种大小;
200 | 分页的作业地址空间是一维的
201 |
202 |
203 | 段是信息的逻辑单位,是出于用户的需要;
204 | 段的长度不固定,取决于用户编写的程序的,通常由编译程序在对源程序进行编译时,根据信息的性质来划分;
205 | 分段的作业地址空间是二维的,除了要给出段名还要给出段长
206 |
207 |
208 |
209 |
210 | #### 段页式
211 |
212 | 段号+段内页号+页内地址
213 |
214 | 首先被分为若干个逻辑段,每一段都有自己的段号,然后在里面分页
215 |
216 | 段页式中,一个进程对应一张段表,每个段对应一张页表
217 |
218 | 需要三次访问:第一次访问段表,第二次访问页表,第三次访问指令/数据的实际地址
219 |
220 | 段页式的地址空间是二维的
221 |
222 | 用分段方法来分配和管理用户地址空间,用分页方法来管理物理存储空间
223 |
224 |
225 |
226 | #### Tips:
227 |
228 | - 链接完成重定位,形成逻辑地址,装载完成从逻辑地址到物理地址的变换
229 | - 内存保护需要操作系统和硬件机构合作完成
230 | - 固定分区、分页、段页都有内部碎片,不过分段没有内部碎片
231 | - 页式管理划分的页面大小是相等的
232 |
233 |
234 |
235 |
236 |
237 |
238 | ### 虚拟内存
239 |
240 | 引入原因:暂时不运行和运行完的程序仍然占用内存
241 |
242 | 虚拟内存技术允许执行进程不必完全在内存中,程序可以比物理内存大,将用户逻辑内存与物理内存分开
243 |
244 | 虚拟存储器:具有请求调入和置换的功能,能从逻辑上对内存容量加以扩充的一种储存系统,其运行速度接近内存,成本又接近外存
245 |
246 | 虚拟内存特征:多次性、对换性、虚拟性
247 |
248 | ##### 局部性原理
249 |
250 | 时间局部性原理:
251 | > 程序中的某一条指令一旦执行,不久以后该指令可能再次执行;某个数据被访问过,不久以后该数据可能再次被访问
252 |
253 |
254 |
255 | 空间局部性原理:
256 | > 某个储存单元被访问,不久之后其附近的储存单元也将被访问,一段时间内访问的地址可能集中在一定的范围内
257 |
258 |
259 | ##### 虚拟内存的实现
260 |
261 | 请求分页储存管理
262 | 请求分段储存管理
263 | 请求段页式储存管理
264 |
265 | ##### 请求分页管理方式
266 |
267 | 页表机制扩充:
268 | - 状态位P
269 | - 访问位A
270 | - 修改位M
271 | - 外存地址
272 |
273 | 
274 |
275 | #### 缺页中断
276 |
277 | 在请求分页系统中,每当所要访问的页面不在内存时,就会产生缺页中断
278 |
279 |
280 |
281 | #### 页面置换算法
282 |
283 | ##### 1. 最佳置换算法(OPT)
284 |
285 | 选择的被淘汰的页面是之后最长时间内不再访问的,可以保证最低的缺页率,但是无法预知未来所以无法实现,可以拿来测量其他算法
286 |
287 | 谁最后访问,先换掉谁
288 |
289 | 
290 |
291 | ##### 2. 先进先出算法(FIFO)
292 |
293 | 优先淘汰最早进入内存的页面,因为不符合实际规律,用的比较少
294 |
295 | 谁呆的时间最长,先换掉谁
296 |
297 | 
298 |
299 | ##### ★ 3. 最近最久未使用算法(LRU)
300 |
301 | 
302 |
303 | 选择过程:
304 | 第一行从后往前数n个(n是物理块数),跳过重复的,那个就是要被替换的
305 |
306 | 比如上面例子第八列的4,往前数3个 0→3→2(重复的0跳过了),那么就替换2
307 | 第十列的3,往前数3个 2->4->0,就替换0
308 |
309 | ##### 4. Clock算法
310 |
311 | #### 页面分配策略
312 |
313 | ##### 驻留集大小
314 |
315 | 驻留集大小确定方式:
316 | 固定分配:给进程分配一定空间的物理块,后面不可变,即驻留集大小不变
317 | 可变分配:给进程分配的物理块在运行期间可以改变
318 | 局部置换:发生缺页时只能置换自己的物理块
319 | 全局置换:可以置换自己的,也可以置换其他进程的
320 |
321 | 有三种组合方式:
322 | 固定分区局部置换:初始分配固定数量,缺点是很难确定初始分多少
323 | 可变分区全局置换:只要缺页就会获得新的物理块,确定是之后缺页率会增多
324 | 可变分区局部置换:根据发生缺页的频率来动态改变进程的物理块
325 |
326 | ##### 调入页面的时机
327 |
328 | 预调页策略:一次调入若干个相邻的页(根据局部性原理),不过成功率不高,一般是运行前采用
329 | 请求调页策略:在缺页时调入,每次调一页,I/O开销大,一般是运行期间采用
330 |
331 | ##### 从何处调入页面
332 |
333 | 1. 有足够的对换区空间:全部从对换区调入所需页面,进程运行之前把所有有关文件从文件区复制到对换区
334 | 2. 没有足够的对换区空间:不会修改的文件直接从文件区调入,要修改的就得先去对换区
335 |
336 | ##### 抖动
337 |
338 | 页面置换中最糟糕的情况:刚换出的页面马上又要换入主存,刚换入的页面马上又要换出,这种频繁的调度称为抖动或颠簸
339 |
340 |
341 | #### Tips
342 |
343 | - 虚拟内存的实现只能建立在离散分配的内存管理的基础上
344 | - 虚拟内存技术是补充内存逻辑空间的技术
345 | - 虚拟储存器理论逻辑上的最大容量由地址长度决定,实际上可能的最大容量为理论最大容量和内存外存和之中较小的那一个
346 | - 所有的调度策略都不可能完全避免抖动
347 |
--------------------------------------------------------------------------------
/进程与线程.md:
--------------------------------------------------------------------------------
1 | ## 操作系统原理(二)进程与线程
2 |
3 | 本部分主要包括四个模块:进程与线程的概念、处理机调度算法、进程同步和死锁问题
4 |
5 | [toc]
6 |
7 | ### 进程与线程
8 |
9 | #### 进程
10 |
11 | 
12 |
13 |
14 | 进程是进程实体的运行过程。是系统进行资源分配和调度的一个独立单位。
15 |
16 | 进程包含运行时的所有信息,不仅仅包含程序代码,还包括当前活动,通过程序计数器和处理器寄存器的内容来表示;另外还经常包括堆栈端和数据段,还有可能包括堆
17 |
18 | 正文段:包括全局变量、常量等
19 | 数据栈段:局部变量、传递的实参等
20 | 数据堆段:被分配的内存
21 | PCB:进程自身的一些信息
22 |
23 | 程序不是进程,程序是被动实体,进程是活动实体,当一个可执行文件被装入内存时,一个程序才可能成为进程。同一个程序也可以通过创建副本而成为多个独立的进程
24 |
25 | 进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。
26 |
27 | **PCB是进程存在的唯一标志,动态性是进程最基本的特征。**
28 |
29 | (下图显示了 4 个程序创建了 4 个进程,这 4 个进程可以并发地执行。)
30 |
31 | 
32 |
33 | 进程是由多程序的并发执行而提出的,和程序是截然不同的概念,进程的特点如下:
34 | (了解即可)
35 |
36 | 1. 动态性,有动态的地址空间,有生命周期(后面有图),动态产生、变化和消亡,最基本的特征
37 | 2. 并发性,多个进程实体能在一段时间内同时运行,引入进程的目的就是为了并发
38 | 3. 独立性,各进程地址空间相互独立
39 | 4. 异步性,各进程按照各自独立、不可预知的速度向前推进
40 | 5. 结构性,程序段、数据段、PCB
41 |
42 | ##### 进程和程序的区别
43 |
44 | - 程序是静态的,进程是动态的(算一个根本区别)
45 | - 程序是有序代码的集合,进程是程序的执行
46 | - 程序是永久的,进程是暂时的
47 | - 进程包括程序、数据和PCB
48 | - 通过多次执行,一个程序可以对应多个进程;通过调用关系,一个进程可以包括多个程序
49 |
50 |
51 | #### 进程状态的切换 ★
52 |
53 | 
54 |
55 | 最重要的一条是从就绪到运行
56 |
57 | - 就绪状态:等待被调度,进程已经获得了除了处理机之外的一切所需资源
58 | - 运行状态:进程正在处理机上运行,对于单CPU而言,同一时间运行的进程只能有一个
59 | - 等待状态:等待资源或等待I/O,即使处理机空闲也无法运行
60 | - 新的状态:进程正在被创建,尚未转化到运行状态
61 |
62 | 应该注意以下内容:
63 |
64 | - 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。
65 | - 不能从等待直接到运行,因为在等I/O,给CPU也没有。不能从就绪到等待,因为就绪只缺CPU,别的都不缺,给了也没用。
66 | - 等待状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。
67 | - 某个时间点上等待只能等一个资源
68 |
69 | #### 进程间通信
70 |
71 | 不和其他任何进程共享数据的进程是独立的,如果互相有意向那么该进程是协作的
72 |
73 | 有许多理由:
74 | 1. 信息共享
75 | 2. 提高运算速度
76 | 3. 模块化
77 | 4. 方便
78 |
79 |
80 | 进程之间交换数据不能通过访问地址空间,因为进程各自的地址空间是私有的
81 |
82 | PV操作是低级进程通信方式,高级进程通信方式包括:**共享内存、消息传递、管道通信、共享文件**
83 |
84 | 共享内存比消息传递快,它可以达到内存的速度,要求通信进程共享一些变量,进程通过使用这些变量来共享信息、主要由程序员提供通信,操作系统只需要提供共享呢内存;
85 |
86 | 消息传递更易于实现,适合交换少量的数据,不需要避免冲突,允许进程交换信息。提供通信的主要职责在于操作系统本身
87 |
88 |
89 |
90 | #### 进程控制块(PCB)
91 | - 进程状态:包括新的、就绪、运行、等待、停止
92 | - 程序计数器:表示要执行的下一个指令的地址
93 | - CPU寄存器:包括很多东西,中断时要保存
94 | - CPU调度信息:包括进程优先级、调度队列指针和其他调度参数
95 | - 内存管理信息:
96 | - 记账信息:
97 | - I/O状态信息:
98 |
99 |
100 | PCB块是进程的唯一标识,OS就是通过控制PCB来对并发执行的进程进行控制和管理的
101 |
102 | 
103 |
104 |
105 | #### 进程调度
106 |
107 | 进程调度会选择一个可用的程序到CPU上执行
108 |
109 | ##### 调度队列
110 |
111 | 进程到系统后会进入作业队列,包括所有进程。等待运行的会进入到就绪队列。通常用链表表示
112 |
113 |
114 | ##### 调度程序
115 |
116 | 操作系统必须按某种方式从队列中选择进程,进程的选择是由相应的调度程序来执行的
117 |
118 | 短期调度程序
119 | 长期调度程序
120 |
121 | ##### 上下文切换
122 |
123 | 一个进程运行时,CPU中所有寄存器中的内容、进程的状态以及运行栈中的内容被称为进程的上下文(和PCB对应)
124 |
125 | 通过执行一个状态保存来**保存**CPU当前状态,之后执行一个状态**恢复**重新开始运行。将CPU切换到另一个进程需要保存当前状态并恢复另一个进程的状态,这一状态切换称为上下文切换。
126 |
127 | #### 进程控制
128 |
129 | ##### 进程的创建
130 |
131 | 允许一个进程创建另一个进程,创建者称为父进程,被创建的称为子进程,子进程可以获得父进程的所有资源,子进程结束会归还资源给父进程,父进程结束必须同时撤销子进程。
132 |
133 | 创建进程的过程:
134 | 1. 分配唯一的进程标志号(pid)
135 | 2. 分配资源
136 | 3. 初始化PCB
137 | 4. 插入到就绪队列
138 |
139 | 通过fork()系统调用,可以创建新进程
140 |
141 | ##### 进程的终止
142 |
143 | 包括正常结束和异常结束
144 |
145 | 终止进程的过程
146 | 1. 根据pid搜索PCB,读出状态
147 | 2. 立即终止
148 | 3. 若还有子进程,一并终止
149 | 4. 归还资源
150 | 5. 把PCB从队列中删除
151 |
152 | 如果一个进程终止,那么它的所有子进程都终止。这种现象称为级联终止
153 |
154 | ##### 进程的阻塞
155 |
156 | 自发的行为 block()
157 |
158 | ##### 进程的唤醒
159 |
160 | wakeup()
161 |
162 | #### 线程
163 |
164 | 线程是CPU使用的基本单元,由线程ID、程序计数器、寄存器集合和栈组成,它与属于同一进程的其他线程共享代码段、数据段和其他操作系统资源
165 |
166 | 线程是独立调度的基本单位,引入线程的目的是为了简化进程间的通信,减小程序在并发执行时所付出的时间开销,提高操作系统的并发性
167 |
168 | 线程自己不拥有系统资源,只有一点点必要的运行资源,但是它可以和同一进程的其他线程一起共享进程拥有的全部资源
169 |
170 | 
171 |
172 |
173 | 举个栗子:QQ和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。
174 |
175 | 多线程编程的优点:
176 | 1. 响应度高
177 | 2. 资源共享
178 | 3. 经济
179 | 4. 多处理器体系机构的利用
180 |
181 | ##### 多线程模型
182 |
183 | 有两种不同的方法来提供线程支持:用户层的用户线程和内核层的内核线程,二者有三种对应关系
184 |
185 | 1. 多对一模型:多个用户线程映射到一个内核线程
186 | 2. 一对一模型:有更好的并发功能,但是开销比较大
187 | 3. 多对多模型
188 |
189 | ##### 线程池
190 |
191 | 为了解决创建线程时间和丢弃,以及没有限制线程数量可能会导致资源用尽的问题,产生了线程池的解决方法
192 |
193 | 线程池会在进程开始时创建一定数量的线程,并放入池中来等待,服务器每次收到请求就会唤醒池中的一个线程,线程完成了任务又会返回池中。如果池中没有可用线程,服务器会等待。
194 |
195 | 优点:
196 | 使用现有线程比创建新线程快
197 | 限制了数量,不会耗尽资源
198 |
199 |
200 | ##### 线程库
201 |
202 | 线程库为程序员提供创建和管理线程的api
203 |
204 | Pthread
205 | Win32
206 | Java
207 |
208 | ##### 多线程问题
209 |
210 | fork()和exec()
211 |
212 | 取消:异步取消、延迟取消
213 |
214 | #### 进程与线程的区别
215 |
216 | ##### 资源方面
217 |
218 | **进程是资源分配的基本单位**,但是线程不拥有资源,线程可以访问隶属进程的资源,线程没有自己独立的地址空间。
219 |
220 | ##### 调度方面
221 |
222 | **线程是独立调度的基本单位**,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
223 |
224 | ##### 系统开销
225 |
226 | 由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
227 |
228 | ##### 通信方面
229 |
230 | 线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。
231 |
232 | ##### 并发性
233 |
234 | 一个进程间的多个线程可以并发
235 |
236 | #### Tips
237 |
238 | - 一个进程被不同的地方调用,会形成不同的进程
239 | - 同一个线程可以被多个进程调用,线程还是同一个
240 | - 对进程的管理和控制是通过执行各种原语来实现的
241 |
242 |
243 | ### 处理机调度
244 |
245 | 
246 |
247 |
248 | 单处理器系统,每次只允许一个进程运行,这个效率啊,efficiency。
249 |
250 | 每当CPU空闲时,操作系统就必须从就绪队列中选择一个进程来执行,由短期调度程序或者CPU调度来执行。调度程序从内存中选择一个能够执行的进程并为之分配CPU
251 |
252 | 就绪队列可实现为FIFO队列、优先队列、树或者简单的无序链表
253 |
254 | CPU调度决策可在如下四种环境下发生:
255 | 1. 进程从运行切换到等待(I/O请求、调用wait等)
256 | 2. 从运行切换到就绪(出现中断)
257 | 3. 从等待切换到就绪(I/O完成)
258 | 4. 终止
259 |
260 | 1、4的情况下,调度方式是非抢占的,2、3是抢占的
261 |
262 |
263 | **调度的层级:**
264 | - 作业调度:给作业获得竞争处理机的机会
265 |
266 | - 中级调度:内存调度,提高内存利用率和系统吞吐量
267 |
268 | - 进程调度:低级调度,从就绪队列选进程分配处理机
269 |
270 | **进程调度方式:**
271 | - 剥夺调度:有另一个优先级高的进程进入,立即调度
272 |
273 | - 非剥夺调度:等一个进程介绍才会调度,不抢
274 |
275 |
276 | #### 调度准则
277 |
278 | 用于比较的特征准则有:
279 |
280 | - CPU使用率
281 | - 吞吐量:一个单位时间内完成进程的数量
282 | - 周转时间:从进程提交到进程完成的时间称为周转时间
283 | - 平均周转时间:多个作业周转时间平均值
284 | - 带权周转时间:周转时间/运行时间
285 | - 平均带权周转时间:多个作业带权周转时间的平均值
286 | - 等待时间:为最就绪队列中等待所花的时间,调度算法不影响进程运行时间,只影响在队列中的等待时间
287 | - 响应时间:从提交请求到响应请求的时间
288 |
289 |
290 | #### 调度算法
291 |
292 | CPU调度是多道程序操作系统的基础,通过在进程之间切换CPU,操作系统可以提高计算机的吞吐率。
293 |
294 | 不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法。
295 |
296 | 几个概念:
297 | **周转时间**:最后完成的时刻-刚进来的时刻,即包括等待时间与运行时间
298 | **带权周转时间**:周转时间/进程运行时间
299 | **响应比**:(等待时间+要求服务时间)/ 要求服务时间
300 |
301 |
302 | ##### 1. 批处理系统
303 |
304 | 批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。
305 |
306 | **1.1 先来先服务 first-come first-serverd(FCFS)**
307 |
308 | 按照请求的顺序进行调度。
309 |
310 | 用FIFO队列就可以实现
311 |
312 | 有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。
313 |
314 | **1.2 短作业优先 shortest job first(SJF)**
315 |
316 | 按估计运行时间最短的顺序进行调度。
317 |
318 | SJF算法调度理论上确实是最佳的,但问题在于如何知道下一个CPU区间的长度
319 |
320 | 长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。
321 |
322 | **1.3 最短剩余时间优先 shortest remaining time next(SRTN)**
323 |
324 | 按估计剩余时间最短的顺序进行调度。
325 |
326 | ##### 2. 交互式系统
327 |
328 | 交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。
329 |
330 | **2.1 时间片轮转**
331 |
332 | 将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。
333 |
334 | **时间片轮转算法是为了多个用户能及时干预系统**
335 |
336 | 时间片轮转算法的效率和时间片的大小有很大关系:
337 |
338 | - 因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
339 | - 而如果时间片过长,那么实时性就不能得到保证。
340 |
341 | **2.2 优先级调度**
342 |
343 | 为每个进程分配一个优先级,按优先级进行调度。
344 |
345 | 为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
346 |
347 | **2.3 多级反馈队列**
348 |
349 | 一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。
350 |
351 | 多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。
352 |
353 | 每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。
354 |
355 | 可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。
356 |
357 | 
358 |
359 |
360 |
361 | ##### 3. 实时系统
362 |
363 | 实时系统要求一个请求在一个确定时间内得到响应。
364 |
365 | 分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。
366 |
367 | #### Tips
368 |
369 | - 考虑到系统资源利用率,要选择让I/O繁忙型作业有更高的优先级
370 | - 作业和进程的区别,作业由用户提交、以用户任务为单位,进程由系统自动生成、以操作系统控制为单位
371 | - 分时操作系统采用时间片轮转调度算法
372 |
373 |
374 | ### 进程同步
375 |
376 |
377 | 
378 |
379 | #### 同步与互斥
380 |
381 | **同步**:直接相互制约同步
382 |
383 | 进程之间的合作,比如A进程向B进程提供数据,多个进程会按一定顺序执行;
384 |
385 | **互斥**:间接相互制约
386 |
387 | 源于资源共享,几个进程抢相同的资源,多个进程在同一时刻只有一个进程能进入临界区。
388 |
389 | 互斥可能会出现两种情况:
390 | 饥饿:拥有其他所有资源却得不到CPU的就绪进程
391 | 死锁:进程之间相互等待对方资源却无法得到,陷入永远的阻塞
392 |
393 | 进程互斥实际上是进程同步的一种特殊情况,可以认为是逐次使用互斥资源
394 |
395 | #### 临界资源和临界区
396 |
397 | **临界资源** :一次仅允许一个进程使用的资源,比如说硬件资源打印机什么的,一些非硬件资源比如一些变量、数据
398 |
399 | **临界区** :对临界资源进行访问的那段代码称为临界区
400 |
401 | 不论是硬件临界资源还是软件临界资源,多个进程必须互斥地对它们进行访问
402 |
403 | 把临界资源的访问分为四个部分:进入区、临界区、退出区、剩余区
404 |
405 | 为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查,看是否正在被访问,如果未被访问就可以进入临界区,并设置它正在被访问的标志。在进入临界区之前执行的这段代码称为进入区。
406 |
407 | 在出来之前也要运行一段代码,把访问标志清除,这段代码称为退出区。
408 |
409 | 进入区和退出区起到互斥保护临界区的作用
410 |
411 | ```c
412 | while(1){
413 | entry section // 进入区
414 | critical section // 临界区
415 | exit section // 退出区
416 | remainer section // 剩余区
417 | }
418 | ```
419 |
420 | 临界区不是原语的,中途可以出现中断等(反正其他的还是进不来)
421 |
422 | **同步机制的原则:**
423 | - 空闲让进
424 | - 忙则等待
425 | - 有限等待
426 | - 让权等待
427 |
428 | #### 信号量
429 |
430 | 信号量(Semaphore)是一个整型变量,可以对其执行wait和signal操作,也就是常见的 P 和 V 操作。
431 |
432 | wait是P(申请资源),signal是V(释放资源)
433 |
434 | S.value的初值就代表资源的数目,如果S.value < 0,那么其绝对值表示阻塞的进程的个数
435 |
436 | ##### 整型信号量
437 |
438 | **P** :如果信号量S大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0;
439 | **V** :对信号量S执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。
440 |
441 | P V操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。
442 |
443 | 缺点是在S<=0时,会出现”忙等”,没有遵循让权等待
444 |
445 | ##### 记录型信号量
446 |
447 | 再增加一个进程链表,链接上述的等待进程
448 | P V操作修改为
449 |
450 | ```C++
451 | P(semaphore S) {
452 | S.value = S.value - 1; // 请求一个资源
453 | if(S.value < 0) block(S.L) // 如果资源没了,就调用block阻塞掉,插入到信号量链表S.L中
454 | }
455 |
456 | V(semaphore S) {
457 | S.value = S.value + 1 // 释放一个资源
458 | if(S.value <= 0) wakeup(S.L) // 如果释放掉后还是堵着的,那么唤醒一个堵着的资源
459 | //(如果释放掉后不堵了,前一步也是0,说明链表原本就没有东西,所以不存在wakeup)
460 | }
461 | ```
462 |
463 | **实现前驱关系**:比如S1结束才能执行S2,可以设置一个信号量a,初始值为0,S1中V(a),S2中P(a)
464 |
465 | **实现互斥关系**:比如S1和S2互斥,可以设置一个信号量a,初始值为1,S1、S2都用P(a) V(a)夹紧中间过程
466 |
467 |
468 | 如果信号量的取值只能为 0 或者 1,那么就成为了 **互斥量(Mutex)** ,0 表示临界区已经加锁,1 表示临界区解锁。
469 |
470 | ```c
471 | typedef int semaphore;
472 | semaphore mutex = 1;
473 | void P1() {
474 | down(&mutex);
475 | // 临界区
476 | up(&mutex);
477 | }
478 |
479 | void P2() {
480 | down(&mutex);
481 | // 临界区
482 | up(&mutex);
483 | }
484 | ```
485 |
486 | #### 实现同步互斥
487 |
488 | ##### 软件实现:Peterson's Algotithm
489 |
490 | flag表示有这个意愿,turn表示把回合让给谁,这是一个很谦让的算法,进程i要来先打个招呼,说我有执行的打算,然后把回合让给进程j,如果j没打算,那么i直接进去,进去之后,j如果再进来,直接把回合又给了i,j达到了while等待条件,只能等着i的执行完之后,j再进去
491 |
492 | P[i]进程
493 | ```C
494 | flag[i] = TRUE;
495 | turn = j;
496 | while(flag[j]&&turn==j); // j正在访问临界区,i等着
497 | critical section;
498 | flag[i] = FALSE;
499 | remainder section;
500 | ```
501 |
502 | P[j]进程
503 | ```C
504 | flag[j] = TRUE;
505 | turn = i;
506 | while(flag[i]&&turn==i);
507 | critical section;
508 | flag[j] = FALSE;
509 | remainer section;
510 | ```
511 |
512 |
513 | #### 典型问题:生产者-消费者问题
514 |
515 | 这是一个非常重要而典型的问题,进程同步60%以上的题目都是生产者消费者的改编
516 |
517 | 问题描述:有一群生产者进程在生产产品,并将这些产品提供给消费者进程去进行消费。使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。
518 |
519 | 因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。(缓冲区用循环队列就可以模拟)
520 |
521 | 为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。
522 |
523 | 注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。
524 |
525 | 顺序执行的时候显然没有任何问题,然而在并发执行的时候,就会出现差错,比如共享变量counter,会出现冲突。解决的关键是将counter作为临界资源来处理。
526 |
527 |
528 |
529 | ```c
530 | ##define N 100
531 | typedef int semaphore;
532 | semaphore mutex = 1;
533 | semaphore empty = N;
534 | semaphore full = 0;
535 |
536 | void producer() {
537 | while(TRUE) {
538 | int item = produce_item();
539 | down(&empty);
540 | down(&mutex);
541 | insert_item(item);
542 | up(&mutex);
543 | up(&full);
544 | }
545 | }
546 |
547 | void consumer() {
548 | while(TRUE) {
549 | down(&full);
550 | down(&mutex);
551 | int item = remove_item();
552 | consume_item(item);
553 | up(&mutex);
554 | up(&empty);
555 | }
556 | }
557 | ```
558 |
559 | **实际例子:吃水果问题**
560 |
561 | 桌子上有一个盘子,可以放一个水果。爸爸每次放一个苹果,妈妈每次放一个桔子;女儿每次吃一个苹果,儿子每次吃一个桔子。
562 |
563 | ```C++
564 | semaphore S = 1, SA = 0, SO = 0; // 盘子的互斥信号量、苹果和桔子的互斥信号量
565 |
566 | father(){
567 | while(1){
568 | have an apple;
569 | P(S);
570 | put an apple;
571 | V(SA);
572 | }
573 | }
574 |
575 | mother(){
576 | while(1){
577 | have an orange;
578 | P(S);
579 | put an orange;
580 | V(SO);
581 | }
582 | }
583 |
584 | daughter(){
585 | while(1){
586 | P(SA);
587 | get an apple;
588 | V(S);
589 | eat an apple;
590 | }
591 | }
592 |
593 | son(){
594 | while(1){
595 | P(SO);
596 | get an orange;
597 | V(S); // 吃完之后要V(S)才能接着让爸爸妈妈接着放
598 | eat an orange;
599 | }
600 | }
601 | ```
602 |
603 | #### 典型问题:哲学家就餐问题
604 |
605 | 这是由Dijkstra提出的典型进程同步问题
606 |
607 | 5个哲学家坐在桌子边,桌子上有5个碗和5支筷子,哲学家开始思考,如果饥饿了,就拿起两边筷子进餐(两支筷子都拿起才能进餐),用餐后放下筷子,继续思考
608 |
609 | 多个临界资源的问题
610 |
611 | 只考虑筷子互斥:可能会产生死锁,比如所有人都同时拿起右边筷子,左边无限等待
612 | 再考虑吃饭行为互斥:同时只让一个人吃饭,肯定不会冲突或死锁,但是资源比较浪费
613 |
614 | 解决方法1:允许4位哲学家同时去拿左边的筷子
615 |
616 | ```C++
617 | semaphore chopstick[5] = {1, 1, 1, 1, 1}; // 筷子信号量
618 | semaphore eating = 4; // 允许四个哲学家可以同时拿筷子
619 |
620 | void philosopher(int i){ // 第i个哲学家的程序
621 | thinking();
622 | P(eating);
623 | P(chopstick[i]); // 请求左边的筷子
624 | P(chopstick[(i+1)%5]); // 请求右边的筷子
625 | eating();
626 | V(chopstick[(i+1)%5]);
627 | V(chopstick[i]);
628 | V(eating);
629 | }
630 | ```
631 |
632 | 解决方式2:奇数位置的哲学家先左后右,偶数位置的哲学家先右后左
633 |
634 |
635 | #### 典型问题:读者写者问题
636 |
637 | 读进程:Reader进程
638 | 写进程:Writer进程
639 |
640 | 允许多个进程同时读一个共享文件,因为读不会使数据混乱;但同时仅仅允许一个写者在写
641 |
642 | 写-写互斥,写-读互斥,读-读允许
643 |
644 | 纯互斥问题,没有同步关系没有先后之分
645 |
646 | 需要多加一个读者计数器,并且修改这个时要同步,所以要再加一个变量保证修改count时的互斥
647 |
648 | ```C++
649 | semaphore rmutex = 1, wmutex = 1; // readcount的互斥信号量,数据的互斥信号量
650 | int readcount= 0;
651 |
652 | Reader(){ // 每次进入和退出因为涉及readcount变化,要保证同时只有一个,所以分别设置rmutex
653 | while(1){
654 | P(rmutex); // 抢readcount信号量,防止多个reader同时进入 导致readcount变化不同
655 | if(readcount==0){
656 | P(wmutex); // 第一个进来的读者,抢公共缓冲区
657 | }
658 | readcount += 1;
659 | V(rmutex); // 其他reader可以进来了
660 |
661 | perform read operation;
662 |
663 | P(rmutex); // 再抢一次,使每次只有一个退出
664 | readcount -= 1;
665 | if(readcount==0){
666 | V(wmutex); // 最后一个reader走了,释放公共缓冲区
667 | }
668 | V(rmutex);
669 | }
670 | }
671 |
672 | Writer(){ // 写者很简单,只需要考虑wmutex公共缓冲区
673 | while(1){
674 | P(wmutex);
675 | perform write operation;
676 | V(wmutex);
677 | }
678 | }
679 | ```
680 |
681 | 上面的算法可能会导致写者可能会被插队,如果是RWR,中间的W被堵了,结果后面的R还能进去,W还要等后面的R读完
682 |
683 | 变形:让写者优先,如果有写者,那么后来的读者都要阻塞,实现完全按照来的顺序进行读写操作。
684 | 解决方法:再增加一个wfirst信号量,初始为1,在读者的Read()和之前的阶段和写者部分都加一个P(wfirst)作为互斥入口,结尾V(wfirst)释放即可
685 |
686 | 变形:让写者真正优先,可以插队
687 | 解决方法:增加一个writecount统计,也就是加一个写者队列,写者可以霸占这个队列一直堵着
688 |
689 |
690 | #### 例子:汽车过窄桥问题
691 |
692 | 来往各有两条路,但是中间有一个窄桥只能容纳一辆车通过,方向一样的车可以一起过
693 |
694 | ```C++
695 | semaphore mutex1=1, mutex2=1, bridge=1;
696 | int count1=count2=1;
697 |
698 | Process North(i){
699 | while(1){
700 | P(mutex1);
701 | if(count1==0){
702 | P(bridge); // 第一辆车来了就抢bridge让对面的车进不来
703 | }
704 | count1++;
705 | V(mutex1);
706 |
707 | cross the bridge;
708 |
709 | P(mutex1);
710 | count1--;
711 | if(count1==0){
712 | V(bridge); // 最后一辆车走了就释放bridge让对面的车可以进来
713 | }
714 | V(mutex1);
715 | }
716 | }
717 |
718 | Process South(i){
719 | while(1){
720 | P(mutex2);
721 | if(count2==0){
722 | P(bridge);
723 | }
724 | count2++;
725 | V(mutex2);
726 |
727 | cross the bridge;
728 |
729 | P(mutex2);
730 | count2--;
731 | if(count2==0){
732 | V(bridge);
733 | }
734 | V(mutex2);
735 | }
736 | }
737 | ```
738 |
739 | #### 管程
740 |
741 | 使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。
742 |
743 | (管程有点像用一个类来封装初始化、共享数据、操作代码等)
744 |
745 | 管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否者其它进程永远不能使用管程。
746 |
747 | 管程定义了一个数据结构和能为并发进程所执行的一组操作,这组操作可以同步进程和改变管程中的数据
748 |
749 | 管程引入了 **条件变量** 以及相关的操作:**wait()** 和 **signal()** 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。
750 |
751 | #### Tips
752 |
753 | - 临界资源同一时间只能被一个进程访问,而共享资源一段时间可以被多个访问,磁盘属于共享设备而不是临界资源
754 | - 要对并发进程进行同步的原因是:并发进程是异步的
755 | - PV操作由两个不可被中断的过程组成,也就是它们两个,它们都属于低级进程通信原语,不是系统调用
756 | - 有五个并发进程涉及到同一个变量A,说明有五段代码,也就是说有5个临界区
757 | - 信箱通信是一种间接通信方式
758 |
759 | ### 死锁
760 |
761 | 
762 |
763 | 多个程序因为竞争共享资源而造成的僵局,若无外力作用,这些进程都将永远无法继续推进
764 |
765 | **★产生死锁的四个条件**,都满足就会产生:
766 | 1. **互斥条件**:要求某资源进行排它性占有
767 | 2. **不剥夺条件**:进程已经获得的资源在未被使用完前不可被剥夺
768 | 3. **请求和保持条件**:已经至少拥有了一个资源,但是又提出了一个新的资源请求
769 | 4. **循环/环路等待条件**(必要而非充分):存在一个进程--资源循环链
770 |
771 | 举个例子,你手上拿着一个苹果,一个苹果只能被一个人拥有,这是互斥条件;别人不能把苹果从你手中抢走,这是不剥夺条件;你抱着这个苹果不放还想去拿别的苹果,这是请求和保持;一圈人都想拿他右边那个人的苹果,陷入循环,这是循环等待条件
772 |
773 | #### 死锁处理
774 | 1. 不让死锁发生:预防死锁、避免死锁
775 | 2. 允许死锁发生,但死锁发生时要能检测到并加以处理:检测死锁、解除死锁
776 |
777 | 
778 |
779 | #### 预防死锁
780 |
781 | 设置某些限制条件,破坏上面四个必要条件的一个或几个:
782 |
783 | - (互斥在很多场合下是必须要遵循的,没办法破坏该条件)
784 | - 摈弃“请求和保持条件”:所有进程一次性获得所有资源
785 | - 摈弃“不可剥夺性”:如果提出新的申请无法满足,则放弃所有已经拥有的资源
786 | - 摈弃“环路等待”:资源按顺序编号,进程必须按顺序申请资源
787 |
788 |
789 | #### 避免死锁
790 |
791 | 在资源的动态分配过程中,用某种方法防止系统进入不安全状态,从而避免死锁
792 |
793 | **银行家算法**(过程省略,找个例子就很容易理解)(卧槽,看完了才发现不考)
794 | 尝试分配,然后检测安全性
795 |
796 | #### 死锁检测及解除
797 |
798 | 不采取任何限制措施,允许死锁,通过系统检测机构及时检测出死锁的产生,并采取某些措施解除死锁
799 |
800 |
801 | 接触死锁的方法:
802 | 1. 剥夺资源
803 | 2. 撤销进程
804 |
805 |
806 | #### Tips
807 |
808 | - 进程产生死锁的主要原因。时间上可能是进程运行中的推进顺序不当,空间上的原因是对独占资源分配不当,而不是系统资源不足
809 | - 死锁的避免是根据防止系统进入不安全状态采取措施,实现的结果是让进程推进合理
810 | - 结束死锁甚至可以终止所有死锁进程,但是一般不会从非死锁进程处抢夺资源
811 | - 死锁状态一定是不安全状态,但是不安全状态不一定有死锁,还可能是其他的
812 | - 限制用户申请资源的顺序属于死锁预防的破坏循环等待,限制给进程分配资源的顺序属于死锁避免
813 |
--------------------------------------------------------------------------------