什么是内存映射文件?一个故事搞懂!
我是阿飞,经过一番努力,我终于考上了Linux帝国的公务员,今天是我入职第一天,我被分配到了文件管理部门。
我的工作是负责读取硬盘上文件的数据,跟我搭档的是负责写入数据的小马,我俩一读一写,为应用程序们提供文件读写服务。
刚来的我没有什么经验,一开始就闹了不少笑话。
不过好在小马干这活已经挺久了,给我传授了不少经验:
“咱们操作系统读取硬盘数据都是以块为单位进行的,在咱们这台计算机上,一个块是4KB,你不要一个字节一个字节读,让人笑话”
“文件虽然有那么大,但它们基本上都是分散在硬盘上的不同地方,不过你不用操心这些细节,文件系统记录了它们的位置,你只需要告诉下面的文件系统驱动程序你要读取的数据在文件中的偏移和长度,他们会自动帮你处理好”
在小马的帮助下,我很快适应了这里的环境。
页缓存
渐渐地,我发现这份工作还挺轻松,每次收到文件读取请求后,做一些转换处理,就交给文件系统驱动部门去处理,让他们去找硬盘要数据。
因为硬盘那家伙是机械式的,读写速度比起内存条可差远了,一般都要等很久才能拿到数据,所以在等待的时间里我还可以划水摸鱼。
“咱们这一直都这样,没什么奇怪的,不是我不想提升工作效率,实在是硬盘太慢了”,一旁的小马告诉我。
我倒是觉得这样实在浪费时间,便提了一个主意:“虽然硬盘快不了,但咱们可以加缓存啊!”
“缓存?那是什么东西?”,小马一听来了兴趣。
“我是听我的好朋友阿Q说的,他们CPU嫌弃内存读写数据太慢,就在它们内部加了存储电路,把内存中的数据读取到这些存储电路中保存起来,后面再读取的时候,就先去这里找,找不到再去内存读取,这个存储电路就是缓存”,我说到。
小马听完眼前一亮,问道:“你是说,咱们也可以依葫芦画瓢,把文件的数据缓存到内存里面来?”
“没错!我估计能节省不少时间,反正现在这样干等着,不如做点什么”
我俩一拍即合,开始谋划起具体的方案来。
没过多久,方案就落地了,我们给每一个要读取的文件建立了一个数据结构,里面记录了已经缓存的文件数据块信息,硬盘读取过来的数据,就缓存到内存中,并记录到这个数据结构中。

以后读取文件的时候,先通过这个数据结构去查询,查到了就直接拷贝给应用程序,查不到再去找硬盘要。
你还别说,CPU的局部性原理在这里也同样适用,就这一个改动,性能提升相当明显,不用每次都找硬盘要数据了。
不过加了一个缓存也带来了一些新的问题,小马在写文件的时候,是先写到缓存页的,并不会立即同步到硬盘上的文件中,要是这个时候突然断电了,那缓存的数据可就丢掉了。
后来我们又提供了一个叫fsync的函数,只要调用它,就会马上进行同步写入到硬盘上。
内存映射文件
时间久了,我开始发现工作中存在的一些问题。
这天晚上,我拉住小马说道:“不知道你发现了没有,现在读取文件的时候,会拷贝两次数据。”
“两次?”,小马有些不解。
“对,第一次,把硬盘上的数据拷贝到我们准备的内核缓存页中。第二次,把内核缓存页中的数据拷贝到应用程序准备的缓冲区中。这不就是两次吗?”

“你这么一说还真是,我写数据的时候也会两次,先把应用程序缓冲区的数据拷贝到内核缓存页,再把内核缓存页的数据拷贝到硬盘上”
“这拷来拷去的有些麻烦啊,而且同样一份数据,在内存里面存了两份,实在是有点浪费空间,读写文件能不能再简单一点呢?”,我紧锁着眉头,认真思考着。
可思来想去,也没想到什么更好的办法,也只能作罢,直到有一天···
这天,我正如往常一般,准备把缓存页中刚刚从硬盘读取过来的数据拷贝到应用程序的缓冲区中,一个念头突然在脑中闪现:
能不能直接就让应用程序来访问这个缓存页面呢?
我赶紧忙完手头的工作找到小马来商量这个想法。
没想到小马当即破了我一盆冷水:“不行不行,这些个缓存页面都在我们内核地址空间中,应用程序没有权限访问,你这个想法根本行不通”。
“那有没有办法给这些页面单独开访问权限呢?”,我不肯放弃地问到。
“你别想了,那怎么可能?”
我叹了口气,刚才的兴奋劲儿一下全无。
“唉,你刚这个问题倒是提醒我了,还真有办法!”,小马突然说到。
我一听又兴奋了起来,“快说快说,什么办法?”
“虽然内核空间的地址应用程序没办法访问,但可以把这个内存页面换一个他们可以访问的地址啊”,小马说到。
“什么意思?没太明白!”
“就是把文件的数据缓存页面映射到进程的用户态地址空间中去,这样让用户空间的缓冲区和我们的缓存页实际上映射的同一个物理内存页!”

“好办法啊!我现在有一个大胆的想法”,我激动的说到。
小马一听也来了兴趣,问道:“什么大胆的想法,你打算怎么做?”
“我想,可以把整个文件或者文件的一部分直接映射到应用程序的地址空间中!”
“把文件映射进来?什么意思”,小马有些疑惑。
我继续说道:“没错。在进程的地址空间中划出一块区域和文件内容建立映射关系。等到应用程序访问这部分区域的时候,发生缺页错误中断,在那个时候,我们再把数据从硬盘上读区到缓存页里面,再把缓存页和进程中发生缺页中断的页面关联起来!对应用程序没有感知,如此一来,不用再使用read、write、fseek这样麻烦的读写文件了,就像访问内存一般方便!”

“妙啊,妙啊,果然是个大胆的想法,这样可比之前的读写文件快多了!”,小马听完赞叹不已。
我俩很快就开始着手推动这项技术落地,他们搞了一个新的API出来:
void* mmap(
void* start,
size_t length,
int prot,
int flags,
int fd,
off_t offset
);
通过这个函数,就能把文件映射到内存中,操作起来不仅比以前方便多了,还减少了内存拷贝,节省了内存页面,一经推出就受到了欢迎。
我们还给这项技术取了一个名字:内存映射文件。