模拟进程创建、终止、阻塞、唤醒原语_【计算机基础】史上最精简的操作系统教程|5进程同步...

582716c3c1e4b7d113cb25a5515f767e.png

本文目录:

  • 1 进程同步基本概念
    • 1.1 进程同步的背景
    • 1.2 进程同步的概念
  • 2 进程同步实现方法
    • 2.1 软件方法
    • 2.2 硬件方法
    • 2.3 信号量
      • 2.3.1 整型信号量
      • 2.3.2 记录型信号量
      • 2.3.3 信号量-实现同步
      • 2.3.4 信号量-实现互斥
      • 2.3.5 信号量-前驱图
  • 3 典型同步问题
    • 3.1 生产者--消费者问题
    • 3.2 读者--写者问题
    • 3.3 哲学家进餐问题

1 进程同步基本概念

1.1 进程同步的背景

假设在没有进程同步机制制约的场景下,有一环形缓冲池,包含n个缓冲区(0~n-1)。有两类进程:一组生产者进程和一组消费者进程,生产者进程向空的缓冲区中放产品,消费者进程从满的缓冲区中取走产品。

生产者代码:

while 

消费者代码:

while (true)  {
     
    while (count == 0) ; // 缓冲池无产品,无法消费,阻塞在这里  
    nextConsumed =  buffer[out];
     // 从缓冲池拿出一个产品进行消费
    out = (out + 1) % BUFFER_SIZE;
    // 因为是环形,所以使用取余数得到缓冲池下一个索引
    count--;
    // 缓冲池占用数-1

}

那么,在上述代码中,其中 count++ 和 count-- 可能会出现问题。

count++ 的执行过程如下(机器指令

register1 = count
register1 = register1 + 1
count = register1

count-- 的执行过程如下(机器指令

register2 = count
register2 = register2 - 1
count = register2

分析:

假设初始状态 count=5,则生产者和消费者两个进程,对应内部的机器指令的执行如下(注意:① 这里是在没有进程同步机制制约的场景下。② 两个进程正在随机占用一个CPU):

  1. S0: producer execute register1 = count {register1 = 5}
  2. S1: producer execute register1 = register1+1 {register1 = 6}
  3. S2: consumer execute register2 = count {register2 = 5}
  4. S3: consumer execute register2=register2-1 {register2 = 4}
  5. S4: producer execute count = register1 {count = 6 }
  6. S5: consumer execute count = register2 {count = 4} // 这个为最终结果了

正确结果应该是: “count = 5

现在的结果是: “count = 4

看到这里,读者朋友应该明白了,这就是因为进程同步没进行制约导致的结果。

1.2 进程同步的概念

多道程序环境下,进程并发执行,不同进程存在相互制约关系。为协调该关系,避免进程冲突,引入进程同步

一些名词解释:

  • 临界资源:一次仅允许一个进程使用的资源称为临界资源。比如:打印机、共享变量等。
  • 临界区:是指并发进程中访问临界资源的程序段
  • 进入区:检查是否可以进入临界区,若可以,设置正在访问临界区标志。
  • 退出区:清除正在访问临界区标志。
do {
    进入区

    critical section; // 代码执行部分

    退出区

    remainder section // 剩余部分
}
  • 进程的互斥:是指若干个进程要使用同一共享资源时,任何时刻最多允许一个进程去使用,其它要使用该资源的进程必须等待,直到占有资源的进程释放该资源。
  • 进程的同步:是解决进程间协作关系的手段。指一个进程的执行依赖于另一个进程的消息,当一个进程没有得到来自于另一个进程的消息时则等待,直到消息到达才被唤醒
  • 进程互斥关系是一种特殊的进程同步关系。

临界区访问原则:

  • 互斥访问– 若已有进程进入临界区,则其他的进程必须等待其离开临界区,释放临界资源。 (忙则等待
  • 前进 – 若没有进程处于其临界区,应允许一个请求进入临界区的进程立即进入临界区,访问临界资源。(空闲让进)。
  • 有限等待 – 对请求访问的进程,应保证能在有限的时间内进入临界区,避免进入“死等”。
  • 让权等待 – 当进程不能进入临界区时,该进程应释放处理机,以免进入“忙等”状态。

2 进程同步实现方法

2.1 软件方法

解释:在进入区设置和检查一些标志来标明是否有进程在临界区,若有则在进入区循环检查进行等待,进程在退出区修改标志,以允许别的进程进入临界区。

Peterson 算法

1981年,由Peterson提出,满足临界区访问的原则。

  • 设有两个进程Pi和Pk,且 LOAD 和 STORE 指令是原子操作。 Pi和Pk共享两个变量:
    • int turn;
    • Boolean flag[2] ;
  • 变量 turn:turn==i 表示Pi可进入其临界区。
  • 数组 flag : flag[i] = true 表示进程Pi请求进入临界区!

例子:

// Pi进程:
while (true) {
      flag[i] = TRUE;   // Pi请求进入临界区
      turn = k;     // Pk可以进入
      while ( flag[k] && turn == k); // 若 (flag[k] && turn == k)为true,说明Pk随后也请求进入了临界区,Pi堵塞,Pk进入临界区了。

      CRITICAL SECTION // 代码执行部分

       flag[i] = FALSE; 给 Pk 信号,Pi随后可以执行

       REMAINDER SECTION // 剩余部分
}


// Pk进程:
while (true) {
      flag[k] = TRUE;  // Pk请求进入临界区
      turn = i;    // Pi可以进入
      while (flag[i] && turn ==i);

      CRITICAL SECTION  // 代码执行部分

       flag[k] = FALSE;  // 给 Pi 信号,Pi随后可以执行

       REMAINDER SECTION  // 剩余部分
}

分析:

若pi和pk同时请求进入临界区,while中的turn变量可保证只允许一个进入临界区,从而实现了互斥。

考虑Pi进程的代码,flag[i] = true; 意味着Pi想进入临界区,同时将turn设置为k, 若Pk在临界区,则Pi的while条件为真,Pi等待

若PK不在临界区,则flag[k]为false,Pi进入临界区,从而避免了死等

2.2 硬件方法

很多系统都提供了解决临界区问题的硬件支持。

方法1

对于单处理器环境 – “禁止中断”

  • 并发进程可以无预设地直线运行
  • 限制了交替执行程序的能力,执行效率明显降低

方法2

许多现代计算机系统提供了特殊的原子(执行该代码时不允许被中断)机器指令:

  • TestAndSet 指令:读出标志并把该标志设置为TRUE。
  • Swap 指令:交换两个内存字的内容。

方法2.1 利用TestAndSet解决临界区访问算法。

  • 声明一个布尔变量互斥锁 lock, 初值为false。
boolean TestAndSet (boolean *target)
{
    boolean old = *target;
    *target = true;
    return old; 
}

while (true) {
    while (TestAndSet(&lock)); // 阻塞

    // critical section

    lock = FALSE;

    // remainder section
}

方法2.2 利用Swap解决同步算法

  • 共享全局布尔变量 lock,初值为FALSE;
  • 每个进程定义一个局部布尔变量 key.
void Swap (bool *a, bool *b)
{
    bool temp = *a;
    *a = *b;
    *b = temp;
}

while (true) {
    key = TRUE;
    while (key == TRUE) 
        Swap(&lock, &key);
    // critical section
    lock = FALSE;
   // remainder section
}

2.3 信号量

Dijkstra发明了两个信号量操作原语P操作原语和V操作原语。

常用的其他符号有:wait和signal;up和down等

—— 原语是操作系统内核中执行时不可中断的过程,即原子操作。

除赋初值外,信号量仅能由同步原语对其进行操作,没有任何其他方法可以检查和操作信号量。

利用信号量和P、V操作既可以解决并发进程的竞争问题,又可以解决并发进程的协作问题。

2.3.1 整型信号量

数据类型:整型信号量S

对信号量S的操作

  • 初始化、
  • 原子操作wait(S)
  • 原子操作signal (S)

wait(S) 和 signal (S) 操作的定义 :

wait(S){ 
    while (S<=0) ;//无资源,则等待    
    S:=S-1;
} 
                         
signal(S){ 
    S:=S+1; 
}

其中 S 初值:一般取可用资源数目。

缺陷:未遵循“让权等待”原则。若S=0,则进程便在while (S<=0) ;无限循环进行等待,白白浪费CPU时间。

2.3.2 记录型信号量

要解决的问题:整型信号量的“让权等待”

办法:等待时进程应阻塞自己

记录型信号量的定义:

typedef struct {
    int   value ;      //可用资源数
    struct process  *L ; //阻塞队列
} semaphore ;

Wait(S)和signal(S)中的S为两个过程的共享变量。

通常,P操作意味着请求一个资源,V操作意味着释放一个资源。

Wait(S) - P操作:
semaphore S;
{
	S.value --;
	if S.value < 0 
            block(S.L)
}
      

Signal(S) - V 操作:
semaphore S;
{		
	S.value ++ ;
	if S.value <= 0
           wakeup(S.L); 
}

Wait(S)相当于P操作;Signal(S) 相当于 V 操作。

2.3.3 信号量-实现同步

例子:

设S为实现进程P1、P2同步的公共信号量,初始值为0。若进程P2中的y要使用进程P1中的x,则同步代码如下:

Semaphore  S=0;

P1(){
      ........
      x;
      V(S);
     ........
}

P2(){
      .........
      P(S);
      y;
      .........
}

解析:

  • P2进程运行,因为P2进程需要P1进程的x,所以第一步先执行了原语操作P(S)
  • P(S) 内的操作为:Semaphore S=0 可知value初始0,S.value -- 为 -1,命中条件 S.value < 0 ,所以进行自我阻塞操作block(S.L),即P2进程阻塞。
  • P1进程过了好久,进来开始执行:x生成,紧接着执行了原语操作 V(S);
  • V(S)内的操作:S.value ++(S.value值是共享的),所以 S.value 为0,命中条件value <= 0,然后P2进程被唤醒wakeup(S.L);
  • P2进程被唤醒执行,有了x变量,正确执行完。

2.3.4 信号量-实现互斥

例子:

设S为实现进程P1、P2互斥的信号量,由于每次只允许一个进程进入临界区,则初始值为1。则互斥代码如下:

Semaphore  S=1;

P1(){
      ........
      P(S);  //加锁
      P1的临界区
      V(S);  //解锁
     ........
}

P2(){
      ........
      P(S);   //加锁
      P2的临界区
      V(S);   //解锁
     ........
}

这个的解析思路跟上面实现同步的一样。或者这里可以直接抽象化,忽略 P(S) 中的底层细节,直接看成一把锁,这两个进程在同一时间内,只能由其中一个进程获取到一把锁并往下执行。

可以确保每次只允许一个进程进入临界区。

2.3.5 信号量-前驱图

例子:

语句S1~S6之间的前趋关系图如下,写出一个可并发执行的程序:

c75509395ea9b60932114b3ce1508445.png

解:

semaphore  a=0,b=0,c=0,d=0,e=0,f=0,g=0;

//其他代码
	{ S1; signal(a); signal(b);               }
	{ wait(a); S2; signal(c); signal(d); }
	{ wait(b); S3; signal(e);                  }
	{ wait(c); S4; signal(f);                   }
	{ wait(d); S5; signal(g);                  }
	{ wait(e); wait(f); wait(g); S6;         }
//其他代码

3 典型同步问题

3.1 生产者--消费者问题

问题描述:

一组生产者进程消费者进程共享一个初始为空的缓冲池,含N个缓冲区,每个缓冲区可以存放一个消息。缓冲池未满时,生产者才能把消息放到缓冲池,否则,生产者等待。缓冲池不空时,消费者才能取出消息,否则,消费者等待。

注:

  • 这个缓冲池是临界资源,同一时间,只允许一个生产者放消息,或者一个消费者取消息。所以,生产者和消费者进程对缓冲池的访问是互斥关系
  • 只有生产者生产之后消费者才能消费,它们也是同步关系

信号量设置:

  • 互斥信号量 mutex,初值为 1,控制互斥访问缓冲池。
  • 同步信号量 full,初值为 0。记录“满”缓冲区的个数。
  • 同步信号量 empty,初值为 N。记录“空”缓冲区的个数

代码例子:

Semaphore mutex=1;   //缓冲池互斥信号量
Semaphore empty =n;  //缓冲池中空闲缓冲区个数,初始值为n
Semaphore full=0;       //缓冲池中满缓冲区个数,初始值为0

生产者进程
While(1){
    produce an item;
    p(empty); //申请空缓冲区
    p(mutex);//申请进入临界区

    数据放入缓冲池中;

    V(mutex);//释放互斥信号量
    V(full);//满缓冲区数加1
}

消费者进程
While(1){
    p(full); //申请一个满缓冲区
    p(mutex);//申请进入临界区

    从缓冲池中取消息;

    V(mutex);//释放互斥信号量
    V(empty);//空缓冲区数加1
    消费数据;
}

注意:对信号量的PV操作成对出现。另多个P操作顺序不能颠倒,否则容易引起死锁。

3.2 读者--写者问题

描述:

读者比较复杂。与其他读者是同步与写者是互斥。这里用一个计数器 ReadCount ,用来记录当前读者数目。

  • 如果读者数目ReadCount为0,则可能有也可能没有写者在写;
  • 如果读者数目ReadCount不为0,则不会有写者在写,请求读的读者便可读;
  • 如果读者数目ReadCount为0,又没有写者在写,则请求写的写者能写。

代码:

int  count=0;       //记录读者数
semaphore mutex=1; //更新count值时的互斥信号量
semaphore rw=1;   //读者写者互斥信号量


写者进程:
While(1){
     p(rw);    //申请访问共享文件
     writing   
     V(rw);   //释放共享文件
}



读者进程:
While(1){
    p(mutex);  //申请访问count

   //若为第一个读者,需申请访问共享文件
    if(count ==0) 
           p(rw);
     count++; //读者数加1
     V(mutex); //释放访问count
     reading;
     P(mutex);
     count --;

     //最后一个读者,需释放共享文件
     if(count ==0)
           V(rw);
      V(mutex);
}

3.3 哲学家进餐问题

问题描述:

5个哲学家致力于思考和进餐,思考不影响其他人,饥饿时拿起左右两边的筷子(一根一根地拿起)。若筷子在别人手中,等待。进餐完,放下筷子继续思考。

e7d68e72929c70d1c2771d754a0f61df.png
  • 关键是如何让一个哲学家拿到左、右两根筷子而不造成死锁或者饥饿现象的发生。
  • 设置一个互斥信号量数组chostick[5]={1,1,1,1,1},用于实现对5根筷子的互斥访问。
Pi 进程: 第i个哲学家的进程(i:0~4)
While (true)  { 
         p( chopstick[i] );
	      p( chopStick[ (i + 1) % 5] );

		 //  eat

	      V( chopstick[i] );

	      V(chopstick[ (i + 1) % 5] );

	      //  think
}

若5个哲学家都要进餐,同时拿起左边的筷子 p( chopstick[i] ); 筷子都被拿光了,等到想拿右边得筷子 p( chopStick[ (i + 1) % 5] ); 时,全都阻塞,造成死锁。

解决方法:

  1. 至多只允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够进餐,并在用毕时能释放出他用过的两只筷子,从而使更多的哲学家能够进餐。
  2. 仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐。
  3. 规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子;而偶数号哲学家则相反。按此规定,将是1、 2号哲学家竞争1号筷子;3、4号哲学家竞争3号筷子。即五位哲学家都先竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一位哲学家能获得两只筷子而进餐。
Semaphore  chopstick[5]={1,1,1,1,1};
Semaphore  mutex =1; //取筷子信号量
Pi进程:
While(1){
    P(mutex);
    P(chopstick[i]);
    P(chopstick[(i+1)%5];
    eat;
    V(chopstick[i]);
    V(chopstick[(i+1)%5];
    V(mutex);
    think;
}

信号量机制的不足:

不正确地使用信号量,可能带来很多问题: 顺序错误: V (mutex) …. P (mutex) 错误使用: V (mutex) … V (mutex) 忘记信号量操作:忘记V (mutex)或 P(mutex) 或两者

需要程序员亲自控制信号量的使用过程,也为编程带来困难。管程是一种高层次的抽象,提供了方便、高效的进程同步机制

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值