计算机系统篇之虚拟内存:再探 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 设置虚拟内存区域访问权限 )