文章目录
同步互斥机制
进程互斥
- 定义: 当很多进程需要共享资源时, 而这些资源又具有排他性,那么各个进程竞争使用这些资源的关系称为进程互斥
临界区
- 临界资源(critical source): 又称作互斥资源, 特点是一次只能给一个进程使用, 当多个进程都需要这些资源时候, 就衍生出了临界区或者说互斥区
- 临界区(critial section): 各个进程中对某个临界资源(共享变量)实施操作的程序片段
- 临界区使用原则
- 临界区没有进程时候, 进程可以随时进入
- 临界区只能有一个进程存在
- 临界区外进程不能阻塞程序进入临界区
- 不能使进程无限等待进入临界区
实现进程互斥方案
- 软件方案
- DEKKER算法(伪代码), 假设只有p, q两个进程, 代码给出了其中一个,另一个一样的思路
// p进程 pTurn = true; while(qTurn){ if (turn == 2){ pTurn = false; while (turn==2); pTurn = true; } } // 这里是临界区 intoSection() // 释放 turn = 1; pTurn = false; // 缺点: 浪费CPU等待时间 // 优点: 确实解决了临界区的问题
- PETERSON算法(伪代码)
void enter_region(int processId){ int otherId; otherId = 1 - processId; // 表示感兴趣进入临界区 interest[processId] = true; turn = processId; // 核心是这一步(保证了先到先进入临界区) while(turn==processId && interest[otherId]==true); } void leave_region(int processId){ interest[processId] = false; }
ps.感觉这种算法依旧会浪费CPU
- DEKKER算法(伪代码), 假设只有p, q两个进程, 代码给出了其中一个,另一个一样的思路
- 硬件方案
- 中断屏蔽方法(伪代码)
关中断 ->临界区 开中断
- 优点: 方便 简洁 适合系统进程
- 缺点: 代价大, 不适合多处理器, 不适合用户进程
- TSC指令
- 一条指令做了两件事: 读寄存器中的值判断临界区有无内容 ,有则继续读, 没则进入并且加锁
- 交换指令
进程同步
- 指多个进程中发生的事件存在某种时序关系, 说白了就是某一个进程需要继续往下走. 需要另一个进程的帮助, 否则就进入阻塞状态
- 生产者/消费者问题
信号量(PV操作)
- 信号量: 用于进程之间传递信息的整数
- 相关结构体
struct semaphore{
int count;
queueType queue;
}
- p操作(伪代码演示)
P(s){
s.count --;
if (s.count < 0){
// 该进程进入阻塞状态, 下CPU进入等待队列队尾
}
// 否则继续执行
}
- v操作(伪代码演示)
V(s){
s.count ++;
if (s.count <= 0){
// 说明有任务在阻塞队列中, 阻塞任务插入就绪队列中
}
}
- 说明: p,v操作都是原子操作
pv操作解决互斥问题
- 初始化信号量mutex=1
- 进入临界区之前P(mutex)
- 出临界区V(mutex)
- 案例
- 假设有三个进程A,B.C
- A进入临界区, mutex=0
- B再进去,mutex=-1进不去,送到等到队列
- C进去 mutex=-2进不去, 送到等待队列
- A出来 mutex=-1 <= 0 V操作唤醒B进程
- B进入临界区 再出来 mutex=0 <= 0唤醒C进程
- 最后C进程进入临界区, 最终mutex=1回到初始状态
信号量解决互斥问题
- 还是针对生产消费者的问题
- 假设互斥信号量mutex=1
- 缓冲区empty个数: N
- 缓冲区full个数: 0
- 看一下生产者和消费者伪代码
// 生产者
void producer(){
while(True){
item = get_product();
P(empty);
// 因为要进入临界区
P(mutex);
insert_item(item);
V(mutex);
V(full);
}
}
// 消费者
void consumer(){
while(True){
// 这里的full和mutex顺序不能颠倒 会导致一直等待
P(full);
P(mutex);
item = get_product();
// 消费
consume_item();
V(mutex);
V(empty);
}
}
读者写者问题
- 多读者 一写者 读者优先
- 场景: 多个进程共享一个数据区
- 读者进程: 在数据区里面读数据
- 写者进程: 在数据区里面写数据
- 条件
- 允许多个读者读
- 不允许多个写者
- 不允许读写同时操作
- 伪代码
// 读者进程
void reader(){
// 由于多个读者, readerCount也应该当做临界区保护起来
P(mutex);
if(readerCount==0) P(w);
readerCount ++;
V(mutex);
// 进去读资源
read();
P(mutex);
readerCount --;
if (readerCount == 0) V(w);
V(mutex);
}
void writer(){
P(w);
// 写数据
write();
V(w);
}
// 写者进程
管程
- 概念: 管程的出现是因为信号量编程繁琐且容易出错, 比如PV操作顺序可能导致死锁问题. 管程就是一组过程, 这组过程主要是对一些共享资源数据结构的管理。
- 管程大致的样子(伪代码)
monitor func
integer i;
condition c;
procedure insert();// 生产过程
...
...
end;
procedure remove(); // 消费过程
...
...
end;
end; //整个过程结束
-
进程与管程的关系: 进程通过调用管程中的过程, 来间接的获取数据。例如: 生产者需要调用insert()过程, 消费者调用remove()过程
-
特性
- 互斥: 使得管程中数据结构完整, 由编译器负责
- 同步: 在管程中设置了条件变量及等待/唤醒操作以解决同步问题, 也是通过忙等待或者唤醒来操作
-
管程中会不会出现两个活跃进程?
- 有可能, 假设A进程进入管程后需要等待资源才能进入下一步, 那么就会在管程内忙等待,并且释放信号量。这时候B进程进入将A唤醒, 这个时候管程里面就有两个活跃进程了。
-
管程中多个进程解决方案
- A等待B执行(MESA: 被唤醒的继续等)
- B等待A执行(Hoare: 被唤醒的先执行)
- 规定唤醒操作为管程中最后一条语句(pascal)
进程间通信
- 上面讲述的管程在多处理器的情况下不适合, 因此有了新的通信机制: 消息传递(send/receive原语)
- 适用场景
- 分布式系统
- 基于共享内存的多处理机系统
- 单处理机系统
- 消息传递过程
- send: 该方法调用时会先陷入内核然后将消息复制进入缓冲区(操作系统会找到一个空缓冲区),操作系统会将消息挂接到接收进程的PCB中(可以认为是指向了PCB),然后去做别的事。其中消息包括消息头(消息类型, 接收进程ID和发送进程ID, 消息长度,控制信息等)和消息体(消息内容)
- receive: 等接收进程上CPU的时候, 执行receive方法, 会陷入内核,操作系统会将缓冲区里面信息复制到相应进程的地址空间
- 共享内存
- 两个进程共享一块物理内存, 类似读者写者问题. 每个进程将自己的一块地址空间映射到一块物理内存, 实现通信
- 管道通信
- 利用一个传输介质连接两个相互通信的进程
典型操作系统IPC机制
Linux进程间通信机制
- 内核外同步机制: 管道, 消息队列, 共享内存, 信号量, 信号, 套接字
- 内核同步机制: 原子操作, 自旋锁, 读写锁, 信号量, 屏障等
原子操作
- 特点
- 不可分割,未执行完之前不会被其他事件中断
- 常用于实现资源的计数
屏障
- 作用: 对一组线程进行协调,个人觉得就是适用于大规模数据场景
参考
[1] 操作系统原理