linux下通过共享内存和mmap实现进程间通讯

  • Post author:
  • Post category:linux


前言

最近在学习GNU/Linux内核,看到mmap的时候书上说:

mmap/munmap接口函数是用户最常用的两个系统调用接口,无论是在用户程序中分配内存、读写大文件、链接动态库文件,还是多进程间共享内存,都可以看到mmap/munmap的身影。

这句话说的很正确,虽然我们日常没有直接使用mmap,但是其实我们都间接地使用了mmap/mumap函数。

举个例子,我们使用动态链接库的时候,我们都知道,动态链接不会把动态库中的代码整合到目标文件中,相反,动态库跟目标文件独立。

那为什么运行时,程序能够获得指定的符号链接?这正是mmap的力量,程序运行时,他将保存在物理内存的动态库的内容(如果物理内存中没有,则先加载如内存)映射到自身的进程地址空间,这样符号所对应的指令数据便存在了。

(通过ldd可获取所依赖的动态库,Linux怎么获取到的,我怀疑跟ELF文件格式有关,待解答)

mmap

mmap是内存映射文件的方法

mmap将一个文件或者其它对象映射进内存。mmap在用户空间映射调用系统中作用很大。

mmap()必须以PAGE_SIZE为单位进行映射,而内存也只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行以PAGE_SIZE的倍数大小进行映射。

头文件

<sys mman.h>

函数原型

void* mmap(void* addr, size_t length,int prot,int flags,int fd,off_t offset); int munmap(void* start,size_t length);


  • addr

    : 用于指定映射到进程地址的起始地址,为了应用程序的可移植性,一般设为nullptr;

  • length

    : 表示映射到进程地址空间的大小;

  • prot

    : 表示设置内存映射区域的读写属性等;

  • flags

    : 用于设置内存映射的属性,例如共享映射、私有映射、匿名映射等;

  • fd

    : 文件句柄,如果不是文件映射,则置为0;

  • offset

    : 文件映射时的文件偏移量。

重复一遍:由于GNU/Linux中,内存分配是以页为单位的,所以length长度不足1页(默认4KB),则按1页来处理。

prot参数detail:

flags参数detail:

共享内存

共享内存Shared Memory,顾名思义就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。

具体来说,共享内存就是一段真实存在的物理内存,不同进程通过访问和修改该段物理内存,最终达到共享内存的目的。

PS: 共享内存不提供数据同步机制,如一个进程在写过程中,另一个进程可以进行读操作和写操作,所以需要通过信号量来解决同步问题。

创建共享内存

函数介绍

函数原型如下:

#include <sys mman.h>
#include <sys stat.h>        /* For mode constants */
#include <fcntl.h>           /* For O_* constants */

int shm_open(const char *name, int oflag, mode_t mode);

int shm_unlink(const char *name);

Link with -lrt.

shm_open()创建并打开一个新的或现有的POSIX共享内存对象。POSIX共享内存对象实际上是一个句柄,它可以被与mmap共享内存相同区域的无关进程使用。

shm_unlink()函数的作用是:删除先前由shm_open()创建的对象。

函数用法

int fd = shm_open("/shm01", O_CREAT | O_RDWR, 0777);

当oflag使用O_CREAT时,mode表示创建文件时的权限,权限写法跟命令chmod一样(

https://www.runoob.com/linux/linux-comm-chmod.html)。

open与shm_open区别

或许你会问:如果是创建一个文件何必使用shm_open呢?为什么不使用open?

理论上来说两个方法可以相互替换。

但是,很重要的一点是:shm_open会将文件创建在/dev/shm目录下,该目录下挂载的文件系统格式是tmpfs,该目录下的文件也只存储在内存(主存)中。如果你使用open方法在该目录下创建和打开文件其实两者就没有本质区别了。

如图所示,/dev/shm是tmpfs文件系统

进程间通讯实例

本测试用例需要依赖googletest测试框架,当然读者可以通过分别建立两个项目来实现下述实例;

实例原理

如图所示,共享内存创建后保存在真实的物理内存中,通过mmap函数我们将该物理内存地址映射到进程地址空间中,最终实现操作共享内存的功能。

实例功能

服务端:创建共享内存,并将内存映射到进程地址空间中,然后进行定时修改内存内容。

客户端:映射共享内存到进程地址空间中,定时获取内存内容。

实例代码

服务端代码

#include <gtest gtest.h>
#include <iostream>
#include <fcntl.h>
#include <sys mman.h>
TEST(shm_test, server) {
    int fd = shm_open("/shm01", O_CREAT | O_RDWR, 0777);
    // int fd = open("/dev/shm/shm01", O_CREAT | O_RDWR);
    if (fd < 0) {
        std::cout << "error to create or open" << std::endl;
        return;
    }
    std::cout << "create or open ok" << std::endl;
    ftruncate(fd, 4096);// 分配4KB内存
    char *ptr = (char *)mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    char a[] = "0\0";
    for (char k = 0; k < 26; ++k) {
        memset(a, 'a' + k, 1 *sizeof(char));
        strcpy(ptr, a);
        sleep(1);
    }
    strcpy(ptr, a);
    mumap((void *)ptr, 4096); // 关闭映射
    exit(0);
}

客户端代码

#include <gtest gtest.h>
#include <iostream>
#include <fcntl.h>
#include <sys mman.h>
TEST(test, client) {
    int fd = shm_open("/shm01", O_RDWR, 0777);
    // int fd = open("/dev/shm/shm01", O_CREAT | O_RDWR);
    if (fd < 0) {
        std::cout << "error to open" << std::endl;
        return;
    }
    char *ptr = (char *)mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    while(true) {
        std::cout << ptr << std::endl;
        sleep(1);
    }
    exit(0);
}

执行结果

开头的z是之前保存在共享内存中的数据

可以看到客户端输出了英语字母。

遇到的问题

  1. 找不到shm_open的符号链接

答:在编译时链接rt库

  1. 使用googletest时,缺失pthread

答:链接pthread库

总结

本篇文章,我们知道了共享内存,通过共享内存和mmap我们能很轻易地完成进程间通讯(IPC Inter-Process Communication),进程间的semaphore(信号量)实现方式也是通过mmap和共享变量实现的。



版权声明:本文为JavaMonsterr原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。