进程的同步与互斥

本文探讨了临界资源与临界区的概念,介绍了单标志法、双标志法、Peterson算法等同步策略,强调了信号量和管程在解决进程互斥和同步问题中的作用,以及为何在多道程序系统中需要进程同步以保证结果的可再现性。
摘要由CSDN通过智能技术生成

相关概念

  1. 临界资源与临界区
    • 临界资源:同一时刻只能由一个进程使用的资源。
      • 如打印机、磁带机、绘图仪等物理设备;由不同进程共享的消息队列、变量、数据、文件等软件资源
    • 临界区:程序中访问临界资源的那一部分代码
      • 进入区、退出区、剩余区:为了保证临界资源的互斥使用,每个进程在进入临界区前,必须检查该临界资源是否空闲。因此临界区前面必须有一段代码用于对临界资源的检查,这段代码称为进入区。同样的,临界区之后必须有一个退出区,表明进程已经释放了相应的临界资源。代码的剩余部分称为剩余区。整体的顺序是:进入区->临界区->退出区->剩余区
  2. 同步与互斥
    • 互斥关系:又称进程的间接相互制约关系。几个进程并发执行,由于需要互斥访问系统中的临界资源,因而形成的相互制约的关系。互斥是竞争关系
    • 同步关系:又称进程的直接相互制约关系。几个进程为了完成一项任务而规定的执行顺序问题,是进程的执行先后问题。同步是协作关系
    • 由于互斥和同步这两种制约关系的存在,进程在执行过程中,是否能获得处理器以及以何种速度向前推进,并不由进程本身控制,这就是进程的异步性。这可能导致进程以不正确的顺序访问共享资源,也可能造成进程每次的运行结果不一致。这些错误往往与时间有关,因此称为“时间相关的错误”。为防止这类错误,必须协调进程的执行顺序,确保它们按顺序执行
  3. 同步机制设计准则
    • 空闲让进
    • 忙则等待
    • 有限等待:对于请求访问临界资源的进程,必须保证它能在有限时间内进入对应的临界区
    • 让权等待:若一个进程无法进入临界区,应立即释放处理器,避免出现“忙等”(如一直在CPU上跑一个while循环),以提高处理器利用率
  4. 实现临界区互斥的基本方法:算法原理、算法的进入区和退出区做了什么、算法存在的缺陷(实现互斥要遵循的四个原则是否满足)
    • 软件同步机制
      算法思想主要问题分析
      单标志法(用一个turn表示当前轮到谁了)在进入区只做检查,不上锁(也不必上锁,因为turn的值唯一),在退出区把临界区的使用权交给另一个进程(即每个进程进入临界区的权限只能被另一个进程赋予)不遵循“空闲让进”原则。各个进程只能轮流地进入临界区
      双标志先检查(flag[0]和flag[1]为false或true分别表示0号进程和1号进程是否想进入临界区在进入区先检查后上锁(先检查对方是否想进入临界区,对方不想就把自己的flag设为true,否则就循环检查),在退出区解锁不遵循“忙则等待”原则(a检查之后调度到b,b检查之后调度到a,a上锁,执行临界区代码,调度到b,b上锁,执行临界区代码。这样就导致两个进程同时进入了临界区)其实这个“先检查后上锁”的思想很好,但是因为进入区的“检查”和“上锁”不是一气呵成的,“检查”后,“上锁”前可能发生进程切换,即出现“通过检查后又犯错导致之前的检查失效”的问题
      双标志后检查(标志的设置和上面相同)在进入区先上锁后检查(先将自己的flag设为true,然后看对方的flag值,若是true就卡在while循环,是false就可往下进入临界区),在退出区解锁不遵循“空闲让进”、“有限等待”原则,可能导致饥饿(a先上锁,调度到b,b上锁,调度到a,a检查,进不了临界区,调度到b,b检查也进不了临界区)若两个进程都卡在while循环,那么它们就永远不可能往下推进,就会无限等待
      Perterson算法(bool flag[0]和flag[1]分别表示两个进程进入临界区的意愿、一个turn表示轮到谁了)主动争取(在进入区先将自己的flag设为true),主动谦让(将turn值设成对方),然后检查,检查的判定是“若对方想用且最后是自己表示了谦让就等待”。在退出区将自己的flag设为false不遵循“让权等待”原则,会发生“忙等”(进不了临界区会一直卡在while循环,这几个算法都有这个毛病)结合了双标志、单标志法。若双方都想进入临界区,那么可以让进程尝试谦让
      • 单标志先检查法
        请添加图片描述
      • 双标志先检查法
        请添加图片描述
      • 双标志后检查法
        请添加图片描述
      • Peterson算法
        在这里插入图片描述
    • 硬件同步机制
      • 关中断方法:在某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个同时访问临界区的情况
        • 不适用于多处理机;只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在内核态,这组指令如果能让用户随意使用会很危险)
      • 硬件指令方法
        • TSL或TS(TestAndSetLock或TestAndSet)指令:用硬件实现,执行过程不允许被中断。其逻辑如下:
          // 共享变量lock用于表示当前临界区是否被加锁
          bool TestAndSet (bool *lock){
          	bool old;
          	old = *lock;
          	*lock = true;
          	return old;
          }
          
          // 下面是使用TSL指令实现互斥的算法逻辑
          while (TestAndSet (&lock));  // 上锁并检查
          临界区代码段。。。
          lock = false;   // 解锁
          剩余代码段。。。
          
          • 实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;适用于多处理机环境。但不满足“让权等待”,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致忙等
        • Swap指令(XCHG指令):使用硬件实现,执行过程不允许被中断。逻辑跟TSL指令差不多。逻辑如下:
          // 交换两个变量的值
          Swap (bool *a, bool *b){
          	bool temp;
          	temp = *a;
          	*a = *b;
          	*b = temp;
          }
          
          // 以下是用Swap指令实现互斥的算法逻辑。lock表示当前临界区是否被加锁
          bool old = true;
          while (old==true)
          	Swap (&lock, &old);
          临界区代码段。。。
          lock = false;
          剩余区代码段。。。
          
          • 实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;适用于多处理机环境。但不满足让权等待
    • 这七种方法都无法解决“让权等待”准则,因此有了下面的信号量机制

在概念理解的基础上应用信号量机制实现多进程的同步和互斥问题

  1. 信号量
    • 原理:在几个进程之间使用简单的信号来实现同步。在这个过程中,一个进程可以被阻塞在某个特定位置,直到它收到一个特殊信号,方可继续运行。一个信号量对应一种资源,信号量的值等于该资源的剩余数量。P(S)表示申请申请一个资源S,V(S)表示释放一个资源S
    • 类型
      • 整型信号量
      • 记录型信号量:在整型信号量的基础上,增加了一个被阻塞进程的队列,用于链接所有等待相应资源的进程,从而实现让权等待
      • 互斥量:在记录型信号量的基础上,规定信号量的值只能为0或1(信号量的值不再表示系统中的资源数量,而是表示某个临界区在此时能否访问),且加锁(P操作)和解锁(V操作)必须在同一个进程进行。
    • 应用
      • 利用信号量实现进程互斥:当两个进程需要互斥访问某一临界资源时,每个进程在访问临界区前需对互斥量mutex执行P操作,退出临界区后对互斥量mutex执行V操作
        semaphore mutex = 1; // 互斥量的初始值为1
        Process_A{
        	while (true){
        		P(mutex);  // 加锁
        		访问临界区;
        		V(mutex);  // 解锁
        		剩余区;
        	}
        }
        
        Process_B的代码同上
        
      • 利用信号量实现进程同步:假设两个进程要协同修改一个文件, P A P_A PA先修改, P B P_B PB后修改,则在两个进程中分别执行P、V操作
        semaphore S = 0;
        Process_A{
        	修改文件;
        	V(S);  // 表示修改完毕
        }
        
        Process_B{
        	P(S);  // 判断修改是否完成
        	修改文件;
        }
        
      • 利用信号量实现前驱关系:为每一个直接前后驱关系设置一个信号量,并初始化为0
        请添加图片描述
  2. 经典同步问题
    • 生产者-消费者问题
    • 哲学家进餐问题
    • 读者-写者问题
  3. 管程
    • 出现原因:虽然信号量机制可以解决进程的同步与互斥问题,但每个希望访问临界资源的进程都必须分别定义自己的PV操作,这样会使众多同步操作分散在各个进程中。不仅让系统难以管理,而且可能会因为误用同步操作,导致死锁发生。因此引入了新的进程同步工具----管程
    • 管程的作用:管程是一个软件模块,进程对共享资源的请求、释放和其他操作都必须通过同一个管程来实现。当多个进程请求访问同一资源时,会根据资源的情况接受或拒绝,确保每次只有一个进程进入临界区,有效实现进程互斥。管程的存在让程序猿写程序时不需要再关心复杂的PV操作,让写代码更加轻松
      • 实现同步:设置条件变量及等待/唤醒操作
      • 实现互斥:由编译器负责保证
    • 管程的特性:
      • 共享性:管程可以被系统中的不同进程以互斥方式访问,是一种共享资源
      • 安全性:管程的过程只能访问属于本管程的局部变量
      • 互斥性:进程对管程的访问是互斥的。在任何时候,一个管程中只能有一个活动进程(每次只允许一个进程在管程内执行某个内部过程)。管程的互斥特性是由编译器负责实现的,程序猿不用关心
        • 因为每次仅有一个进程能在管程当中的某一个内部过程当中执行,就意味着每一次对这个共享数据结构的访问,肯定只有一个进程正在进行,而不可能有多个进程同时访问这个共享数据结构,所以管程的互斥性就天然地保证了管程内共享数据结构的互斥性
      • 封装性:管程中的过程只能使用管程中的数据结构,同时,管程中的数据结构都是私有的,只能在管程中使用,进程可以通过调用管程中的过程来使用临界资源
    • 管程的语法描述:将表达共享资源的数据结构和对其进行操作的进程封装在一个对象中,并封装了同步操作,从而对进程隐藏同步的细节,简化了调用同步功能的接口。管程由以下四部分组成:
      • 管程的名称
      • 局部于管程的共享数据结构说明(局部于管程的数据只能被局部于管程的过程所访问,一个进程只有通过调用管程内的过程才能进入管程访问共享数据)
      • 对该数据结构进行操作的一组过程
      • 对局部于管程的共享数据设置初始值的语句
      Monitor monitor_name{ // 管程名
      	share variable declarations;  // 共享变量说明
      	cond declarations;  // 条件变量说明
      	public:  // 能被进程调用的过程
      		void P1{  // 对数据结构操作的过程
      			...
      		}
      		void P2{
      			...
      		}
      		...
      		{  // 管程主体
      			initialization code;  // 初始化代码
      			...
      		}
      }
      
    • 举例:用管程解决生产者消费者问题:在管程中定义访问共享数据(如生产者消费者问题中的的缓冲区)的入口,即一个用于将产品放入缓冲区的函数和一个用于从缓冲区取出产品的函数
      monitor ProducerConsumer    // 管程名
      	condition full, empty;  // 条件变量用来实现同步(排队)
      	int count = 0;  // 缓冲区中的产品数
      	void insert (Item item){ // 把产品item放入缓冲区
      		if (count == N)
      			wait(full);
      		count++;
      		insert_item(item);
      		if (count==1)   // 若在缓冲区中增加一个产品后,产品数为1,说明之前缓冲区
      						// 是空的,那么就有可能存在一些正在等待的消费者进程(排在empty
      						// 队列中)。所以这里会执行一个唤醒操作(signal操作),
      						// 用于唤醒等待在empty这个条件变量对应的等待队列当中的某一个进
      						// 程(一般都是唤醒排在队头的进程)
      			signal(empty);
      	}
      	Item remove(){ // 从缓冲区中取出一个产品
      		if (count==0)
      			wait(empty);
      		count--;
      		if (count==N-1)
      			signal(full); 
      		return remove_item();
      	}
      end monitor;
      
      // 生产者进程
      producer(){
      	while(1){
      		item = 生产一个产品;
      		ProducerConsumer.insert(item);
      	}
      }
      
      // 消费者进程
      consumer(){
      	while(1){
      		item = ProducerConsumer.remove();
      		消费产品item;
      	}
      }
      
    • 条件变量:在管程中设置条件变量和等待/唤醒操作以解决同步问题
      • “条件”指引起进程阻塞或唤醒的事件或原因。若进程通过管程请求临界资源,但请求未得到满足,管程就会调用wait操作将进程阻塞,进程就会插入到相应的等待队列中;或像上面一样,请求的资源得到了之后,就会唤醒进程,将其从等待队列中释放。这样可以避免对管程的持续占用,实现管程的有序排队使用
      • 设有一个条件变量x(上面的形式是wait(full),这里的形式是full.wait),对x的操作如下:
        • x.wait():若调用管程的进程由于条件x而需要被阻塞或暂停,则释放管程,挂起调用进程并插入到条件x的等待队列中,直到另一个进程向条件变量执行了x.signal()操作
        • x.signal():当调用管程的进程注意到条件x已经改变,则调用对应的x.signal()操作。若存在某个进程由于x.wait()操作被挂起,则将其释放;若没有其他进程被阻塞,则不做任何操作(区别于信号量机制中的V操作,V操作总会进行+1操作)
        • x.queue():若至少有一个进程由于条件变量而被挂起,就返回TRUE,否则返回FALSE

为什么需要进程同步

现代操作系统中多道程序异步并发,虽然可以有效提高资源利用率,大大增加系统吞吐量,但也使得系统更加复杂。每个进程的运行结果存在不可再现性,以及程序的执行存在不确定性。

因此为了确保多个进程有序执行,需要在多道程序系统中引入进程同步机制。具体来说,单处理器系统中的进程同步机制有:软/硬件同步机制、信号量机制、管程机制等,这些机制用于保证程序执行结果的可再现性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值