前言
网络上有很多描述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 |
|