操作系统:进程同步三大经典问题
日期:2019/4/15
内容:进程同步;生产者与消费者;读写者;哲学家进餐;信号量机制。
一、生产者与消费者问题
1.1 版本1
- 代码
void producer() { while (count == n) ; buff[in] = produce_item(); in = (in + 1) % n; count++; } |
void consumer() { while (count == 0) ; item = buff[out]; print(item); out = (out + 1) % n; count--; } |
-
存在问题
>>两个while循环一直在"忙等",不符合进程同步的"让权等待"原则。
>>对于count变量的访问没有保护。(需要加锁保护)
1.2 版本2:使用信号量
- 代码
semaphore empty = n, full = 0; void producer() { while (true) { wait(empty); buffer[in] = produce_item(); in = (in + 1) % n; signal(full); } } |
void consumer() { while (true) { wait(full); item = buffer[out]; print(item); out = (out + 1) % n; signal(empty); } } |
-
存在问题
>>如果有2个producer进程,empty>=2时,同时进入wait(empty)之后的临界区,对于buff的写和in的写产生竞争。
>>如果有2个consumer进程,full>=2时,同时进入wait(full)之后的临界区,对于out的写产生竞争。
1.3 版本3:临界区加锁(正确版本)
- 代码
semaphore pmutex = 1, cmutex = 1; semaphore empty = n, full = 0; void producer() { while (true) { wait(empty); wait(pmutex); buff[in] = produce_item(); in = (in + 1) % n; signal(pmutex); signal(full); } } |
void consumer() { while (true) { wait(full); wait(cmutex); item = buff[out]; print(item); out = (out + 1) % n; signal(cmutex); signal(empty); } } |
注:教材对于producer和consumer的临界区都使用了同一个mutex,表示producer和consumer互斥进入临界区。但是个人感觉似乎没必要,因为producer和consumer对于buff的访问不存在竞争关系,只需要保证多个producer进程之间互斥,多个consumer进程之间互斥即可。
二、读者与写者问题
对于多个进程访问同一文件:
- 写者:可以读,也可以写
- 读者:只读
- 允许多个读者同时读
- 某一写者在写,不允许其他读写操作
R |
R |
1 |
R |
W |
0 |
W |
R |
0 |
W |
W |
0 |
与生产者消费者的区别:
- 生产者不仅仅是写进程,还必须调整in指针,但是在读写者问题当中每个进程的文件读写指针是相互独立的。
- 消费者同理。
2.1 版本1
- 代码
semaphore mutex = 1; void writer() { while (true) { wait(mutex); write_operation(); signal(mutex); } } |
void reader() { while (true) { wait(mutex); read_operation(); signal(mutex); } } |
-
问题
>>不满足"同时读"。
2.2 版本2:增加读者计数器
- 代码
semaphore wmutex = 1; void writer() { while (true) { wait(wmutex); write_operation(); signal(wmutex); } } |
void reader() { while (true) { if (reader_count == 0) wait(wmutex); reader_count++;
read_operation();
reader_count--; if (reader_count == 0) signal(wmutex); } } |
-
问题
>>可以允许多个reader进程同时读,但是对reader_count的访问存在竞争。
2.3 版本3:给reader_count加锁
rmutex实际上只用于保护reader_count被正确更新。
- 代码
semaphore wmutex = 1, rmutex = 1; int reader_count = 0; void writer() { while (true) { wait(wmutex); write_operation(); signal(wmutex); } } |
void reader() { while (true) { if (reader_count == 0) wait(wmutex); wait(rmutex); reader_count++; signal(rmutex);
read_operation();
wait(rmutex); reader_count--; signal(rmutex); if (reader_count == 0) signal(wmutex); } } |
-
问题
>>举例说明红色部分代码问题,reader1和reader2同时执行,同时读取reader_count均为0,那么(假设)先执行reader1的wait1(wmutex),reader1进入阻塞队列;后执行reader2的wait2(wmutex),reader进入阻塞队列。(wait是原语操作,1和2必有先后之分)。但是在reader2执行的时候reader_count的值应为1(但实际是0),这就会使reader2成为僵死进程。
2.4 正确版本1:读者优先
- 代码
semaphore wmutex = 1, rmutex = 1; int reader_count = 0; void writer() { while (true) { wait(wmutex); //保证了W与W互斥 write_operation(); signal(wmutex); } } |
void reader() { while (true) { wait(rmutex); //保证只能有一个reader访问reader_count if (reader_count == 0) wait(wmutex); reader_count++; signal(rmutex);
read_operation();
wait(rmutex); //保证只能有一个reader访问reader_count reader_count--; if (reader_count == 0) signal(wmutex); signal(rmutex); } } |
-
问题
>>当读者进程≥1时,随后读者进程直接进入临界区,这是读者优先的表征。
>>写者饿死问题。
>>假设有进程{R1, W1, R2, R3, ..., Rn}
>>>>执行R1,那么wmutex变为0,执行read_operation
>>>>执行W1,wmutex变为-1,阻塞W1
>>>>执行R2,wmutex不变,执行read_operation
>>>>对于若干Ri,均是如此,如果CUP资源不足,Ri会进入就绪队列
>>>>那么W1则很长时间无法调度,就算被siganl操作移入就绪队列也是在队列尾部,产生写者饿死问题。
2.5 正确版本2:写者优先
解决写者饿死问题:保证一个写进程想写时(即使它有可能进入阻塞队列),不允许新的读进程访问临界区。(注意这并不是为了解决"读和写不能同时进行")。上面读者优先的症结在于写进程想写,但是读进程优先,不断地进入临界区,即读的调度优先级比写高,从而导致读者饿死。
- 代码
int reader_count = 0, writer_count = 0; semaphore x = 1, y = 1, z = 1; semaphore wmutex = 1, rmutex = 1; void writer() { while (true) { wait(y); if (writer_count == 0) wait(rmutex); writer_count++; signal(y);
wait(wmutex); write_operation(); signal(wmutex);
wait(y); writer_count--; if (writer_count == 0) wait(rmutex); signal(y); } } |
void reader() { while (true) { wait(z); wait(rmutex); wait(x); if (reader_count == 0) wait(wmutex); reader_count++; signal(x); signal(rmutex); signal(z);
read_operation();
wait(x); reader_count--; if (reader_count == 0) wait(wmutex); signal(x);
} } |
-
解析
- x:控制reader_count的访问竞争。
- y:控制writer_count的访问竞争。
- wmutex:控制有写进程在写的时候,读进程不能进入临界区。
- rmutex:写进程先把rmutex拿到,保证在写进程运行时,其他所有读进程均无法进入临界区(只能阻塞在rmutex队列上,见红色代码)。
- z:保证了在rmutex的阻塞队列上,只有一个读进程在排队,其余所有读进程在等待rmutex之前,在z的队列上排队。(尝试把wait(z)和signal(z)去掉理解一下)如果没有z,则都在rmutex上排队。
- 为什么需要z?在rmutex上不允许长的排队,否则写进程不能跳过这个长队列。
-
举例说明:{R1, W1, R2, R3, ..., Rn}
- 如果有z,reader1先wait1,rmutex=0;然后writer再wait(rmutex),rmutex=-1;后面尽管有再多的reader都在堵塞在z。此时,只需要等待reader1执行signal(rmutex),writer即能够进入就绪状态,优先于z中阻塞的reader。
- 如果无z,writer和所有的reader都进入rmutex排队,实质上无法保证writer优先于reader。
三、哲学家进餐问题
-
问题描述
哲学家需要只吃饭和思考,需要用2把叉子才能吃饭。叉子只能用自己座位两侧的。需要避免死锁和饥饿。
3.1 版本1
- 代码
semaphore fork[5] = {1, 1, 1, 1, 1}; void philosopher(int i) { while (true) { think(); wait(fork[i]); wait(fork[(i + 1) % 5]); eat(); signal(fork[i]); signal(fork[(i + 1) % 5]); } }
void main() { for (int i = 0; i < 5; i++) create_process(philosopher(i)); } |
-
问题
>>死锁。每个人同时拿起自己左边的叉子,即philosopher[i]拿起fork[i],那么每个人在wait(fork[(i + 1) % 5])上都会阻塞。
3.2 版本2(正确版本)
解决方案:同时只允许4个人进入房间就餐,那么即至少能保证有1人可以拿到2个fork。
- 代码
semaphore fork[5] = {1, 1, 1, 1, 1}; semaphore room = 4; void pholosopher(int i) { think(); wait(room); wait(fork[i]); wait(fork[(i + 1) % 5]); eat(); signal(fork[i]); signal(fork[(i + 1) % 5]); signal(room); } void main() { for (int i = 0; i < 5; i++) create_process(philosopher(i)); } |
-
解析
保证不会死锁和饥饿。(管程解决方案待续)