上课老师给我们讲了一些操作系统的经典题目,一天过去了,今天我就来尝试一下用自己的理解来解释这些题目。
信号量解题的关键
设置信号量的步骤
- 信号量的设置
- 给信号量赋初值(常用的互斥和同步信号量值的大小)
- P、V操作安排的位置(其中,P的顺序不能颠倒,V的顺序任意)(一般情况下是这样的)
需要注意
- 公用信号量:互斥时使用的信号量(二元信号量):它仅允许取值为0与1,用作互斥。它联系着一组共行进程,初值为1,每个进程均可对之施加P、V操作。
- 私用信号量(一般信号量(资源信号量)):它联系着一组共行进程,但其初值为0,或为某个正整数n,表示资源的数目,主要用于进程同步。只允许拥有它的进程对之施加P操作。
生产者-消费者问题(进程同步)
生产者是先制造产品,在存放到公共仓库,随后由消费者从仓库中取出这些产品进行消费。尽管生产者和消费者是以异步的方法运行的,但他们之间必须保持同步,即消费者不能到空的仓库去取产品消费,生产者也不能讲产品存放到已满的仓库中。此外仓库每次也只允许一个人进出。
其实计算机中很多并发进程之间的同步关系都能抽象成生产者-消费者模型,比如说计算进程和打印进程。
下面给出解题的思路
-
想接收数据时,有界缓冲区中至少有一个单元是满的
-
生产者想发送数据时,有界缓冲区中至少有一个单元是空的
由于缓冲区是临界资源,所以生产者和消费者之间必须互斥的访问临界资源
首先我们需要设置一个公有信号量为mutex
来表示仓库这个公有资源的状态,初值为1
;其次我们需要给出一个消费者的私有信号量full
来表示仓库产品的个数,初值为0
;生产者私用信号量avail
来表示仓库的空位置的个数,初值为n
。
伪代码
Deposit(data):
begin
P(avail) // 检查仓库中是否有空位 执行后n - 1
P(mutex) // 检查仓库是否可用 执行后 mutex = 0
将产品送入仓库
V(full) // 将产品个数加1
V(mutex) // 释放仓库这个资源
end
Remove(data):
begin
P(full) // 检查仓库中是否有产品
P(mutex) // 检查仓库是否可以使用
从仓库中取出一个产品
V(avail) // 将空位置的个数加1
V(mutex) // 释放仓库这个资源
end
其实这个伪代码是这样运行的,我们先可以假设生产者要先发送数据。当生产者想要将发送数据的时候,他就必须要先检查仓库是否是有空位也就是伪代码中的P(avail)
,当仓库有空位的时候,就让avail
-1,表示仓库有空位,生产者可以存放产品。接着就会继续执行P(mutex)
去占用的公共仓库资源,假如占用的话就会让原来的mutex = 1
变成mutex = 0
,即表示公共资源被申请占用,假如这个时候下面的消费者就算检测到仓库中有产品,但是当执行下面Remove(data)
中P(mutex)
的时候,就会让mutex = 0
变成mutex = -1
这样消费者取产品这个动作就会变成阻塞状态,这样就两者就无法对仓库进行操作,即每次只能进入一个人。这里我们需要提出的一个问题就是P(avail)
和P(mutex)
能否顺序调换呢?答案当然是不能的,假设一种情况,当我们的仓库没有空位的时候,而Deposit(data)
中的P(mutex)
先执行,导致仓库被加锁,消费者无法从中取出产品,这就导致系统死锁,消费者无法取出商品,而生产者一直在P(avail)
。所以每个操作中P
原语是不能调换的,但是V
原语是可以调换的。
读者和写者问题(进程同步)
一个文件可能被多个进程共享,为了保证读写的正确性和文件的一致性,系统要求,当有读者进程读文件时,不允许任何写者进程写,但允许多读者同时读;当有写者进程写时,不允许任何其它写者进程写,也不允许任何读者进行读。
为了解决读者和写者问题,需要设置两个信号量
- 互斥信号量
rmutex
用于读者互斥地访问共享变量readcont
,而readcont
是记录有多少读者正在读; - 互斥信号量
wmutex
用于实现读写互斥
伪代码
struct semapore rmutex, wmutex = 1, 1;
int readcount = 0;
cobegin
void readeri(void)(i = 1 , 2, …k) {
while(true){
p(rmutex);
if readcount = 0 then p(wmutex);
readcount:=readcount + 1;
v(rmutex);
读文件;
p(rmutex);
readcount:=readcount - 1;
if readcount = 0 then v(wmutex);
v(rmutex);
}
}
void writerj(void)(j = 1, 2, …m) {
while(true){
p(wmutex);
写文件;
v(wmutex);
}
}
coend
现在我们先假设有写者在写时,而没有读者进行读操作,就会去执行writerj()
中的P(wmutex)
申请进入共享数据区,而后wmutex
变为0
,然后开始进入了一系列的写操作,我们可以假设这个时候有读者想要进行读操作,首先是当然是锁readcount
即P(rmutex)
,然后需要判断现在的读者数量,为0,进行P(wmutex)
操作,这个时候wmutex
变为-1,导致写进程阻塞,完成了读写的互斥操作。那我们继续设想假如说我的写操作完成了,并且释放了wmutex
即wmutex
重新变为了0,这样读操作就成功拿到了共享资源的访问权限,就可以readcount + 1
,假设当我们的读者读文件完成之后,我就会重新申请rmutex
申请访问共享变量readcount
。然后就算在有读者来访问时,一定要等前一个读者释放rmutex
才能继续操作。同样的,如果读者数量变为0的时候,我们就需要释放wmutex
,即wmutex + 1 = 0
,这样写者就继续写操作。
图书馆阅览室问题(进程同步)
假定阅览室最多可同时容纳100个人阅读,读者进入时,必须在阅览室门口的一个登记表上登记,内容包括姓名、座号等,离开时要撤掉登记内容。
解题思路
读者有任意多个,但进入阅览室阅读最多为100人,为此可设一个信号量s
,代表空座位的数目;另登记表为临界资源,需设一个用于互斥的信号量mutex
,防止2个及以上的读者进程同时对此表访问。对于每个读者的动作包括进入、阅读、离开。
伪代码
struct semaphore s, mutex = 100, 1;
cobegin
void readeri(void) (i = 1, 2, …, k) {
while(TRUE){
P(s);
P(mutex);
查登记表,置某座位为占用;
V(mutex);
......
reading;
P(mutex);
登记表,置某座位为空;
V(mutex);
V(s);
}
}
coend
首先要说明为什么这是并发代码,明明只有一个部分,因为这个阅览室是可以进行共同访问的,所以也可以产生并发。假设我们有读书人想要进入阅览室,那么就会执行P(s)
让这个s
的值-1
,然后就开始申请占用登记表的资源P(mutex)
,这样mutex
就变为了0
,就占用了登记表的资源,这样其他读书人先要再次访问登记表的时候就会阻塞,只能等待前一个读书人是释放mutex
,然后就是读书完成,退还的时候,还是同样的道理。