mmap函数小实验
- 文章目的
- 参数 length 不是页大小的整数倍会怎样?
- 研究过程
- length结论
- 参数 offset 取不同的值时会怎样?
- 研究过程
- offset 结论
- 参考链接
文章目的
本文是为了深入理解mmap的参数length与offset对mmap函数行为的影响,从而更好地理解内存映射。
man手册中看到的mmap的参数如下:
函数输入参数介绍:
- 参数1:addr:指定映射的起始地址,通常设为NULL,由内核来分配
- 参数2:length:代表将文件中映射到内存的部分的长度,单位为字节数,一般要求为页大小整数倍
- 参数3:prot:映射区域的保护方式。可以为以下几种方式的组合:
PROT_EXEC 映射区域可被执行
PROT_READ 映射区域可被读取
PROT_WRITE 映射区域可被写入
PROT_NONE 映射区域不能存取 - 参数4:flags:影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此。
MAP_SHARED对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
等等,在man手册中查看其详细介绍。 - 参数5:fd:要映射到内存中的文件描述符,即调用mmap前调用open函数的返回值。(如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。)
- 参数6:offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。
必要知识点:
- mmap并不分配空间, 只是将文件映射到调用进程的地址空间里(但是会占用掉你的 virutal memory)
- 普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作,然后你就可以用memcpy等操作写文件, 而不用write()了。
- 内存中的内容并不会立即更新到文件中,而是有一段时间的延迟,你可以调用msync()来显式同步一下。
- 取消内存映射,需调用munmap()。
- mmap映射完之后,相当于把文件中的内容在用户空间做了映射,内核空间对内容的修改也会反映到用户空间,用户空间对内核的修改也会反映到内核空间。(待确认)
参数 length 不是页大小的整数倍会怎样?
参数length不是页大小整数倍有两种情况,一是小于一个页的大小,二是大于页大小,第二种情况中,超出部分也不足一个页,其实都属于第一种情况。
于是可以用length小于一个页大小时,来实验分析mmap的行为:
- mmap实际占用的虚拟内存区域的大小是length的大小还是其他?
- 内存映射对象为普通文件时,修改该虚拟内存中大于length的部分,是否可共享?
- 内存映射对象为普通文件时,修改该虚拟内存中大于length的部分,是否被同步到普通文件中?
研究过程
使用mmap函数创建一个可读写、共享的虚拟内存区域,内存映射对象为Linux中普通文件a.txt,然后写一些数据到该虚拟内存区域中大于length的部分,最后,调用msync函数显式地同步这些数据到 a.txt 文件中。
mmap_test.c源码:
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/mman.h>/* Usage: ./mmap_test <file> <length> <offset>*/
int main(int argc, char *argv[])
{int fd;uint8_t *pbase = NULL;if (argc < 4) {printf("Usage: ./mmap_test <file> <length> <offset> \n");return 0;}fd = open(argv[1], O_RDWR);if (fd == -1)printf("error: open filed!\n");const size_t length = atoi(argv[2]); const size_t offset = atoi(argv[3]); pbase = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset);if (MAP_FAILED == pbase) {perror("mmap failed, reason:");return EXIT_FAILURE;}close(fd);printf("page size is: %ld bytes \n", sysconf(_SC_PAGE_SIZE));const size_t end = length + 13;for (int i = length; i < end; i++) {pbase[i] = '6';}printf("\n");msync(pbase, end, MS_SYNC);return 0;
}
编译:
gcc mmap_test.c -o mmap_test
查看 a.txt 文件的初始内容(ASCII 码,共 110 字节):
执行
./mmap_test a.txt 10 0
然后再查看a.txt内容:
结果发现,超出了mmap指定length的数据也照样被修改了,那如果mmap中length正好指定了4096Byte还会有同样的结果吗?
实验过程是这样的,我先将一个文件内填充超过一个扇区(4096字节)的数据,然后修改上面的代码,分以下几种情况:
- mmap的length参数为4096:试图修改超过第4096字节的数据,发现无法修改。
- mmap的length参数为4090:试图修改第4090到第4190字节的数据,发现只有第4091到第4096这6个字节的数据被修改了。
length结论
mmap实际创建的虚拟内存大小不小于length的页大小的最小整数倍。
注:期间遇到了个问题:我先打开了个空文件,然后想通过mmap往空文件中写东西,发现怎么都写不进去,因为这违反了mmap不分配空间的规定。
参数 offset 取不同的值时会怎样?
为了探索offset取不同的值时,mmap究竟表现如何,继续使用上面的源代码,共测试以下几种情况(先设定文件大小为超过4096字节但并不是4096的整数倍,length大小为文件对应最小整数被页的大小):
- offset取负值,mmap的行为
- offset取值为5时(即不是页面大小的整数倍),mmap的行为
- offset取值为4096(即是页面大小的整数倍),mmap的行为
- offset取值为内存映射文件的大小时,mmap的行为
- offset取值为大于内存映射文件的大小但正好时页面大小的整数倍时,mmap的行为
研究过程
- offset取负值:报
"mmap failed, reason:: Invalid argument"
- offset取值为5:报
"mmap failed, reason:: Invalid argument"
- offset取值为4096:此时mmap得到的虚拟地址对应文件从起始位置偏移4096字节后的位置。
- offset取值为内存映射文件的大小时(但不是页面大小整数倍):报
"mmap failed, reason:: Invalid argument"
- offset取值是页面大小的整数倍(但大于内存映射文件的大小):报
Bus error
offset 结论
offset的取值必须是页面大小(4096字节)的整数倍,隐含该值必须大于等于零,一般取0,否则会报无效参数报错,且offset不能超过文件大小,否则有bus error。
如果你非要设置offset,按下方法设置:
//需要包含头文件 #include <unistd.h>
const auto true_offset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1);
参考链接
计算机系统篇之虚拟内存(4):再探 mmap