操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)(4)

简介: 操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)

操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)(3):https://developer.aliyun.com/article/1511049

读者、写者问题中有一个潜在的问题:


只要有读进程还在读,进程就要一直阻塞等待,可能"饿死"。因此,这种算法中,读进程是优先的


例如,第一个进程到来后,执行P(rw)操作,使得rw由1变为0,那么写进程的P(rw)操作就会堵塞等待,若此时源源不断有读进程执行读操作,那么写进程就会一直等待。

我们可以再设置一个信号量,用于直线"写优先"

1.若两个读者之间并发运行:

读者1与读者2是并发运行的,即宏观上同时读,但是微观上读者1先执行读操作,再读者2进行读操作,若读者1执行到P(rw)操作后,切换到读者2进程,那么读者2进程会被堵塞再P(w)

直到读者1执行完V(w),读者2才能继续执行,由于这里会跳过if语句,所以读进程是能并发运行的。


2.两个写者进程并发运行:


第一个写进程没有执行完之前,第二个写进程会被堵塞再P(w),直到第一个写进程执行完V(w)之后,第二个写进程才能继续执行,两个写进程是能实现互斥访问的。


3.一个写者和一个读者并发运行:


(1)写者-->读者


写进程在写文件时,w的信号量已经由1变为0,此时读者进程会被堵塞在P(w),直到写进程执行完V(w),读者进程才有可能访问共享文件。实现了"写优先"。


(2)读者1--->写者1--->读者2


1.读者进程在读完文件后w值为1,写者进程能执行P(w),w的值由1变为0,但是写者进程会被堵塞在P(rw)


2.若此时还有读者想要读共享文件,由于w的值为0,所以会被堵塞在P(w)中


3.那么写者1和读者2都会堵塞,当读者读完文件后,count--,因为只有1个读者读文件,所以count--后,count值为0,那么就会执行V(rw)


4.这样就可以唤醒堵塞在P(rw)的写者进程了,实现了"写优先"

(3)写者1-->读者1-->写者2

1.当写者写文件时,执行P(w)和P(rw),那么w=0,rw=0


2.读者在读文件时,就会堵塞在P(w)


3.若写者2也想写进程的话,那么第二个写者进程也会被阻塞在P(w)中。


4.由于读者先对w进行了P操作,所以读者1在等待队列的对头位置,而写者进程2排在后面,所以当写者1执行完V操作后,唤醒的是读者进程。

结论:


在这种算法中,连续进入的多个读者可以同时读文件;写者和其他进程不能同时访问文件;写者不会饥饿,但也并不是真正的“写优先”,而是相对公平的先来先服务原则。有的书上把这种算法称为"读写公平法"。


4.哲学家进餐问题

一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭。哲学家们倾注毕生的精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿起两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。

1.关系分析。系统中有5个哲学家进程,5位哲学家与左右邻居对其中间筷子的访问是互斥关系


2.整理思路。这个问题中只有互斥关系,但与之前遇到的问题不同的是,每个哲学家进程需要同时持有两个临界资源才能开始吃饭。如何避免临界资源分配不当造成的死锁现象,是哲学家问题的关键。


3.信号量设置。定义互斥信号量数组chopstick[5]={1,1,1,1,1},用于实现对5个筷子的互斥访问。并对哲学家按0~4编号,哲学家i左边的筷子编号为i,右边的筷子编号为(i+1)%5。

这里设置每个哲学家吃饭前依次拿起左、右两支筷子。

若此时5个哲学家并发地拿起了自己左手边的筷子,每位哲学家循环等待右边的人放下筷子(阻塞)

发生“死锁。

如何避免死锁:

①可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐。这样可以保证至少有一个哲学家是可以拿到左右两只筷子的(剩余的筷子分配给相邻的哲学家即可)。

②要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起第一只筷子,另一个会直接阻塞。这就避免了占有一支后再等待另一只的情况。


③仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子。

(1)若1号哲学家执行P(mutex)操作,使mutex由1变为0,继续执行P(chopstick[i]),此时

若切换为2号哲学家,此时2号哲学家会被堵塞在P(mutex)中,只有等待1号哲学家执行完P(chopstick[(i+1)%5])后,拿起右边筷子后,才能释放mutex。

(2)若0号哲学家执行完以下操作,就会释放mutex

此时1号哲学家可以通过P(mutex),但是因为0号拿了两支筷子,所以1号哲学家会被堵塞在P(chopstick[i])


而2号哲学家由于1号哲学家已经执行了P(mutex),所以mutex由1变为0,所以2号哲学家又会被阻塞在P(mutex),所以即使2号哲学家左右两边筷子都可以用,但是依然会被堵塞。


因此这种方法并不能保证只有两边的筷子都可用时,才允许哲学家拿起筷子。

(3)0号哲学家拿起左右两边的筷子开始吃饭,此时4号哲学家可以拿起左边的筷子,但是不能拿右边的筷子,即会被阻塞在P(chopstick[(i+1)%5])

因此此时4号右边的筷子不可用,但4号仍然会先拿起左边的筷子



总结:


各哲学家拿筷子这件事必须互斥的执行。这就保证了即使一个哲学家在拿筷子拿到一半时被阻塞,也不会有别的哲学家会继续尝试拿筷子。这样的话,当前正在吃饭的哲学家放下筷子后,被阻塞的哲学家就可以获得等待的筷子了。

哲学家进餐问题的关键在于解决进程死锁。


这些进程之间只存在互斥关系,但是与之前接触到的互斥关系不同的是,每个进程都需要同时持有两个临界资源,因此就有“死锁”问题的隐患。若遇到了一个进程需要同时持有多个临界资源的情况,应该参考哲学家问题的思想,分析题中给出的进程之间是否会发生循环等待,是否会发生死锁。


四.管程

1.为什么要引入管程

由于信号量机制存在编写程序困难、易出错的问题,例如:

像这样如果写错了P操作的顺序,按 ①②③执行,就会发生死锁

所以设置了管程,使程序员写程序时不需要关注复杂的PV操作

2.管程的定义和基本特征

管程是一种特殊的软件模块,有这些部分组成:

1.局部于管程的共享数据结构说明;

2.对该数据结构进行操作的一组过程(函数)

3.对局部于管程的共享数据设置初始值的语句;

4.管程需要有一个名字。

为了用管程实现进程间的互斥和同步,管程需要以下特征:

1.局部于管程的数据只能被局部于管程的过程所访问;

2.一个进程只有通过调用管程内的过程才能进入管程访问共享数据;

3.每次仅允许一个进程在管程内执行某个内部过程。

3.管程的例子

1.用管程解决生产者消费者问题


①生产者只用将函数的参数传入ProducerConsumer.insert();其他互斥同步等问题都由管程解决。


②消费者也可以直接调用函数,从缓存中取出产品,中间的过程只由管程负责。


由编译器负责实现各进程互斥进入管程的过程,生产者进程、消费者进程只需要调用管程中的函数就可以了

1.实现进程互斥

若第一个进程正在调用insert函数,若第二个进程也想调用,编译器就会阻止第二个进程使用该函数。只有第一个进程使用完毕,第二个进程才能使用。


2.实现进程同步


管程中会设置条件变量和等待/唤醒操作,用来解决同步问题,例如:


消费者1想要从缓冲区取产品,但是初始count==0,所以第一个消费者进程会等待在empty这一条件变量的队列中,消费者2进程同理

生产者生产产品后,会将产品放入缓冲区中,并且检查count是否为1,即自己放入的产品是不是缓冲区中的第一个产品,这就意味着可能由消费者进程在等待产品,所以此时会用signal(empty),唤醒在empty条件变量中等待产品的进程,在这里会先唤醒对头进程,也就是消费者1。

若执行消费者进程后,count--,此时count==N-1,那么就说明原本缓冲区是满的,现在消费者取走一个进程,那么生产者就可以继续生产了,就会使用signal(full),唤醒生产者进程。

总结:

引入管程的目的无非就是要更方便地实现进程互斥和同步。

1.需要在管程中定义共享数据(如生产者消费者问题的缓冲区)

2.需要在管程中定义用于访问这些共享数据的“入口”----其实就是一些函数(如生产者消费者问题中,可以定义一个函数用于将产品放入缓冲区,再定义一个函数用于从缓冲区取出产品)


3.只有通过这些特定的“入口”才能访问共享数据


4.管程中有很多“入口”,但是每次只能开放其中一个“入口”,并且只能让一个进程或线程进入(如生产者消费者问题中,各进程需要互斥地访问共享缓冲区。管程的这种特性即可保证一个时间段内最多只会有一个进程在访问缓冲区。


注意:这种互斥特性是由编译器负责实现的,程序员不用关心


5.可在管程中设置条件变量及等待/唤醒操作以解决同步问题。可以让一个进程或线程在条件变量上等待(此时,该进程应先释放管程的使用权,也就是让出“入口”);可以通过唤醒操作将等待在条件变量上的进程或线程唤醒。


程序员可以用某种特殊的语法定义一个管程(比如:monitor ProducerConsumer…end monitor)之后其他程序员就可以使用这个管程提供的特定“入口”很方便地使用实现进程同步/互斥了。即程序设计中封装的思想(将复杂的细节隐藏,对外提供简单易用的接口)。

java中类似管程的机制:

Java 中,如果用关键字 synchronized 来描述一个函数,那么这个函数同一时间段内只能被一个线程调用

每次只能有一个线程进入insert 函数,如果多个线程同时调用 insert 函数,则后来者需要排队等待。

目录
相关文章
|
2天前
|
算法 调度 UED
深入理解操作系统:进程管理与调度策略
【10月更文挑战第34天】本文旨在探讨操作系统中至关重要的一环——进程管理及其调度策略。我们将从基础概念入手,逐步揭示进程的生命周期、状态转换以及调度算法的核心原理。文章将通过浅显易懂的语言和具体实例,引导读者理解操作系统如何高效地管理和调度进程,保证系统资源的合理分配和利用。无论你是初学者还是有一定经验的开发者,这篇文章都能为你提供新的视角和深入的理解。
13 3
|
4天前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
3天前
|
消息中间件 算法 调度
深入理解操作系统:进程管理的艺术
【10月更文挑战第33天】本文旨在揭示操作系统中进程管理的神秘面纱,带领读者从理论到实践,探索进程调度、同步以及通信的精妙之处。通过深入浅出的解释和直观的代码示例,我们将一起踏上这场技术之旅,解锁进程管理的秘密。
8 0
|
5天前
|
算法 Linux 调度
深入理解操作系统之进程调度
【10月更文挑战第31天】在操作系统的心脏跳动中,进程调度扮演着关键角色。本文将深入浅出地探讨进程调度的机制和策略,通过比喻和实例让读者轻松理解这一复杂主题。我们将一起探索不同类型的调度算法,并了解它们如何影响系统性能和用户体验。无论你是初学者还是资深开发者,这篇文章都将为你打开一扇理解操作系统深层工作机制的大门。
13 0
|
5月前
|
监控 Linux 应用服务中间件
探索Linux中的`ps`命令:进程监控与分析的利器
探索Linux中的`ps`命令:进程监控与分析的利器
121 13
|
4月前
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能
|
4月前
|
弹性计算 Linux 区块链
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
156 4
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
|
3月前
|
算法 Linux 调度
探索进程调度:Linux内核中的完全公平调度器
【8月更文挑战第2天】在操作系统的心脏——内核中,进程调度算法扮演着至关重要的角色。本文将深入探讨Linux内核中的完全公平调度器(Completely Fair Scheduler, CFS),一个旨在提供公平时间分配给所有进程的调度器。我们将通过代码示例,理解CFS如何管理运行队列、选择下一个运行进程以及如何对实时负载进行响应。文章将揭示CFS的设计哲学,并展示其如何在现代多任务计算环境中实现高效的资源分配。
|
4月前
|
存储 缓存 安全
【Linux】冯诺依曼体系结构与操作系统及其进程
【Linux】冯诺依曼体系结构与操作系统及其进程
170 1
|
4月前
|
小程序 Linux
【编程小实验】利用Linux fork()与文件I/O:父进程与子进程协同实现高效cp命令(前半文件与后半文件并行复制)
这个小程序是在文件IO的基础上去结合父子进程的一个使用,利用父子进程相互独立的特点实现对数据不同的操作
102 2
下一篇
无影云桌面