理解mmap

mmap普通文件

小时候,我以为mmap就是用来把磁盘上的文件加载到内存中,是除了read/write系统调用外,读写文件的另一种方式。使用read/write系统调用读写文件时,我们需要提供一个指针,表明我们需要读写文件的位置。同时需要指明内存缓冲区的地址及其大小,来让read把文件的某一部分存进缓冲区,或是让write把缓冲区的数据写进文件的指定位置。如果使用mmap,系统会把整个文件一次性读取到内存中(对用户来说是一次性,但实现时可以使用缺页异常的方法按需读取,该操作对用户透明)。这段内存就成了磁盘中文件的映像,操作系统会把我们对这片内存的操作,同步到磁盘上。对于用户来说,操作这片内存就是在操作文件。

mmap设备文件

后来,当我看到设备文件也能被mmap到内存中,我开始尝试理解mmap这个称谓的由来—— 原来 memory map,就是把一段memory映射到其它memory空间的过程!设备驱动的作者通过书写自己的mmap回调函数,就可以引导操作系统把设备上的存储空间映射到进程虚拟地址空间上。对于用户来说,操作这片内存,就是在操作设备(上的存储器)。

“统一”的混乱

回头再看mmap普通文件错误但深刻的理解

回想mmap把磁盘上的文件加载到内存的过程,和mmap设备文件的过程竟是完美统一的—— 感谢“一切皆设备”的unix理念!其实不仅设备文件本身也是文件,而且存储文件的磁盘也是设备。这样一来,mmap普通文件,是不是就是在mmap磁盘上某个文件占用的设备memory呢?当设备是可直接访存(如NVDIMM盘)的情况下也许想法是正确,但多数情况下不是这样的。为什么呢?

因为mmap的目的就是想把对设备memory的操作抽象成对内存的操作。如果CPU没有办法直接寻址设备的memory,那么mmap本身是没有意义的。

不幸的是,我们常见的机械硬盘、SSD啥的都没有把存储单元暴露给CPU,所以文件就得先读到内存中,再对内存进行操作,最后同步回设备。相比于直接操作SSD上的存储颗粒,这种做法是不是有一种“缓冲”的意思呢?

mmap块设备自身

一切皆设备的unix设计的大一统理念虽然完美,不过可能会造成一点混乱…… 除了上面说的:错误地认为mmap普通文件就是mmap磁盘上某个文件占用的设备memory,下面谈谈另一个疑虑——mmap一个磁盘块设备自己(块设备文件)会出现什么情况,跟我mmap这个磁盘块设备上的普通文件,有什么不同?

我们可以尝试写一个简单的例子看看mmap块设备文件会发生啥情况:

filp = fopen(“/dev/sda”, “r");
fd = fileno(filp);
buf = mmap(NULL, SIZE, PROT_READ, MAP_PRIVATE, fd, 0);
for (i=0; i<SIZE; i++)
     printf(“%x”, buf[i]);
munmap(buf, SIZE);
fclose(filp);

输出结果(部分):

63eb1090ffffd08ebcffffb8b00ffffd88effffc08effffbefb7c00bfffffb906200ffffa4f321ea6ffffbe00
7be438b75ffffc683ffff8110fffffefe7507ffffebf3ffffb416ffffb002ffffbb017c00ffff80b2748affff
...

再看看磁盘头一个扇区的数据(部分):

eb63 9010 8ed0 bc00 b0b8 0000 8ed8 8ec0
fbbe 007c bf00 06b9 0002 f3a4 ea21 0600
...

忽略读数据时大小端的影响,是不是同一个东西?实验说明,mmap一个块设备文件,就相当于把整块磁盘给mmap了(而不是胡思乱想的什么mmap磁盘控制器页面啦、什么设备上的缓冲区啦等等高级的东西!)。这个mmap跟磁盘上的具体文件系统无关(所谓绕过文件系统),而是把磁盘作为一个超级大的、数据块都是连续的文件给mmap进虚拟内存了。相反看mmap磁盘块设备上的一个普通文件,是跟磁盘上的文件系统息息相关的——这个mmap操作本身就是在这个文件系统提供的。mmap普通文件的数据块在磁盘上不一定是连续的,需要文件系统给出了索引数据块的方法。

扩展阅读:

malloc背后的故事mmap在进程内存分配中扮演的角色及其它一些与文件相关的系统调用。

一个IO的传奇一生 介绍了mmap块设备文件和普通文件的区别。不止于此,这个系列对文件系统和块设备IO有总结性的讲解。