操作系统笔记(6) 进程同步(Process Synchronization)

一、背景

  • 对共享数据的并发访问会导致数据不一致性(数据的处理结果与人规定的既定规则下的结果不同)

例子:在生产者和消费者体系中,二者修改一个count,但却在各自的进程空间进行,但是修改过程中可能会出现这种情况:
在这里插入图片描述

二、 临界区(Critical-Section)

上述对count的修改就是critical section

理论上的解决方案要满足(单核状态):

  1. Mutual Exclusion :如果某个进程正在进行它的critical section,那么其他进程不能进行其critical section。
  2. Process:如果没有进程处于critical section,且有进程想要进入critical section,需要选出一个不能被无限期推迟的进程进入critical section
  3. Bounded Waiting :当一个进程需要且已经被授权准入critical section时,它等待其他进程先进入critical section的时间应当是有限的。

另外:

  • 假设每个进程都以非0速度执行
  • 没有进程间相对执行速度相对执行速度的概念

Peterson’s Solution

进程的情况

在这里插入图片描述
代码情况:

while (true) {
	flag[i] = TRUE;
	turn = j;
	while ( flag[j] && turn == j);
	CRITICAL SECTION
	flag[i] = FALSE;
	REMAINDER SECTION
}
// Pj代码完全对称
while (true) {
	flag[j] = TRUE;
	turn = i;
	while ( flag[i] && turn == i);
	CRITICAL SECTION
	flag[j] = FALSE;
	REMAINDER SECTION
}

简单的形式?

在这里插入图片描述
当一个process的remainder section很长,难以结束,另一个就无法进入critical section。

三、 硬件同步机制(Synchronization Hardware)

很多系统提供对critical section code的硬件帮助

  • 使用单处理器时,可以使处理器进入不可中断模式
  • 处理器硬件提供原子性的操作,要么使用TestAndSet,要么使用Swap

TestAndSet Instruction

// 被共享的lock初始化为false

// 指令描述
boolean TestAndSet (boolean *target){
	boolean rv = *target;
	*target = TRUE;
	return rv;
}

// 使用
while (true) {
	while ( TestAndSet (&lock )); /* do nothing*/
		// critical section
	lock = FALSE;
		// remainder section
}

这种方法不能够满足Bounded waiting,之后的更新后的ppt有可以实现Bounded waitng的TestAndSet方法。

Swap Instruction

不满足Bounded Waiting

// Definition:
void Swap (boolean *a, boolean *b){
	boolean temp = *a;
	*a = *b;
	*b = temp:
}
// Solution:
while (true) {
	key = TRUE;
	while ( key == TRUE) Swap (&lock, &key );
		// critical section
	lock = FALSE;
		// remainder section
}

四. 信号量(Semaphores)

简单的同步化工具,通过两个原子化的标准操作修改一个integer值 S S Swait()signal()(一开始也被称为P()V()

wait(S){
	while(S<=0) /*此处如果不空,则这个进程在忙等待*/;
	S--;
}
signal(S){
	S++;
}

显然S不应为负。

Semaphore的版本

  • Counting Semaphore:S的值是不被约束的整数
  • Binary Semaphore:S的值只能是0或1,也被成为mutex locks

简单的使用
在这里插入图片描述
上图S的值应当取1
在这里插入图片描述
上图中,S的初始值应当为0

Semaphore Implementation

  • 应该确保任意两个进程不能在同一时刻分别对t同一个S运行wait()和signal(),也即wait和signal本身也是critical section。
  • 有忙等待和非忙等待两种方法

busy waiting

(是自旋锁?)

no busy waiting

如果while中的时间不能被忽略,就需要把这些操作放在waiting queue,而不是放在wait操作中。
此时 Semaphore本身应该具有两个字段:

1. 值(以int形式存储)
2. 指向PCB链表(实际上就是waiting queue)的指针

使用2个系统内核和提供的系统调用

  • block
  • wakeup

这样的Semaphore的实现方法:

Implementation of wait:
wait (S){
	value--;
	if (value < 0) {
	// add this process to waiting queue
		block(); 
	}
}

Implementation of signal:
	Signal (S){
	value++;
	if (value <= 0) {
		// remove a process P from the waiting queue
	wakeup(P); }
}

block是让自己睡觉,wakeup是把包括自己在内的 别人叫醒

当使用多处理器并且占用率较低时,busy waiting可以填充CPU时间

Deadlock & Starvation(死锁和饿死)

  • 死锁:所有进程都在等待其他进程执行完成。
    在这里插入图片描述
    在上图中,P0和P1在都执行完第一句后就会产生死锁

  • starvation:

五. 经典同步问题(Classic Problems of Synchronization)

!!Bouded Buffer Problem

// producer的代码
BoundedBuffer::Producer(){
	while (true) {
		// produce an item
		wait(empty);
		wait(mutex);
		// add the item to the buffer
		signal(mutex);
		signal(full);
	}
}

// consumer的代码
BoundedBuffer::Consumer(){
	while (true) {
		wait(full);
		wait(mutex);
		// remove an item from buffer
		signal(mutex);
		signal(empty);
		// consume the removed item
	}
}

full和empty确定buffer产生和消费的串行化,而mutex确定从buffer中放入和放出item的critical section的原子性。

但是其实这个mutex对buffer的控制是粗粒度的。如果producer与consumer访问的item在buffer中的位置不相同,这显然不需要采用mutex确保串行化,可以并行访问。只有他们访问同一个item的时候,才需要保护。

!!Readers-Writers Problem

经典问题

  • dataset被一些并发进程共享,readers只读取数据,writers既可以读,又可以写
  • 应当允许多个readers可以一起读一个数据项,但不能允许多个writers(或者writers和readers)一起写(或有写有读)一个数据项。

解决方案:制定一些共享数据:
在这里插入图片描述

  • dataset
  • 一个Semaphore:mutex初始化为1,来确保mutual exclusion,其实也是对readcount的保护
  • Semaphore:wrt初始化为1(也是binary的)
  • readcount:integer值,初始化为0
DataBase{
	BinarySemaphore mutex=1,wrt=1;
	int readCount = 0;
	
	// writer进程的代码
	void writer(){
		while (true) {
			wait(wrt) ;
			OP_WRITE();// 执行写操作
			signal(wrt);
		}
	}
	
	// reader进程的代码
	void reader(){
		while (true) {
			wait(mutex); // 可以看到,上下两段wait(mutex)其实保护了readcount的操作
			readcount++ ;
			if (readcount == 1) wait(wrt) ;// 当只有一个reader时,应当把他当做writer对待
							// 但之后就不能这样做,否则wrt会一直阻塞(因为之后只会signal一次)
							//此时如果有writer,在这里就会阻塞住,直到没有writer(不论是真的还是假的)
			signal(mutex);
			OP_READ();// 执行读操作
			wait(mutex) ;
			readcount -- ;
			if (readcount == 0) signal(wrt) ;
			signal(mutex) ;
		}
	}
}

dining philosophers problem

在这里插入图片描述
五个人五根筷子,这是共享数据的情况。只有拿到2根筷子才能吃饭(data)
故:

  • 米饭=>data set
  • Semaphore chopstick[5] init to 1

此时,phil的代码(会产生死锁)

while(true){
wait( chopstick[i] );
wait( chopStick[ (i + 1) % 5] );
	OP_EAT();
signal( chopstick[i] );
signal(chopstick[ (i + 1) % 5] );
	OP_THINK();
}

在这样的情况下,如果每个phil都拿起左边的筷子,全局就会形成死锁

之后的monitor方法会防止死锁

其他同步问题

1. 使用二进制semaphore实现counting semaphore

struct S{
    int val;               // init k
    BinarySemaphore sem;   // init 0,用于对等待S时的排队同步
    int waitCount;         // init 0,表示在S上等待的进程的数量
    BinarySemaphore mutex; //init 1,用于保护对waitCount和val的更新
};

// wait操作
void wait(struct S s){
    wait(s.mutex);// 保护val和waitCount
    if (s.val == 0){// 目前没有空闲,val=0
        s.waitCount++; // 等待的进程增加
        signal(s.mutex); // 退出临界区
        wait(s.sem); // 等待有进程完成了对S的操作,signal掉了sem
    }
    else{
        s.val--;
        // 还有剩下的val,所以根本不需要waitCount
        signal(s.mutex); // 退出临界区
    }
}

void signal(struct S s){
    wait(s.mutex);// 同样是保护两个数值
    if(s.waitCount>0){
        s.waitCount--;
        signal(s.sem); // 从相当于从等待sem的进程中挑一个执行
    }
    else{
        s.val--;
        // 这里根本就没有等待的进程,因此也不需要signal(s.sem)
    }
    signal(s.mutex);// 最终释放保护信号量
}

总结:

  • 用整数表示计数的方法,用mutex保护操作的整数,但是这个mutex要在整数不使用后马上释放
  • 对整数的判断和修改都需要加mutex锁

理发师问题

在这里插入图片描述
解法:

#define n 10
// 其中,costumers不是二进制信号量,因为可能有多个顾客
Semaphore mutex = 1, barber = 1, costumers = 0;
int freeChair = n;

/**
 * 理发师进程 
 */
void Barber()
{
    while (1)
    {
        wait(costumers); // 等待客人,如果没有就睡觉
        // 客人来到
        wait(mutex);    // 临界区保护
        freeChair++;    // barber起身,空出一个座位
        signal(mutex);  // 临界区结束
        signal(barber); // 理发师去剪发
        cutHair();      // 给顾客剪头
    }
}

/**
 * 顾客进程 
 */
void Costomers()
{
    while (1)
    {
        wait(mutex);
        // 对freeChair的判断值也可能因为多进程而不准确
        // 所以用mutex保护
        if (freeChair > 0)
        {
            freeChair--;
            signal(mutex);     // 完成临界区
            signal(costumers); // 此时顾客已经到来
            wait(barber);      // 等待理发师剪头
            haveHairCut();     //顾客被剪头
        }
        else
        {
            signal(mutex); // 退出临界区,没空位走了
        }
    }
}

六、 管程(Monitor)

Semaphore的使用问题

  • 顺序不能错
  • 必须1对1对应

容易出错的原因是Semaphore的运行模式其实是与普通代码一样的。

monitor的提出

在一个时刻,1个condition variable只允许有1个process在monitor中处于active状态,但可以有多个process被挂起(也可以说sleeping)

monitor monitor-name{
	// shared variable declarations
	procedure P1() {. }
	…
	procedure Pn() {……}
	Initialization code(.) {}}

整体结构:
在这里插入图片描述
其中椭圆内部的每个矩形对应着1个operation。

Condition Variable

CV允许为管程提出个性化的同步方案

这个变量允许的也是两个操作:wait和signal,但语义和Semaphore有区别
condition x;

  • x.wait():调用这个操作的process会被挂起
  • x.signal():从因为x挂起的process中选一个,但如果没有进程在这里等待,就什么也不做

所以condition variable具有一个waiting queue

condition variable的整体结构
在这里插入图片描述

从这个结构可以看到,condition variable时monitor的关键,它应当有:

  • 一个指向被挂起进程(也就是PCB)队列的指针
  • 一个用于实现同步的锁和对应的机制(下文使用的就是Semaphore)

monitor模式对dining philosopher问题解法的实现(使用CV)

monitor DP{
	enum { THINKING; HUNGRY, EATING) state[5] ;
	condition self[5]; //philosopher i can delay herself when unable to get chopsticks
	
	void pickup(int i) {
		state[i] = HUNGRY;
		test(i);
		if (state[i] != EATING) self[i].wait();
	}
	
	void putdown(int i) {
		state[i] = THINKING;
		// test left and right neighbors
		test((i + 4) % 5);
		test((i + 1) % 5);
	}
	void test(int i) {
		if ( (state[(i + 4) % 5] != EATING) &&
		(state[i] == HUNGRY) &&
		(state[(i + 1) % 5] != EATING) ) {
			state[i] = EATING ;
			self[i].signal() ;
	}
}
	initialization_code() {
		for (int i = 0; i < 5; i++)
			state[i] = THINKING;
	}
}

调用时,每个phil只需要如下的调用:

dp.pickup(phil_id);
OP_EAT();
dp.putdown(phil_id);

!!使用Semaphore实现monitor

不使用CV的实现方法

每一个管程都拥有如下的数据结构,它被所有procedure所共有

Semaphore mutex;// init = 1,保护next_count和过程体
Semaphore next;//init 0,把别的进程唤醒,自己等待?
int next_count = 0;// 让位等待的process数,也即在next上等待的进程数

不使用CV时,使用默认的同步方案,这样的方案也是通过Semaphore实现的

procedureF(){
	wait(mutex);
	// actual body of F
	if(next_count>0) signal(next);// 优先调用让位给别人的process
	else signal(mutex);
}

使用CV时monitor的实现方法

对每个condition variable,除了拥有上述3个属于整个管程的变量之外,用于进程同步的字段和方法有:

  • 信号量 x_sem(init 0):
  • 整数值 x_count(init 0):
  • 成员wait操作
  • 成员signal操作

它们的实现如下

struct ConditionVariable{

    // CV使用的,实现定制化同步的手段
    BinarySemaphore x_sem = 0;
    int x_count = 0;
    PCB wait_queue; // 指向进程的队列

    void wait(){// 其中的操作应当是原子性的
        x_count++;
        if(next_count>0){
            signal(next);
        }
        else{
            signal(mutex);
        }
        wait(x_sem);
        x_count--;
    }

    void signal(){
        if(x_count>0){
            next_count++;
            signal(x_sem);
            wait(next);
            next_count--;
        }
    }
};
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值