欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【操作系统】-- PV原语(哲学家进餐问题)

程序员文章站 2022-07-04 23:44:14
...
  • 微信搜索:编程笔记本
  • 微信搜索:编程笔记本
  • 微信搜索:编程笔记本

点击上方蓝字关注我,我们一起学编程
欢迎小伙伴们分享、转载、私信、赞赏

小伙伴儿们看完以后可不可以帮我点亮一下在看呀~

信号量与进程同步、互斥

1 进程同步

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

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

2 进程互斥

我们把一个时间段内只允许一个进程使用的资源称为临界资源。许多物理设备(摄像头、打印机等)都属于临界资源。对临界资源的访问,必须互斥地进行。

互斥亦称间接制约关系。**进程互斥是指当一个进程访问某临界资源时,另一个想要访问该临界资源地进程必须等待。**当前访问临界资源的进程访问结束,释放该资源之后,另一个进程才能去访问临界资源。

3 信号量机制

用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便地实现进程互斥与进程同步。

原语:是一种特殊的程序段,其执行只能一气呵成,不可被中断。原语是由关中断/开中断指令实现的。
信号量: 其实就是一个变量(可以是一个整型变量,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量。

上面提到的操作系统提供的一对原语,是指:wait(S)原语和signal(S)原语。这一对原语常被分别称之为 P、V 操作wait(S)signal(S)两个操作分别写为 P(s)V(S)

3.1 整型信号量

用一个整型变量作为信号量,用来表示系统中某种资源的数量。

下面用一个例子来说明:某计算机系统中有一台打印机。

int S = 1;             // 初始化整型信号量S,表示当前系统中可用的打印机资源数

void wait(int S)       // wait原语,相当于“进入区”
{
    while (S <= 0);    // 如果资源数不够,就一直循环等待
    --S;               // 如果资源数够,就占用一个资源
}

void signal (int S)    // signal原语,相当于“退出区”
{
    ++S;               // 使用资源后,在退出区释放资源
}

/* 进程P0 */
...
wait(S);               // 进入区,申请资源
使用打印机资源          // 临界区,使用资源
signal(S);             // 退出区,释放资源
...

/* 进程P1 */
...
wait(S);               // 进入区,申请资源
使用打印机资源          // 临界区,使用资源
signal(S);             // 退出区,释放资源
...
  • 微信搜索:编程笔记本
  • 微信搜索:编程笔记本
  • 微信搜索:编程笔记本

3.2 记录型信号量

上面介绍的整型信号量有一个缺陷是:当资源不可用时,执行原地盲等。为了弥补这个不足,人们又提出“记录型信号量”,即用记录型数据结构表示的信号量

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

/* 某进程需要使用资源时,通过wait原语申请 */
void wait(semaphore S)
{
    --S.value;            // 拟占用资源
    if (S.value < 0) {    // 若资源不可用则阻塞等待资源
        block(S.L);
    }
}

/* 进程使用完资源后,通过signal原语释放 */
void signal(semaphore S)
{
    ++S.value;             // 释放资源
    if (S.value <= 0) {    // 若有进程正等待该类资源则将其唤醒
        wakeup(S.L);
    }
}

wait 中,若执行 --S.value 后其值小于 0 了,说明之前就已经是小于等于 0 了,也就是说没有可用的资源了,那么就使用 block 原语使进程从运行态进入阻塞态,并将其挂到信号量 S 的等待队列当中。

signal 中,若执行 ++S.value 后其值仍小于 0 ,说明仍然有进程在等待这种资源,那么就使用 wakeup 原语唤醒一个进程,通知其使用资源。

下面来看一个例子:某计算机系统中有两台打印机。

/* 初始化 */
typedef struct {
    int value;            // 2
    struct process *L;    // nullptr
} semaphore;

/* 进程P0
 * 剩余资源数:1
 * 等待队列:空
 */
...
wait(S);
使用打印机资源
signal(S);
...

/* 进程P1
 * 剩余资源数:0
 * 等待队列:空
 */
...
wait(S);
使用打印机资源
signal(S);
...

/* 进程P2
 * 剩余资源数:-1
 * 等待队列:P2
 */
...
wait(S);
使用打印机资源
signal(S);
...

在原语的表达中,wait(S) 可以记为 P(S) ,用于资源的申请; signal(S) 可以记为 V(S) ,用于资源的释放。

  • S.value 的初值表示系统中某种资源的数目。

  • 对信号量 S 的一次 P 操作意味着进程请求一个单位的该类资源,因此需要执行 --S.value ,表示资源数减少 1 个单位。当 S.value < 0 时表明该类资源早已分配完毕,因此进程调用 block 原语进行自我阻塞(放弃了处理机的使用权,这点不同于整型信号量)。

  • 对信号量 S 的一次 V 操作意味着进程释放一个单位的该类资源,因此需要执行 ++S.value ,表示资源数增加 1 个单位。若 S.value ≤ 0 表明依然有进程在等待该类资源,因此调用 wakeup 原语唤醒队列中的第一个进程。

  • 微信搜索:编程笔记本

  • 微信搜索:编程笔记本

  • 微信搜索:编程笔记本

4 用信号量实现进程互斥、同步

4.1 用信号量实现进程互斥

步骤:

  • (1) 分析并发进程的关键活动,划定临界区
  • (2) 设置互斥信号量 mutex ,初值为 1

我们将临界区视作为一种特殊的资源,并且只有 1 个这样的资源。只有其他进程退出(释放资源)临界区,另一个进程才能进入临界区。

  • 在临界区之前执行 P(mutex)
  • 在临界区之后执行 v(mutex)

上述步骤,在临界区之前执行 P(mutex) ,可以申请临界区资源,如果有这样的资源则占用,若没有则等待。当一个进程退出临界区时,先执行 V(mutex) 操作释放临界区资源,再判断是否有进程正在等待进入临界区,有的话则唤醒该进程。

4.2 用信号量实现进程同步

进程同步,即要求各并发执行的进程按要求有序地推进。

步骤:

  • 分析何处需要实现“同步关系”,即保证“一前一后”执行的两个操作
  • 设置同步信号量 S ,初始值为 0
  • 在“前操作”之后执行 V(S)
  • 在“后操作”之前执行 P(S)

为何要将信号量初始化为 0 呢?
因为,我们默认系统中没有这样的资源。那么我们在“后操作”之前执行 P 操作申请这样的资源就会失败,所以“后操作”不能被执行。只有在“前操作”完成之后,我们执行 V 操作,这样会导致我们“释放一个”该类资源(其实是凭空产生)。此时“后操作”等待的资源已具备,因而“后操作”得以顺利执行。因而实现了两个操作的前后顺序控制。

5 哲学家进餐问题

问题描述:

一张圆桌上坐着 5 名哲学家,每两个哲学家之间有一根筷子,桌子的中间是一份餐食。哲学家们倾注毕生的精力用于思考和进餐,哲学家在思考时,并不影响其他人。只有哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)进餐。如果要拿的筷子已在他人手上,则需等待。用餐完毕后,将筷子放回原处。

  • 关系分析
    系统中有 5 个哲学家进程,5 位哲学家与左右邻居对其中间筷子的访问是互斥关系
  • 整理思路
    每个哲学家需要同时持有两个互斥资源才能开始用餐。如何避免临界资源分配不当造成的死锁现象,是哲学家进餐问题的精髓。
  • 信号量设置
    定义互斥信号量数组 chopstick[5] = {1,1,1,1,1} 用于实现对 5 根筷子的互斥访问。

哲学家 i 左边的筷子编号为 i ,右边的筷子编号为 (i+1)%5 。

semaphore chopstick[5] = {1, 1, 1, 1, 1};
Pi() {                                // 哲学家i的进程
    while (1) {
        P(chopstick[i]);              // 拿左
        P(chopstick[(i + 1) % 5]);    // 拿右
        吃饭
        V(chopstick[i]);              // 放左
        V(chopstick[(i + 1) % 5]);    // 放右
        思考
    }
}

若 5 个哲学家并发地拿起了自己左手边地筷子,又在循环等待右手边地筷子,这就导致了死锁。

为了解决这个问题,我们可以做出一些限制:在同一时间,只允许一位哲学家尝试拿起筷子进餐。为此,我们需要设置一个初始值为 1 的互斥信号量。

semaphore chopstick[5] = {1, 1, 1, 1, 1};
semaphore mutex = 1;
Pi() {                                // 哲学家i的进程
    while (1) {
        P(mutex);                     // 互斥地取筷子
        P(chopstick[i]);              // 拿左
        P(chopstick[(i + 1) % 5]);    // 拿右
        V(mutex);                     // 已经取好筷子
        吃饭
        V(chopstick[i]);              // 放左
        V(chopstick[(i + 1) % 5]);    // 放右
        思考
    }
}
  • 微信搜索:编程笔记本
  • 微信搜索:编程笔记本
  • 微信搜索:编程笔记本