操做系统知识回顾(3)--进程的同步与通讯

OS 中引入进程后,系统中的多道程序能够并发执行,但系统却变得更加复杂,为使进程有序运行,引入了同步机制。在进程之间传送大量数据,也须要利用进程通讯工具。这篇文章总结了进程的几种同步方式和进程之间的通讯方式。数组

1. 进程间同步

1.1 基本概念

为避免竞争条件,操做系统须要利用同步机制在并发执行时,保证对临界区的互斥访问。进程同步的解决方案主要有:信号量和管程。bash

对于同步机制,须要遵循如下四个规则:数据结构

  • 空闲则入:没有进程在临界区时,任何进程能够进入;
  • 忙则等待:有进程在临界区时,其余进程均不能进入临界区;
  • 有限等待:等待进入临界区的进程不能无限期等待;
  • 让权等待(可选):不能进入临界区的进程,应该释放 CPU,如转换到阻塞态;

1.2 信号量

信号量机制(semaphore)是一种协调共享资源访问的方法。信号量由一个变量 semaphore 和两个原子操做组成,信号量只能经过 PV 操做来完成,并且 PV 操做都是原子操做。并发

将信号量表示以下:工具

typedef struct {
    int value;
    struct process_control_block *list;
} semaphore;
复制代码

相应的 P(wait) 操做和 V(signal) 操做以下实现:ui

wait(semaphore *S) {
    S->value--;
    if(S->value < 0) {
        block(S->list);
    }
}
signal(semaphore *S) {
    S->value++;
    if(S->value <= 0) {
        wakeup(S->list);
    }
}
复制代码

信号量可分为两类:互斥信号量,信号量大小为为 01,用来实现进程的互斥访问;资源信号量,信号量大小为资源数,用来表示系统资源数目。spa

资源信号量操作系统

表明资源信号量时,S->value 初值表示系统资源的数目,P 操做意味着进程请求一个资源,因而系统中可分配的资源数减一,若是 S->value < 0,表示该类资源已分配完毕,所以阻塞该进程,并插入信号量链表 S->list 中。小于 0 时,S->value 的绝对值表示该信号量链表中阻塞的进程数。线程

V 操做表示进程释放一个资源,因而系统中可分配的资源数加一,若是增长一后仍然 S->value <= 0,表示该信号量链表中仍然有阻塞的进程,所以调用 wakeup,将 S->list 中的第一个进程唤醒。code

互斥信号量

表明互斥信号量时,S->value 初值为 1,表示只容许一个进程访问该资源。

利用信号量实现两个进程互斥描述以下:

semaphore mutex = 1;
P() {
    wait(mutex);
    临界区;
    signal(mutex);
}
复制代码

mutex = 1 时,表示两个进程都没有进入临界区,当 mutex = 0 时,表示一个进程进入临界区运行;当 mutex = -1 时,表示一个进程进入临界区运行,另外一个进程被阻塞在信号量队列中。

1.3 管程

管程采用面向对象思想,将表示共享资源的数据结构及相关的操做,包括同步机制,都集中并封装到一块儿。全部进程都只能经过管程间接访问临界资源,而管程只容许一个进程进入并执行操做,从而实现进程互斥。

Monitor monitor_name {
    share variable declarations;
    condition declarations;
    
    public:
    void P1(···) {
        ···
    }
    
    {
        initialization code;
    }
}
复制代码

管程中设置了多个条件变量,表示多个进程被阻塞或挂起的条件,条件变量的形式为 condition x, y;,它也是一种抽象数据类型,每一个变量保存了一条链表,记录因该条件而阻塞的进程,与条件变量相关的两个操做:condition.cwaitcondition.csignal

  • condition.cwait:正在调用管程的进程因 condition 条件须要被阻塞,则调用 condition.cwait 将本身插入到 condition 的等待队列中,并释放管程。此时其余进程能够使用该管程。
  • condition.csignal:正在调用管程的进程发现 condition 条件发生变化,则调用 condition.csignal 唤醒一个因 condition 条件而阻塞的进程。若是没有阻塞的进程,则不产生任何结果。

2. 经典同步问题

2.1 生产者-消费者问题

生产者-消费者问题描述的是:生产者和消费者两个线程共享一个公共的固定大小的缓冲区,生产者在生成产品后将产品放入缓冲区;而消费者从缓冲区取出产品进行处理。

它须要保证如下三个问题:

  • 在任什么时候刻只能有一个生产者或消费者访问缓冲区(互斥访问);
  • 当缓冲区已满时,生产者不能再放入数据,必须等待消费者取出一个数据(条件同步);
  • 而当缓冲区为空时,消费者不能读数据,必须等待生产者放入一个数据(条件同步)。

利用信号量解决

用信号量解决生产者-消费者问题,使用了三个信号量:

  • 互斥信号量 mutex:用来保证生产者和消费者对缓冲区的互斥访问;
  • 资源信号量 full:记录已填充的缓冲槽数目;
  • 资源信号量 empty:记录空的缓冲槽数目。
#define N 10
int in = 0, out = 0;
item buffer[N];
semaphere mutex = 1, full = 0, empty = N;

void producer(void) {
    while(TRUE) {
        item nextp = produce_item();
        wait(empty);          
        wait(mutex);                 
        buffer[in] = nextp;
        in = (in + 1) % N;
        signal(mutex);              
        signal(full);
    }
}

void consumer(void) {
    while(TRUE) {
        wait(full);
        wait(mutex);
        item nextc = buffer[out];
        out = (out + 1) % N;
        signal(mutex);
        signal(empty);
        consume_item(nextc);
    }
}
复制代码

须要注意的是进程中的多个 wait 操做顺序不能颠倒,不然可能形成死锁。例如在生产者中,当系统中没有空的缓冲槽时,生产者进程的 wait(mutex) 获取了缓冲区的访问权,但 wait(empty) 会阻塞,这样消费者也没法执行。

利用管程解决

利用管程解决时,须要为它们创建一个管程,其中 count 表示缓冲区中已有的产品数目,条件变量 fullemptycwaitcsignal 两个操做,另外还包括两个过程:

  • put(x):生产者将本身生产的产品放入到缓冲区中,而若是 count >= N,表示缓冲区已满,生产者须要等待;
  • get(x):消费者从缓冲区中取出一个产品,若是 count <= 0,表示缓冲区为空,消费者应该等待;
Monitor producerconsumer {
    item buffer[N];
    int in, out;
    condition full, emtpy;
    int count;
    
    public:
    void put(item x) {
        if(count >= N) { 
            cwait(full);
        }
        buffer[in] = x;
        in = (in + 1) % N;
        count++;
        csignal(emtpy);
    }
    item get() {
        if(count <= 0) {
            cwait(emtpy);
        }
        x = buffer[out];
        out = (out + 1) % N;
        count--;
        csignal(full);
    }
    
    { in = 0; out = 0; count = 0; }
}
复制代码

因而生产者和消费者可描述为:

void producer() {
    while(TRUE) {
        item nextp = produce_item();
        producerconsumer.put(nextp);
    }
}
void consumer() {
    while(TRUE) {
        item nextc = producerconsumer.get();
        consume_item(nextc);
    }
}
复制代码

2.2 哲学家就餐问题

哲学家就餐问题描述的是:有五个哲学家共用一个圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们交替地进行思考和进餐。哲学家在平时进行思考,在饥饿时试图获取左右两只筷子,拿到两只筷子才能进餐,进餐完后放下筷子继续思考。

为实现筷子的互斥使用,能够用一个信号量表示一只筷子,五个信号量构成信号量数组,也都被初始化为 1

semaphore chopstick[5] = {1, 1, 1, 1, 1};
复制代码

i 位哲学家的活动可描述为:

void philosopher(int i) {
    while(TRUE) {
        wait(chopstick[i]);
        wait(chopstick[(i + 1) % 5]);
        // eat
        signal(chopstick[i]);
        signal(chopstick[(i + 1) % 5]);
        // think
    }
}
复制代码

上述解法中,若是五位哲学家同时饥饿而都拿起左边的筷子,再试图去拿右边的筷子时,会出现无限期等待而引发死锁。

2.3 读者-写者问题

读者-写者问题描绘的是:一个文件能够被多个进程共享,容许多个 Reader 进程同时读这个文件,但不容许 Wirter 进程和其余 Reader 进程或 Writer 进程同时访问这个文件。因此读者-写者须要保证一个 Writer 进程必须与其余进程互斥地访问共享对象。

解决这个问题须要设置两个互斥信号量和一个整形变量:

  • 互斥信号量 wmutext:实现 Reader 进程和 Writer 进程在读或写时的互斥;
  • 整形变量 readcount:正在读的进程数目;
  • 互斥信号量 rmutext:实现多个 Reader 进程对 readcount 变量的互斥访问;
semaphore rmutex = 1, wmutex = 1;
int readcount = 0;

void Reader() {
    while(TRUE) {
        wait(rmutex);
        if(readcount == 0) {
            wait(wmutex);
        }
        readcount++;
        signal(rmutex);
        // perform read opertaion
        wait(rmutex);
        readcount--;
        if(readcount == 0) {
            signal(wmutex);
        }
        signal(rmutex);
    }
}
void Writer() {
    while(TRUE) {
        wait(wmutex);
        // perform wirte opertaion
        signal(wmutex);
    }    
}
复制代码

只要有一个 Reader 进程在读,便不容许 Writer 进程去写。因此,仅当 readcount = 0,表示没有 Reader 进程在读时,Reader 进程才须要执行 wait(wmutex) 操做,而 readcount != 0 时,表示有其余 Reader 进程在读,也就确定没有 Writer 在写。同理,仅当 readcount = 0 时,才执行 signal(wmutex) 相似。

3. 进程通讯

进程通讯是指进程之间的信息交换。在进程间要传送大量数据时,应利用高级通讯方法。

3.1 共享内存

在共享内存系统中,多个通讯的进程共享某些数据结构或存储区,进程之间可以经过这些空间进行通讯。

可分为两种类型:

  • 基于共享数据结构的通讯方式。多个进程共用某些数据结构,实现进程之间的信息交换,例如生产者-消费者问题中的缓冲区。这种方式仅适用于少许的数据,通讯效率低下。
  • 基于共享存储区的通讯方式。在内存中分配一块共享存储区,多个进程可经过对该共享区域的读或写交换信息。通讯的进程在通讯前,须要先向系统申请共享存储区的一个分区,以便对其中的数据进行读写。

3.2 管道

管道(Pipe)是指用于链接一个读进程和一个写进程以实现进程间通讯的一个共享文件。发送进程以字符形式将数据送入管道,而接收进程则从管道中接收数据。

管道机制提供了三方面的协调能力:

  • 互斥:当一个进程对管道执行读或写操做时,其余进程必须等待;
  • 同步:当写进程把必定数量的数据写入管道,便睡眠等待,直到读进程取走数据后再把它唤醒;
  • 肯定对方是否存在,只有肯定对方存在才能通讯。

3.3 消息传递

消息传递机制中,进程以格式化的消息为单位,将通讯的数据封装在消息中,并利用操做系统提供的原语,在进程之间进行消息传递,完成进程间数据交换。

按照实现方式,可分为两类:

  • 直接通讯方式:发送进程利用操做系统提供的发送原语,直接把消息发送给进程,接收进程则利用接收原语来接收消息;
  • 间接通讯方式:发送和接收进程,经过共享中间实体方式进行消息的发送和接收,完成进程间的通讯。
相关文章
相关标签/搜索