电话+V:192606-48052 ,欢迎咨询mmap内容如何更改,[专业新媒体运营推广],[各种商圈业内交流],[抖音运营推广课程],[微信运营推广课程],[小红书运营推广课程],[让你站在风口忘记焦虑]
在《一文看懂零拷贝技术》中我们介绍了零拷贝技术的原理,我们知道mmap也是零拷贝技术的一种实现。在本文中,我们将深入探讨mmap的原理。
通常情况下,修改一个文件的内容需要三个步骤:读取文件、修改缓存数据、写回文件。在这个过程中,页缓存充当了中间层,内核利用页缓存与文件的数据块关联。因此,应用程序实际操作的是页缓存,而非直接读写文件。
为优化读写文件过程,我们可以直接在用户空间读写页缓存,而无需将页缓存数据复制到用户空间缓冲区。实现这一目标的技术就是mmap。通过mmap系统调用,我们可以将用户空间的虚拟内存地址与文件进行映射。这样,在映射后的虚拟内存地址进行读写操作时,就如同直接对文件进行操作一样。关键在于,mmap映射的正是文件的页缓存,而非磁盘文件本身。
读写文件需要经过页缓存,因此mmap映射的是文件的页缓存。这就涉及到了同步问题,即页缓存何时将数据同步到磁盘。Linux内核不会主动同步mmap映射的页缓存到磁盘,而是需要用户主动触发同步操作。具体而言,有四个时机可以触发同步操作:文件关闭、进程终止、内存映射被删除或内存映射被替换。
在使用mmap时,我们可以通过mmap函数实现文件映射。函数原型如下,其中参数各具用途:
下面通过一个简单的例子展示如何使用mmap。我们首先以可读写方式打开文件,然后使用mmap函数进行映射。映射过程如下:
mmap函数返回映射后的内存地址,我们可以通过此地址对文件进行读写操作。下图展示了上述例子在内核中的结构。
对于mmap,您是否能从原理上解析以下三个问题:
要解决这些疑问,可能还需要在操作系统层面多了解。本文将尝试通过这些问题深入剖析,希望通过这篇文章,能使大家对mmap有较深入的认识,也能在存储引擎的设计中,有所参考。
最近在研发分布式日志存储系统,这是一个基于Raft协议的自研分布式日志存储系统,Logstore则是底层存储引擎。
Logstore中,使用mmap对数据文件进行读写。Logstore的存储结构简化如下图:
Logstore使用了SegmentsFiles+IndexFiles的方式存储Log,SegmentFile是存储主体,用于存储Log数据,使用定长的方式,默认每个512M,IndexFile主要用于SegmentFile的内容检索。
Logstore使用mmap的方式读写SegmentFile,SegmentsFiles的个数,主要取决于磁盘空间或者业务需求,一般情况下,Logstore会存储1T~5T的数据。
我们先看看什么是mmap。
在<<深入理解计算机系统>>这本书中,mmap定义为:Linux通过将一个虚拟内存区域与一个磁盘上的对象(object)关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memorymapping)。
在Logstore中,mapping的对象是普通文件(SegmentFile)。
我们先来简单看一下mapping一个文件,mmap做了什么事情。如下图所示:
假设我们mmap的文件是FileA,在调用mmap之后,会在进程的虚拟内存分配地址空间,创建映射关系。
这里值得注意的是,mmap只是在虚拟内存分配了地址空间,举个例子,假设上述的FileA是2G大小
在mmap之后,查看mmap所在进程的maps描述,可以看到
由上可以看到,在mmap之后,进程的地址空间7f35eea8d000-7f366ea8d000被分配,并且map到FileA,7f366ea8d000减去7f35eea8d000,刚好是2147483648(ps:这里是整个文件做mapping)
在Linux中,VM系统通过将虚拟内存分割为称作虚拟页(VirtualPage,VP)大小固定的块来处理磁盘(较低层)与上层数据的传输,一般情况下,每个页的大小默认是4096字节。同样的,物理内存也被分割为物理页(PhysicalPage,PP),也为4096字节。
上述例子,在mmap之后,如下图:
在mmap之后,并没有在将文件内容加载到物理页上,只上在虚拟内存中分配了地址空间。当进程在访问这段地址时(通过mmap在写入或读取时FileA),若虚拟内存对应的page没有在物理内存中缓存,则产生"缺页",由内核的缺页异常处理程序处理,将文件对应内容,以页为单位(4096)加载到物理内存,注意是只加载缺页,但也会受操作系统一些调度策略影响,加载的比所需的多,这里就不展开了。
(PS:再具体一些,进程在访问7f35eea8d000这个进程虚拟地址时,MMU通过查找页表,发现对应内容未缓存在物理内存中,则产生"缺页")
缺页处理后,如下图:
我认为从原理上,mmap有两种类型,一种是有backend,一种是没有backend。
这种模式将普通文件做memorymapping(非MAP_ANONYMOUS),所以在mmap系统调用时,需要传入文件的fd。这种模式常见的有两个常用的方式,MAP_SHARED与MAP_PRIVATE,但它们的行为却不相同。
1)MAP_SHARED
这个方式我认为可以从两个角度去看:
2)MAP_PRIVATE
这是一个copy-on-write的映射方式。虽然他也是有backend的,但在写入数据时,他会在物理内存copy一份数据出来(以页为单位),而且这些数据是不会被回写到文件的。这里就要注意,因为更新的数据是一个副本,而且不会被回写,这就意味着如果程序运行时不主动释放,若更新的数据超过可用物理内存+swapspace,就会遇到OOMKiller。
无backend通常是MAP_ANONYMOUS,就是将一个区域映射到一个匿名文件,匿名文件是由内核创建的。因为没有backend,写入/更新的数据之后,若不主动释放,这些占用的物理内存是不能被释放的,同样会出现OOMKiller。
到这里,这个问题就比较好解析了。我们可以将此问题分离为:
--虚拟内存是否会出问题:
回到上述的"mmap在进程虚拟内存做了什么",我们知道mmap会在进程的虚拟内存中分配地址空间,比如1G的文件,则分配1G的连续地址空间。那究竟可以maping多少呢?在64位操作系统,寻址范围是2^64,除去一些内核、进程数据等地址段之外,基本上可以认为可以mapping无限大的数据(不太严谨的说法)。
--物理内存是否会出问题
回到上述"mmap的分类",对于有backend的mmap,而且是能回写到文件的,映射比内存+swap空间大是没有问题的。但无法回写到文件的,需要非常注意,主动释放。
MAP_NORESERVE是mmap的一个参数,MAN的说明是"Donotreserveswapspaceforthismapping.Whenswapspaceisreserved,onehastheguaranteethatitispossibletomodifythemapping."。
我们做个测试:
场景A:物理内存+swapspace:16G,映射文件30G,使用一个进程进行mmap,成功后映射后持续写入数据
场景B:物理内存+swapspace:16G,映射文件15G,使用两个进程进行mmap,成功后映射后持续写入数据
从上述测试可以看出,从现象上看,NORESERVE是绕过mmap的校验,让其可以mmap成功。但其实在RESERVE的情况下(序列4),从测试结果看,也没有保障。
mmap的性能经常与系统调用(write/read)做对比。
我们将读写分开看,先尝试从原理上分析两者的差异,然后再通过测试验证。
我们先来简单讲讲write系统调用写文件的过程:
再来简单讲讲使用mmap时,写入文件流程:
系统调用会对性能有影响,那么从理论上分析:
下面我们对两者进行性能测试:
场景:对2G的文件进行顺序写入(go语言编写)
每次写入大小|mmap耗时|write耗时
---------------|-------|--------|--------
|1byte|22.14s|>300s
|100bytes|2.84s|22.86s
|512bytes|2.51s|5.43s
|1024bytes|2.48s|3.48s
|2048bytes|2.47s|2.34s
|4096bytes|2.48s|1.74s
|8192bytes|2.45s|1.67s
|10240bytes|2.49s|1.65s
可以看到mmap在100byte写入时已经基本达到最大写入性能,而write调用需要在4096(也就是一个pagesize)时,才能达到最大写入性能。
从测试结果可以看出,在写小数据时,mmap会比write调用快,但在写大数据时,反而没那么快(但不太确认是否go的slicecopy的性能问题,没时间去测C了)。
测试结果与理论推导吻合。
我们还是来简单分析read调用与mmap的流程:
从图中可以看出,read调用确实比mmap多一次copy。因为read调用,进程是无法直接访问kernelspace的,所以在read系统调用返回前,内核需要将数据从内核复制到进程指定的buffer。但mmap之后,进程可以直接访问mmap的数据(pagecache)。
从原理上看,read性能会比mmap慢。
接下来实测一下性能区别:
场景:对2G的文件进行顺序读取(go语言编写)
(ps:为了避免磁盘对测试的影响,我让2G文件都缓存在pagecache中)
每次读取大小|mmap耗时|write耗时
---------------|-------|--------|--------
|1byte|8215.4ms|>300s
|100bytes|86.4ms|8100.9ms
|512bytes|16.14ms|1851.45ms
|1024bytes|8.11ms|992.71ms
|2048bytes|4.09ms|636.85ms
|4096bytes|2.07ms|558.10ms
|8192bytes|1.06ms|444.83ms
|10240bytes|867.88μs|475.28ms
由上可以看出,在read上面,mmap比write的性能差别还是很大的。测试结果与理论推导吻合。
对mmap的深入了解,能帮助我们在设计存储系统时,更好地进行决策。
比如,假设需要设计一个底层的数据结构是B+Tree,node操作以Page单位的单机存储引擎,根据上述推论,写入使用系统调用,而读取使用mmap,可以达到最优的性能。而LMDB就是如此实现的。
电话+V: 192606-48052
专注于网络营销推广配套流程服务方案。为企业及个人客户提供高性价比的运营方案,解决小微企业和个人创业难题