前言

网络上有很多描述mmap的文章,但是看完之后还是会让人感觉迷惑,原因是只是单纯的介绍了mmap的使用,其中提的最多的就是减少拷贝次数,并没有解释为什么要这样做。在计算机中,所有东西的出现必然有其原因,了解整个的前因对于对mmap的理解会很有好处,下面我们就从前因开始。

操作系统如何访问文件

要说这个就得先说下虚拟内存

虚拟内存

现代的操作系统,都会支持虚拟内存,它将物理内存对应用层开发透明,应用程序都是使用虚拟地址,其在编译的时候就确定,当然在链接的时候可能还会重定位修改。虚拟内存让进程以为自己独占系统所有的内存,当然是以一种欺骗的方式,因为现在的操作系统都支持多进程,比如我现在就在写这篇文章的同时,在听着杰伦的Mojito。且为了效率,虚拟地址的单位大小是页,一般是4096字节。也就是4kb。从前面的描述可以看出,虚拟内存和进程其实是密切相关的,一般来说两个不同的进程,同样的虚拟地址映射的是不同的物理内存(如果是同样的,岂不是乱套了),所以在操作系统内部必然要存在物理地址和虚拟地址的转换,这个就是MMU,即memory manage unit。转换的时候需要使用到Page Tables,Page Tables包含Page Directory(PGD)、Page mid-level Directory(PMD)、Page Table(PT)。三者都是链表的结构,主要介绍一下PT,里面的每个元素被称为Page Table Entry,其中包含的值就是物理地址。

说了这么多,还是没开始说操作系统如何访问文件,别急,前置知识还是要了解的,还需要再介绍的前置知识就是用户空间和内核空间。

内核空间和用户空间

我们假设虚拟地址的大小是4G,那么用户空间和内核空间分别占有3G和1G的空间,且内核空间的地址是高地址,即0xC0000000 到 0xFFFFFFFF,两者权限不同,内核可以访问计算机所有的资源,比如磁盘、所有的CPU指令、所有的虚拟空间。而用户空间只能访问受限的资源,一个常见的用户空间结构包含:stack、heap、text、data、bss、内存映射区域等。引入内核空间主要是基于系统的安全考量。因为如果所有的进程都可以访问系统资源,那么恶意进程对计算机的破坏就太恐怖了,比如一个恶意进程将计算机所有的文件删除。

操作系统如何访问文件

说了这么多,终于到重头戏了,有了这些前置知识,假如你是一个操作系统的设计者,你会如何设计一个文件操作子系统。不妨停下想一想。

我们知道磁盘的读写速度和内存的读写速度不在一个量级上,加上前面说的用户空间和内核空间,因此频繁的访问磁盘肯定会降低程序的速度,因为会涉及到用户态和内核态的切换,正如解决计算机问题的经典方式,加一个中间层,那么自然就会想到在内核中使用一个buffer存储用户态传来的数据,当到一个容量的时候,再将数据写会磁盘。而且这样还有好处,比如,当进程A访问文件B,而进程B也访问文件B,在进程访问文件A后,文件的内容已经在内核的buffer中了,此时,进程B访问的时候就不需要再从磁盘将数据读入内核了。加快了进程B访问文件的速度。事实上,现代操作系统就是这样做的,负责磁盘和内核交互的模块是DMA,即direct memory access,而负责内核和用户空间的数据交互是CPU,分开的好处在于减轻CPU的工作,在从磁盘读文件的时候可以做其他事,此时的buffer有一个专门的名字叫做page cache。但是细想一下,如果只提供这样一种方式,是不是不友好,因为在文件的访问过程中可能会涉及到多次的用户态和内核态切换。那么有什么方式呢?也可以停下想一想。

上述的过程中,直接将内核空间的page cache和用户空间的映射,如何实现映射呢?只要内核空间的page cache中对应那块文件的内存的PT和用户空间需要映射的内存相同就行,这样两者所访问的物理内存就相同,当然最后映射返回的地址肯定要属于用户空间,因为只有这样用户空间才能访问。这样就可以减少一次内核态到用户态的一次拷贝。即现代操作系统的mmap,返回的值便是映射区域的地址(可以将地址打印出来,用于验证前面说的返回的地址是属于用户空间),关于mmap的介绍可以直接看官方文档这里停下思考下,为什么我说的是内核态到用户态的一次拷贝,而不是双向的,即为什么只是read减少了一次拷贝,而write没有呢

我们想下,在实际的代码中,当我们需要往某块地址中写数据的时候,比如我们有数据char *data = “hello world”,那么当我们拿到mmap返回的地址后,我们假设是mmapAddress,我们往mmapAddress写数据的数据,肯定需要将数据从data中拷贝到mmapAddress,只不过相比直接使用fwrite方法而言,少了内核态和用户态的切换。而读数据的时候就会减少拷贝,即我们可以直接访问mmapAddress地址。下面直接给代码

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <errno.h>

int main(int argc, char *argv[]) {
char * map;
int fd, offset = 0;
struct stat fileInfo;
size_t fileSizeOld, fileSizeNew, textSize;
const char *text = "hello world";
const char *filePath = "/Users/jdk/TestMMAP/testmmap.txt"; //your mapping file path
printf("We will write text '%s' to '%s'.\n", text, filePath);
// Open a file for writing.
// Creating the file if it doesn't exist.
if ((fd = open(filePath, O_RDWR | O_CREAT, (mode_t)0664 )) == -1) {
perror("open");
exit(1);
}
if (stat(filePath, &fileInfo) == -1) {
perror("stat");
exit(1);
}
// If the file is not empty, show its content
if (fileInfo.st_size != 0) {
map = mmap(0, fileInfo.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
close(fd);
perror("mmap");
exit(1);
}
printf("The content in '%s' before writing:\n", filePath);
while (offset < fileInfo.st_size) {
printf("%c", map[offset]);
offset++;
}
printf("\n");
if (munmap(map, fileInfo.st_size) == -1) {
close(fd);
perror("Error un-mmapping the file");
exit(1);
}
}
// Stretch the file size to write the array of char
fileSizeOld = fileInfo.st_size;

textSize = strlen(text);
fileSizeNew = fileInfo.st_size + textSize;

if (ftruncate(fd, fileSizeNew) == -1) {
close(fd);
perror("Error resizing the file");
exit(1);
}
// mmap to write
map = mmap(0, fileSizeNew, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
close(fd);
perror("mmap");
exit(1);\
}
printf("mmap address= %p",map);
//memcpy(map,text,textSize);
for (size_t i = 0; i < textSize; i++) {
/* printf("Writing character %c at %zu\n", text[i], i); */
map[i+fileSizeOld] = text[i]; //copy
}
// Write it now to disk
if (msync(map, fileSizeNew, MS_SYNC) == -1) {
perror("Could not sync the file to disk");
}

for(int i =0;i<textSize;i++){
printf("value =%c \n",map[i]); //no need to copy
}
// Free the mmapped memory
if (munmap(map, fileSizeNew) == -1) {
close(fd);
perror("Error un-mmapping the file");
exit(1);
}
// Un-mmaping doesn't close the file, so we still need to do that
close(fd);

return 0;
}