[原创] 深入剖析mmap原理 - 从三个关键问题说起

[原创] 深入剖析mmap原理 - 从三个关键问题说起,第1张

对于mmap,您是否能从原理上解析以下三个问题:

要解决这些疑问,可能还需要在 *** 作系统层面多了解。本文将尝试通过迅销拍这些问题深入剖析,希望通过这篇文章,能使大家对mmap有较深入的认识,也能在存储引擎的设计中,有所参考。

最近在研发分布式日志存储系统,这是一个基于Raft协议的自研分布式日志存储系统,Logstore则是底层存储引擎。

Logstore中,使用mmap对数据文件进行读写。Logstore的存储结构简化如下图:

Logstore使用了Segments Files + Index Files的方斗液式存储Log,Segment File是存储主体,用于存储Log数据,使用定长的方式,默认每个512M,Index File主要用于Segment File的内容检索。

Logstore使用mmap的方式读写Segment File,Segments Files的个数,主要取决于磁盘空间或者业务需求,一般情况下,Logstore会存储1T~5T的数据。

我们先看看什么是mmap。

在<<深入理解计算机系统>>这本书中,mmap定义为:Linux通过将一个虚拟内存区域与一个磁盘上的对象(object)关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)。

在Logstore中,mapping的对象是普通文件(Segment File)。

我们先来简单看一下mapping一个文件,mmap做了什么事情。如下图所示:

假设我们mmap的文件是FileA,在调用mmap之后,会在进程的虚拟内存分配地址空间,创建映射关系。

这里值得注意的是, mmap只是在虚拟内存分配了地址空间 ,举个例子,假设上述的FileA是2G大小

在mmap之后,查看mmap所在进程的maps描述,可以看到

由上可以看到,在mmap之后,进程的地址空间7f35eea8d000-7f366ea8d000被分配,并且map到FileA,7f366ea8d000减去7f35eea8d000,刚好是2147483648(ps: 这里是整个文件做mapping)

在Linux中,VM系统通过将虚拟内存分割为称作虚拟页(Virtual Page,VP)大小固定的块来处理磁盘(较低层)与上层数据的传输,一般情况下,每个页的大小默认是4096字节。同样的,物理内存也被分割为物理页(Physical Page,PP),也为4096字节。

上述例子,在mmap之后,如下图:

在mmap之后,并没有在将文件内容加载到物理页上,只上在虚拟内存中分配了地址空间。当进程在访问这段地址时(通过mmap在写入或读取时FileA),若虚拟内存对应的page没有在物理内存中缓存,则产生"缺页",由内核的缺页异常处理程序处理,将文件对应内容,以页为单位(4096)加载到物理内存,注意是只加载缺页,但也会受 *** 作系统一些调度策略影响,加载的比所需的多,这里就不展开了。

(PS: 再具体一些,进程在访问7f35eea8d000这个进程虚拟地址时,MMU通过查找页表,发现对应内容未缓存在物理内亩羡存中,则产生"缺页")

缺页处理后,如下图:

我认为从原理上,mmap有两种类型,一种是有backend,一种是没有backend。

这种模式将普通文件做memory mapping(非MAP_ANONYMOUS),所以在mmap系统调用时,需要传入文件的fd。这种模式常见的有两个常用的方式,MAP_SHARED与MAP_PRIVATE,但它们的行为却不相同。

1) MAP_SHARED

这个方式我认为可以从两个角度去看:

2) MAP_PRIVATE

这是一个copy-on-write的映射方式。虽然他也是有backend的,但在写入数据时,他会在物理内存copy一份数据出来(以页为单位),而且这些数据是不会被回写到文件的。这里就要注意,因为更新的数据是一个副本,而且不会被回写,这就意味着如果程序运行时不主动释放,若更新的数据超过可用物理内存+swap space,就会遇到OOM Killer。

无backend通常是MAP_ANONYMOUS,就是将一个区域映射到一个匿名文件,匿名文件是由内核创建的。因为没有backend,写入/更新的数据之后,若不主动释放,这些占用的物理内存是不能被释放的,同样会出现OOM Killer。

到这里,这个问题就比较好解析了。我们可以将此问题分离为:

-- 虚拟内存是否会出问题:

回到上述的"mmap在进程虚拟内存做了什么",我们知道mmap会在进程的虚拟内存中分配地址空间,比如1G的文件,则分配1G的连续地址空间。那究竟可以maping多少呢?在64位 *** 作系统,寻址范围是2^64 ,除去一些内核、进程数据等地址段之外,基本上可以认为可以mapping无限大的数据(不太严谨的说法)。

-- 物理内存是否会出问题

回到上述"mmap的分类",对于有backend的mmap,而且是能回写到文件的,映射比内存+swap空间大是没有问题的。但无法回写到文件的,需要非常注意,主动释放。

MAP_NORESERVE是mmap的一个参数,MAN的说明是"Do not reserve swap space for this mapping. When swap space is reserved, one has the guarantee that it is possible to modify the mapping."。

我们做个测试:

场景A:物理内存+swap space: 16G,映射文件30G,使用一个进程进行mmap,成功后映射后持续写入数据

场景B:物理内存+swap space: 16G,映射文件15G,使用两个进程进行mmap,成功后映射后持续写入数据

从上述测试可以看出,从现象上看,NORESERVE是绕过mmap的校验,让其可以mmap成功。但其实在RESERVE的情况下(序列4),从测试结果看,也没有保障。

mmap的性能经常与系统调用(write/read)做对比。

我们将读写分开看,先尝试从原理上分析两者的差异,然后再通过测试验证。

我们先来简单讲讲write系统调用写文件的过程:

再来简单讲讲使用mmap时,写入文件流程:

系统调用会对性能有影响,那么从理论上分析:

下面我们对两者进行性能测试:

场景:对2G的文件进行顺序写入(go语言编写)

每次写入大小 | mmap 耗时 | write 耗时

--------------- | ------- | -------- | --------

| 1 byte | 22.14s | >300s

| 100 bytes | 2.84s | 22.86s

| 512 bytes | 2.51s | 5.43s

| 1024 bytes | 2.48s | 3.48s

| 2048 bytes | 2.47s | 2.34s

| 4096 bytes | 2.48s | 1.74s

| 8192 bytes | 2.45s | 1.67s

| 10240 bytes | 2.49s | 1.65s

可以看到mmap在100byte写入时已经基本达到最大写入性能,而write调用需要在4096(也就是一个page size)时,才能达到最大写入性能。

从测试结果可以看出,在写小数据时,mmap会比write调用快,但在写大数据时,反而没那么快(但不太确认是否go的slice copy的性能问题,没时间去测C了)。

测试结果与理论推导吻合。

我们还是来简单分析read调用与mmap的流程:

从图中可以看出,read调用确实比mmap多一次copy。因为read调用,进程是无法直接访问kernel space的,所以在read系统调用返回前,内核需要将数据从内核复制到进程指定的buffer。但mmap之后,进程可以直接访问mmap的数据(page cache)。

从原理上看,read性能会比mmap慢。

接下来实测一下性能区别:

场景:对2G的文件进行顺序读取(go语言编写)

(ps: 为了避免磁盘对测试的影响,我让2G文件都缓存在pagecache中)

每次读取大小 | mmap 耗时 | write 耗时

--------------- | ------- | -------- | --------

| 1 byte | 8215.4ms | >300s

| 100 bytes | 86.4ms | 8100.9ms

| 512 bytes | 16.14ms | 1851.45ms

| 1024 bytes | 8.11ms | 992.71ms

| 2048 bytes | 4.09ms | 636.85ms

| 4096 bytes | 2.07ms | 558.10ms

| 8192 bytes | 1.06ms | 444.83ms

| 10240 bytes | 867.88µs | 475.28ms

由上可以看出,在read上面,mmap比write的性能差别还是很大的。测试结果与理论推导吻合。

对mmap的深入了解,能帮助我们在设计存储系统时,更好地进行决策。

比如,假设需要设计一个底层的数据结构是B+ Tree,node *** 作以Page单位的单机存储引擎,根据上述推论,写入使用系统调用,而读取使用mmap,可以达到最优的性能。而LMDB就是如此实现的。

SP无论是commit 还是apply都会产生ANR。

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。 从 2015 年中至今在微信上使用,其性能和稳定性经过了时间的验证。近期也已移植到 Android / macOS / Windows 平台,一并开源。

1.读写方简粗伍式:直接I/O

2.数据格式:xml

3.写入方式:全量更新

Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)。

1.mmap防止数据丢失,提高读写效率:MMAP对文件的读写 *** 作只需要从磁盘到用户主存的一次数据拷贝过程拦或,减少了数据的拷贝次数,提高了文件读写效凳信率。

2.MMAP使用逻辑内存对磁盘文件进行映射, *** 作内存就相当于 *** 作文件,不需要开启线程, *** 作MMAP的速度和 *** 作内存的速度一样快;

3.MMAP提供一段可供随时写入的内存块,App 只管往里面写数据,由 *** 作系统如内存不足、进程退出等时候负责将内存回写到文件,不必担心 crash 导致数据丢失。

1.mmap防止数据丢失,提高读写效率

2.精简数据,以最少的数据量表示最多的信息,减少数据大小

3.增量更新,避免每次进行相对增量来说大数据量的全量写入:

不管key是否重复,直接将数据追加在前数据后。

扩容非常简单,只需要重新设定文件大小,然后重新mmap映射即可

MMKV通过mmap 内存映射文件来进行读写 *** 作的,这是其效率高于普通IO的原因。

传统的read首先将文件内容从硬盘拷贝到内核空间的一个缓冲区,然后再将这些数据拷贝到用户空间,这个过程中,实际上完成了 。

mmap将文件直接映射到用户空间,所以中断处理函数根据这个映射关系,直接将文件从硬盘拷贝到用户空间,只进行了 ,因此,mmap内存映射的效率要比read/write效率高。

既然MMKV使用的内存映射优于IO,为什么还要使用IO?

首先要明白,直接将文件映射到虚拟内存,意味着没有数据没有缓存在内核缓存空间,而是直接读到了用户空间,而系统的IO和内核缓存搭配可以使得部分的文件使用效率更高。(OS会根据局部性原理在一次read()系统调用的时候预读取更多的文件数据到内核空含返间缓冲区中,这样当下一次read()系统调用的时候发现要读取的数据已经存在于内核空间缓冲区中的时候只要直接拷贝数据到用户空间缓冲区中即可,无需再进行一次低效的磁盘I/O *** 作,且磁盘的大小要远远超过内存)

而且mmap映射的文件是大于一个内存页大小的( ),并且是 。

也就是说两个方式都是有优缺点的,所以不存在代替这个说法,只能通过分析其场景而选择不同的方式。

protobuf 是google开源的一个序列化框架,类似xml,json,最大的特点是基于二进制,比SharedPreferences使用的传统的XML表示同样一段内容要短小得多。同样这也不能说明Protobuf优于XML,关于Protobuf的更多内滑返容如下:

标准 protobuf 和SharedPreferences 一样,每次写入kv对象都必须全量写入。也就是写入之前将所有数据加载到内存中,然后判断新增的key是否已经存在,完成更新或增加后在全部写入文件。

MMKV中采用增量更新的方式处理protobuf,当需要写入kv对象谈让饥时,不论是新增还是更新都将其直接加入文件的末尾,这样大大增加了写入效率。

上面的做法必然带来两个问题

1.必然导致同一key值会有新旧若干份数据,最新的数据在最后。

2.文件大小会增长得不可控。同一个 key 不断更新的话,是可能耗尽几百 M 甚至上 G 空间。

针对第一个问题,在读取时,针对同一个 key使用后读入的 value 替换之前的值,就可以保证数据是最新有效的。

针对第二个问题,有上文可知MMKV的文件必然是稍大于 (一个内存页的大小)的倍数,当写入的数据小于4k时,可以继续写入,因为本身文件大小就已经略大于4k了,有点很小的浪费,当写入数据超过4k的倍数后,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件在增加4k,直到空间足够。

文件系统、 *** 作系统都有一定的不稳定性,MMKV使用crc 校验确保数据有效性,关于crc 校验,可以参考:

这一步官方有详尽的说明,如下:

多进程设计与实现


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/tougao/12252120.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-22
下一篇 2023-05-22

发表评论

登录后才能评论

评论列表(0条)

保存