清华大学操作系统公开课(八)-同步互斥

1.什么是互斥

在计算机执行过程中,对于多个任务,它们共享着一个资源,要求对该资源的存取过程是排他的。

2.为什么要有互斥

不考虑SMP情况,仅分析单CPU情况,因为SMP只不过是更复杂的一种情况,原理类似。
有如下代码片段,其中share_data是一个全局变量。

int share_data = 0;

void foo(void)
{
	share_data++;		//写共享资源
	show share_data;	//读共享资源
}

2.1线程间

如果两个线程都执行上述片段,执行过程如下图所示:

首先是Thread A开始执行,此时share_data为0,执行完①后,如果执行流程不被打断,则继续往下走,最终show share_data得到的数据应该是1。但是当Thread A执行完步骤①后,CPU发生了调度,切到Thread B上执行②,而share_data对于两个线程是共享的,所以Thread B此刻看到的share_data是经过了一次自增的,为1。最终执行完流程③,④。Thread A看到的结果是share_data自增了两次,与预期不符。

2.2线程与中断间

还一种情况是代码片段分别在一个线程和中断中:

过程和上面类似,只不过执行流程是被中断打断的。

3.并发和竞态

以上多个执行流在一个时间段并发(Concurrency)执行,并且围绕共享资源进行访问导致了竞态(Race Conditions)。就是一种你争我夺的状态。而这个共享的资源就是他们争夺的对象,可以是(全局的变量,同一个硬件资源,文件系统上的一个文件等)。

与并发常常一起提到的另一个词叫并行,并行指同一个时刻,同时进行,例如SMP上多个CPU执行多个线程这种情况。

3.1如何解决竞态

以上看到了竞态带来了我们预期之外的效果,按照我们程序的设计,对于一个功能模块,同样的输入应该得到一致的输出才行。考虑生活中一个这样的问题:A和B住一起,冰箱里面没面包了就要去买,但是我们要避免两个人重复买面包这种情况。如何解决?

  • 方案一
    打开冰箱,发现没面包了,留张纸条贴在冰箱上,买回面包放进去,撕去纸条。用代码表示:
do
{
	if (noNote)
	{
		if (noBread)
		{
			leave Note;
			buy bread;
			remove Note;
		}
	}
}while (1);

能解决问题吗?不能。假设A看到 if (noNote) ---> if (noBread),刚好有别的事打断了他,A出去了,这时B进来,也看到了if (noNote) ---> if (noBread),然后恰巧B也被打断了,A此时进来,于是他继续前面被打断的工作,留下纸条,买回面包,撕去纸条,走了。然后B回来了,继续前面被打断的工作,留下纸条,买回面包,撕去纸条,走了。这时会发现A和B都买了面包。

  • 方案二
    以上问题看上去好像是没提前贴好纸条导致,那如果先贴纸条呢
do{
	leave Note;
	if (noNote)
	{
		if (noBread)
		{
			buy bread;
			remove Note;
		}
	}
}while(1);

分析这个过程看得到,双方留下纸条后,进去检查if (noNote)都是不成立的,这样,两个人都不会去买面包,如果这种巧合一直按照这种顺序发生,那么永远不会有人买面包。

  • 方案三
    标签加以区别:
do{
	leave my Note;
	if (no Other's Note)
	{
		if (noBread)
		{
			buy Bread;
		}
		remove my Note;
	}
}while(1);

显而易见,依旧是不行的。

  • 方案四
do{
	leave Note1;
	while (is Note2 Exist)					
	{										
		do nothing;							
	}
	
	if (noBread)
	{
		buy Bread;
	}
	remove Note1;
}while(1);
do{
	leave Note2;
	if (noNote1)
	{
		if(noBread)
		{
			buy Bread;
		}
	}
	remove Note2;
}while(1);

这个方案可行吗?可行。但是明显看的出和前面3个方案的不同,这里用了两段不同的代码。此方案有如下缺点:

  • 同样是2个人买面包,上面三种方案,购买流程一套代码即可实现,而此情况需要2套代码。
  • 如果处理线程数超过2个,处理逻辑的复杂度会呈指数级增长。
  • 并且第一段处理有一个死循环,如果下面一段的处理比较耗时,则上一段的死循环会持续很久,导致比较高的CPU占用。
  • 方案五
    既然方案四已经否定了,综合前三个解决方法来看,可以看出,主要是因为放下纸条和检查纸条这个过程被打断了,2个动作可以拆分为2次完成。如果通过一些手段把这两个动作绑定在一起,看看会是怎么样。
do
{
	while(check_and_leave());	//执行不可打断

	if (noBread)
	{
		buy Bread;
	}

	remove Note;
}while(1);

check_and_leave()里面的逻辑是:

if (noNote)
{
	leave Note;
	return FALSE;
}
else
{
	return TRUE;
}

这样就可以解决竞态问题了。只要实现check_and_leave()执行动作的不可打断就可以了。

4.临界区

4.1概念

enter section;
	critical section;
exit section;
	remainder section;

临界区(critical section):

  • 任务执行时,需要互斥的一段区域。

进入临界区(enter section):

  • 进入临界区需要先检查是否有人已进入临界区。
  • 如果可进入临界区,设置进入标志。

退出临界区(exit section):

  • 清除进入临界区标志。

4.2访问规则

  • 空闲则进入
    没有任务进入了临界区,任何任务都可以进入。
  • 繁忙则等待
    有任务进入临界区,其他任务都得等待。
  • 有限等待
    未进入临界区的任务不能无限等待。
  • 让权等待(可选)
    未进入临界区的任务,应释放CPU使用权(如切换到阻塞态)。

4.3如何实现临界区的访问

4.3.1禁止中断

禁止中断相当于是硬件方法实现,在第三节的分析可知,临界区出现了竞态主要是因为任务调度引起,或者中断引起。而任务调度也是由中断方法实现(如时间片,超时则引发调度,由时钟中断导致),所以禁止了中断,可以实现临界区的访问。

local_irq_save(unsigned int flags);
critical section;
local_irq_restore(unsigned int flags);

进入临界区(enter section):

  • 禁止所有中断,保存CPSR。

退出临界区(exit section):

  • 使能中断,恢复CPSR。

缺点:

  • 禁止中断后,执行的任务将无法响应任何外部的输入,例如信号,中断,一直执行下去。
  • 如果临界区执行耗时较长,那么对系统的性能有很大的影响。

4.3.2软件方法

4.3.3高级抽象

原子操作

猜你喜欢

转载自www.cnblogs.com/thammer/p/12584712.html