• Linux 下进程间通讯之内存映射详解


    引用:前面的系列文章介绍管道,本文介绍另外一种比较高效的进程间通讯方式——内存映射

    一、内存映射概述

    内存映射(Memory-mapped I/O)使得一个磁盘文件与存储空间中的一个缓冲区相映射,相当于将磁盘文件的数据映射到内存中,用户通过修改内存就能修改磁盘文件

    在这里插入图片描述

    于是当从缓冲区中取数据,就相当于读文件中的相应字节。以此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不使用 read 和 write 函数的情况下,使用地址(指针)完成 I/O 操作(通过内存操作函数完成I/O操作)。

    内存映射也是进程间通讯的一种方式,而且效率比较高,因为它相当于直接对内存进行操作。其原理是把磁盘文件中的数据映射到内存当中,映射之后返回映射地址,在程序中就可以直接操作这块内存,操作过程中会把数据同步到磁盘文件中,这样可以实现进程间通讯。

    二、内存映射 API

    mmap 函数

    #include 
    
    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
    功能:一个文件或者其它对象映射进内存中
    参数:
        addr :  指定映射的起始地址, 通常设为NULL, 由系统指定。
           	【补充】如果 addr 为 NULL,则内核会自行挑选一个页对齐的地址;
            	   如果 addr 不为 NULL ,则内核只是将其作为一个提示。
        length:映射到内存的文件长度,这个值不能为 0(即文件大小 > 0);建议直接使用文件的长度。
            【补充】获取文件长度可通过 stat()、lseek() 等函数
        prot:  映射区的保护方式(【注意】要操作映射内存,必须要有读的权限):
            a) 读:PROT_READ
            b) 写:PROT_WRITE
            c) 读写:PROT_READ | PROT_WRITE
        flags:  映射区的特性, 可以是
            a) MAP_SHARED : 写入映射区的数据会复制回文件, 即映射区的数据会自动和磁盘文件同步;
    			且允许其他映射该文件的进程共享,所以进程间通信,必须要设置这个选项。
            b) MAP_PRIVATE : 对映射区的写入操作会产生一个映射区的复制(copy - on - write),
    			对此区域所做的修改不会写回原文件,即映射区的数据会自动和磁盘文件不同步。
        fd:由 open() 返回的文件描述符, 代表要映射的文件。注意点如下:
            a) 文件的大小不能为 0;     
            b) open() 指定的权限不能和 prot 参数冲突(即映射区的权限 <= 文件打开的权限):
                    prot: PROT_READ                	open:只读/读写 
                    prot: PROT_READ | PROT_WRITE   	open:读写
        offset:以文件开始处的偏移量, 必须是4k的整数倍;
                    一般不用,所以通常为0, 表示从文件头开始映射(4k是页大小)
    返回值:
        成功:返回创建的映射区首地址
        失败:MAP_FAILED宏
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    munmap 函数

    #include 
    int munmap(void *addr, size_t length);
    功能:释放内存映射区
    参数:
        addr:使用 mmap 函数创建的映射区的首地址
        length:映射区的大小,即要释放的内存的大小,要和mmap函数中的length参数的值一样。
    返回值:
        成功:0
        失败:-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    API 使用注意事项

    1. 创建映射区的过程中,隐含着一次对映射文件的读操作。

    2. 当 MAP_SHARED 时,要求:映射区的权限 <= 文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为 mmap 中的权限是对内存的限制。

    3. 映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。

    4. 特别注意:

      当映射文件大小为0时,不能创建映射区。所以用于映射的文件必须要有实际大小;

      mmap 函数使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。

    5. munmap 函数传入的地址一定是 mmap 的返回地址;所以对于mmap 函数的返回值,建议不要对该指针进行 ++ 操作。如果确实需要这样做,需要保存 ++ 前的地址,这样在释放空间的时候,传入 ++ 前的地址才是正确释放空间。

    6. 文件偏移量必须为 4K 的整数倍,如果不是 4k 的整数倍,则函数调用出错,返回MAP_FAILED。

    7. mmap 函数创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

    三、内存映射使用场景

    内存映射实现进程间通信

    (1)有关系的进程间通信

    内存映射实现父子进程间通信

    1. 准备一个大小不是 0 的磁盘文件
    2. 还没有子进程的时候,通过唯一的父进程,先创建内存映射区
    3. 有了内存映射区以后,创建子进程
    4. 父子进程共享创建的内存映射区
    5. 【注意】内存映射区通信,是非阻塞。

    参考示例:创建一个 test.txt 文件,并保证该文件大小大于 0。

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    int main() 
    {
        // 1.打开一个文件
    	int fd = open("test.txt", O_RDWR);// 打开一个文件
        int len = lseek(fd, 0, SEEK_END);//获取文件大小
    
        // 2.创建内存映射区
        void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        if (ptr == MAP_FAILED)
        {
            perror("mmap error");
            exit(-1);
        }
        close(fd); //关闭文件
    
        // 创建子进程
        pid_t pid = fork();
        if (pid == 0) //子进程
        {
            sleep(1); //保证父进程先执行
    
            // 读数据
            printf("%s\n", (char*)ptr);
        }
        else if (pid > 0) //父进程
        {
            // 写数据
            strcpy((char*)ptr, "i am u father!!");
            // 回收子进程资源
            wait(NULL);
        }
    
        // 释放内存映射区
        int ret = munmap(ptr, len);
        if (ret == -1)
        {
            perror("munmap error");
            exit(-1);
        }
        
        // 关闭文件
        close(fd);
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    在这里插入图片描述

    运行结果:

    yxm@192:~$ gcc test.c -o test
    yxm@192:~$ ./test
    i am u father!!
    yxm@192:~$ 
    
    • 1
    • 2
    • 3
    • 4

    (2)没有关系的进程间通信

    内存映射实现不同进程间通讯

      1. 准备一个大小不是 0 的磁盘文件
      2. 进程 1 通过磁盘文件创建内存映射区,得到一个操作这块内存的指针
      3. 进程 2 通过磁盘文件创建内存映射区,得到一个操作这块内存的指针。【注意】进程 1 与进程 2 是通过同一磁盘文件创建内存映射区的。
      4. 使用内存映射区通信
      5. 【注意】内存映射区通信,是非阻塞。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    参考示例:创建一个 test.txt 文件,并保证该文件大小大于 0。

    // write.c
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(void)
    {
        int fd = -1;
        int ret = -1;
        pid_t pid = -1;
        void *addr = NULL;
        
        // 1 以读写的方式打开一个文件
        fd = open("test.txt", O_RDWR);
        if(-1 == fd)
        {
            perror("open");
            return 1;
        }
        int len = lseek(fd, 0, SEEK_END);//获取文件大小
        
        // 2 将文件映射到内存
        addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        if (addr  == MAP_FAILED)
        {
            perror("mmap");
            return 1;
        }
        printf("文件存储映射ok.....\n");
        
        // 3 关闭文件
        close(fd);
        
    	// 4 写入到存储映射区
        memcpy(addr, "1234567890", 10);  
        
        // 5断开存储映射
        munmap(addr, 1024);
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    // read.c
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(void)
    {
        int fd = -1;
        int ret = -1;
        pid_t pid = -1;
        void *addr = NULL;
        
        // 1 以读写的方式打开一个文件
        fd = open("test.txt", O_RDWR);
        if(-1 == fd)
        {
            perror("open");
            return 1;
        }
        int len = lseek(fd, 0, SEEK_END);//获取文件大小
        
        // 2 将文件映射到内存
        addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        if (addr  == MAP_FAILED)
        {
            perror("mmap");
            return 1;
        }
        printf("文件存储映射ok.....\n");
        
        // 3 关闭文件
        close(fd);
        
    	// 4 读存储映射区数据
       printf("addr:%s\n", (char*)addr);
        
        // 5断开存储映射
        munmap(addr, 1024);
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    运行结果:

    yxm@192:~$ gcc write.c -o write
    yxm@192:~$ gcc read.c -o read
    yxm@192:~$ ./write 
    文件存储映射ok.....
    yxm@192:~$ ./read
    文件存储映射ok.....
    addr:1234567890
    yxm@192:~$ 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    匿名映射实现父子进程通信

    通过使用我们发现,使用内存映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个大小不为 0 的文件才能实现。

    通常为了建立映射区要 open 一个 temp 文件,创建好了再 unlink、close 掉,比较麻烦。其实 Linux 系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区,这样可以直接使用匿名映射来代替前面提到的内存映射,【注意】匿名映射只能用于具有血缘关系的进程间通讯 。

    匿名映射同样需要借助标志位参数 flags 来指定,使用 MAP_ANONYMOUS 或 MAP_ANON(MAP_ANON 已经被废弃) 特性即可实现。

    int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
    
    • 1
    • 4 是随意举例,该位置表示映射区大小,可依实际需要填写。
    • MAP_ANONYMOUS 和 MAP_ANON 这两个宏是Linux操作系统特有的宏。

    程序示例:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main() 
    {	
    	// 创建匿名内存映射区
        int len = 4096;
        void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
        if (ptr == MAP_FAILED)
        {
            perror("mmap error");
            exit(1);
        }
    
        // 创建子进程
        pid_t pid = fork();
        if (pid > 0) //父进程
        {
            // 写数据
            strcpy((char*)ptr, "hello mike!!");
            // 回收
            wait(NULL);
        }
        else if (pid == 0)//子进程
        {
            sleep(1);	//保证父进程先执行
            // 读数据
            printf("%s\n", (char*)ptr);
        }
    
        // 释放内存映射区
        int ret = munmap(ptr, len);
        if (ret == -1)
        {
            perror("munmap error");
            exit(-1);
        }
        
       	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    运行结果:

    yxm@192:~$ gcc test.c -o test
    yxm@192:~$ ./test
    hello, world
    
    • 1
    • 2
    • 3

    内存映射的方式操作文件

    共享内存除了可以实现进程间通讯外,还可以实现文件操作。不过,很少有人使用内存映射的方式操作文件,此处只简单举例说明:

    // 使用内存映射实现文件拷贝的功能
    /*
        思路:
            1.对原始的文件进行内存映射
            2.创建一个新文件(拓展该文件)
            3.把新文件的数据映射到内存中
            4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存中
            5.释放资源
    */
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main() 
    {
    
        // 1.对原始的文件进行内存映射
        int fd = open("english.txt", O_RDWR);
        if(fd == -1) 
        {
            perror("open");
            exit(0);
        }
    
        // 获取原始文件的大小
        int len = lseek(fd, 0, SEEK_END);
    
        // 2.创建一个新文件(拓展该文件)
        int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);
        if(fd1 == -1) 
        {
            perror("open");
            exit(0);
        }
        
        // 对新创建的文件进行拓展
        truncate("cpy.txt", len);
        write(fd1, " ", 1);
    
        // 3.分别做内存映射
        void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        void * ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);
    
        if(ptr == MAP_FAILED) 
        {
            perror("mmap");
            exit(0);
        }
    
        if(ptr1 == MAP_FAILED) 
        {
            perror("mmap");
            exit(0);
        }
    
        // 内存拷贝
        memcpy(ptr1, ptr, len);
        
        // 释放资源
        munmap(ptr1, len);
        munmap(ptr, len);
    
        close(fd1);
        close(fd);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    yxm@192:~$ gcc test.c -o test
    yxm@192:~$ ./test 
    yxm@192:~$ ls -l
    total 332
    -rw-rw-r--  1 yxm yxm 129772 Sep  6 02:45 cpy.txt
    -rw-rw-r--  1 yxm yxm 129772 Sep  6 02:44 english.txt
    .....
    -rwxrwxr-x  1 yxm yxm   9016 Sep  6 02:45 test
    -rw-rw-r--  1 yxm yxm   1546 Sep  6 02:44 test.c
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    四、内存映射注意事项

    • 如果 open 时模式为 O_RDONLY,mmap 时 prot 参数指定 PROT_READ | PROT_WRITE 会怎样?

      此时调用 mmap() 函数会出错:返回MAP_FAILED。

      如果想要函数调用正常,open() 函数中的权限需要和 prot 参数的权限保持一致,权限不能和 prot 参数冲突。

    • mmap 什么情况下会调用失败?

      • 第二个参数:length = 0
      • 第三个参数:prot 参数只指定了写权限
      • 第五个参数:fd 参数,open 指定的权限和 prot 参数有冲突。
    • 可以open 的时候 O_CREAT 一个新文件来创建映射区吗?

      可以的,但是创建的文件的大小如果为0的话,肯定不行,此时可以使用 lseek() truncate()函数对新的文件进行扩展。

    • mmap 后关闭文件描述符,对 mmap 映射有没有影响?
      int fd = open(“XXX”);
      mmap(,fd,0);
      close(fd);

      映射区还存在,创建映射区的fd被关闭,没有任何影响。

    • 如果文件偏移量为 1000 会怎样?
      偏移量必须是 4K 的整数倍,如果不是,则会报错并返回 MAP_FAILED

  • 相关阅读:
    PT的一些setting
    Spring AOP快速使用教程
    【图像检测】基于K-L实现人脸检测附matlab代码
    天才基本法中预测犯罪发生地点的数学建模真的可以为所欲为【全国大学生数学建模竞赛】
    普通卷积、转置卷积详细介绍以及用法
    React 路由 V5(完整版)
    Linux基本使用和web程序部署
    参与开源社区还有证书拿?
    【学习笔记】AGC035
    【好文推荐】openGauss 5.0.0 数据库安全——全密态探究
  • 原文地址:https://blog.csdn.net/weixin_45004203/article/details/126730246