几种常见进程间通信(IPC)方式-共享存储

前言

进程间通信是指在不同进程之间传播或交换信息,在Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间,进程之间不能相互访问。必须通过内核才能进行数据交换。如图:
在这里插入图片描述
常见的通信方式有以下几种:

  • 管道pipe
  • 有名管道FIFO
  • 消息队列MessageQueue
  • 共享存储
  • 信号量Semaphore
  • 信号Signal
  • 套接字Socket

接下来我们将详细介绍共享存储

共享存储

内存映射I/O

在讲解内存映射之前,我们先简单了解一些虚拟内存的概念。

虚拟内存为每个进程提供了一个大的、一致的和私有的地址空间,它提供了3个能力:

  • 将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据。
  • 它为每个进程提供了一致的地址空间,从未简化了内存管理。
  • 它保护了每个进程的地址空间不被其他进程破坏。

VM系统将虚拟内存分割为虚拟页,物理内存也被分隔为物理页。
使用寄存器中的内存管理单元[MMU(Memory Management Unit)],利用存放在主存中的页表来动态翻译虚拟地址,就可以使用虚拟地址去访问相应的物理地址。

内存映射
内存映射,通过将虚拟内存区域与磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容。
磁盘上的对象可以是两类,Linux文件系统中的普通文件,或是匿名文件(由内核创建)。
在这里插入图片描述

共享内存映射
将虚拟地址指向同一个物理地址。如图所示
在这里插入图片描述

mmap函数

Linux进程可以使用mmap函数来创建新的虚拟内存区域,并将对象映射到这些区域中。

void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset); 
//成功:返回创建的映射区首地址;失败:MAP_FAILED宏

参数

  • addr: 建立映射区的首地址,由Linux内核指定。使用时,直接传递NULL
  • length: 欲创建映射区的大小
  • prot: 映射区权限PROT_READPROT_WRITEPROT_READ|PROT_WRITE
  • flags:标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
    MAP_SHARED: 会将映射区所做的操作反映到物理设备(磁盘)上。
    MAP_PRIVATE: 映射区所做的修改不会反映到物理设备。
  • fd: 用来建立映射区的文件描述符
  • offset: 映射文件的偏移(4k的整数倍)

mmap建立的映射区在使用结束后也应调用类似free的函数来释放。

int munmap(void *addr, size_t length);	
//成功:返回0; 失败:返回-1
mmap进程间通信
  • 父子等有血缘关系的进程之间可以通过 mmap建立的映射区来完成数据通信。但是相应的要在创建映射区的时候指定对应的标志位参参数flags。
    MAP_PRIVATE(私有映射)父子进程各自独占映射区
    MAP_SHARED(共享映射)父子进程共享映射区
  • 也可以通过匿名映射,不需要以来一个文件就能够实现。同样也需要依赖于标志位的设定(只适用于有血缘关系的进程之间通信)
    使用MAP_ANONYMOUS (或MAP_ANON), 如:
    int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
  • 没有血缘关系的进程之间通信时需要,用open打开同一个文件 得到fd,再调mmap指定fd,将虚拟内存地映射到同一个物理内存地址上。

代码样例 父子进程间通信

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>

int var = 100;

int main(void)
{
    int *p;
    pid_t pid;

    int fd;
    fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
    if(fd < 0){
        perror("open error");
        exit(1);
    }
    unlink("temp");                             //删除临时文件目录项,使之具备被释放条件.
    //将参数fd指定的文件大小改为参数length指定的大小
    ftruncate(fd, 4);

    //MAP_SHARED
    //父子进程各自独占映射区
    p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    //MAP_PRIVATE
    //父子进程共享映射区
    //p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
    if(p == MAP_FAILED){                //注意:不是p == NULL
        perror("mmap error");
        exit(1);
    }
    close(fd);                                  //映射区建立完毕,即可关闭文件

    pid = fork();                               //创建子进程
    if(pid == 0){
        *p = 2000;
       //非共享变量
       //在子进程中改为1000,父进程中仍为100
        var = 1000;
        printf("child, *p = %d, var = %d\n", *p, var);
    } else {
        sleep(1);
        printf("parent, *p = %d, var = %d\n", *p, var);
        wait(NULL);

        int ret = munmap(p, 4);                         //释放映射区
        if (ret == -1) {
            perror("munmap error");
            exit(1);
        }
    }
    return 0;
}
文件进程间通信

使用文件也可以完成进程间通信,父进程使用fork创建子进程后,父子进程共享文件描述符,也就是说,共享打开的文件。

/*
 *父子进程共享打开的文件描述符------使用文件完成进程间通信.
 */
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/wait.h>


int main(void)
{
        int fd1, fd2; pid_t pid;
        char buf[1024];
        char *str = "---------test for shared fd in parent child process-----\n";


        pid = fork();
        if (pid < 0) {
                perror("fork error");
                exit(1);
         //子进程进入
        } else if (pid == 0) {
                fd1 = open("test.txt", O_RDWR);
                if (fd1 < 0) {
                        perror("open error");
                        exit(1);
                }
                write(fd1, str, strlen(str));
                printf("child wrote over...\n");

        //父进程进入
        } else {
                fd2 = open("test.txt", O_RDWR);
                if (fd2 < 0) {
                        perror("open error");
                        exit(1);
                }
                sleep(1);                   //保证子进程写入数据

                int len = read(fd2, buf, sizeof(buf));
                write(STDOUT_FILENO, buf, len);

                wait(NULL);
        }

        return 0;
}

看到这里你可能会有一些疑惑,文件进程间通信和有名管道FIFO有什么区别呢?

我们知道管道只有两端,读端和写端,有名管道可以控制读写两端,保证他们的同步性。
但是对于文件进程间进程通信而言,我们必须自己定义一些同步机制来控制读数据和写数据,否则可能会造成一些错误。

总结

共享存储是非常重要的一种进程间通信方式,理解起来也相对来说比较困难有点,但是在了解了一些基本概念之后再去学习就会容易得多。

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐