计算机系统篇之虚拟内存(3):如何使用内存映射文件在进程之间实现数据共享

Author: stormQ

Created: Sunday, 11. October 2020 10:30AM

Last Modified: Friday, 13. November 2020 07:33PM



摘要

本文介绍了内存映射的基本概念,并描述了在 Linux 系统中如何利用内存映射在父子进程间或任意进程间进行数据共享。

什么是内存映射

Linux 通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)。

虚拟内存区域可以映射到以下两种类型对象中的一种:

无论在哪种情况下,一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件(swap file)或交换空间(swap space)之间换来换去。需要注意的是,在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。


如何使用匿名文件实现父子进程间数据共享

在 Linux 系统中,我们可以通过mmap函数将一个虚拟内存区域映射到一个共享的匿名文件,从而实现父子进程间的数据共享。

需要注意的是,以匿名文件作为内存映射对象的方式,仅适用于父子进程间的数据共享,并且该匿名文件必须是共享对象(即必须指定MAP_SHARED选项)。也就是说,共享匿名文件的共享范围仅限于父子进程间。

mmap函数定义在sys/mman.h头文件中(即使用时需要包含头文件: #include <sys/mman.h>),其函数原型为:

void *mmap (void *__addr, size_t __len, int __prot, int __flags, int __fd, __off_t __offset));

注:

注意:

下面语句的作用:让内核创建一个新的大小为length字节的可读可写、共享、请求二进制零的虚拟内存区域。

mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -10);

接下来,实现一个最简单的父子进程间数据共享的示例。在该示例中,首先父进程将一个虚拟内存区域映射到一个共享的匿名文件,然后子进程写一些数据到该匿名文件中,最后父进程在子进程退出后从该匿名文件中读取相同长度的数据。也就是说,该示例未涉及父子进程同时访问共享数据的情形,也就不必使用进程间的同步措施。

源码,vm1_main.cpp:

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

int Wait(int *status)
{
  int pid = 0;
  do
  {
    pid = wait(status);
  } while (-1 == pid && EINTR == errno);

  if (nullptr != status && WIFEXITED(*status))
  {
    *status = WEXITSTATUS(*status);
  }
  return pid;
}

int main()
{
  auto addr = static_cast<int *>(mmap(NULL16
    PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -10));
  if (MAP_FAILED == addr)
  {
    perror("mmap failed, reason:");
    return EXIT_FAILURE;
  }

  printf("page size:%ld bytes\n", sysconf(_SC_PAGE_SIZE));
  const auto iter_count = 10;

  if (fork() == 0)
  {   /*in child process*/
    for (int i = 0; i < iter_count; i++)
    {
      addr[i] = i;
    }
    return EXIT_SUCCESS;
  }

  int status = 1;
  Wait(&status);
  -1 == status ? printf("child exit status:unknown\n")
    : printf("child exit status:%d\n", status);

  for (int i = 0; i < iter_count; i++)
  {
    printf("%d ", addr[i]);
  }
  printf("\n");

  return EXIT_SUCCESS;
}

编译:

$ g++ -o vm1_main vm1_main.cpp -g

运行:

$ ./vm1_main
page size:4096 bytes
child exit status:0
0 1 2 3 4 5 6 7 8 9

从运行结果中可以看出:1)该系统的页面大小为 4KB;2)父进程正常读取了子进程所写入匿名文件的数据,即实现了父子进程间数据共享。


如何使用普通文件实现任意进程间数据共享

在 Linux 系统中,我们可以通过mmap函数将一个虚拟内存区域映射到一个共享的 Linux 普通文件,从而实现任意进程间的数据共享。

下面语句的作用:让内核创建一个新的可读可写、共享的虚拟内存区域,并将文件描述符fd指定的对象的一个连续的片映射到这个新的虚拟内存区域。连续的对象片的大小为length字节,从距文件开始处偏移量为offset字节的地方开始。

mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset);

源码,vm2_main.cpp(实现写一些数据到内存映射文件):

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

int main(int argc, char *argv[])
{
  if (argc < 4)
  {
    printf("Usage, needed args: backed-file length offset\n");
    return EXIT_FAILURE;
  }

  int fd = open(argv[1], O_RDWR);
  if (-1 == fd)
  {
    perror("open failed, reason:");
    return EXIT_FAILURE;
  }

  const auto length = atoi(argv[2]);
  const auto offset = atoi(argv[3]);
  auto addr = static_cast<int *>(mmap(NULL, length, 
    PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset));
  if (MAP_FAILED == addr)
  {
    perror("mmap failed, reason:");
    return EXIT_FAILURE;
  }
  close(fd);

  printf("page size:%ld bytes\n", sysconf(_SC_PAGE_SIZE));

  const auto iter_count = length;
  for (int i = 0; i < iter_count; i++)
  {
    addr[i] = i;
  }
  printf("\n");

  return EXIT_SUCCESS;
}

源码,vm3_main.cpp,用于实现从内存映射文件读取相同的数据。vm3_main.cpp 将 vm2_main.cpp 中的语句addr[i] = i;替换为printf("%d ", addr[i]);,其他相同。

编译:

$ g++ -o vm2_main vm2_main.cpp -g
$ g++ -o vm3_main vm3_main.cpp -g

内存映射文件为 a.txt,其初始内容为(ASCII 码,共 110 字节):

$ cat a.txt 
1234512345
1234512345
1234512345
1234512345
1234512345
1234512345
1234512345
1234512345
1234512345
1234512345

运行:

$ ./vm2_main ./a.txt 10 0
page size:4096 bytes

$ ./vm3_main ./a.txt 10 0
page size:4096 bytes
0 1 2 3 4 5 6 7 8 9

从运行结果中可以看出,一个进程写了一些数据到内存映射文件,而另一个进程从该内存映射文件中正常读取了这些数据,即实现了无亲缘关系进程间的数据共享。

另外,我们也可以通过od命令查看修改后的 a.txt 的内容:

$ od -tu4 a.txt
0000000          0          1          2          3
0000020          4          5          6          7
0000040          8          9  171258931  875770417
0000060  858927413  822752564  892613426  875770417
0000100  842074677  825570355  892613426  858927370
0000120  842085684  171258931  875770417  858927413
0000140  822752564  892613426  875770417       2613
0000156

注:最左侧一列为八进制(默认格式)的地址,其余为数据。-tu4选项表示将文件中的内容看作int类型(占用 4 字节)的数组。


References


下一篇:计算机系统篇之虚拟内存(4):再探 mmap

上一篇:计算机系统篇之异常控制流(10):Chapter 8 Exceptional Control Flow 章节习题与解答

首页