进程同步

首页 / 操作系统 / 正文

操作系统中许多进程的并发执行不一定完全独立,会相互依赖,从这个问题就可以引出进程同步。

信号量(Semaphore)

信号量是一个整型变量,可以对其执行P、V操作

  • P:如果信号量大于0,执行-1操作;如果信号量等于 0,进程睡眠,等待信号量大于 0
  • V:对信号量执行+1操作,唤醒睡眠的进程让其完成 down 操作

P、V操作必须是原子操作,通常执行时屏蔽中断,当信号量取值只能为0或1时,这个信号量就是互斥量(Mutex),0表示临界区加锁,1表示临界区解锁

typedef int semaphore;
semaphore mutex = 1;
void P1() {
    p(&mutex);
    // 临界区
    v(&mutex);
}

void P2() {
    p(&mutex);
    // 临界区
    v(&mutex);
}

临界区

对临界资源进行访问和修改的那段代码称为临界区,为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。

多个进程在同一时刻只有一个能进入临界区,也被称为互斥

//进入区
//临界区
//退出区

消费者和生产者问题

问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。

因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。

为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。

#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;

void producer() {
    while(TRUE) {
        int item = produce_item();
        p(&empty);
        p(&mutex);
        insert_item(item);
        v(&mutex);
        v(&full);
    }
}

void consumer() {
    while(TRUE) {
        p(&full);
        p(&mutex);
        int item = remove_item();
        consume_item(item);
        v(&mutex);
        v(&empty);
    }
}

管程

定义:将分散的临界段集中到抽象数据类型模版中

组成:局部于该管程的共享数据、局部于该管程的一组操作过程(函数)、局部于该管程的数据的初始化

特点:管程拥有很好的封装性、管程互斥使用、管程中有进程等待队列和相应等待和唤醒的操作

哲学家就餐问题

截屏2021-09-10 下午8.54.51.png

五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。

下面是一种解法

#define N 5

void philosopher(int i) {
    while(TRUE) {
        think();
        take(i);       // 拿起左边的筷子
        take((i+1)%N); // 拿起右边的筷子
        eat();
        put(i);
        put((i+1)%N);
    }
}

但是这种解法下,如果所有哲学家同时拿起左手边的筷子,那么所有哲学家都在等待其它哲学家吃完并释放自己手中的筷子,导致死锁。

为了防止死锁的发生,可以设置两个条件:

  • 必须同时拿起左右两根筷子;
  • 只有在两个邻居都没有进餐的情况下才允许进餐。
#define N 5
#define LEFT (i + N - 1) % N // 左邻居
#define RIGHT (i + 1) % N    // 右邻居
#define THINKING 0
#define HUNGRY   1
#define EATING   2
typedef int semaphore;
int state[N];                // 跟踪每个哲学家的状态
semaphore mutex = 1;         // 临界区的互斥,临界区是 state 数组,对其修改需要互斥
semaphore s[N];              // 每个哲学家一个信号量

void philosopher(int i) {
    while(TRUE) {
        think(i);
        take_two(i);
        eat(i);
        put_two(i);
    }
}

void take_two(int i) {
    down(&mutex);
    state[i] = HUNGRY;
    check(i);
    up(&mutex);
    down(&s[i]); // 只有收到通知之后才可以开始吃,否则会一直等下去
}

void put_two(i) {
    down(&mutex);
    state[i] = THINKING;
    check(LEFT); // 尝试通知左右邻居,自己吃完了,你们可以开始吃了
    check(RIGHT);
    up(&mutex);
}

void eat(int i) {
    down(&mutex);
    state[i] = EATING;
    up(&mutex);
}

// 检查两个邻居是否都没有用餐,如果是的话,就 up(&s[i]),使得 down(&s[i]) 能够得到通知并继续执行
void check(i) {         
    if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) {
        state[i] = EATING;
        up(&s[i]);
    }
}

读者写者问题

允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。

一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。

typedef int semaphore;
semaphore count_mutex = 1;
semaphore data_mutex = 1;
int count = 0;

void reader() {
    while(TRUE) {
        down(&count_mutex);
        count++;
        if(count == 1) down(&data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问
        up(&count_mutex);
        read();
        down(&count_mutex);
        count--;
        if(count == 0) up(&data_mutex);
        up(&count_mutex);
    }
}

void writer() {
    while(TRUE) {
        down(&data_mutex);
        write();
        up(&data_mutex);
    }
}    

进程通信

  • 共享存储:在通信的进程之间存在一片可直接访问的共享空间,通过对这片空间的读写操作实现它们之间的信息交换
  • 消息传递

    • 管道

      管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系或兄弟进程
      截屏2021-09-10 下午9.08.13.png

    • 命名管道(FIFO)

      有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信

    • 消息队列

      消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。

      消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限

      避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法

      消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难

      读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收

    • 信号量

      前文已有,主要作为进程间以及同一进程内不同线程之间的同步手段

    • 套接字

      与其它通信机制不同的是,它可用于不同机器间的进程通信

无标签
评论区
头像
文章目录