第二章 Part3 进程同步与互斥的实现

第二章 Part3 进程同步与互斥的实现

第一节 进程同步和进程互斥

什么是进程同步

  • 进程具有异步性,并发进程的状态独立、不可预知

需要保证并发执行的进程按预期的顺序执行

OS需要提供进程同步的功能

  • 同步又称直接制约关系

  • 用于协调进程的工作次序

什么是进程互斥

进程的并发需要共享的配合

两种共享

同时共享:一个时间段允许多个进程访问

互斥共享:一个时间段内只允许一个进程访问该资源

此类资源称作临界资源

对此类资源的访问需要互斥的进行

  • 所以进程互斥也称作简介制约关系

实现方式:进入区和推出区完成进程互斥的实现。临界区是访问临界资源的代码段

四个部分

进入区

临界区

退出区

剩余区

四个原则

空闲让进:临界区空闲,需要允许程序访问

忙则等待:有程序正在访问时,其他程序需要等待

有限等待:需要保证不会饥饿

让权等待:进入不了临界区的进程,需要释放处理机

第二节 进程互斥的软件实现

  1. 算法思想
  2. 伪代码演示
  3. 优缺点

单标志法

算法思想:进程访问完临界区之后,将临界区使用权交给另一个进程,每个进程进入临界区的权限由另一个进程来赋予

(伪)代码演示:

int turn = 0; // turn表示当前进程编号

/*
 * P0进程
 */
void P0_start() {
    while (turn != 0); // 进入区,不是0号则一直阻塞 
    critical section; // 临界区,读取和写入
    turn = 1; // 交还使用权
    reminder section; // 剩余区
}

/*
 * P1进程
 */
void P1_start() {
    while(turn != 1); // 不是1,阻塞
    critical section; // 临界区,读取和写入
    turn = 0; // 交还使用权
    reminder section; // 剩余区
}

优缺点:

优:能实现进程互斥

缺:

若P0执行完之后,P1尚不需要访问临界区,则此时P0也不能访问临界区

  • 违反了“空闲让进”的原则

双标志先检查:先检查后上锁

算法思想:设置一个bool型数组,标志各进程进入临界区的“意愿”,每个进程进入临界区之前先检查是否之前还有程序想进入临界区,没有则自己进入

伪代码表示:

bool flag[2];
flag[0] = false;
flag[1] = false; // 开始时均无意愿进入临界区

/*
 * P0进程
 */
void P0_start() {
    while(flag[1]); // 检查P1是否在访问临界区
    flag[0] = true; // 标记P0想进入
    critcal section;// 访问临界区
    flag[0] = false;// P0访问完成
    reminder section;
}

/*
 * P1进程
 */
void P1_start() {
    while(flag[1]); // 检查P0是否在访问临界区
    flag[0] = true; // 标记P0想进入
    critcal section;// 访问临界区
    flag[0] = false;// P0访问完成
    reminder section;
}

优缺点:

优:能实现进程互斥

缺:可能违反“忙则等待”,多个进程一起访问临界区

问题在于

 while(flag[1]); // 检查P1是否在访问临界区
flag[0] = true; // 标记P0想进入

进入去检查和上锁不是同步完成,检查后上锁前仍然有可能发生切换

双标志后检查:先上锁后检查

算法思想:大致类双标志先检查

伪代码表示:

bool flag[2];
flag[0] = 0;
flag[1] = 0; //初始状态,两个进程均无意愿

void P0_start() {
 flag = true; //先表示访问的意愿
 while(flag[1]); // 若已经有进程在使用,阻塞
 critical section;
 flag[0] = false;
 reminder section;
}

void P1_start() {
 flag[1] = true;
 while(flag[0]); // 有进程在访问,阻塞
 critical section;
 flag[1] = false;
 reminder section;
}

优缺点:

优:能完成进程互斥的功能

缺:可能违反空闲让进有限等待的原则

flag[0] = true;
flag[1] = true; // 进程0表达意愿之后紧接着进程1也表达
while(flag[1]); // 此时进程0、1均无法进入临界区

Peterson算法

算法思想:主动让给对方使用临界区

伪代码表示

bool flag[2];
int turn = 0; // 表示让那个(标号的)进程优先使用临界区

void porcess_0() {
    flag[0] = true;
    turn = 1; // 主动让1进入
    while(flag[1] && turn == 1); // 若都想进入,则阻塞当前进程
    crtical section;
    flag[0] = false;
    reminder section;
}

void process_1() {
    flag[1] = true;
    turn = 0;
    while(flag[0] && turn == 0);
    critical section;
    flag[1] = false;
    reminder section;
}

优缺点:

优:遵循空闲让进、忙则等待、有限等待

缺:未能遵循让权等待

第三节 进程互斥的硬件实现方法

  1. 原理
  2. 优缺点

中断屏蔽

原理

在一个进程访问临界区前将中断暂时关闭

不可能在访问临界区时发生中断

优缺点

优:简单高效

不适用多核处理器(可能多个核心中的进程同时访问一个临界区)

不适用于用户进程(开关中断指令只能运行在内核态)

TestAndSet(简称TS或TSL)

原理

使用硬件标示对临界区“上锁”

优点

优:保证上锁和检查一步完成(由硬件保证不被中断)

缺:导致忙等

Swap指令(Exchange指令或XCHG指令)

原理与优缺点类TSL(均为硬件实现)

第四节 信号量机制

使用一对原语来对信号量进行操作

  • 信号量是一个变量

  • 表示系统中某个资源的数量

  • 一对原语:wait(S)signal(S)原语,S代表信号量参数,常称作P、V操作,也简写做P(S)、V(S)

整型信号量

用一个整数变量作为信号量

与普通整型变量的区别,仅有初始化、P、V三种操作

伪代码示例

int S = 1; // 初始化信号量S

void wait(int S) { // wait原语,相当于进入区
    while(S <= 0); // 资源不足,阻塞
    S = S -1; // 资源足够,占用其中一个
}

void signal(int S) { // Signal原语,相当于退出区
    S = S + 1; // 释放资源
}

不满足让权等待

记录型信号量

使用数据结构表示信号量

伪代码表示

struct {
    int value; // 剩余资源数
    struct process *L; // 指向等待队列
} semaphore;

/*
 *wait原语,某进程需要使用资源
 */
void wait(semaphore S) {
    S.value--;
    if(S.value < 0) { // 若无系统资源
        block(S.L); // 使用block原语使进程进入阻塞态,并将信号挂在S的等待(阻塞)队列中
    }
}

/*
 *signal操作
 */
void signal(semaphore S) {
    s.value++;
    if (S.value <= 0) { // 此时仍有进程处于等待队列未被分配到资源
        wakeup(S.L); //从等待队列中唤醒一个进程
    }
}

S.value()小于0时绝对值就是阻塞队列进程中进程的数量

第五节 信号量机制的应用

信号量机制实现进程互斥

划定临界区

  • 设置互斥信号量 mutex,初始值为1(实现进程同步是设为0)

伪代码表示(见第四节:记录型信号量)

semaphroe mutex = 1; // 初始化信号量
P1() {
    // 略
    P(mutex); // 加锁,mutex--
    // 临界区操作
    V(mutex); // 解锁,mutex++
    // 略
}

P2() {
    // 略
    P(mutex);
    // 临界区操作
    V(mutex);
    // 略
}
  • 不同临界资源设置不同的信号量
  • 临界区前进行P(),临界区后进程V()

信号量机制实现进程同步

并发进程执行存在异步性,需要实现进程同步保证代码的执行次序

  • 设置同步信号量S = 0
  • 在“前操作”之后执行V(S)
  • 在“后操作”之前执行P(S)

信号量机制实现进程的前驱关系

  • 为每一对前驱关系设定一个同步变量
  • 前操作之后V(S)(S++),后操作之前P(S)(S–)

第六节 生产者-消费者问题

系统进程存在消费者和生产者进程

生产者每次生产一个产品,放入缓冲区,缓冲区已满时,需要等待(被阻塞)

消费者每次从缓冲区里拿走一个产品,缓冲区为空时,需要等待(被阻塞)

共享使用一个初始为空,大小为n的缓冲区

使用P、V操作来实现生产者、消费者的互斥运作分析

  1. 几类进程?

    两类进程:生产者和消费者

  2. 那些进程存在同步和互斥关系?

    同步:特别注意多对的同步关系

    缓冲区满,生产者等待

    缓冲区空,消费者等待

    互斥

    生产者和消费者不能同时访问临界区

  3. 为每一对关系设置信号量

    互斥信号量设置为1,同步信号量根据资源总数确定

    semaphore mutex = 1; // 互斥信号量,实现对缓冲区的互斥访问
    semaphore empty = n; // 同步信号量,表示缓冲区的数量(缓冲区空间大小)
    semaphore full = 0; // 同步信号量,表示非空缓冲区的数量(产品数量)
    

    生产者每次需要P一次,消费者每次V一次

生产者-消费者问题的具体实现

producer() {
    while(1) {
        生产一个产品;
        P(empty); // 消耗一个缓冲区
        P(mutex); 
        放入缓冲区;
        V(mutex);
        V(full); // 产品数量+1
    }
}
consumer() {
    while(1) {
        P(full); // 消耗一个非缓冲区
        P(mutex);
        取出一个产品;
        V(mutex);
        V(empty);
        使用产品;
    }
}
  • 先进行进程同步的P操作,然后进行进程互斥的P操作

    V操作的进程可以交换

    例如:

    empty = 0; //缓冲区已满
    full = n;
    

    此时

    producer {
        …………
        P(mutex);
        P(empty); // 被阻塞
        …………
    }
    
    

    若此时再执行

    consumer {
        P(mutex); // 此时消费者进程也被阻塞
    }
    

    导致了死锁的现象

第七节 多生产者-多消费者问题

例子:两对生产者-消费者关系

父亲 - 苹果 - 女儿

母亲 - 橘子 - 儿子

一个盘子(缓冲区)

分析如下

  1. 几类进程

    生产者:父亲、母亲

    消费者:儿子、女儿

    临界资源:苹果、橘子

    临界区大小:1

  2. 那些进程存在同步和互斥关系

    同步:

    父亲放 - 女儿拿

    母亲放 - 儿子拿

    • 盘子为空才能放水果

      容易忽略

      儿子女儿都能触发

    互斥:

    父亲放 - 母亲放

    儿子拿 - 女儿拿

  3. 定义信号量

    /*同步信号量*/
    semaphore apple = 0;
    semaphore orange = 0;
    semaphore 	plate = 1;
    /*互斥信号量*/
    semaphore mutex = 1;
    

具体实现

dad() {
    while(1) {
        准备一个苹果(生产一个资源);
        P(plate); // 占用一个资源
        P(mutex); // 占用临界区
        放入一个苹果;
        V(mutex);
        V(apple);
    }
}
mom() {
    while(1) {
        准备一个橘子(生产一个资源);
        P(plate);
        P(mutex);
        放入一个橘子;
        V(mutex);
        V(orange);
    }
}
daughter() {
    while(1) {
        P(apple); // 检查资源
        P(mutex);
        取出苹果;
        V(mutex);
        V(plate);
        吃掉苹果;
    }
}
son() {
    while(1) {
        P(orange); // 检查资源
        P(mutex);
        取出苹果;
        V(mutex);
        V(plate);
        吃掉橘子;
    }
}

临界区大小为1时,可以不设置互斥信号量mutex

第八节 吸烟者问题

问题描述

三个抽烟者进程,一个供应者进程

三个吸烟者分别拥有烟草、纸、胶水

供应者可以供应上述三种材料,一次供应三种中的两种,此时有剩下一种材料的用户将取走这两种材料,并卷成烟抽掉

供应者需要让吸烟者轮流的吸烟

关系分析

  1. 几个元素?

    三个吸烟者、一个供应者

    三种物品组合

    对组合的容量为1

  2. 同步和互斥

    桌上有

    组合1 - 吸烟者1

    组合2 - 吸烟者2

    组合3 - 吸烟者3

    完成信号 - 供应者放上下一组材料

    缓冲区大小为1,可以不设置互斥信号量

  3. 设置信号量

    semaphore offer1 = 0; // 组合1的数量
    semaphore offer2 = 0;
    semaphore offer3 = 0;
    semaphore finish = 0; // 完成信号
    int i = 0; // 指示供应给哪一位吸烟者,实现轮流让吸烟者吸烟
    

具体实现

provider() {
    while(1) {
        if (i = 0) {
            放上组合1;
            V(offer1);
        } else if(i == 1) {
            放上组合2;     
            V(offer2);
        } else if(i == 2) {
            放上组合3;   
            V(offer3);
        }
        i = (i++) % 3;
        P(finish);
    }
}
smoker1() {
    while(1) {
        P(offer1); // 取走组合1
        V(finish); // 完成信号
    }
}
smoker2() {
    while(1) {
        P(offer2); // 取走组合2
        V(finish);
    }
}
smoker2() {
    while(1) {
        P(offer3); // 取走组合2
        V(finish);
    }
}

第九节 读者 - 写者问题

读者进程不会改变数据,所以可以多个读者同时访问共享数据

问题概述

读者、写者两组并发进程共享一个文件

  1. 允许多个读者同时对文件进行操作
  2. 只允许一个写者向文件中写信息
  3. 写者操作时,不允许其他单位同时操作

问题分析

  1. 几个元素

    读者

    写者

  2. 几组关系

    互斥关系

    写进程 - 写进程

    写进程 - 读进程

  3. 设置信号量

    semaphore rw = 0; // 表示当前是否有进程在访问共享文件
    semaphore mutex = 1; // 保证对conut变量的互斥访问
    semaphore w = 1; // 保证写进程优先,否则可能导致写进程饥饿
    int count = 0; // 记录当前有几个读进程正在访问文件
    

具体操作

writter() {
    while(1) {
        P(w);
        P(rw);
        写文件;
        V(rw);
        V(w);
    }
}
reader() {
    while(1) {
        P(w);
        P(mutex);
        if (count == 0) { // 此时,该读进程是第一个要访问临界区的读进程
            P(rw); // 给文件“加锁”,阻塞写进程
        }
        count++;
        V(mutex);
        V(w);
        
        读文件;
        
        P(mutex);
        count--;
        if(count == 0) { // 此时,该读进程是最后一个访问临界区的读进程
            V(rw); // 为文件解锁
        }
        V(mutex);
    }
}

设置mutex的目的:在第一个读进程P(rw)前切换到第二个进程,则进程二的检查也能通过,但是P(rw)被阻塞导致进程二被阻塞。此时进程一和进程二无法同时访问文件

第十节 哲学家就餐问题

问题描述

圆桌上五位哲学家

思考 || 进餐

桌上5支筷子,需要拿起左右两侧的筷子才能进餐

问题分析

  1. 元素分析

    五个哲学家

    每人两个临界资源

  2. 几组关系:每个哲学家和对应的两个资源

  3. 信号量设置

    semaphore chopstick[5] = {1, 1, 1, 1, 1};
    semaphore mutex = 1; // 互斥的取筷子,一个时刻只能由一个哲学家取筷子
    

    对哲学家编号为0~4,哲学家i左侧的筷子编号为 i i i ,右侧的编号为 ( i + 1 ) % 5 (i + 1) \% 5 (i+1)%5

应当保证哲学家拿筷子的动作是互斥的

具体实现

pi() { // i号哲学家进程
    while(1) {
        P(mutex);
        P(chopstick[i]); // 拿左
        P(chopstick[(i + 1) % 5]); // 拿右
        V(mutex);
        
        吃饭;
        
        P(chopstick[i]);
        P(chopstick[(i + 1) % 5]);
    }
}

其他解决方案

  1. 最多4名哲学家吃饭

…………

第十一节 管程

为什么引入管程

通过信号量机制编写程序困难、易出错

管程是一种高级的同步机制

管程的定义和基本特征

管程的本质是用class或者structPV操作进行封装

定义

  1. 对应共享数据结构的说明(如缓冲区)
  2. 对该数据结构进行操作的一组过程(function)
  3. 对于共享数据设置初始值的语句(初始化数据结构)
  4. 管程的名字

特征

  1. 管程中的数据结构只能由管程中的函数修改
  2. 每次仅允许一个进程使用管程中的函数

拓展

用管程解决生产者-消费者问题

Java中类似管程的机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值