本文主要探索以mmap接口访问文件时,文件自身大小、mmap映射范围和我们所能访问区间之间的关系。主要通过几个小的实验程序来说明。
本文假定读者了解mmap可以作为文件访问的接口,若没有用过可以在Linux中直接man mmap
看相关说明,或者去网上搜索其他资料。简单来说,文件的某一段经过mmap
系统调用映射后会返回一个地址,这样我们可以像操纵内存一样操纵磁盘上的数据,因此”open
+mmap
+memcpy
+msync
“这套文件操作可以在很多的时候代替”open
+read/write
+fsync
“这套文件操作。
但是相比write
进行追加写的操作,被mmap
映射的地址是无法做到改变被映射文件大小的,那么我们如果想改变文件大小怎么办?如果我们写的地址大于实际文件大小会出现什么情况?如果我们写的地址大于所映射的地址范围会有什么情况?
通过两组简单的测试,我们可以探究这个问题:
测试1:”文件范围内, mmap范围外” 会产生SIGSEGV段错误
测试1是简单的情况,我们mmap映射的范围小于文件的实际大小,那么当我们访问在文件范围内但不是映射区范围内的地址时,会产生”segmentation fault”(SIGSEGV
)错误!这很好理解,因为我们访问了非法的内存地址。
如下图,具体的,我们创建一个1 MB的文件,然后将其前512 KB用mmap
映射,然后尝试访问文件第800 KB,第800 KB虽然在文件的范围内,但是不在映射范围内。结果是产生segmentation fault (SIGSEGV) 段错误。
1 2 3 4 5 6 7 8 |
+------------------+------------------+ file_testmap: | mmapped | not mmapped | +------------------+------------------+ 0 (KB) 512 ^ 1024 | we try to access here --+ (it will cause seg. fault) |
- 程序1:
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 |
#define _GNU_SOURCE #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> #include <assert.h> #define ALLOC_SIZE 1024*1024 #define MMAP_SIZE 1024*512 #define ACCESS_OFF 1024*800 int main() { // open int fd = open("file_testmmap", O_CREAT | O_RDWR | O_DIRECT | O_TRUNC, 0755); assert(fd); // alloc 1MB int ret = fallocate(fd, 0, 0, ALLOC_SIZE); assert(!ret); // mmap char *addr = mmap(NULL, MMAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); assert(addr); char a = *(addr + ACCESS_OFF); // we expect program abort here! printf("The fisrt read succeed!\n"); *(addr + ACCESS_OFF) = 'j'; printf("The fisrt write succeed!\n"); return 0; } |
- 运行结果1:
1 2 3 |
zjc@~/test_mmap$ ./a.out 段错误(吐核) |
测试2:”文件范围外,mmap范围内” 会产生SIGBUS总线错误
测试2稍微复杂,我们验证mmap范围大于文件范围的情况,我们还验证了sparse file的情况 (关于sparse file和文件打洞hole punching的详细介绍本博客另一篇文章[1])。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
+---------+------------------------------+--------+ file_testmap: |#########| hole |########| (shadow means +---------+------------------------------+--------+ allocated ) 0 (MB) 1 5 6 mmapped area: +---------------------------------------------------------------------+ start from 0 KB end at 20000KB ^ ^ ^ TESTs access: [1] 500 (KB) [2] 1500 [3] 10000 Our mmap area is much larger than [1] [2] and [3], so there will be no seg. fault. TESTs: [1]: normal access [2]: although we access a hole, the FS will alloc a block(4KB) automatically, the space the file ocuppied will be 4KB larger, but no error will happen. [3]: we will get an SIGBUS abort. |
如上图,具体的:
A. 我们创建一个1 MB的文件,然后从第5 MB再分配1 MB的空间给这个文件,这样,我们就得到了一个文件6 MB但是实际分配块占磁盘2 MB大小的文件,中间的4 MB是一个空洞,所以这个文件是一个sparse file稀疏文件。
B. 我们用mmap
映射一个20000 KB (比6 MB大)的地址addr
。
C. 分别以500 KB、1500 KB 和 10000 KB 为 addr
的偏移量访问映射区:
- 测试的三种情况的具体说明:
Case 1. 500 KB在文件的非空洞区,肯定是正常访问的。
Case 2. 1500 KB 在文件的空洞区,我们在xfs和ext4上都做了测试,也是可以正常访问的,文件系统会自动在1500 KB所在的区域为文件分配一个块,这会导致文件的实际占用空间从2048 KB 涨到 2052 KB,因为我们一般的文件系统的存储单元是一个4 K块。
Case 3. 10000 KB 在mmap区,但是大于文件的大小,这是会产生总线错误SIGBUS
。至于原因:不是段错误的原因,因为我们用mmap
分配了20000 KB给addr
变量,所以程序并非访问了非法的内存空间;在文件系统真正处理mmap
缺页时,会检查所访问的内容是否超过文件当前的大小,若文件小于所请求地址的偏移,那么会返回SIGBUS
错误。
注意,case2中我们是先进行mmap
映射,再用fallocate
产生的1 MB之后的空洞和5~6MB的实际分配空间,这还可以说明,用fallocate
、write
等方法改变文件的大小后并不用改变已经用mmap
映射的较大的addr
范围。这也就是说,我们可以在程序最开始用mmap
映射远大于文件的addr
,然后在程序需要访问大于文件大小的地址前,保证文件被增长到相应位置即可保证不发生错误。
- 程序2
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 |
#define _GNU_SOURCE #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> #include <assert.h> #include <linux/falloc.h> #define ALLOC_SIZE 1024*1024 #define MMAP_SIZE 1024*20000 #define ACCESS_OFF1 1024*500 #define ACCESS_OFF2 1024*1500 #define ACCESS_OFF3 1024*10000 void print_size(int fd) { struct stat st; fstat(fd, &st); printf("fstat:\t file size: %d KB (%d KB allocate).\n\n", st.st_size / 1024, st.st_blocks * 512 / 1024); } int main() { // open int fd = open("file_testmmap", O_CREAT | O_RDWR | O_DIRECT | O_TRUNC, 0755); assert(fd); // alloc 1MB int ret = fallocate(fd, 0, 0, ALLOC_SIZE); assert(!ret); // mmap char *addr = mmap(NULL, MMAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); assert(addr); // === TEST 1. As expected, access to a normal area of a mmapped file will not cause an abort. printf("=== TEST 1. As expected, access to a normal area of a mmapped file will not cause an abort.\n"); char a = *(addr + ACCESS_OFF1); printf("The first read (at %d KB) succeed!\tchar: %c\n", ACCESS_OFF1 / 1024, a); *(addr + ACCESS_OFF1) = 'j'; printf("The first write (at %d KB) succeed!\tchar: %c\n", ACCESS_OFF1 / 1024, *(addr + ACCESS_OFF1)); print_size(fd); // === TEST 2. access to a mmapped file hole will not cause an abort. printf("=== TEST 2. Access to a mmapped file hole will not cause an abort.\n"); // we first create a hole ret = fallocate(fd, 0, ALLOC_SIZE * 5, ALLOC_SIZE); assert(!ret); printf("File fallocated and punched:\n"); print_size(fd); // although we are reading and writing into a hole area, // it will not cause a SIGABRT since we are write within file size char b = *(addr + ACCESS_OFF2); printf("The second read (at %d KB) succeed!\tchar: %c\n", ACCESS_OFF2 / 1024, b); *(addr + ACCESS_OFF2) = 'c'; printf("The second write (at %d KB) succeed!\tchar: %c\n", ACCESS_OFF2 / 1024, *(addr + ACCESS_OFF2)); print_size(fd); // === TEST 3. access beyond the file size will cause an abort printf("=== TEST 3. access beyond the file size will cause an abort.\n"); char c = *(addr + ACCESS_OFF3); // we expect program abort here! we will get a SIGBUS printf("The third read (at %d KB) succeed!\tchar: %c\n", ACCESS_OFF3 / 1024, c); *(addr + ACCESS_OFF3) = '!'; printf("The third write (at %d KB) succeed!\tchar: %c\n", ACCESS_OFF3 / 1024, *(addr + ACCESS_OFF3)); print_size(fd); return 0; } |
- 运行结果2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
zjc@~/test_mmap$ ./a.out === TEST 1. As expected, access to a normal area of a mmapped file will not cause an abort. The first read (at 500 KB) succeed! char: The first write (at 500 KB) succeed! char: j fstat: file size: 1024 KB (1024 KB allocate). === TEST 2. Access to a mmapped file hole will not cause an abort. File fallocated and punched: fstat: file size: 6144 KB (2048 KB allocate). The second read (at 1500 KB) succeed! char: The second write (at 1500 KB) succeed! char: c fstat: file size: 6144 KB (2052 KB allocate). === TEST 3. access beyond the file size will cause an abort. 总线错误(吐核) |
[1] 文件打洞 (Hole Punching) 及其应用, http://blog.jcix.top/2018-09-28/hole_punching/