操作系统笔记——进程同步、进程通信

文章目录

  • 进程同步
    • 临界资源
    • 临界区
    • 同步和互斥
    • 信号量
    • 互斥量
  • 经典进程同步问题
    • 生产者消费者问题
    • 读者写者问题
  • 进程通信
    • 匿名管道通信
    • 命名管道通信(FIFO)
    • 信号
    • 信号量
    • 共享存储
    • 套接字
    • 消息队列

进程同步

临界资源

一次仅允许一个进程使用的资源称为临界资源。

临界区

对临界资源进行访问的那段代码称为临界区。

同步和互斥

同步: 多个进程因为合作产生的直接制约关系,使得进程有一定的先后执行关系。
互斥: 多个进程在同一时刻只有一个进程能进入临界区。

信号量

进程之间经常会存在互斥和同步两种关系.为了有效的处理这两种情况,提出了信号量 (semaphore)和PV操作。

信号量是一个整形变量,可以对其执行PV操作。

  • P操作:也称为down()操作/wait()操作,使S=S-1,若S<0,进程暂停执行,放入信号量等待队列
  • V操作:也称up()/signal()操作,使S=S+1,若S<=0,唤醒等待队列中的一个进程。

PV操作被设计成原语,不可分割。

互斥量

如果信号量的取值只能为0或1,那么就成为了互斥量。

  • 0表示临界区加锁
  • 1表示临界区解锁

经典进程同步问题

生产者消费者问题

问题描述

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

解释

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

为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以用信号量来统计,需要两个信号量:empty记录空缓冲区的数量,full记录满缓冲区的数量。

empty信号量是在生产者进程中使用,当empty不为零时,生产者才能放入物品;

full信号量是在消费者进程中使用的,方full信号量不为0时,消费者才可以取走物品。

注意

不能对缓冲区先进行加锁再测试信号量。即不能先P(mutex)再P(empty)。否则可能会出现如下情况。

生产者对缓冲区加锁,执行P(mutex)在执行P(empty)操作,发现empty=0,此时生产者睡眠。

消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行V(empty)操作,empty永远都是0,导致生产者永远等待,不释放锁,消费者也因此永远等待。

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

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

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

读者写者问题

描述

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

解决

  • 一个整形变量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);
    }
}

进程通信

  • 进程同步:控制多个进程按一定的顺序执行
  • 进程通信:进程间传输信息

匿名管道通信

管道是通过调用 pipe 函数创建的,pipefd[0] 用于读,pipefd[1] 用于写。

#include 
int pipe(int pipefd[2]);

调用pipe函数,会在内核中开辟出一块缓冲区用来进行进程间通信,这块缓冲区称为管道,它有一个读端和一个写端。

可以通过read(pipefd [0]);或者write(pipefd [1]) 操作管道

通信步骤

  1. 父进程创建管道,pipefd[]指向管道的两端
    操作系统笔记——进程同步、进程通信_第1张图片
  2. 利用fork函数创建出子进程,子进程的pipefd[]指向同一管道
    操作系统笔记——进程同步、进程通信_第2张图片
  3. 父进程关闭读端(pipe[0]),子进程关闭写端pipe[1],则此时父进程可以往管道中进行写操作,子进程可以从管道中读,从而实现了通过管道的进程间通信。

特点

  • 只能在父子进程或者兄弟进程中使用
  • 是一种半双工通信,单向交替传输

命名管道通信(FIFO)

操作系统笔记——进程同步、进程通信_第3张图片
匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了命名管道(FIFO)。

命名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以命名管道的文件形式存在于文件系统中,这样,即使与命名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过命名管道相互通信,因此,通过命名管道不相关的进程也能交换数据。

命名管道遵循先进先出原则,对匿名管道及命名管道的读总是从开始处返回数据,对它们的写则把数据添加到末尾。命名管道的名字存在于文件系统中,内容存放在内存中。

信号

信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。

信号量

信号量本质上是一个计数器,用于多进程对共享数据对象的读取,它和管道有所不同,它不以传送数据为主要目的,它主要是用来保护共享资源,使得资源在一个时刻只有一个进程独享。

共享存储

  • 允许多个进程共享一个给定的存储区。是最快的一种 IPC,因为数据不需要在进程之间复制。
  • 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。
  • 由于多个进程共享一段内存,因此需要依靠如信号量来达到进程间的同步及互斥。

套接字

借助套接字,要进行通信的进程,可以在本地单机上进行,也可以跨网络进行。
它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。

消息队列

  • 消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。
  • 消息队列允许一个或多个进程向它写入与读取消息。
  • 消息队列的通信数据都是先进先出的原则。
  • 读进程可以根据消息类型有选择地接收消息。

你可能感兴趣的:(计算机理论与基础,进程)