Mmap的实现原理和应用(mmap内核实现)
wptr33 2025-01-23 21:51 30 浏览
概述
对于mmap,您是否能从原理上解析以下三个问题:
- mmap比物理内存+swap空间大情况下,是否有问题?
- MAP_SHARED,MAP_PRIVATE,MAP_ANONYMOUS,MAP_NORESERVE到底有什么区别?
- 常听说mmap的读写比传统的系统调用(read, write)快,但真的是这样子吗?原因是什么?
要解决这些疑问,可能还需要在操作系统层面多了解。本文将尝试通过这些问题深入剖析,希望通过这篇文章,能使大家对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。
在<<深入理解计算机系统>>这本书中,mmap定义为:Linux通过将一个虚拟内存区域与一个磁盘上的对象(object)关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)。
在Logstore中,mapping的对象是普通文件(Segment File)。
mmap的原理
mmap在进程虚拟内存做了什么
我们先来简单看一下mapping一个文件,mmap做了什么事情。如下图所示:
map file.png
假设我们mmap的文件是FileA,在调用mmap之后,会在进程的虚拟内存分配地址空间,创建映射关系。
这里值得注意的是,mmap只是在虚拟内存分配了地址空间,举个例子,假设上述的FileA是2G大小
[dragon@xxx.xxx] ls -lat FileA
2147483648 Apr 25 10:22 FileA
在mmap之后,查看mmap所在进程的maps描述,可以看到
[dragon@xxx.xxx] cat maps
....
7f35eea8d000-7f366ea8d000 rw-s 00000000 08:03 13110516 FileA
....
由上可以看到,在mmap之后,进程的地址空间7f35eea8d000-7f366ea8d000被分配,并且map到FileA,7f366ea8d000减去7f35eea8d000,刚好是2147483648(ps: 这里是整个文件做mapping)
mmap在物理内存做了什么
在Linux中,VM系统通过将虚拟内存分割为称作虚拟页(Virtual Page,VP)大小固定的块来处理磁盘(较低层)与上层数据的传输,一般情况下,每个页的大小默认是4096字节。同样的,物理内存也被分割为物理页(Physical Page,PP),也为4096字节。
上述例子,在mmap之后,如下图:
virtual-physical.png
在mmap之后,并没有在将文件内容加载到物理页上,只上在虚拟内存中分配了地址空间。当进程在访问这段地址时(通过mmap在写入或读取时FileA),若虚拟内存对应的page没有在物理内存中缓存,则产生"缺页",由内核的缺页异常处理程序处理,将文件对应内容,以页为单位(4096)加载到物理内存,注意是只加载缺页,但也会受操作系统一些调度策略影响,加载的比所需的多,这里就不展开了。 (PS: 再具体一些,进程在访问7f35eea8d000这个进程虚拟地址时,MMU通过查找页表,发现对应内容未缓存在物理内存中,则产生"缺页")
缺页处理后,如下图:
virtual-physical assign.png
mmap的分类
我认为从原理上,mmap有两种类型,一种是有backend,一种是没有backend。
有backend
backend mmap.png
这种模式将普通文件做memory mapping(非MAP_ANONYMOUS),所以在mmap系统调用时,需要传入文件的fd。这种模式常见的有两个常用的方式,MAP_SHARED与MAP_PRIVATE,但它们的行为却不相同。
1) MAP_SHARED
这个方式我认为可以从两个角度去看:
- 进程间可见:这个被提及太多,就不展开讨论了
- 写入/更新数据会回写backend,也就是回写文件:这个是很关键的特性,是在Logstore设计实现时,需要考虑的重点。Logstore的一个基本功能就是不断地写入数据,从实现上看就是不断地mmap文件,往内存写入/更新数据以达到写入文件的目的。但物理内存是有限的,在写入数据超过物理内存时,操作系统会进行页置换,根据淘汰算法,将需要淘汰的页置换成所需的新页,而恰恰因为是有backend的,所以mmap对应的内存是可以被淘汰的(若内存页是"脏"的,则操作系统会先将数据回写磁盘再淘汰)。这样,就算mmap的数据远大于物理内存,操作系统也能很好地处理,不会产生功能上的问题。
2) MAP_PRIVATE
这是一个copy-on-write的映射方式。虽然他也是有backend的,但在写入数据时,他会在物理内存copy一份数据出来(以页为单位),而且这些数据是不会被回写到文件的。这里就要注意,因为更新的数据是一个副本,而且不会被回写,这就意味着如果程序运行时不主动释放,若更新的数据超过可用物理内存+swap space,就会遇到OOM Killer。
无backend
无backend通常是MAP_ANONYMOUS,就是将一个区域映射到一个匿名文件,匿名文件是由内核创建的。因为没有backend,写入/更新的数据之后,若不主动释放,这些占用的物理内存是不能被释放的,同样会出现OOM Killer。
mmap比内存+swap空间大情况下,是否有问题
到这里,这个问题就比较好解析了。我们可以将此问题分离为:
- 虚拟内存是否会出问题
- 物理内存是否会出问题
-- 虚拟内存是否会出问题:
回到上述的"mmap在进程虚拟内存做了什么",我们知道mmap会在进程的虚拟内存中分配地址空间,比如1G的文件,则分配1G的连续地址空间。那究竟可以maping多少呢?在64位操作系统,寻址范围是2^64 ,除去一些内核、进程数据等地址段之外,基本上可以认为可以mapping无限大的数据(不太严谨的说法)。
-- 物理内存是否会出问题 回到上述"mmap的分类",对于有backend的mmap,而且是能回写到文件的,映射比内存+swap空间大是没有问题的。但无法回写到文件的,需要非常注意,主动释放。
MAP_NORESERVE
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,成功后映射后持续写入数据
场景 序列 映射类型 结果 A 1 MAP_PRIVATE mmap报错 A 2 MAP_PRIVATE + MAP_NORESERVE mmap成功,在持续写入情况下,遇到OOM Killer A 3 MAP_SHARED mmap成功,在持续写入正常 B 4 MAP_PRIVATE mmap成功,在持续写入情况下,有一个进程会遇到OOM Killer B 5 MAP_PRIVATE + MAP_NORESERVE mmap成功,在持续写入情况下,有一个进程会遇到OOM Killer B 6 MAP_SHARED mmap成功,在持续写入正常
从上述测试可以看出,从现象上看,NORESERVE是绕过mmap的校验,让其可以mmap成功。但其实在RESERVE的情况下(序列4),从测试结果看,也没有保障。
mmap的性能
mmap的性能经常与系统调用(write/read)做对比。
我们将读写分开看,先尝试从原理上分析两者的差异,然后再通过测试验证。
mmap的写性能
我们先来简单讲讲write系统调用写文件的过程:
write process.png
- Step1:进程(用户态)调用write系统调用,并告诉内核需要写入数据的开始地址与长度(告诉内核写入的数据在哪)。
- Step2:内核write方法,将校验用户态的数据,然后复制到kernel buffer(这里是Page Cache)。
[ ps: 特意查了ext4 write的内核实现,write是直接将user buffer copy到page中 ] - Step3: 由操作系统调用,将脏页回写到磁盘(通常这是异步的)
再来简单讲讲使用mmap时,写入文件流程:
- Step1:进程(用户态)将需要写入的数据直接copy到对应的mmap地址(内存copy)
- Step2:
2.1) 若mmap地址未对应物理内存,则产生缺页异常,由内核处理
2.2) 若已对应,则直接copy到对应的物理内存 - Step3:由操作系统调用,将脏页回写到磁盘(通常这是异步的)
系统调用会对性能有影响,那么从理论上分析:
- 若每次写入的数据大小接近page size(4096),那么write调用与mmap的写性能应该比较接近(因为系统调用次数相近)
- 若每次写入的数据非常小,那么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了)。
测试结果与理论推导吻合。
mmap的读性能
我们还是来简单分析read调用与mmap的流程:
read process.png
从图中可以看出,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.88us | 475.28ms
由上可以看出,在read上面,mmap比write的性能差别还是很大的。测试结果与理论推导吻合。
使用方法
下面mmap使用方法(经供参考):
#include <sys/mman.h> /* for mmap and munmap */
#include <sys/types.h> /* for open */
#include <sys/stat.h> /* for open */
#include <fcntl.h> /* for open */
#include <unistd.h> /* for lseek and write */
#include <stdio.h>
int main(int argc, char **argv)
{
int fd;
char *mapped_mem, * p;
int flength = 1024;
void * start_addr = 0;
fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
flength = lseek(fd, 1, SEEK_END);
write(fd, "\0", 1); /* 在文件最后添加一个空字符,以便下面printf正常工作 */
lseek(fd, 0, SEEK_SET);
mapped_mem = mmap(start_addr, flength, PROT_READ, //允许读
MAP_PRIVATE, //不允许其它进程访问此内存区域
fd, 0);
/* 使用映射区域. */
printf("%s\n", mapped_mem); /* 为了保证这里工作正常,参数传递的文件名最好是一个文本文件 */
close(fd);
munmap(mapped_mem, flength);
return 0;
}
相关推荐
- [常用工具] git基础学习笔记_git工具有哪些
-
添加推送信息,-m=messagegitcommit-m“添加注释”查看状态...
- centos7安装部署gitlab_centos7安装git服务器
-
一、Gitlab介1.1gitlab信息GitLab是利用RubyonRails一个开源的版本管理系统,实现一个自托管的Git项目仓库,可通过Web界面进行访问公开的或者私人项目。...
- 太高效了!玩了这么久的Linux,居然不知道这7个终端快捷键
-
作为Linux用户,大家肯定在Linux终端下敲过无数的命令。有的命令很短,比如:ls、cd、pwd之类,这种命令大家毫无压力。但是,有些命令就比较长了,比如:...
- 提高开发速度还能保证质量的10个小窍门
-
养成坏习惯真是分分钟的事儿,而养成好习惯却很难。我发现,把那些对我有用的习惯写下来,能让我坚持住已经花心思养成的好习惯。...
- 版本管理最好用的工具,你懂多少?
-
版本控制(Revisioncontrol)是一种在开发的过程中用于管理我们对文件、目录或工程等内容的修改历史,方便查看更改历史记录,备份以便恢复以前的版本的软件工程技术。...
- Git回退到某个版本_git回退到某个版本详细步骤
-
在开发过程,有时会遇到合并代码或者合并主分支代码导致自己分支代码冲突等问题,这时我们需要回退到某个commit_id版本1,查看所有历史版本,获取git的某个历史版本id...
- Kubernetes + Jenkins + Harbor 全景实战手册
-
Kubernetes+Jenkins+Harbor全景实战手册在现代企业级DevOps体系中,Kubernetes(K8s)、Jenkins和Harbor组成的CI/CD流水...
- git常用命令整理_git常见命令
-
一、Git仓库完整迁移完整迁移,就是指,不仅将所有代码移植到新的仓库,而且要保留所有的commit记录1.随便找个文件夹,从原地址克隆一份裸版本库...
- 第三章:Git分支管理(多人协作基础)
-
3.1分支基本概念分支是Git最强大的功能之一,它允许你在主线之外创建独立的开发线路,互不干扰。理解分支的工作原理是掌握Git的关键。核心概念:HEAD:指向当前分支的指针...
- 云效Codeup怎么创建分支并进行分支管理
-
云效Codeup怎么创建分支并进行分支管理,分支是为了将修改记录分叉备份保存,不受其他分支的影响,所以在同一个代码库里可以同时进行多个修改。创建仓库时,会自动创建Master分支作为默认分支,后续...
- git 如何删除本地和远程分支?_git怎么删除远程仓库
-
Git分支对于开发人员来说是一项强大的功能,但要维护干净的存储库,就需要知道如何删除过时的分支。本指南涵盖了您需要了解的有关本地和远程删除Git分支的所有信息。了解Git分支...
- git 实现一份代码push到两个git地址上
-
一直以来想把自己的博客代码托管到github和coding上想一次更改一次push两个地址一起更新今天有空查资料实践了下本博客的github地址coding的git地址如果是Gi...
- git操作:cherry-pick和rebase_git cherry-pick bad object
-
在编码中经常涉及到分支之间的代码同步问题,那就需要cherry-pick和rebase命令问题:如何将某个分支的多个commit合并到另一个分支,并在另一个分支只保留一个commit记录解答:假设有两...
- 模型文件硬塞进 Git,GitHub 直接打回原形:使用Git-LFS管理大文件
-
前言最近接手了一个计算机视觉项目代码是屎山就不说了,反正我也不看代码主要就是构建一下docker镜像,测试一下部署的兼容性这本来不难但是,国内服务器的网络环境实在是恶劣,需要配置各种镜像(dock...
- 防弹少年团田柾国《Euphoria》2周年 获世界实时趋势榜1位 恭喜呀
-
当天韩国时间凌晨3时左右,该曲在Twitter上以“2YearsWithEuphoria”的HashTag登上了世界趋势1位。在韩国推特实时趋势中,从上午开始到现在“Euphoria2岁”的Has...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
程序员的开源月刊《HelloGitHub》第 71 期
-
详细介绍一下Redis的Watch机制,可以利用Watch机制来做什么?
-
假如有100W个用户抢一张票,除了负载均衡办法,怎么支持高并发?
-
Java面试必考问题:什么是乐观锁与悲观锁
-
如何将AI助手接入微信(打开ai手机助手)
-
SparkSQL——DataFrame的创建与使用
-
redission YYDS spring boot redission 使用
-
一文带你了解Redis与Memcached? redis与memcached的区别
-
如何利用Redis进行事务处理呢? 如何利用redis进行事务处理呢英文
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)