操作系统之互斥记录
忙等待:
while (1); // 占用CPU资源
饥饿:
高个子(进程1)和矮子(进程2)抢篮板(CPU执行权), 矮子抢不到篮板, 矮子饥饿.
一. 临界区的四个特点
1. 互斥
2. 前进
3. 有限等待
4. 无忙等待(可选)
临界区很短时, 允许忙等待
临界区很长时, 考虑基于上下文切换实现无忙等待
二. 实现互斥的四种方式
1. 禁用硬件中断
OS如何实现多进程?
通过时钟中断, 导致进程切换, 实现并发.
具体方法:
进入临界区 禁用中断
退出临界区 开启中断
局限性:
- 硬件事件无法及时响应
- 临界区长, 影响系统效率
- 多CPU下, 无法实现互斥
2. 软件实现
2.1Peterson算法
适用于双进程
Peterson算法的多进程变式:
Eisenberg and Meauire's 算法
两个重要的参数:
flag 需要?
turn 谁来?
伪代码(双进程)
1 process (i) { 2 do { 3 flag[i] = true; 4 turn = j; // 进程i将机会让给进程j 5 while (turn == j && flag[j]); // 进程j把握机会且本身也想进入临界区 6 // 临界区 7 flag[i] = false; 8 } while (1); 9 }
可以使用反证法证明Peterson算法满足临界区互斥, 前进和有限等待的属性.
2.2 Dekker算法
适用于双进程
2.3 面包店算法
适用于多进程
3. 原子指令
3.1 锁
两个方法:
acquire: 等待解锁, 获取锁
release: 解锁, 唤醒
锁方法具体实现:
3.1.1 基于test-and-set机器指令
test-and-set内部实现
1 boolean TestAndSet(boolean* target) 2 { 3 boolean rv = *target; 4 *target = true; 5 return rv; 6 }
忙等待版:
1 class Lock { 2 int value = 0; 3 } 4 5 Lock::Acquire() { 6 while (test-and-set(value)); 7 } 8 9 Lock::Release() { 10 value = 0; 11 }
两种情况:
锁被释放, value=0, value=1且返回0, 跳过while, 进入临界区
锁被占用, value=1, value=1且返回1, 自旋while, 忙等待状态
若临界区很长时, 应该考虑基于上下文切换实现无忙等待
无忙等待版:
1 class Lock { 2 int value = 0; 3 WaitQueue q; 4 } 5 6 Lock::Acquire() { 7 while (test-and-set(value)) { 8 add this TCB to wait queue q; 9 schedule(); // 线程睡眠, 阻塞, 让出CPU 10 } 11 } 12 13 Lock::Release() { 14 value = 0; 15 remove one thread t from q; 16 wakeup(t); // 唤醒 17 }
3.1.2 基于exchange机器指令
exchange内部实现
1 void Exchange(boolean* a, boolean* b) 2 { 3 boolean temp = *a; 4 *a = *b; 5 *b = temp; 6 }
伪代码:
1 int lock = 0; 2 3 thread (i) { 4 int key; 5 do { 6 key = 1; 7 while (key == 1) exchange(lock, key); 8 // 临界区 9 lock = 0; 10 } while (1); 11 }
3.2 信号量
可实现同步和互斥
3.2.1 信号量的实现
1 // 信号量 2 class Semaphore { 3 int sem; 4 // 等待队列 5 WaitQueue q; 6 } 7 8 // P操作 9 Semaphore::P() { 10 sem--; 11 if (sem < 0) { 12 add this thread t to q; 13 block(t); 14 } 15 } 16 17 // V操作 18 Semaphore::V() { 19 sem++; 20 if (sem <= 0) { 21 remove a thread t from q; 22 wakeup(t); 23 } 24 }
4. 管程(可实现同步或互斥)
schedule和wakeup操作
1 schedule(): 当前thread原地睡眠(醒来继续向下执行), 获取一个就绪thread 2 wakeup(): 将sleep进程 -> 就绪态进程
4.1. 实现管程
管程由Lock和条件变量组成
Lock保证管程中只有唯一一个线程
4.1.1 Lock实现
见上文
4.1.2 条件变量实现
1 // 条件变量 2 Class Condition { 3 // 等待队列默认为空 4 int numWaiting = 0; 5 WaitQueue q; 6 } 7 8 // 条件变量等待操作 9 Condition::Wait(lock) { 10 numWaiting++; 11 add this thread t to q; 12 // 释放当前锁 13 release(lock); 14 schedule(); 15 // 重新获得锁 16 require(lock); 17 } 18 19 // 条件变量唤醒操作 20 Condition::Signal() { 21 // 等待队列不为空时 22 if (numWaiting > 0) { 23 remove a thread from q; 24 wakeup(t); 25 numWaiting--; 26 } 27 }
4.2. 使用管程
生产者消费者问题
1 // 缓冲区 2 class BoundedBuffer { 3 ... 4 Lock lock; 5 Condition notFull, notEmpty; 6 // 缓冲区产品个数, 默认为空 7 int count = 0; 8 } 9 10 // 生产者 11 BoundedBuffer::Deposit(c) { 12 lock->Acquire(); 13 // 缓冲区为满 14 while (count == n) 15 notFull.Wait(&lock); 16 add c to the buffer; 17 // 缓冲区产品个数增加1, 执行此操作之后, 缓冲区必定不为空 18 count++; 19 notEmpty.Signal(); 20 lock->Release(); 21 } 22 23 // 消费者 24 BoundedBuffer::Remove(c) { 25 lock->Acquire(); 26 // 缓冲区为空 27 while (count == 0) 28 notEmpty.Wait(&lock); 29 remove c from buffer; 30 // 缓冲区产品个数减少1, 执行此操作之后, 缓冲区必定不为满 31 count--; 32 notFull.Signal(); 33 lock->Release(); 34 }
三. 进程通讯
1. 通讯方式
间接通讯: 管道 消息队列(先进先出msg) 共享内存(shm)
直接通讯: 信号
2. 阻塞和非阻塞
阻塞 同步
非阻塞 异步