清华向勇《操作系统》学习笔记10:信号量与管程

本文深入探讨了操作系统中的信号量和管程两种同步机制。信号量作为资源调度的基础,分为二进制和资源信号量,常用于解决临界区互斥和线程同步问题。管程则提供了更好的封装,通过条件变量实现线程间的协作。文中通过生产者-消费者问题举例,展示了如何使用信号量和管程来解决同步问题。同时,文章还提到了经典同步问题如哲学家就餐和读者-写者问题。最后,讨论了信号量使用中的挑战以及管程的主要特点。
摘要由CSDN通过智能技术生成

10.1 信号量概念

信号量(Semaphore)是另一种临界区的保护机制,它是操作系统提供的一种协调共享资源访问的方法。
它将资源纳入全局考虑,从操作系统的层面对资源进行宏观的调配。

这个机制由Dijkstra在20世纪60年代提出,是早期操作系统的主要同步机制。

信号量Semphore中的整数sem就是这个系统资源剩余量。申请时减少,释放时增加即可,没有时等待分配,所以先进先出
实现的结构和lock很像。

具体的实现接口为P()V(),分别是荷兰语(?)增加prolagg和减少verhoog的缩写。

信号量的实现(其中sem是剩余资源)

class Semaphore {
	int sem;
	WaitQueue q;
}

Semaphore::P(){
	sem--;
	if (sem < 0) {
		Add thread t to q;
		block(p);
	}
}
Semaphore::V(){
	sem++;
	if (sem <= 0) { // 说明队列中有线程在等待
		Remove a thread t from q;
		Wakeup(t);
	}
}

如果弄懂了clock的机制,从上面的代码中信号量应当是非常好理解的,这个机制拥有如下的特征:

  • sem是由操作系统全局保护的整数变量,信号量的相关操作是原子操作。不像软件方法,相关操作不会被打断。
  • P()可能阻塞,V()不会阻塞
  • 通常假定信号量是“公平的”:
    • 线程不会无限地阻塞在P()操作
    • 假定信号量等待按先进先出

10.2 信号量的使用

信号量可分为二进制信号量(资源数目为0或1)和资源信号量(资源数目为任何非负值),基于其一可以实现另一个。

信号量可以用于实现临界区的互斥访问和线程间的条件同步

用信号量实现临界区的互斥访问,示例如下:

mutex = new Semaphore(1);

mutex->P();
Critical Section;
mutex->V();

必须成对使用P()操作和V()操作,不能颠倒、重复或遗漏

利用P和V配对的性质,可以将P和V分开,从而实现条件同步。
在如下图的线程A中,如果调用P,那么信号量将变为-1,进入等待状态(这是信号量设计中很精髓的FIFO)。线程B调用V之后,信号量变为0,这时A重新开始运行,从而就实现了两个线程的同步。
在这里插入图片描述
生产者-消费者问题

  • 任何时候只能有一个线程操作缓冲区(互斥访问)
  • 缓冲区空时,消费者必须等待生产者(条件同步)
  • 缓冲区满时,生产者必须等待消费者(条件同步)

用信号量描述在这个问题当中的约束:

  • 二进制信号量mutex表示I/O是否被占用,初始化为1
  • 资源信号量fullBuffers表示当前占满的缓冲区数量,用于限制读出,初始化为0
  • 资源信号量emptyBuffers表示当前为空的缓冲区数量,用于限制写入

伪代码如下:

class BoundedBuffer{
	mutex = new Semaphore(1);
	fullBuffers = new Semaphore(0);
	emptyBuffers = new Semaphore(n);
}

其生产、消费两个过程相互对称,

BoundedBuffer::Deposit(c){
	emptyBuffers->P();//条件同步
	mutex->P();    //互斥访问
	Add c to buffer; //核心操作
	mutex->V();    //互斥访问
	fullBuffers->V();//条件同步
}
BoundedBuffer::Withdraw(c){
	fullBuffers->P();//条件同步
	mutex->P();    //互斥访问
	Add c to buffer; //核心操作
	mutex->V();    //互斥访问
	emptyBuffers->V();//条件同步
}

使用信号量的困难在于:

  • 读/开发都比较困难
  • 容易出错
  • 不能系统性地解决死锁问题

10.3 管程

管程是一种更为现代的实现互斥访问的方法。与临界区相比,管程有更好的封装,如果设计得当,则处理各种问题时,使用将极为便捷。但我感觉这可能是迄今为止最难以理解的一节。

在这里插入图片描述类似我们要为临界区实现提供lock和semaphore两种工具类,对于管程,我们还要实现一个条件变量condition类。

管程通过使用条件变量提供对同步的支持,这些条件变量包含在管程中,并且只有管程才能访问
在这里插入图片描述
与信号量的一个最为重要的不同在于,使用条件变量时,如果进入等待状态,还能暂时放弃管程的互斥访问等待事件出现时恢复,从而并行进行其他操作,而不是简单地像对临界区那样“固守”。
这一点是使用了条件变量的好处,可以看到中途释放了lock。

用管程解决生产者-消费者问题
在这里插入图片描述
通过良好的设计封装,可以降低使用过程的使用难度。
具体使用情景下,Deposit和Remove都只需要简单的对外接口,但有着有效的互斥访问机制。

管程中的条件变量按照不同的释放处理方式分为两种:

  • 内部的线程优先执行(Hoare)
  • 正占用管程处于执行状态的线程优先执行(Hansen)

在这里插入图片描述

具体的实现,按照老师的说法是将管程中的while非常关键,它保证能在管程T2退出之后再恢复T1的管程执行,这样实现的Hansen管程,切换次数较少,实际使用效率更高。

Hoare管程只需要将while改成if。正确性更容易说明,更适合理论分析和教学。(srds我还是不懂

10.4 经典同步问题

哲学家就餐问题(五人和五个叉子交替围圈,每个人有思考吃饭两个状态,吃饭需要左右两只叉子)
要防止死锁,又要有尽可能多的人吃到。
一个合理的思路是,避免大家完全用同样的手先发。

#define N 5
semaphore fork[5];
void philosopher(int i){
	while (true)
	{
		think();
		if (i % 2)
		{
			p(fork[i]);
			p(fork[(i+1)%N]);
		} else {
			p(fork[(i+1)%N]);
			p(fork[i]);
		}
		eat();
		V(fork[i]); // 释放不阻塞,因而不用使用分支区别对待
		V(fork[(i+1)%N]);
	}
}

更详细的解析,见https://blog.csdn.net/theLostLamb/article/details/80741319

读者-写者问题
正是之前所提到的同步互斥的一个抽象版本。要求读读不互斥,读写和写写都互斥。管程可以简化同步问题的实现方法。

这里略去,可见https://blog.csdn.net/hhypractise/article/details/107150434

10.5 练习题

一个进程由阻塞队列进入就绪队列,可能发生了哪种情况_________(A)

  • A. 一个进程释放一种资源
  • B. 系统新创建了一个进程
  • C. 一个进程从就绪队列进入阻塞队列
  • D. 一个在阻塞队列中的进程被系统取消了

如果有5个进程共享同一程序段,每次允许3个进程进入该程序段,若用PV操作作为同步机制则信号量S为-1时表示什么_________(C)

  • A. 有四个进程进入了该程序段
  • B. 有一个进程在等待
  • C. 有三个进程进入了程序段,有一个进程在等待
  • D. 有一个进程进入了该程序段,其余四个进程在等待

管程的主要特点有__________(ABD)

  • A. 局部数据变量只能被管程的过程访问
  • B. 一个进程通过调用管程的一个过程进入管程
  • C. 不会出现死锁
  • D. 在任何时候,只能有一个进程在管程中执行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值