计算机系统篇之虚拟内存:再探 mmap

Posted csstormq

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了计算机系统篇之虚拟内存:再探 mmap相关的知识,希望对你有一定的参考价值。

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

Author:stormQ

Created: Friday, 13. November 2020 07:13AM

Last Modified: Tuesday, 17. November 2020 12:39PM



若参数 length 不是页大小的整数倍,那么 mmap 的行为?

参数length不是页大小的整数倍的情形可以细分为两种:1)length 小于页大小;2)length 大于页大小,但不是页大小的整数倍。这两种情况在本质上没有什么区别。因此,我们以第一种情况作为研究示例,最终结论同样适用于第二种情况。

如果参数length小于页大小时,我们通过研究以下问题来分析mmap函数的真正行为。

  • 实际创建的虚拟内存区域大小,是参数length的大小还是其他的?

  • 内存映射对象为普通文件时,修改该虚拟内存区域中大于length的部分,是否可共享?

  • 内存映射对象为普通文件时,修改该虚拟内存区域中大于length的部分,是否会被同步到普通文件中?

研究过程:

step 0: 准备研究示例

该示例首先使用mmap函数创建一个可读写、共享的虚拟内存区域,内存映射对象为 Linux 系统中的普通文件——a.txt。然后,写一些数据到该虚拟内存区域中大于length的部分。最后,调用msync函数显式地同步这些数据到 a.txt 文件中。

源码,vm3_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 end = length + 10;
  for (int i = length; i < end; i++)
  
    addr[i] = i;
  
  printf("\\n");

  msync(addr, end, MS_SYNC);

  return EXIT_SUCCESS;

编译:

$ 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

使用od命令查看 a.txt 的所有内容:

$ od -A x -tc a.txt 
000000   1   2   3   4   5   1   2   3   4   5  \\n   1   2   3   4   5
000010   1   2   3   4   5  \\n   1   2   3   4   5   1   2   3   4   5
000020  \\n   1   2   3   4   5   1   2   3   4   5  \\n   1   2   3   4
000030   5   1   2   3   4   5  \\n   1   2   3   4   5   1   2   3   4
000040   5  \\n   1   2   3   4   5   1   2   3   4   5  \\n   1   2   3
000050   4   5   1   2   3   4   5  \\n   1   2   3   4   5   1   2   3
000060   4   5  \\n   1   2   3   4   5   1   2   3   4   5  \\n
00006e

注:最左侧一列为地址,选项-A x表示地址显示为十六进制。默认情况下,地址显示的是八进制。选项-tc表示文件内容以 ASCII 码显示,通过od命令可以看出文件中所有字节的值,包括换行符,共 110 字节。

step 1: 查看新创建的虚拟内存区域

1)使用 gdb 运行可执行目标文件——vm3_main

$ gdb -q --args ./vm3_main ./a.txt 10 0
Reading symbols from ./vm3_main...done.

2)启动并执行到“调用mmap”的下一条语句处(对应源文件中的第 26 行)

(gdb) start
Temporary breakpoint 1 at 0x4007b5: file vm3_main.cpp, line 9.
Starting program: /home/test/vm/vm3_main ./a.txt 10 0

Temporary breakpoint 1, main (argc=4, argv=0x7fffffffdae8) at vm3_main.cpp:9
9	  if (argc < 4)
(gdb) b 26
Breakpoint 2 at 0x400864: file vm3_main.cpp, line 26.
(gdb) c
Continuing.

Breakpoint 2, main (argc=4, argv=0x7fffffffdae8) at vm3_main.cpp:26
26	  if (MAP_FAILED == addr)

断点被击中时,表示我们已完成了创建内存映射的过程。

3)查看进程的内存映射

(gdb) i proc mappings 
process 17039
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x401000     0x1000        0x0 /home/test/vm/vm3_main
            0x600000           0x601000     0x1000        0x0 /home/test/vm/vm3_main
            0x601000           0x602000     0x1000     0x1000 /home/test/vm/vm3_main
      0x7ffff7a0d000     0x7ffff7bcd000   0x1c0000        0x0 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7ffff7bcd000     0x7ffff7dcd000   0x200000   0x1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7ffff7dcd000     0x7ffff7dd1000     0x4000   0x1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7ffff7dd1000     0x7ffff7dd3000     0x2000   0x1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
      0x7ffff7dd3000     0x7ffff7dd7000     0x4000        0x0 
      0x7ffff7dd7000     0x7ffff7dfd000    0x26000        0x0 /lib/x86_64-linux-gnu/ld-2.23.so
      0x7ffff7fd1000     0x7ffff7fd4000     0x3000        0x0 
      0x7ffff7ff6000     0x7ffff7ff7000     0x1000        0x0 /home/test/vm/a.txt
      0x7ffff7ff7000     0x7ffff7ffa000     0x3000        0x0 [vvar]
      0x7ffff7ffa000     0x7ffff7ffc000     0x2000        0x0 [vdso]
      0x7ffff7ffc000     0x7ffff7ffd000     0x1000    0x25000 /lib/x86_64-linux-gnu/ld-2.23.so
      0x7ffff7ffd000     0x7ffff7ffe000     0x1000    0x26000 /lib/x86_64-linux-gnu/ld-2.23.so
      0x7ffff7ffe000     0x7ffff7fff000     0x1000        0x0 
      0x7ffffffdd000     0x7ffffffff000    0x22000        0x0 [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

从上面可以看出,我们所创建的虚拟内存区域的详细信息,比如:起始地址为 0x7ffff7ff6000、大小为 0x1000、内存映射对象为 /home/test/vm/a.txt 文件且偏移量为 0。

另外,我们可以查看mmap函数所返回的虚拟内存区域的起始地址,即局部变量addr的值也确实是 0x7ffff7ff6000:

(gdb) p/x addr
$1 = 0x7ffff7ff6000

也就是说,我们新创建的虚拟内存区域的地址范围为[0x7ffff7ff6000, 0x7ffff7ff7000)(前闭后开),大小为 0x1000 字节(即 4KB,也就是页面大小)。

因此,可以得出结论:如果参数length不是页大小的整数倍时,mmap函数实际创建的虚拟内存区域大小 = 不小于length的页大小的最小整数倍。

step 2: 修改该虚拟内存区域中大于length的部分,观察这些修改是否对其他进程可见

1)在另一个 shell 窗口中再次执行可执行目标文件——vm3_main(记为进程 B,步骤step 1中的记为进程 A)

$ gdb -q --args ./vm3_main ./a.txt 10 0
Reading symbols from ./vm3_main...done.
(gdb) start
Temporary breakpoint 1 at 0x4007b5: file vm3_main.cpp, line 9.
Starting program: /home/test/vm/vm3_main ./a.txt 10 0

Temporary breakpoint 1, main (argc=4, argv=0x7fffffffdae8) at vm3_main.cpp:9
9	  if (argc < 4)
(gdb) b 26
Breakpoint 2 at 0x400864: file vm3_main.cpp, line 26.
(gdb) c
Continuing.

Breakpoint 2, main (argc=4, argv=0x7fffffffdae8) at vm3_main.cpp:26
26	  if (MAP_FAILED == addr)

2)分别查看进程 A 和进程 B 的虚拟内存区域中length后面 40 字节(即 10 个int类型的值)的内容

进程 A 中:

(gdb) x/10wd addr+length
0x7ffff7ff6028:	171258931	875770417	858927413	822752564
0x7ffff7ff6038:	892613426	875770417	842074677	825570355
0x7ffff7ff6048:	892613426	858927370

进程 B 中:

(gdb) x/10wd addr+length
0x7ffff7ff6028:	171258931	875770417	858927413	822752564
0x7ffff7ff6038:	892613426	875770417	842074677	825570355
0x7ffff7ff6048:	892613426	858927370

可以看出,此时(即两个进程都未对各自的虚拟内存区域的内容进行修改)两个进程的各自虚拟内存区域中length后面 40 字节的内容是相同的。

3)进程 B 继续执行到“写一些数据到其虚拟内存区域中length后面 40 字节”的下一条语句处(对应源文件中的第 40 行),而进程 A 保持不动

进程 B 中:

(gdb) b 40
Breakpoint 3 at 0x4008d7: file vm3_main.cpp, line 40.
(gdb) c
Continuing.
page size:4096 bytes

Breakpoint 3, main (argc=4, argv=0x7fffffffdae8) at vm3_main.cpp:40
40	  printf("\\n");

4)再次查看进程 B 和进程 A 的虚拟内存区域中length后面 40 字节(即 10 个int类型的值)的内容

进程 B 中:

(gdb) x/10wd addr+length
0x7ffff7ff6028:	10	11	12	13
0x7ffff7ff6038:	14	15	16	17
0x7ffff7ff6048:	18	19

可以看出,进程 B 确实已经修改了其虚拟内存区域中length后面 40 字节的内容。

进程 A 中:

(gdb) x/10wd addr+length
0x7ffff7ff6028:	10	11	12	13
0x7ffff7ff6038:	14	15	16	17
0x7ffff7ff6048:	18	19

可以看出,进程 A 的虚拟内存区域中length后面 40 字节的内容与进程 B 的相同。但此时,进程 A 还未对其虚拟内存区域中length后面 40 字节的内容进行修改。

因此,可以得出结论:如果参数length不是页大小的整数倍且内存映射对象为普通文件时,一个进程修改该虚拟内存区域中大于length的部分,对其他也映射该区域的进程可见。

注意: 这里的所修改的大于length的部分仍在该虚拟内存区域的地址范围之内。如果超出了这个范围,可能会产生 coredump。

step 3: 调用msync函数显式地将进程 B 中所修改的数据进行同步操作,观察 a.txt 中是否有这些修改

1)在调用msync函数前,查看 a.txt 文件的当前内容(十进制)

$ od -tu4 a.txt
0000000  875770417  858927413  822752564  892613426
0000020  875770417  842074677  825570355  892613426
0000040  858927370  842085684         10         11
0000060         12         13         14         15
0000100         16         17         18         19
0000120  842085684  171258931  875770417  858927413
0000140  822752564  892613426  875770417       2613
0000156

从上面可以看出,内核已经自动地将进程 B 所修改的数据同步到 a.txt 中了。

因此,可以得出结论:如果参数length不是页大小的整数倍且内存映射对象为普通文件时,一个进程修改该虚拟内存区域中大于length的部分,这些被修改的内容会被内核同步到内存映射文件中。

注意: 上述示例中大于length的部分还未超出内存映射文件的大小范围。

于是,可以很自然地引申出这样一个问题:如果所修改的虚拟内存区域超出内存映射文件的大小范围时,那么默认情况下这些被修改的内容还会被内核同步到内存映射文件中吗?让我们继续研究。

2)在进程 B 中,修改其虚拟内存区域中起始地址为 addr+27,长度为 4 字节的内容(其中两个字节超出了内存映射文件的大小范围)

(gdb) p/x *(addr+27)=0x12345678
$10 = 0x12345678

再次查看 a.txt 文件的当前内容(十进制,且将每个元素看作 4 字节):

$ od -tu4 a.txt
0000000  875770417  858927413  822752564  892613426
0000020  875770417  842074677  825570355  892613426
0000040  858927370  842085684         10         11
0000060         12         13         14         15
0000100         16         17         18         19
0000120  842085684  171258931  875770417  858927413
0000140  822752564  892613426  875770417      22136
0000156

从上面的结果中可以看出,a.txt 文件中的最后一个值被修改了。

查看 a.txt 文件的当前内容(十六进制,且将每个元素看作 1 字节):

$ od -A x -tx1 a.txt
000000 31 32 33 34 35 31 32 33 34 35 0a 31 32 33 34 35
000010 31 32 33 34 35 0a 31 32 33 34 35 31 32 33 34 35
000020 0a 31 32 33 34 35 31 32 0a 00 00 00 0b 00 00 00
000030 0c 00 00 00 0d 00 00 00 0e 00 00 00 0f 00 00 00
000040 10 00 00 00 11 00 00 00 12 00 00 00 13 00 00 00
000050 34 35 31 32 33 34 35 0a 31 32 33 34 35 31 32 33
000060 34 35 0a 31 32 33 34 35 31 32 33 34 78 56
00006e

从上面的结果中可以看出,只有 0x56、0x78 这两个字节的内容被同步到了 a.txt 文件。上述程序是在字节序为小端的机器上运行的。所以,起始地址为 addr+27 的后面 4 字节的内容依次为(从低地址到高地址):0x78、0x56、0x34、0x12。也就是说,0x34、0x12 这两个字节对应的地址超出了内存映射文件的大小范围。

因此,可以得出结论:如果所修改的虚拟内存区域超出内存映射文件的大小范围时,那么默认情况下这些被修改的内容不会被内核同步到内存映射文件中。

研究结论:

如果参数length不是页大小的整数倍时,mmap函数的真正行为:

  • 实际创建的虚拟内存区域大小 = 不小于length的页大小的最小整数倍。

  • 内存映射对象为普通文件时,一个进程修改该虚拟内存区域中大于length的部分,对其他也映射该区域的进程可见。

  • 内存映射对象为普通文件时,一个进程修改该虚拟内存区域中大于length的部分(隐含着未超出该虚拟内存区域的地址范围),可以分为两种情形:

    • 大于length的部分且未超过内存映射文件的大小范围,那么这些修改会被内核自动地同步到普通文件中。

    • 大于length的部分且已超过内存映射文件的大小范围,那么这些修改不会被内核自动地同步到普通文件中。

    注意: 这两个结论的默认前提是内存映射文件的大小一直保持不变。比如:未对内存映射文件进行文件截断操作。


参数 offset 取不同值时,对 mmap 的行为有何影响?

我们通过研究以下问题来分析参数offset取不同值时,mmap函数的真正行为。

  • 参数offset的取值小于 0 时,mmap函数的行为?

  • 参数offset的取值等于 0 时,mmap函数的行为?

  • 参数offset的取值等于 1 时(即不是页面大小的整数倍),mmap函数的行为?

阅读完整内容见微信公众号同名文章(技术专栏 -> 计算机系统)

以上是关于计算机系统篇之虚拟内存:再探 mmap的主要内容,如果未能解决你的问题,请参考以下文章

再探进程间通信

再探进程间通信

了解内存分配

Linux 内核 内存管理内存管理系统调用 ① ( mmap 创建内存映射 | munmap 删除内存映射 | mprotect 设置虚拟内存区域访问权限 )

mmap

Linux内存从0到1学习笔记(七,用户空间虚拟内存之三 - 内存映射)