操作系统

进程并发编程基础

2019-06-24  本文已影响15人  madao756

前言:看了操作系统并发编程的基础,做个笔记并用 C 实现常见的一种并发编程的模型——消费者、生产者模型

进程之间的关系

第一种是竞争关系

系统中的多个进程之间彼此无关,它们并不知道其他进程的存在,并且也不受其他进程执行的影响。因而, 必然要出现多个进程竞争资源的问题。当多个进程竞争共享硬设备、存储器、处理器 和文件等资源时,操作系统必须协调好进程对资源的争用

进程的互斥(mutual exclusion )是解决进程间竞争关系(间接制约关系)的手段。 进程互斥指若干个进程要使用同一共享资源时,任何时刻最多允许一个进程去使用,其他要使用该资源的进程必须等待,直到占有资源的进程释放该资源。

第二种是协作关系

某些进程为完成同一任务需要分工协作,由于合作的每一个进程都是独立地以不可预知的速度推进,这就需要相互协作的进程在某些协调点上协 调各自的工作。

进程的同步(Synchronization)是解决进程间协作关系(直接制约关系)的手段。进程同步指两个以上进程基于某个条件来协调它们的活动。一个进程的执行依赖于另一个协作进程的消息或信号,当一个进程没有得到来自于另一个进程的消息或信号时则需等待,直到消息或信号到达才被唤醒。

不难看出,进程互斥关系是一种特殊的进程同步关系,即逐次使用互斥共享资源,也是对进程使用资源次序上的一种协调

如何实现互斥

无论是竞争还是协作,都必须实现互斥。

而实现互斥的关键在于:进程 A 正在使用的临界资源,哪怕在使用的过程中被中断了,其他进程的临界区都不能动进程 A 的资源。

硬件方式实现互斥

用硬件的方式增加一条汇编指令,比如 TestAndSet

伪代码如下

// 主程序
global lock = false

    

repeate {nothing} until TestAndSet(&lock)
// 临界区代码
// do someting
// 临界区结束
lock = false

这里的 TestAndSet 使用汇编指令实现的,也就是他不能被中断。lock 是全局变量,所有的进程都能访问到

实现的功能就是:检查 lock 是否是 false,如果是 false 就把 lock 设为 true。如果 lock 是 false。就不会执行repeate {nothing}

有的进程执行的时候,lock 是 true。就会一直执行 repeate {nothing},直到 CPU 时间片结束,结束调用。这样就实现了互斥。

但是,这样有一个很大的缺点就是忙等,我们不想要忙等,我们想要 lock = true 的时候进程被阻塞,然后等待调用

用信号量(semaphore)实现互斥 重点!

semaphore 最重要的是两个函数:

这是系统给的原语。原语和其他函数最大的区别就是:它虽然也是一系列指令构成,但是在执行的过程中不可中断

wait 和 signal 的伪代码如下:

wait(semaphore* s) {
    if(s.count-- < 0) {
        // 阻塞
    }
}

signal(semaphore* s) {
    if(s.count++ <= 0) {
        // 调用某个被阻塞的进程
        call();
    }
}

那怎么用这两个函数实现互斥呢?我们在访问临界资源的时候会先执行 wait 函数如果 (s.count-- < 0)。则被阻塞,而不是忙等。否则就执行临界区代码(资源够用,不需要阻塞)。

而执行完临界区以后,会执行 signal 函数。如果 (s.count++ <= 0) 。则去唤醒一个被阻塞的函数,否则啥也不做,仅仅加一(没有被阻塞的进程)。

通过一个实例来理解这一个过程

#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <unistd.h>


sem_t s;


int main(int argc, char const *argv[])
{
    int value = 0;

    sem_init(&s, 0, 10);

    sem_getvalue(&s, &value);
    printf("initialization s.count = %d\n", value);

    // wait
    sem_wait(&s);
    sem_getvalue(&s, &value);
    printf("after wait s.count = %d\n", value);

    // signal
    sem_post(&s);
    sem_getvalue(&s, &value);
    printf("after signal s.count = %d\n", value);

    
    sem_destroy(&s);

    return 0;
}

输出:

initialization s.count = 10
after wait s.count = 9
after signal s.count = 10

C 实例

我们通过两个经典模型,来深入理解进程并发编程

消费者、生产者模型

该问题描述了共享固定大小缓冲区的两个进程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <unistd.h>

#define SIZE 1 /*buffer size*/



sem_t empty;
sem_t full;
sem_t mutex;

int buffer[SIZE] = {0};

void producer(int idx, int value) {
    sem_wait(&full);
    sem_wait(&mutex);

    buffer[idx] = value;

    sem_post(&mutex);
    sem_post(&empty);
}

void consumer(int idx) {

    sem_wait(&empty);
    sem_wait(&mutex);

    printf("从 buffer[%d] 取出 %d \n", idx, buffer[idx]);
    buffer[idx] = -1;

    sem_post(&mutex);
    sem_post(&full);
}

int main(int argc, char const *argv[]) {
    int value;


    // 多个生产者和消费者,只有一个进程能够拥有 buffer,这是所有进程的互斥
    sem_init(&mutex, 0, 1);


    // 生产者在满的时候不能生产
    sem_init(&full, 0, SIZE);

    // 消费者不能在空的时候消费
    sem_init(&empty, 0, 0);

    // 假设有四个进程  
    for(int i = 0; i < 4; i++) {
        int p = fork();


        if(p == 0) {

            switch (i)
            {
                case 0:
                    producer(0, 10);
                    break;
                case 1:
                    producer(0, 100);
                    break;
                case 2:
                    consumer(0);
                    break;
                case 3:
                    consumer(0);
                    break;
                default :
                    // producer(0, 10);
                    break;
            }

        } else {
            printf("我是主进程:%d \n", p);
        }

        
    }

    sem_destroy(&mutex);
    sem_destroy(&empty);
    sem_destroy(&full);

    return 0;
}
上一篇下一篇

猜你喜欢

热点阅读