操作系统之 同步互斥
并发进程的正确性
执行过程是不确定性和不可重现的 程序错误可能是间歇性发生的
- 独立进程- 不和其他进程共享资源或状态
- 确定性 输入状态决定结果
- 可重现 能够重现起始条件
- 调度顺序不重要
 
- 并发进程- 在多个进程间有资源共享(都要CPU 内存)
- 不确定性
- 不可重现
 
进程并发执行的好处
进程需要与计算机中其他进程或设备进行协作
- 共享资源
- 加速- I/O操作和CPU计算可以并行
- 程序可划分成多个模块放在多个处理机上并行执行
 
- 模块化- 将大程序分解成小程序(gcc会调用 cpp cc1 cc2 as ld)
- 使系统易于复用和扩展
 
原子操作(Atomic Operation)
一次不存在任何中断或失败的操作 要么执行成功 要么没有执行
操作系统需要利用同步机制 即允许并发执行 以便进行资源共享 和提高速度 同时保证一些操作是原子操作
进程的交互关系
- 互斥(Mutual Exclusion)- 一个进程占用资源 其他进程不能使用
 
- 死锁(Deadlock)- 多个进程各占用部分资源 形成循环等待
 
- 饥饿(Starvation)- 其他进程可能轮流占用资源 一个进程一直得不到资源
 
临界区(Critical Section)
进程访问临界资源的一段需要互斥执行的代码
| 1 | entry section | 
- 进入区(entry section)- 检查 可否 进入临界区的一段代码
- 若可进入 设置相应 正在访问临界区 标志
 
- 退出区(exit section)- 清除 正在访问临界区 标志
 
- 剩余区(remainder section)- 代码中的其余部分
 
临界区访问规则
- 空闲则入- 没有进程在临界区时 任何进程可进入
 
- 忙则等待- 有进程在临界区时 其他进程不能进入临界区
 
- 有限等待- 等待进入临界区的进程不能无限期等待
 
- 让权等待(可选)- 不能进入临界区的进程 应释放CPU
 
临界区的实现方法
- 禁用中断
- 软件方法(共享变量)
- 更高级的抽象方法(操作系统)
不同临界区实现机制衡量标准
- 性能和并发级别
禁用中断
没有中断 没有上下文切换 因此没有并发
- 硬件将中断处理延迟到中断被启用之后
| 1 | local_irq_save(unsigned long flags); | 
- 进入临界区- 禁止所有中断 并保存标志
 
- 退出临界区- 使能所有中断 并恢复标志
 
禁用中断缺点
- 禁用中断后 进程无法被停止- 整个系统为此停下来
- 可能导致其他进程处于饥饿状态
 
- 临界区可能很长- 导致无法确定响应中断所需的时间
 
基于软件同步方法
线程通过共享变量来同步它们的行为
Peterson算法
共享变量
| 1 | int turn; // 表示该谁进入临界区 | 
进程i 进入区代码
| 1 | flag[i] = true; | 
退出区代码
| 1 | flag[i] = false; | 
| 1 | 当进程 j 没有申请进入临界区 则 flag[j] = false; turn = j; | 
Peterson算法
| 1 | do { | 
Dekkers算法
对比 Peterson算法 优点是可以很方便扩展到多个线程
| 1 | 进程 i 申请进入临界区 flag[i] = true; | 
| 1 | do { | 
N线程的软件方法(Eisenberg 和 McGuire)
将线程排成环 若要进入临界区 则将 线程的 flag 标志 填写好 再去看 turn 标志
- 线程Ti要等待从 turn 到 i-1的线程 都退出临界区后访问临界区
- 线程Ti退出时 将turn改成下一个请求线程

基于软件同步方法的缺点
- 复杂- 需要两个进程间共享变量
 
- 需要忙等待- 浪费CPU时间 (需要频繁查询共享变量的状态)
 
更高级的抽象方法
硬件提供一些同步原语
- 中断禁用
- 原子操作指令(从硬件上保证其原子性)
锁(Lock)
一个抽象的数据结构 基于原子操作指令
- 一个二进制变量(锁定/解锁)
- Lock::Acquire()- 锁被释放前一直等待 然后得到锁
 
- Lock::Release()- 释放锁 唤醒任何等待的进程
 
原子操作指令
现代CPU体系结构提供的一些特殊原子操作指令
测试和置位指令(TS Test-and-Set)
- 从内存单元中读取值
- 测试该值是否为1(真或假)
- 内存单元值设置为1
| 1 | boolean TestAndSet(boolean *target) { | 
交换指令(Exchange)
交换内存中的两个值
| 1 | void Exchange (boolean *a, boolean *b) { | 
TS指令实现自旋锁(Spinlock)
线程在等待的时候消耗CPU时间
| 1 | class Lock { | 
TS指令实现无忙等待锁
在锁的数据结构中加入等待队列
| 1 | class Lock { | 
原子操作指令锁特征
- 优点- 适用于单处理机或者共享内存的多处理机中任意数量的进程同步(禁用中断只适用于单处理机 多处理机的情况下 禁止单个处理机的中断 其他处理机仍然能够响应中断)
- 简单且容易证明
- 支持多临界区
 
- 缺点- 忙等待锁会消耗处理机时间
- 可能导致饥饿 进程离开临界区时有多个等待进程的情况
 
- 死锁- 拥有临界区的低优先级进程 但请求访问临界区的高优先级进程获得处理机并等待临界区
 
同步方法总结
锁是一种高级的同步抽象方法
- 互斥可以使用锁来实现
- 需要硬件支持
常用三种同步实现方法总结
- 禁用中断(仅限于单处理机)
- 软件方法(共享变量 复杂)
- 原子操作指令(单处理机或多处理机均可)