【操作系统】 进程同步

3使用多道批处理系统不仅能有效的改善资源的利用率, 还可以显著地提高系统的吞吐量, 但同时会使系统变得更加复杂,会使程序的运行结果存在不确定性。所以必须引入进程同步机制从而保证多个程序有条不紊的并发进行。

概念

协调多个进程,使并发执行的程序之间按照一定规则共享系统资源,能够很好的相互合作,从而时程序的执行具有可再现性。

两种形式的制约关系

  1. 间接相互制约关系

多个程序在并发执行,需要共享某些系统资源,如I/O,CPU等,导致这些并发执行的程序之间形成相互制约的关系。
例如打印机,磁带机等资源, 必须保证多个进程只能互斥访问。

  1. 直接相互制约关系

为了完成某些任务的两个或多个进程之间的制约关系来源于他们的合作。
例如A程序对缓冲区写数据, B程序从缓冲区取数据

临界资源(Critical Resouce)

只允许互斥访问的资源, 即同一时间内只允许一个进程访问的资源称为临界资源。
例如打印机,磁带机等。

临界区(Critical section)

进程中访问邻接资源的代码片段成为临界区。每个进程互斥地进入自己的临界区,则可实现对临界的互斥访问。

因此每个进程要进入临界区时都应对临界资源进行检查,对应的代码称为进入区,进入临界区后哟啊设置它正在被访问的标志,最后退出临界区的后面要加上一段称为退出区的代码。其余的代码成为剩余区。

while(ture)
{
		进入区
		临界区
		退出区
		剩余区	
}

同步机制应遵循的规则

  • 空闲让进
    当邻接资源空闲时,应允许一个请求进入邻接区的进程进入临界区,从而有效的利用资源
  • 忙则等待
    当邻接资源被占用时,其他进程要进入临界区需要等待
  • 有限等待
    要求进入临界区的进程能够在有限时间内进入,以免陷入死等的状态
  • 让权等待
    进程不能进入临界区时要释放CPU,不能进入忙等状态

硬件同步机制

利用软件方法解决同步问题有一定难度, 并且存在很大的局限性,目前计算机多采用硬件指令来解决临界区的问题

管理临界区时可以将标志看做一个锁, 锁开进入,锁关等待。

1. 关中断

在进入测试锁之前关闭中断,直到运行玩临界区释放锁后再打开中断。
由于获取锁的同时关闭了中断,所以临界区运行时不会被中断,从而保证了临界资源的互斥。
但代价很大,会严重影响系统的效率。且不适用于多核CPU。

2. 利用Test-and-Set指令实现互斥
借助一条硬件指令TS(Test-and-Set)实现互斥,TS指令的一般性描述如下:

boolean TS(boolean *lock)
{
	boolean old;
	old = *lock;
	*lock = TRUE;
	return old;
}

这条指令可以看做是一个原语, 即不可分割的.

使用TS管理临界区时, 为每个临界资源设置一个布尔变量lock, 当lock初值为false时表示该临界资源空闲. 程序进入临界区前用TS指令测试lock, 如果返回false表明没有进程在临界区内,可以进入, 并将lock赋值为true,等效于关闭了临界资源.

利用TS指令实现互斥

do{
	...
	while TS(&lock);
	critical section;
	lock = false;
	remainder section;
}while(TRUE);

3. 利用swap指令实现进程互斥

该指令为对换指令, 用于交换两个字的内容, 其处理过程如下.

void swap(boolean *a, boolean *b)
{
	boolean temp;
	temp = *a;
	*a = *b;
	*b = temp;
}

为每个临界资源设置一个全局的布尔变量lock, 初值为false, 在每个进程中利用一个局部变量key, 用swap指令实现互斥.

do{
	key = TRUE;
	do{
		swap(&lock, &key);
	}while(key != FALSE);
	临界区;
	lock = FALSE;
	....
}while(TRUE);

利用上述指令能够实现进程的互斥, 但当临界资源被占用时, 其他进程必须不断的进行测试, 处于一种忙等的状态, 不符合让权等待原则. 造成CPU的浪费, 同时也很难将他们用于解决复杂的进程同步问题.

信号量机制

一种卓有成效的进程同步机制,现已经被广泛应用于单处理机和多处理及系统及计算机网络中

1. 整形信号量

定义一个整形量S用来表示资源数目

S仅能通过两个标准的原子操作wait(S)和signal(S)来访问

wait(S)
{
	while(S <= 0);
	S--;
}

signal(S)
{
	S++;
}

2. 记录型信号量

整型信号量不符合让权等待原则, 会使进程处于忙等状态

所以在信号量机制中, 需要一个变量S代表资源数量外, 还需要一个进程链表指针链接所有等待的进程. 让所有没有获取到资源的进程阻塞, 等待被唤醒.

该数据类型描述如下

typedef struct
{
	int value;
	struct process_control_block *list;
}

wait(semephore *S)
{
	S->value--;
	if(S->value <= 0) block(S->list);
}

signal(semaphore *S)
{
	S->value++;
	if(S->value <= 0) wakeup(S->list);
}

进程进入临界区前调用wait(S), 资源数量自减, 之后判断资源数量是否小于0, 若小于0则将该进程阻塞block, 并将该进程链接在阻塞链表后. 如果资源数量大于零则不阻塞进程, 让其继续运行

临界区执行完后调用signal(S), 让资源数量自增, 之后判断你如果资源数量大于0, 则将等待链表里的第一个进程唤醒, 如果资源数仍小于零, 则不执行唤醒操作.

由此可见, 该机制遵循了让权等待准则, 如果设置S->value的初值为1, 则可实现对临界资源的互斥访问.

3. AND型信号量

前面所述都是解决针对一个资源的并发问题, 如果一个进程要获得两个或两个以上的资源才能进入临界区的话, 则需要用到AND型信号量, 否则进程会有可能出现僵持状态

例如

process A
wait(x);
wait(y);

process B
wait(y);
wait(x);

AB进程并发执行, 如果某一时刻A获取到了x, 同时B获取到了y, 此时A需要等待B的y释放, 而B需要等待A的x释放, 这时AB互不相让陷入僵持状态, 也称这两个进程死锁.

AND机制的思想为:
如果某一个进程需要同时申请多个资源, 只有它一次性申请到所有资源后才能继续运行, 运行完后将所有资源释放, 如果该进程只申请到一个资源, 则一个资源也不给他. 这样就可以避免上述死锁的发生.

Swait(S1, S2, ..... Sn)
{
		if(Si >= 1 && ..... && Sn>=1)
		{
			for(i = 1; i < n; i++)
				 Si--;
			break;
		}else{
			将该进程放在第一个Si<1资源的等待队列中, 
			阻塞调用进程,将pc置于Swait开始.
		}
	
}

Ssignal(S1, S2, .... Sn)
{
		for(i = 1; i <= n; i++)
		{
			Si--;
			将Si等待队列的进程唤醒.
		}
}

4. 信号量集

前面所述的wait(S)和signal(S)操作仅能对信号量实施加一或减一操作, 意味着只能一次申请一个Si资源或释放一个Si资源, 如果一个进程一次需要N个Si资源, 便要进行N次wait(S)操作, 这样不但低效且不安全.

根据这点可以对AND型信号量进行扩充,对Si分配下限值Ti, 如果Si小于Ti则不予分配, 如果对于该资源的需求为di ,一旦分配则一次性分配di个Si. 即Si = Si - di,

对应的函数代码

Swait(S1,t1, d1,  S2, t2, d2 ..... Sn, tn, dn)
{
		if(S1 >= t1 && ..... && Sn>=tn)
		{
			for(i = 1; i < n; i++)
				 Si = Si - di;
			break;
		}else{
			将该进程放在第一个Si<1资源的等待队列中, 
			阻塞调用进程,将pc置于Swait开始.
		}
	
}

Ssignal(S1, d1,  S2, d2 ..... Sn, dn)
{
		for(i = 1; i <= n; i++)
		{
			Si = Si + di;
			将Si等待队列的进程唤醒.
		}
}

信号量的应用

1. 利用信号量实现互斥

将各进程访问该资源的临界区置于wait()和signal()之中, 为临界资源设置一个互斥信号量mutex,值域(-1, 0 1) 并设置其初值为1, 当mutex = 1时表示没有一个进程进入临界区, 当mutex = 0时表示有一个进程进入临界区, 等mutex = -1 时表示有一个以上的进程进入临界区.

semaphore mutex = 1;
Pa()
{
while(true)
	{
		wait(mutex);
		临界区;
		signal(mutex);
		剩余区;
	}
}

Pb()
{
while(true)
	{
		wait(mutex);
		临界区;
		signal(mutex);
		剩余区;
	}
}

Pa和Pb同时while(TRUE)实现并发互斥访问资源.

注: wait(mutex)和signal(mutex)需要成对出现.
缺少wait()会导致系统混乱, 不能保证临界资源的互斥访问.
缺少signal()会导致临界资源永不被释放, 从而使阻塞的资源永不被唤醒

2. 利用信号量实现前趋关系

在这里插入图片描述

假设程序p1中有语句s1, 程序p2中有语句s2, 如果希望p1执行完s1后p2再执行s2的话, 可以利用信号量的方法实现其前趋关系.

上图所示前趋关系代码实现描述.

p1()
{
	s1;
	signal(a);
	signal(b);
}
p2()
{
	wait(a);
	s2;
	signal(c);
	signal(d);
}
p3()
{
	wait(b);
	s3;
	signal(e);
}
p4()
{
	wait(c);
	s4;
	signal(f);
}
p5()
{
	wait(d);
	s5;
	signal(g);
}
p6()
{
	wait(e);
	wait(f);
	wait(g);
	s6;
}

int main()
{
	semaphore a,b,c,d,e,f,g;
	a.value = b.value = c.value = d.value = e.value = f.value = g.value = 0;
	p1();p2();p3();p4();p5();p6();
}

管程

信号量虽然方便且有效, 但每个进程都需自备wait()和signal(), 这样会使大量的同步操作分散在各个进程中. 会给系统的管理带来麻烦, 并且会因为使用不当导致系统死锁.

管程的定义

利用数据结构抽象表示系统中的共享资源的状态和对资源的一些操作.
对于任意的共享资源进程必须通过管程来访问和释放.
管程包含了面向对象的思想, 他表征了共享资源的数据结构和对其数据结构操作的一组过程, 包括同步机制,都集中并封装在一个对象内部, 内藏了内部实现.

管程由四部分组成

  1. 管程的名称
  2. 局部与管程的共享数据说明
  3. 对该数据结构进行操作的一组过程
  4. 对局部与管程的共享数据结构设置初始值的语句

语法描述

Monitor monitor_name {
	share variable declarations;   // 共享变量
	cond declarations;             // 条件变量
	public:
	void p1(....)                  // 操作
	{
	.......
	}
	void p1(....)
	{
	.......
	}

	void p1(....)
	{
	.......
	}

	{
		init code;     // 初始化代码
		}
}

管程的特性

  • 模块化,管程是一个基本的程序单位, 可以单独编译
  • 抽象数据类型, 不但有数据还有对应的操作
  • 信息掩蔽, 只能通过管程定义的操作访问数据, 具体实现外部不可见

猜你喜欢

转载自blog.csdn.net/z944733142/article/details/89060624