4、操作系统原理知识复习(day04)--进程同步与互斥概述

583 阅读24分钟

第四章:进程同步与互斥

1、知识概览

image.png

2、进程同步

2.1、进程同步的概念

知识回顾

上节说到进程具有异步性的特征,异步性是指各并发执行的进程以各自独立的、不可预知的速度向前推进。

image.png

以上面的管道通信为例,读进程和写进程并发地运行,由于并发执行的进程必然导致异步性,因此“写数据”和“读数据”两个操作执行的先后顺序是不确定的。而实际应用中,又必须按照“写数据 --> 读数据”的顺序来执行的。如何解决这种异步问题,就是“进程同步”所讨论的内容。

同步具体定义:

相互合作的一组并发进程,其中每个进程都以各自独立的、不可预知的速度向前推进; 但它们又需要密切合作,以实现一个共同的任务,即彼此“知道”相互的存在和作用。例如,相互合作的进程之间需要交换信息,当某进程未获得其合作进程发出的消息之前,该进程就等待,直到所需信息收到时才变为就绪状态(即被唤醒)以便继续执行,从而实现了诸进程的协调运行。

所谓同步,就是多个并发进程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程同步。同步意味着两个或多个进程之间根据它们一致同意的协议进行相互作用。同步的实质是使各合作进程的行为保持某种一致性或不变关系。 要实现同步,一定存在着必须遵循的同步规则,要分析清楚合作进程之间的同步关系。

同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调他们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。

2.2、同步的例子

同步的例子不仅在操作系统中有,在日常生活中也大量存在, 下面分析几个同步的例子。

(1)病员就诊

医院中病员就诊涉及多个活动(即为进程),现在分析这些活动之间的关系。医生为某病员看病这是一个活动,医生问诊后认为需要做某些化验,于是,就为病员开子化验单。病员取样送到化验室,等待化验完毕交回化验结果,然后继续看病。化验室的化验工作又是另个活动,化验室接到化验单后开始化验,然后提交化验报告。

看病和化验是各自独立的活动单位,但它们共同完成医疗任务,所以需要交换信息。上述这两个合作进程之间有一种同步关系:化验进程只有在接收到看病进程的化验单后才开始工作;而看病进程只有获得化验结果后才能继续为该病员看病,并根据化验结果确定医疗方案。

(2)计算进程和打印进程

操作系统中有大量的进程合作的例子。现给出计算进程( Compute Process, CP )和打印进程( Intup-Output Process, IOP )共享单缓冲区的同步问题,如下图所示。其中,计算进程负责对数据进行计算,打印进程负责打印计算结果。

image.png

CP进程和IOP进程共享单缓冲(一次只能存放一个数据)时,这两个进程的同步关系如下:

  • 当CP进程把数据送人buf时,IOP进程才能从buf中取出数据去打印,即当buf内有新数据时,IOP进程才能动作,否则必须等待;
  • 当IOP进程把buf中的数据取出后,CP进程才能把下一个数据送乳buf中,即只有当buf为空时,CP进程才能动作,否则必须等待。

3、进程互斥

3.1、临界资源

进程的“并发”需要“共享”的支持。各个并发执行的进程不可避免的需要共享一些系统资源(比如内存,打印机、摄像头等I/O设备)

知识回顾:操作系统中两种资源共享方式:

  1. 互斥共享方式:系统中的某些资源,虽然可以提供给多个进程使用,但一个时间段内只允许一个进程访问该资源
  2. 同步共享方式:系统中的某些资源,允许一个时间段内由多个进程“同时”对它们进行访问。

我们把一个时间段内只允许一个进程使用的共享的资源称为临界资源。许多物理设备(比如摄像头、打印机)都属于临界资源。此外还有许多公共变量、数据、内存缓冲区等都属于临界资源。对临界资源的访问,必须互斥地进行。互斥,亦称间接制约关系

3.2、临界区

对临界资源的互斥访问,可以在逻辑上分为如下四个部分:

do{
    entry section; // 进入区
    critical section; // 临界区
    exit section; // 退出区
    remainder section; // 剩余区
}while(true);
  • 进入区是负责检查是否可进入临界区,若可进入,则应设置正在访问临界资源的标志( 可理解为“上锁”),以阻止其他进程同时进入临界区
  • 临界区是进程中访问临界资源的代码段,也可称为临界段
  • 退出区是负责解除正在访问临界资源的标志(可理解为“解锁")
  • 剩余区做其他处理

重点关注临界区:

一组进程共享某一临界资源,这组进程中的每一个进程对应的程序中都包含了一个访向该临界资源的程序段。在每个进程中,访问该临界资源的那段程序能够从概念上分离出来,称为临身区或临界段。

临界区是进程中对公共变量(或存储区)进行访问与修改的程序段,称为相对于该公共交量的临界区。诸进程进入临界区必须互斥,即仅当进程A进入临界区完成相应的操作,并退出临界区后,进程B才允许访间其对应的临界区;反之亦然。否则就会发生错误

关于临界区的概念要注意以下几点:

  • 临界区是针对某一临界资源而言的;
  • 相对于某临界资源的临界区个数就是共享该临界资源的进程个数
  • 相对于同一公共变量的若干个临界区,必须是互斥地进入,即一个进程执行完毕且出了临界区,另一个进程才能进入它的临界区。

如果一个进程暂时不能进入临界区那么该进程是否应该一直占着CPU?该进程有没有可能一直进不了临界区?因此为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循以下原则:

  1. 空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区;
  2. 忙则等待:当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
  3. 有限等待:对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿)
  4. 让权等待:当进程不能进入临界区时,应立即释放处理机,防止进程忙等待。

3.3、进程互斥

进程互斥指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待。当前访问临界资源的进程访问结束,释放该资源之后,另一个进程才能去访问临界资源。

多进程对公共变量(或公用存储区)这样的临界资源的共享具有这样的特点:

  • 共享的各进程不能同时读写同一数据区,只有当一方读、写完毕后,另方才能读、写。
  • 进程互斥可描述为:在操作系统中,当某进程正在访问某存储区域资源时,就不允许其他进程来读出或者修改该存储区的内容,否则,就会发生后果无法估计的错误。进程之间的这种相互制约关系称为互斥。

4、进程互斥的软件实现方法

4.1、知识总览

image.png

  • 单标志法
  • 双标志先检查
  • 双标志后检查
  • Peterson算法

学习提示:

  • 理解各个算法的思想、原理。
  • 结合上小节学习的"实现互斥的四个逻辑部分",重点理解各算法在进入区、退出区都做了什么
  • 分析各算法存在的缺陷(结合“实现互斥要遵循的四个原则”进行分析)

4.2、单标志法

算法思想

两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程,也就是说每个进程进入临界区的权限(锁)只能被另一个进程赋予

image.png

解释:

  • turn变量的初值为0,即刚开始只允许0号进程进入临界区。
  • 若P1先上处理机运行,则会一直卡在5步的进入区进行循环检查【满足循环条件,一直在进入区检查】。直到P1的时间片用完,发生调度,切换P0上处理机运行,代码1不会卡住P0,P0可以正常访问临界区,如果在P0访问临界区期间即时切换回P1,P1依然会卡在5。只有P0在退出区将turn改为1后,P1才能进入临界区。
  • 因此,该算法可以实现"同一时刻最多只允许一个进程访问临界区"

算法缺陷:

turn表示当前允许进入临界区的进程号,而只有当前允许进入临界区的进程在访问了临界区之后,才会修改turn的值。也就是说,对于临界区的访问,一定是按P0→P1→P0→P1....这样交替轮流访问。

这种必须"轮流访问"带来的问题是,如果此时允许进入临界区的进程是P0,而P0一直不访问临界区【即turn变量一直是0】,那么虽然此时临界区空闲,但是并不允许P1访问临界区。因此,单标志法存在的主要问题是:违背“空闲让进”原则。

4.3、双标志先检查法

算法思想

设置一个布尔型数组flag[],数组中各个元素用来标记各进程想进入临界区的意愿,比如"flag[0] = ture"意味着0号进程P0现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有,则把自身对应的标志flag[i]设为true,之后开始访问临界区。

image.png

但是如果P0和P1两个进程并发执行,即中间发生了CPU切换,按照152637..的顺序交替着执行,P0和P1将会同时访问临界区。

因此,双标志先检查法的主要问题是:违反"忙则等待"原则。原因在于,进入区的"检查"和"上锁"两个处理不是一气呵成的原子操作,即"检查"后,"上锁"前可能发生进程切换。

4.4、双标志后检查法

算法思想:双标志先检查法的改版。前一个算法的问题是先“检查”后“上锁”,但是这两个操作又无法一气呵成,因此导致了两个进程同时进入临界区的问题。因此,人们又想到先“上锁”后“检查”的方法,来避免上述问题。

image.png

但是这样若按照1526..的顺序两个进程并发执行,发生切换交替着执行,则P0和P1将都无法进入临界区,因此,双标志后检查法虽然解决了"忙则等待"的问题,但是又违背了"空闲让进"和"有限等待"原则,会因各进程都长期无法访问临界资源而产生"饥饿"现象。

4.5、Peterson算法

算法思想:双标志后检查法中,两个进程都争着想进入临界区,但是谁也不让谁,最后谁都无法进入临界区。Gary L. Peterson想到了一种方法,如果双方都争着想进入临界区,那可以让进程尝试"孔融让梨",主动让对方先使用临界区。

image.png

进入区:

  • 主动争取:
  • 主动谦让:
  • 检查对方是否也想使用,且最后一次是不是自已说了“客气话”

Peterson算法用软件方法解决了进程互斥问题,遵循了空闲让进、忙则等待、有限等待三个原则,但是依然未遵循让权等待的原则。Peterson算法相较于之前三种软件解决方案来说,是最好的,但依然不够好。

5、进程互斥的硬件实现方法【了解】

5.1、知识总览

image.png

5.2、中断屏蔽方法

利用 "开/关中断指令"实现(与原语的实现思想相同,即在某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个同时访问临界区的情况)【lock/unlock】

image.png

优点:简单、高效 缺点:不适用于多处理机;只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在内核态,这组指令如果能让用户随意使用会很危险)

5.3、TestAndSet(TS/TSL)

简称TS指令,也有地方称为TestAndSetLock指令,或TSL指令。TSL指令是用硬件实现的,执行的过程不允许被中断,只能一气呵成。

以下是用C语言描述的逻辑伪代码

//布尔型共享变量lock 表示当前临界区是否被加锁
//true表示已加锁,false 表示来加锁
bool TestAndSet (bool *lock){
    bool old;
    old = *lock; //old用来存放lock 原来的值
    *lock = true; //无论之前是否已加锁,都将lock 设为true
    return old; // 返回lock原来的值
}

//以下是使用 TSL指令实现互斥的算法逻辑
while (TestAndSet (&lock)); //"上锁”并“检查”
临界区代码段...
lock = false; //"解锁”
剩余区代码段... 

若刚开始lock是false,则TSL返回的old值为false, while 循环条件不满足,直接跳过循环,进入临界区。若刚开始lock是true,则执行TLS后old返回的值为true, while 循环条件满足,会一直循环,直到当前访问临界区的进程在退出区进行“解锁”。

相比软件实现方法,TSL 指令把“上锁”和“检查”操作用硬件的方式变成了一气呵成的原子操作。

  • 优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞:适用于多处理机环境
  • 缺点:不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。

5.4、Swap指令(XCHG)

有的地方也叫Exchange指令,或简称XCHG指令。Swap指令是用硬件实现的,执行的过程不允许被中断,只能一气呵成。以下是用C语言描述的逻辑

//Swap指令的作用是交换两个变的值
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;
剩余区代码段...

逻辑上来看Swap和TSL并无太大区别,都是先记录下此时临界区是否已经被上锁(记录在old变量上),再将上锁标记lock设置为true,最后检查old,如果old为false则说明之前没有别的进程对临界区上锁,则可跳出循环,进入临界区。

  • 优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞:适用于多处理机环境
  • 缺点:不满足"让权等待"原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。

6、信号量机制

6.1、知识总览

image.png

之前学习的这些进程互斥的解决方案分别存在哪些问题?

  • 进程互斥的三种硬件实现方式(中断屏蔽方法、TS/TSL指令、Swap/XCHG指 )
  • 进程互斥的四种软件实现方式(单标志法、双标志先检査、双标志后检査、Peterson算法)
    • 在双标志先检査法中,进入区的"检查"、"上锁"操作无法一气呵成,从而导致了两个进程有可能同时进入临界区的问题
    • 所有的解决方案都无法实现"让权等待"

1965年,荷兰学者 Dijkstra提出了一种卓有成效的实现进程互斥、同步的方法一一信号量机制【有些地方也称信号灯机制】

6.2、信号量和P/V操作简介

在现代操作系统中,有大量的并发进程在活动,它们都处在不断地申请资源、使用资源、释放资源以及与其他进程的相互制约的活动中,这些进程什么时候该停止运行,什么时候该继续向前推进,应根据事先的约定来规范它们的行为,这就需要操作系统提供信号量(信号灯)机制。

  1. 用户进程可以通过使用操作系统提供的一对称为P/V操作的原语来对信号量s进行操作,从而很方便的实现了进程互斥、进程同步。

  2. 信号量其实就是一个变量s(可以是一个具有非负初值的整型变量,也可以是更复杂的记录型变量)。可以用一个信号量来表示系统中某种资源的数量或状态,比如:系统中只有一台打印机,就可以设置一个初值为1的信号量。

  3. 原语是一种特殊的程序段,其执行只能一气呵成,不可被中断。原语是由关中断/开中断指令实现的。软件解决方案的主要问题是"进入区的各种检查、上锁操作无法一气呵成",因此如果能把进入区、退出区的操作都用“原语”实现,使这些操作能"一气呵成"就能避免问题。

  4. 一对原语: wait(s)原语和signal(S)原语,可以把原语理解为我们自己写的函数,函数名分别为wait和 signal,括号里的信号量S其实就是函数调用时传入的一个参数

  5. wait、signal原语常简称为P、Ⅴ操作(来自荷兰语 proberen和 verhogen)。因此,做题的时候常把Wait(S)、signal(S)两个操作分别写为 P(S)、V(S)--申请资源/释放资源

  6. 信号量s的值可以改变,以反映资源或并发进程状态的改变【s作为记录型信号量】。操作系统提供称为P、V操作原语来实施对信号灯值的操作。要提及的一点是,信号灯的值只能通过P、V操作原语来改变,由用户程序给出信号量的初值,其后,信号量s在进程同步过程中的值不能由用户程序直接修改。信号量s可能的取值范围是负整数值、零、正整数值

6.3、整型信号量

用一个整数型的变量作为信号量,用来表示系统中某种资源的数量。(与普通整数变量的区别:对整数型信号量的操作只有三种即初始化、P操作、V操作)

Eg:某计算机系统中有一台打印机

image.png

第一点:即如果P0进程在执行wait操作后,资源数s变为0,如果此时P1进程也想进入临界区,于是也会执行wait操作,但是由于s变为0,满足循环,就会一直阻塞在这里,直到P0进程使用完打印机资源,执行了signal操作退出以后,将s变成了1,恢复成了初始值,这时候P1进程才能进入临界区使用打印机资源,如此循环下去,因此这样实现了检查和上锁的一气呵成。

第二点:如果一个进程一直占用着处理机资源【但是一般不会】,就会导致另外一个进程不能进入临界区,就会一直卡在wait这个原语的循环里面,发生忙等现象。

6.4、记录型信号量【重点】

整型信号量的缺陷是存在"忙等"问题,因此人们又提岀了“记录型信号量”,即用记录型数据结构【s,L】表示的信号量。

/*记录型信号量的定义*/
typedef struct {
    int value ; //剩余资源数
    Struct process *L; // 等待队列
} semaphore;

/*某进程需要使用资源时,通过wait原语申请*/
void wait (semaphore S) {
    S.value--;
    if(S.value<0){
        //如果剩余资源数不够,使用block原语使进程从运行态进入阻寒态,并挂到信号量S的等待队列(即阻塞队列)中
        block(S.L);  
    }
}

/*进程使用完资源后,通过signal 原语释放*/
void signal (semaphore S) {
    s.valuet+;
    if (S.value <= 0) {
        //释放资源后,若还有别的进程在等待这种资源,则使用wakeup原语唤醒等待队列中的1个进程,该进程从阻塞态变为就绪态
        wakeup(S.L); 
    }
}

在考研题目中wait(S). signal(S) 也可以记为P(S). V(S),这对原语可用于实现系统资源的“申请”和“释放”。

  • S.value的初值表示系统中某种资源的数目。
  • 对信号量S的一次P操作意味着进程请求一个单位的该类资源,因此需要执行S.value--表示资源数减1,当S.value<0时表示该类资源已分配完毕,因此进程应调用block原语进行自我阻塞(当前运行的进程从运行态→阻塞态),主动放弃处理机,并插入该类资源的等待队列S.L中。可见,该机制遵循了“让权等待”原则,不会出现“忙等”现象。
  • 对信号量s的一次V操作意味着进程释放一个单位的该类资源,因此需要执行S.value++, 表示资源数加1, 若加1后仍S.value <=0,表示依然有进程在等待该类资源,因此应调用wakeup原语唤醒等待队列中的第一个进程(被唤醒进程从阻塞态->就绪态)

注:若考试中出现P(S)、 V(S)的操作,除非特别说明,否则默认S为记录型信号量。

7、信号量机制实现进程同步和进程互斥、前驱关系

7.1、知识总览

image.png

7.2、信号量实现进程互斥

互斥信号量的初值实际上是表示某种资源的数量,而我们这里临界区在同一时间段内只允许一个进程来访问,所以可以把临界区理解成一种特殊的资源,该资源只有一个,只能被分配给一个进程使用,只有这个进程释放了,才能被其他进程使用。如果一个进程需要使用临界区这种特殊的资源的时候,那么在使用之前就应该对其所对应的信号量进行P操作,使用之后进行V操作。

  1. 分析并发进程的关键活动,划定临界区(如:对临界资源打印机的访问就应放在临界区)
  2. 设置互斥信号量S.value(临界资源),初值为1
  3. 进入临界区之前对信号量执行P操作(S.value--),互斥信号量值为0,表示资源被占用,切换到其它进程时,就不能继续执行而主动堵塞
  4. 离开临界区之后对信号量执行V操作(S.value++),互斥信号量值为1,此时其它进程就可以占用此资源,如果有进程正在阻塞中,就将阻塞队列中队头进程唤醒
/*信号量机制实现互斥*/
//要会自己定义记录型信号量,但如果题目中没特别说明,可以把信号量的声明简写成这种形式

semaphore mutex=1; // 初始化信号量

P1(){
    .....
    P(mutex);//使用临界资源前需要加锁
    临界区代码段...
    V(mutex) ;//使用临界资源后需要解锁
    .....
}

P2(){
    ....
    P(mutex);
    临界区代码段...
    V(mutex);
    .....
}

解释:

上述方法能正确实现进程互斥。任何欲进入临界区的进程,必先在互斥信号量上执行P操作,在完成对临界资源的访问后再执行v操作。

  • 由于互斥信号灯的初始值为1,当第一个进程执行P操作后mutex值变为0,说明临界资源可分配给该进程,使之进入临界区。
  • 若此时又有第二个进程欲进入临界区,也应先执行P操作,结果使mutex变为负值,这就意味着临界资源已被占用,因此第二个进程被阻塞。
  • 直到第一个进程执行V操作,释放临界资源而恢复mutex值为0后,方可唤醒第二个进程,使之进入临界区,待它完成临界资源的访问后,又执行v操作,使mutex恢复为初始值。

对于两个并发进程,互斥信号灯的值仅取1、0和-1三个值。

  • 若mutex=1,表示没有进程进入临界区;
  • 若mutex=0,表示有一个进程进入临界区;
  • 若mutex=1,表示一个进程进入临界区,另一个进程等待进入。

注意:

对不同的临界资源需要设置不同的互斥信号量。P、V操作必须成对出现。缺少P(mutex)就不能保证临界资源的互斥访问。缺少V(mutex)会导致资源永不被释放,等待进程永不被唤醒。如下图所示

image.png

7.3、信号量实现进程同步

进程同步:要让各并发进程按要求有序地推进。

比如,P1、P2 并发执行,由于存在异步性,因此二者交替推进的次序是不确定的。 若P2的“代码4”要基于P1的“代码1”和“代码2”的运行结果才能执行,那么我们就必须保证“代码4”一定是在“代码2”之后才会执行。 这就是进程同步问题,让本来异步并发的进程互相配合,有序推进。

P1(){
代码1; 
代码2;
代码3;
}
P2(){
代码4;
代码5;
代码6;
}

用信号量实现进程同步:

  1. 分析什么地方需要实现“同步关系”,即必须保证“一前一后”执行的两个操作( 或两句代码)

  2. 设置同步信号量S,初始为0

    *信号量机制实现同步*/
    semaphore S=0; // 初始化同步信号量,初始值为0
    
  3. 在“前操作”之后执行V(S)

  4. 在“后操作”之前执行P(S)

以这个为例

image.png

  1. 分析问题,找出哪里需要事件“一前一后”的同步关系,假设A进程在B进程之前执行

  2. 设置同步信号量(用来唤醒后面的B进程),初值为0

  3. 在A进程执行后,执行V操作(S.value++)这时value=1,告诉进程B,A进程已经完成了,你可以执行了,如果在进程A没有执行之前B进程先执行了,此时value=0,B进程知道A进程没有执行就主动进入堵塞状态。

  4. 在通过了对value值为1的检查后,B进程就可以访问临界区了,在B访问临界区之前需要执行P操作(S.value--)将value变为0,告诉其它进程该资源被占用,其它进程不能访问该临界资源

7.4、信号量机制实现前驱关系

进程P1中有句代码S1,P2中有句代码S2...P6中有句代码S6。这些代码要求按如下前驱图所示的顺序来执行:

image.png

  1. 分析问题,画出前驱图,把每一对前驱关系都看成一个同步问题
  2. 为每一个前驱关系设置不同的同步信号量,初值为0
  3. 在每个前进程执行之后,执行V操作,把同步信号量变为1,唤醒下一个进程
  4. 在每个后进程执行之后,执行P操作,把同步信号量变为0,不让其它进程访问该资源
  5. 其实每一对前驱关系都是一个进程同步问题(需要保证一前一后的操作)

因此,

  • 要为每一对前驱关系各设置一个同步变量
  • 在“前操作”之后对相应的同步变量执行V操作
  • 在“后操作”之前对相应的同步变量执行P操作

image.png