《操作系统精髓与设计原理》 第5章 并发性:互斥和同步(学习笔记)

核心问题,进程和线程的管理。

5.1并发的原理:

支持并发进程的基本要求是加强互斥的能力。一个进程被授予互斥能力时,在其活动期间,具有排斥所以其他进程的能力。
支持互斥的硬件机制,操作系统或编译器支持的互斥解决方案,最后,信号量,管程和消息传递。

术语介绍:
原子操作:指令操作期间不会被别的进程打断。
临界区域:一段访问公共区域的代码,一个进程在执行这段代码,别的进程就不能执行。
这里指的是代码。
死锁:两个或两个以上的进程等待其他进程做完某事而不能继续执行。
活锁:
互斥:多个进程共同访问同一个临界区域,只能一个进程访问。
竞争条件:多个进程访问同一共享数据,结果依赖于执行的相对时间。
饥饿:进程长时间无法获得调度器的调度。

进程间的资源竞争 互斥:会导致死锁,饿死。
进程间通过共享合作 通过一个参数,一个文件做共享对象,以实现互斥,做到临界区数据的保护

互斥的要求:
1、一次只能一个进程进入临界区
2、非临界区进程不能干涉其他进程。退出临界区必须释放资源和上述的共享对象
3、不能出现饿死或死锁。(关闭中断,内核做schedule,优先级反转)
4、没有进程在临界区时,别的进程需要立刻能进入。
5、支持多处理器(多处理都能访问共享对象做互斥)
6、进程在临界区处理的时间需要短。

5.2硬件的互斥

中断禁止,专用机器指令

5.3信号量

两个或多个进程可以通过简单的信号进行合作。以做到两个进程各自的某段代码顺序执行。一个执行semSignal,一个执行semWait。
1、信号量初始化为0或1
2、进程执行semWait时,会将信号量减1,如果信号量小于0,则该进程阻塞。如果大于等于0,进程排入就绪队列。
3、当进程执行semSignal时,会将信号量加1。如果小于或等于0,则从阻塞队列里取出一个进程。
信号量负值的大小,代表有多少进程在阻塞队列中。

5.3.1互斥

通过信号量做到互斥,避免多个进程同时进入到某一个临界区域。

semWait
临界区域
semSignal

5.3.2生产者/消费者问题

5.3.3信号量的实现

信号量的操作必须是原子操作,可以通过软件的算法Dekker/Peterson。或者硬件来实现compare&swap指令。
单处理器系统,可以禁止中断,避免信号量的操作被中断打断。

5.4管程

https://blog.csdn.net/qq_35212671/article/details/52713822

5.5 消息传递

消息传递的原语是,send(dest, message),receive(source, message)
发送分阻塞和非阻塞,接收也分阻塞和非阻塞。常见的是非阻塞发送,阻塞接收。需要实现消息应答机制。
非阻塞发送还要考虑,错误会导致重复的消息发送。

5.5.5互斥

利用消息传递和信箱的方式,可以做到进程间互斥。进程阻塞接受box中的消息,得到消息后,执行临界区代码。
完成后,将邮件还给邮箱。
p(int i)
message msg;
while(true){
receive(box, msg)
临界区
send(box, msg)
}
parbegin(p(1), p(2), p(3), p(4), p(5), p(6), p(7))
有1条消息,只会发送给一个进程,其他进程被阻塞。

5.6 读写者问题

有一块共享的数据区域,需要满足如下条件:
1、任意进程都可以读取,相互之间不需要互斥
2、只能允许一个进程写入,写入时,其他进程不能读取内容。
读写者问题和一般互斥问题,生产者和消费的区别:

一般互斥问题,读取者效率低,读取和写入者都需要做互斥。
生产者和消费者都需要对队列做读写,无法像读取者那样,不修改共享区域内容

读优先的方式,会导致将数据的权限一直保持给读取者,结果会引发写入者饿死的情况。

Page165
void read(){
    semWait(x)
    readcount++
    if(readcount == 1)
        semWait(wsem)
    semSignal(x)
	READ_UINT()
	
	semWait(x)
    readcount--
    if(radcount == 1)
    	semWait(wsem)
    semSignal(x)

}

void write(){
	semWait(wsem)
	WRITE_UINT()
	semSignal(wsem)
}

另外一种解决办法,当写进程尝试写入时,就不会让新的读进程进入。

  1. 增加一个rsem,用于锁住读进程,writecount,和对应保护writecount的信号量y。
  2. 对于读进程,用一个sem z来避免太多读进程在rsem上排队。(不然rsem就挡不住这些读进程了)
    sem z是保证读进程排队串行去检查rsem。
void reader(){
    semWait(z)
    	semWait(rsem)
    		semWait(x)
    			read_counter++
    			if(counter==1)
    				semWait(wsem)
    		semSignal(x)
    	semSignal(rsem)
    semSignal(z)
	READ_UINT()
	semWait(x)
		read_counter--
		if(counter==0)
			semSignal(wsem)
	semSignal(x)
}

void writer(){
	semWait(y)
		write_counter++
		if(write_count==1)
			semWait(rsem)
	semSignal(y)
	
	semWait(wsem)
	WRITE_UINT()
	semSignal(wsem)
	
	semWait(y)
		write_counter--
		if(write_count==0)
			semSignal(rsem)
	semSignal(y)
}

更好的方式,是采用消息传递的方式来达到写入者优先。由控制者决定写入和控制的权限。

5.7 小结

进程互斥和同步可以通过信号量,信箱的方式去实现。

复习题:

  1. 并发相关的4种设计问题:

  • 操作系统需要记住每个活跃的进程
  • 操作系统需要为每个进程分配或释放硬件资源
    • CPU处理时间,调度
    • 存储器,虚拟存储方案
    • 文件
    • IO设备
  • 操作系统保护每个进程的数据和物理资源,避免重入和干涉。多个线程之间的交互。
  • 一个进程的功能和输出结果与执行速度无关。

  • 进程间交互,通信解决
  • 进程间资源互斥,锁,互斥量解决
  • 进程间同步,信号量解决
  • 进程的执行,时间片解决

  1. 产生并发的三种上下文环境是什么?
  1. 执行并发的基本要求
  • 操作系统有能力做进程切换,保存上下文环境并恢复。
  • 有能力做进程间互斥
  1. 进程间相互知道的3种程度,简单定义
  • 进程间不知道对方的存在,独立工作
  • 进程间间接知道对方的存在,共享某些对象,表现为合作关系
  • 进程间直接知道对方的存在,直接通过进程ID通信,合作关系
  1. 竞争进程和合作进程的区别:
竞争进程 合作进程
不知道对方存在 知道对方存在
  1. 列出与竞争进程相关的三种控制问题:
    第一个是互斥,因为竞争进程不知道对方的存在,对于临界区域需要做到互斥。
    互斥带来的2个问题是,分别是死锁和饿死。
    死锁是当两个进程分别各占有1个资源,都在等对方释放另一个资源,导致都无法执行。
    饿死是由于优先级的关系,低优先级的进程始终得不到运行的问题。

  2. 列出互斥的要求
    一次只有1个进程可以进入到临界区域。
    一个在非临界区域停止的进程不能干涉其他进程,否则会导致死锁。
    不能出现等待临界区域的进程无限延迟。
    没有进程在临界区时,需要进入必须立刻可以进入。
    进程执行速度和支持多核。
    进程在临界区的时间是有限的。

  3. 信号量上可以执行什么操作
    一个信号量初始化为一个非负数
    semWait可以使信号量减1,如果非负,即可得到执行,如果负数,阻塞进程
    semSignal可以使信号量加1,如果值小于或等于0,被阻塞的得到执行

9.二元信号量和一般信号量的区别:
二元信号量初始化为0或1
semWaitB操作,如果为0,进程受阻,如果为1,变为0,执行该进程
semSignal操作,队列是否有阻塞的进程,有就执行,没有就把值改为1

10.强信号量和弱信号量的区别
先进先出的从阻塞队列中取出进程,是强信号量。
没有规定进程dequeue的顺序,是弱信号量,会出现饥饿的情况。

习题

5.1

5.4 让2个进程以A,B,A,A的顺序运行:

#include 
#include 
#include 
#include 


int a=0;
int b=0;

sem_t *sem_a;
sem_t *sem_b;

void * thread1(void*arg)
{
  printf("a\n");
  sem_wait(sem_a);
  //sleep(2);
  printf("a2\n");
  sem_post(sem_b);
  return 0;
}


void * thread2(void*arg)
{
  printf("b\n");
  sem_wait(sem_b);
  //sleep(2);
  printf("b2\n");
  sem_post(sem_a);
  sem_post(sem_a);
  return 0;
}

int main()
{
  pthread_mutex_t m_j;
  int r,r1,r2,r3;
  pthread_t thread_1;
  pthread_t thread_2;
  pthread_t thread_3;
  pthread_t thread_4;
  pthread_mutex_init(&m_j,0 );
  
  sem_a = sem_open("/mysem_a", O_CREAT, S_IRUSR|S_IWUSR, 0);
  sem_b = sem_open("/mysem_b", O_CREAT, S_IRUSR|S_IWUSR, 0);
  printf("thread create\n");
  r = pthread_create(&thread_1,NULL,thread1,NULL);
  r1= pthread_create(&thread_2,NULL,thread1,NULL);
  r2= pthread_create(&thread_3,NULL,thread1,NULL);
  r3= pthread_create(&thread_4,NULL,thread2,NULL);
  sleep(2);
      sem_post(sem_a);
  sleep(5);
  sem_unlink("/mysem_a");
  sem_unlink("/mysem_b");
  return 0;
}

5.5 忙等待的含义,避免忙等待的另一种技术
忙等待是为了满足某一条件,进程持续执行测试变量的指令来得到访问权限。
另一种技术是阻塞式等待,可以释放当前CPU使用权,由调度器来分配cpu时间。
会耗费CPU在进程切入切出的时间。

你可能感兴趣的:(操作系统精髓与设计原理)