操作系统概念笔记——第六章:进程同步

Posted 叶卡捷琳堡

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统概念笔记——第六章:进程同步相关的知识,希望对你有一定的参考价值。

第六章:进程同步

6.1 背景

在多进程状态下,共享数据的并发访问可能会产生数据的不一致。这章会讨论各种机制,以确保共享同一逻辑地址空间的协作进程可以有序地执行

之前提到了生产者-----消费者问题,采用了共享内存的方法
虽然生产者和消费者程序各自都正确,但是在并发运行时,可能会产生不正确的结果

比如counter++这条语句,在机器中实际上是这样实现的
register1为CPU局部寄存器

  • register1 = counter
  • register1 = register1 + 1
  • count = register1

count–语句实际上是这样实现的

  • register2 = counter
  • register2 = register2 - 1
  • counter = register2

而并发执行counter++和counter–相当于按任意顺序交替执行上面的语句,比如以下的执行方式
(counter的初值为5)
在这里插入图片描述
原本实现counter++和counter–得到的结果是5,但是这里得到的结果却是4
如果将T4和T5做交换,得到的结果会是6

在上面这种情况中,多个进程并发访问和操作同一数据且执行结果与访问发生的特定顺序有关,这样的情况被称为竞争条件(race condition)

6.2 临界区问题

  • 每个进程都有一个代码片段被称为临界区,在这个临界区内,进程可能改变共同变量,更新一个表,写一个文件等
  • 操作系统的重要特征是当一个进程进入临界区时,没有其它进程可被允许在临界区中执行
  • 临界区问题(critical-section problem)时设计一个以便进程协作的协议。每个进程必须请求允许进入其临界区。实现这一请求的代码片段被称为进入区,临界之后可有退出区,其他代码为剩余区
    在这里插入图片描述

临界区需要满足三个条件

  • 互斥:如果一个进程在其临界区中执行,那么其它进程不能进入临界区
  • 前进:如果没有进程在临界区执行,那么只有那些不在剩余区的进程可以参加选择,确定谁能进入下一个临界区
  • 有限等待:从一个进程做出进入临界区的请求,直到该请求允许为止,其他进程允许进入其临界区的次数有上限

6.3 Peterson算法

  1. Peterson算法是一个基于软件的临界区问题的解答
  2. Peterson算法需要在两个进程(Pi, Pj)之间共享两个数据项:int turn; boolean flag[2];
    在这里插入图片描述

变量turn代表了哪个进程可以进入其临界区,数组flag表示哪个进程想进入临界区
注意:上面这个图仅仅演示了进程Pi的结构,如果是进程Pj的话,相应的i和j的值需要改变

6.4 硬件同步

1.一般来说,任意临界区问题的解决方法都需要使用“锁”这个工具
2.一个进程在进入临界区之前必须得到锁,而在退出临界区时释放锁
在这里插入图片描述
3.本节会介绍一些简单的硬件指令,并描述如何利用它们来解决临界区问题
4.对于单处理器系统,解决临界区问题比较简单:在修改共享变量时禁止中断出现即可
5.对于多处理器系统,由于涉及消息的传递,如果选择关中断,那么会降低系统的效率
6.许多现代计算机系统提供了特殊的硬件指令允许原子地(不可中断地)检查和修改字地内容或交换两个字的内容。可以使用这些特殊指令来解决临界区问题

例1:指令TestAndSet()

boolean类型的变量lock初始值为false
在这里插入图片描述
这个指令的执行是原子的,当一个进程进入临界区时,其它进程不能进入临界区。

例2:Swap指令

布尔类型变量lock,初始化为false
在这里插入图片描述
这个指令同样是原子执行的,与上面的指令类似

这两个指令要求完全理解并掌握写法

6.5 信号量

上一节描述了基于硬件的临界区问题的解决方案,但对于程序员来说,使用比较复杂,因此我们可以使用信号量这个工具

信号量S是一个整型变量,除了初始化以外,只能通过两个标准原子操作来访问
这两个原子操作分别是:wait(),signal(),这两个操作也被称为P和V(来源是荷兰语)

wait()的定义可表示为:

wait(S)
{
	/*忙等*/
	while(S <= 0);
	s--;
}

signal()的定义可表示为:

signal(S)
{
	s++;
}

在这两个操作中,对信号量S的修改必须不可分地执行,当一个进程修改信号变量的时候,不能有其它进程同时修改同一信号量的值

6.5.1 用法

操作系统将信号量区分为计数信号量和二进制信号量。计数信号量的值域不受限制,二进制信号量的值只能为0或1。有的系统将二进制信号量称为互斥锁

使用二进制信号量处理多进程的临界区问题,这n个进程共享一个信号量mutex,mutex的初始值为1

do
{
	waiting(mutex);
	//进入临界区
	//临界区代码片段
	signal(mutex);
	//剩余区
	//剩余区代码
}while(TRUE);

具体的执行流程要求完全掌握

6.5.2 实现

  • 上面定义的信号量的缺点是需要忙等待(busy waiting)
  • 当一个进程在临界区内时,其它要进入临界区的进程必须进行无限循环并忙等
  • 为了克服忙等,需要修改信号量操作wait()和signal()的定义。

需要定义一个新的信号量,信号量的结构如下

typedef struct
{
	int value;
	struct process* list;
}semaphore;

每个信号量都有一个int类型的value和一个进程列表list。当一个进程必须等待信号量时,就加入到进程链表上,而signal()则会唤醒其中的一个进程

信号量wait()的新定义

wait(semaphore* s)
{
	s->value--;
	//如果此时临界区中有进程
	if(s->value < 0)
	{
		//将这个进程添加到list列表中
		list.add(p);
		//进程阻塞
		block();
	}
}

信号量signal()的新定义

signal(semaphore* s)
{
	s->value++;
	if(s->value <= 0)
	{
		//将一个进程从list列表中移除
		list.del(p);
		//唤醒在list中的一个进程
		wakeup(p);
	}
}

注意

  1. 在本实现中,信号量value的值可能为负值
  2. 如果信号量的值为负,那么其绝对值就是等待该信号量的进程的个数(比如value = -1,则队列长度为1,正在等待的进程数为1)
  3. 信号量的关键之处在于它们是原子地执行的。
  4. 实际上wait和signal的代码是被包装在之前的TestAndLock指令当中的,以保证实现原子地执行

6.5.3 死锁与饥饿

两个或多个进程无限地等待一个事件,而这个事件只能由这些等待进程置之一产生,这些进程就被称为死锁

例:由P0和P1组成的系统,每个都访问信号量S和Q,且这两个信号量的初值均为1

   P0 		 		P1
wait (S) ; 		wait (Q);
wait (Q) ; 		wait (S) ;
signal(S); 		signal (Q) ;
signal (Q); 	signal(S);

假设P0先执行wait(S),接着P1执行wait(Q)。
这里P0执行wait(Q)后必须等待P1执行signal(Q),当P1执行wait(S)时,它必须等待P0执行signal(S),因此两个进程陷入死锁

无限期阻塞饥饿,指的是进程在信号量内无限期等待

6.6 经典同步问题

6.6.1 有限缓冲问题

假定缓冲池有n个缓冲项,每个缓冲项能存一个数据项
信号量mutex初始值为1,信号量empty初始值为n和full初始值为0

生产者进程

do
{
	wait(empty);
	wait(mutex);
	//将生产的东西添加到缓冲区
	signal(mutex);
	signal(full);
	
}while(1);

消费者进程

do
{
	wait(full);
	wait(mutex);
	//在缓冲区消费
	signal(mutex);
	signal(empty);
	
}while(1);

该程序通过信号量的wait和signal方法可以实现:当缓冲区为空时,消费者进程处于等待状态,当缓冲区满时,生产者进程处于等待状态
要求理解清楚这个程序,并能自己独立写出

6.6.2 读者—写者问题

一个数据库可能被多个进程所共享,有的进程负责读(称为读者),有的数据库负责写(称为写者)
如果两个读者同时访问数据库,不会产生问题,但是一个写者和另一个进程同时访问数据库,则可能出现问题

在这个问题中,信号量mutex和wrt的初始值为1,readcount初始值为0
信号量mutex用于确保在修改readcount时互斥
readcount用于表示当前有多少个进程正在读
wrt信号量使写者互斥

写者进程

do
{
	wait(wrt);
	//写操作
	signal(wrt);
}while(1);

读者进程

do
{
	wait(mutex);
	readcount++;
	//如果是第一个读进程
	if(readcount == 1)
	{
		//保证在写时无法读,在读时无法写
		wait(wrt);
	}
	signal(mutex);
	//写操作
	wait(mutex);
	readcount--;
	if(readcount == 0)
	{
		signal(wrt);
	}
	signal(mutex);
}while(1);

这个程序的核心是readcount,要求完全理解程序原理并独立写出

6.6.3 哲学家进餐问题

有5个哲学家,他们的一生用于思考和吃饭,哲学家共用一张圆桌,圆桌的中间是米饭,总共有五只筷子
一个哲学家每次试图拿起他旁边的两个筷子,只有拿起两只筷子,才能吃饭

使用信号量chopsticks[i]表示筷子,总共有5支筷子

哲学家i的进程结构

do
{
	wait(chopsticks[i]);
	wait(chopsticks[(i+1)%5]);
	//哲学家吃饭
	signal(chopsticks[i]);
	signal(chopsticks[(i+1)%5]);
	//哲学家思考
}while(1);

6.7 管程

这一部分由于老师生病请假,没有讲,因此这一部分是自学的。
当然,这一部分属于边缘内容,不太重要

虽然信号量提供了一种方便而有效的机制用于处理进程同步,但是如果不正确使用信号量,仍然会导致出现时序问题

比如以下几种错误情况

  • 交换wait(mutex)和signal(mutex)的顺序
  • 写了两个wait(mutex)
  • wait和signal函数只写一个的情况
  • wait和signal函数都没写

如果出现以上几种情况,则可能出现死锁并破坏互斥机制
为了解决上面的错误,提出了管程类型

以上是关于操作系统概念笔记——第六章:进程同步的主要内容,如果未能解决你的问题,请参考以下文章

进程同步

第六章 总线

操作系统学习笔记 第六章:设备管理(王道考研)

操作系统学习笔记 第六章:设备管理(王道考研)

读书笔记第六章

计算机网络学习笔记第六章(应用层)超详细整理