操作系统-进程间互斥与同步
同时运行的进程要访问共享资源,此共享资源某一时刻只允许某个进程访问,其他进程需要等待,就称为进程间的互斥,操作此共享资源的代码片段称为临界去。
临界的访问要遵循四个原则:
- 如果没有进程在临界区,则想要进入临界区的进行可以直接进入。
- 如果某个进行在临界区,另外想进入临界去的进程需要等待。
- 正在临界区外的进程不能影响想进入临界区的进程。
- 进程不能无限期的等待进入临界区。
进程互斥有软件方法和硬件方法:
(1) 软件方法
- 单标志法,即判断某个全局变量,形式为
while{forbidden_access};
forbidden_access = true
进入临界区;
离开临界区;
forbidden_access = false - 双标志前检查法
- 双标志后检查法
- peterson法
总结:peterson法可以实现进程互斥访问,但需要忙等待,也就是占据cpu的进程在一直检测某个条件是否达成,不能主动将进程休眠。
(2) 硬件方法 - 关闭中断法
使用开、关中断指令,避免cpu的进程调度。
缺点: 多cpu不试用、开关中断指令不会交给用户进程 - TestAndSet指令
- Swap指令
后两者也是忙等待。
在以上软件、硬件的实现方法中,都存在忙等待现象,即进程不能主动休眠自己,需要不断检测某个条件是否达成,空耗cpu资源,并且会导致进程调度的“优先级反转”问题。
(优先级反转,正常情况优先级高的进程如果处于就绪态,进程调度就要执行此进程;在忙等环境中,如果优先级高的进程处于忙等,等待优先级低的进程退出临界区,而由于调度策略,cpu将不会切换到优先级低的进程去执行,则临界区将一直不会释放。)
为了可以主动让进程陷入休眠,不进行忙等,设计了两种原语:sleep和wakeup。
但在执行此原语时,一般会同判断条件放在一起,比如生产者消费者模型中,生产者进程中有:if 缓冲区已满:生产者sleep; 消费者进程中有: if 消费了一个产品后数量为buf-1: wakeup生产者。
但由于if判断和wakeup、sleep原因整体不是一个原子操作,中间可能会有cpu调度导致切换,就会产生信号丢失问题。
为了解决信号丢失,提出了一种经典的进程间同步策略:利用共享内存实现的信号量机制以及对应的p、v操作。
信号量机制
信号量机制的基本原理:两个或多个进程可以利用彼此间收发的简单信号来实现“正确的”并发执行,
一个进程在收到一个执行信号前,会被迫在一个确定的或者需要的地方停下来,从而保持同步或互斥。
- 数字型信号量
- 记录型信号量
信号量是一个整数,表示资源数,进程可以对它进行p\v操作。p操作将值减一,如果减后<0, 则说明资源耗尽,进程将自身标识存入信号量的等待队列中,将自己休眠,然后cpu进行调度,执行另外的进程;当另外的进程执行v操作–将资源数+1,如果发现结果<=0,说明有进程在等待,则唤醒等待队列中的一个进程。而由于p、v操作都是原子操作,因此判断和休眠、唤醒都是不可分割的,就不会再产生之前提到的问题。
当信号量初始值为1,就表示互斥。
使用信号亮解决生产者-消费者模型:
要满足的条件:
- 缓冲区不能同时访问
- 缓冲区满时不能写
- 缓冲区空时不能读
因此同时存在同步与互斥关系,访问缓冲区时需要互斥,而生产和消费时存在同步。
Producer{
生产一个产品;
P(empty);
P(mutex);
放入缓冲区;
V(mutex);
V(full);
}
Customer{
P(full);
P(mutex);
从缓冲区获取一个产品;
V(empty);
V(mutex);
使用产品;
}
mutex表示互斥,empty表示空位的信号量、full表示产品数量的信号量。
ps: P(empty)和P(mutex)、P(full)和P(mutex)中的两个p操作不能颠倒位置。每个进程中v操作可以颠倒位置,但会扩大临界区,会降低程序性能。
使用信号亮解决读者-写者模型(读者优先)
读者写者模型,两个进程,一个只读、一个只写,不会存在一个进程又读又写,需要满足关系:
(1)当前只有读时,可以同时读
(2) 有写时,不能读不能写
(3) 有读时,不能写
因此,读进程需要面临的情况:
1 当前没有读、没有写,可以直接读
2.当前写在等待,有读正在读,则 可以直接读
3.当前正在写,则不能读
写进程需要面临的情况:
1.当前没有读、没有写,可以直接写
2.当前有读,不能写
3.当前有写,不能写
可以看到,读和写应该是互斥的,但不是每次都互斥,如果有读操作,此时新来的读可以直接读,而写则需要等到所有的读完成才可写。因此只有第一次的读会执行P操作,最后一次的读会执行V操作,而每次写操作都要执行PV操作。
使用一个全局变量cnt表示当前读进程的数量,这个数量由于多进程访问,也要加锁,因此整个逻辑如下:
读进程{
P(mutex)
cnt++;
if cnt == 1 {
P(w)
}
V(mutex)
读操作;
P(mutex)
cnt--;
if cnt == 0 {
V(w)
}
V(mutex)
}
写进程 {
P(w)
写操作;
v(w)
}
Producer{
生产一个产品;
P(empty);
P(mutex);
放入缓冲区;
V(mutex);
V(full);
}
Customer{
P(full);
P(mutex);
从缓冲区获取一个产品;
V(empty);
V(mutex);
使用产品;
}
读进程{
P(mutex)
cnt++;
if cnt == 1 {
P(w)
}
V(mutex)
读操作;
P(mutex)
cnt--;
if cnt == 0 {
V(w)
}
V(mutex)
}
写进程 {
P(w)
写操作;
v(w)
}
Produer{
生产一个产品;
pthread_mutex_lock();
while(empty <=0) {
pthread_cond_wait(mutex, cond1)
}
放入产品;
pthread_cond_signal(mutex, cond2)
pthread_mutex_unlock();
}
Customer{
pthread_mutex_lock(); // 加锁
while(full <=0) { // 判断条件,是一种同步关系
pthread_cond_wait(mutex, cond2) // wait是释放锁后,陷入阻塞,如果被唤醒,则重新获得锁
}
取出产品;
pthread_cond_signal(mutex, cond1) // 唤醒等待empty的生产着
pthread_mutext_unlock();
使用产品;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix