这个专题按 概述 → 架构框架 → 分点细节 → 实践指南 → 易错点分析 → 词汇表 的统一模板重组,目标不是帮助你“答对问题”,而是帮助你 设计、实现、评审和维护真正可落地的驱动 mmap 方案。
阅读路线
01|驱动 mmap 到底在映射什么?先把对象讲对02|从用户态 mmap 到驱动 .mmap:内核链路到底怎么走03|缓存属性:为什么 mmap 最阴的坑常常不是代码,而是 cache04|DMA 一致性:真正做过驱动的人,为什么不会忘这件事05|权限、安全与边界:不是所有内存都配被用户态摸06|生命周期:用户还在用,底层内存就不能先死07|落地方法论:把 mmap 组织成可复用的驱动设计框架08|经典 mmap 实战:V4L2 采集与异构共享内存通信数据库、动态库加载 为什么用 mmap
核心判断:数据库和动态库都用
mmap,本质原因不是“显得高级”,而是它们都需要把“文件内容”变成“进程地址空间里可直接访问的一段内存”。区别在于:
场景 | mmap 映射的是什么 | 为什么要用 |
数据库 | 数据文件 / WAL / 索引文件的一部分 | 少拷贝、按需加载、让操作系统管理缓存、适合随机访问 |
动态库 | .so 文件里的代码段、只读数据段、可写数据段 | 多进程共享代码页、按需加载、快速启动、方便动态链接和重定位 |
1. 先把 mmap 的本质说清楚
mmap 做的事是:把一个文件的一段内容,映射到进程的虚拟地址空间里。
之后程序访问这段地址,就像访问普通内存一样;真正的数据加载由缺页异常触发。
比如:
映射后:
这行看起来只是普通内存访问,但底层可能发生:
所以
mmap 的关键不是“立刻把文件读进内存”,而是:先建立虚拟地址到文件的关系;真正访问到哪一页,再把哪一页加载进内存。
这叫 按需加载。
2. 数据库如何使用 mmap
数据库用
mmap,常见是把:- 数据文件
- 索引文件
- WAL / redo log 文件
- 元数据页
映射到进程地址空间。
典型代码大概是这样:
然后数据库访问某个页:
这时候数据库不需要:
而是直接:
也就是说,数据库把文件当成一个“大数组”:
3. 数据库为什么要用 mmap
3.1 少一次拷贝
传统
read() 路径:mmap() 路径:少了这一步:
所以对于大量随机访问的数据库页,
mmap 很香。不是因为它“没有内核参与”,别乱说。
缺页、回写、页表建立都要内核参与。
它快,是因为减少了重复拷贝和系统调用次数。
3.2 让 OS 帮你做缓存
数据库访问文件时,本质是在访问一堆页。
如果用
read/write,数据库通常要自己维护 buffer pool:这可能出现双重缓存:
用
mmap 后,数据库可以直接利用内核 page cache:好处:
- 内核负责淘汰冷页
- 内核负责预读
- 内核负责脏页回写
- 数据库代码可以少维护一层缓存逻辑
但这里有个坑:成熟数据库不一定喜欢完全依赖 mmap。
比如很多高性能数据库更倾向于自己管理 buffer pool,因为数据库比操作系统更懂:
- 哪些页是热点索引页
- 哪些页马上会被扫描
- 哪些页不能轻易淘汰
- 什么时候刷脏页最安全
所以
mmap 不是数据库万能灵药。小型数据库、嵌入式数据库、只读索引、LSM 的部分文件可能很适合;大型数据库核心路径未必全靠它。
3.3 适合随机访问
数据库不是只顺序读文件,经常是:
如果每次都
pread():那每次都要进内核。
用
mmap 后:访问某个地址即可。
缺页时内核自动把对应页读进来;已经在 page cache 里,就直接映射。
这对 B+ 树索引、只读 SSTable、内存映射索引文件非常合适。
3.4 写入可以通过 MAP_SHARED 回写文件
数据库如果用:
修改映射区:
这个修改会先改变内存里的 page cache,并把页标记为 dirty。
之后由内核 writeback 机制异步刷回磁盘。
如果想主动刷盘:
或配合:
但注意:
page[0] = 'A'成功,不等于数据已经落盘。
这和普通
write() 一样,甚至更容易误判。mmap 只是让你写内存,持久化仍然要靠 msync/fsync 和文件系统保证。4. 数据库使用 mmap 的风险
这部分面试官很喜欢追。因为只说优点像背广告词,讲风险才像做过系统。
4.1 缺页异常会造成不可控延迟
你访问一个地址:
看起来是一条普通 load 指令。
但如果对应页不在内存里,会触发 major page fault,从磁盘读数据。
这意味着:
对于实时系统或低延迟数据库,这是很危险的。
4.2 写回时机不完全由数据库控制
修改 mmap 区域后,脏页什么时候被写回,主要由内核 writeback 控制。
数据库如果要保证事务持久性,必须非常小心:
msync()是否调用?
fsync()是否调用?
- WAL 是否先落盘?
- 数据页是否晚于日志刷盘?
- 文件系统是否开启 barrier?
- 掉电时磁盘缓存是否可靠?
一句话:
mmap 可以简化访问,但不会自动帮你保证事务一致性。
4.3 SIGBUS 风险
如果 mmap 之后,文件被截断:
而进程继续访问原来映射范围内、现在已经超出文件大小的地址,可能收到:
这不是普通的
read() 返回错误,而是进程直接被信号打断。数据库必须处理这种风险。4.4 地址空间压力
在 32 位系统上,虚拟地址空间有限。
如果数据库文件很大,直接 mmap 整个文件可能不现实。
64 位系统缓解了这个问题,但也不是完全没成本:
- VMA 数量增加
- 页表开销增加
- TLB 压力增加
- 大范围映射管理复杂
5. 动态库如何使用 mmap
现在看动态库。
当你运行一个动态链接程序,比如:
大致过程是:
动态库本身是 ELF 文件,里面有多个 segment。不是简单把整个
.so 一股脑读入内存。典型有:
Segment | 权限 | 内容 |
text | r-x | 机器指令 |
rodata | r-- | 常量字符串、只读表 |
data | rw- | 已初始化全局变量 |
bss | rw- | 未初始化全局变量,映射为零页 |
动态链接器会根据 ELF Program Header,把不同段用不同权限 mmap 进来。
概念上类似:
注意这里通常是:
不是
MAP_SHARED。因为动态库的代码段可以共享,但进程对数据段的修改不能影响别的进程。
6. 动态库为什么要用 mmap
6.1 多进程共享代码页
假设有 100 个进程都用了
libc.so。如果不用 mmap,每个进程都把 libc 代码读一份到自己的内存,那就是:
浪费到离谱。
用 mmap 后,
libc.so 的只读代码段可以这样:代码段是只读可执行的:
没人能改,所以可以安全共享。
这就是动态库用 mmap 的最大收益之一:
同一份.so的代码页,在物理内存中只需要一份,被多个进程共享。
6.2 按需加载,加快启动
动态库可能很大,但程序启动时未必会用到所有函数。
用 mmap 后,内核不需要立刻把整个
.so 读进内存。它只建立映射:
真正执行到某个函数所在页面时,才触发缺页,把那一页从磁盘读入。
这叫 demand paging。
好处:
- 启动更快
- 少占物理内存
- 没用到的代码页不用加载
- 多进程还能共享已加载页面
6.3 配合权限保护
动态库不同段权限不同:
mmap 可以直接按页设置权限。这很重要:
- 代码段不能写,防止被篡改
- 数据段不能执行,降低攻击风险
- 只读数据不能改
- 可写数据不能和其他进程共享修改
这就是 W^X 思路:
一页内存通常不应该同时可写又可执行。
6.4 支持 Copy-on-Write
动态库的数据段通常用
MAP_PRIVATE。意思是:
初始内容来自.so文件;进程读的时候可以共享;一旦某个进程要写,就给它复制一份私有物理页。
比如两个进程都加载同一个动态库:
进程 A 修改某个全局变量时:
这就是 COW。
没有 COW 的话,进程 A 改动态库全局变量,进程 B 也跟着变,那系统就疯了。
这是“共享代码,不共享私有状态”的关键机制。
7. 数据库 mmap vs 动态库 mmap 的关键区别
对比点 | 数据库 | 动态库 |
映射对象 | 数据文件、索引、日志 | .so ELF 段 |
常用映射方式 | MAP_SHARED 较常见 | MAP_PRIVATE 为主 |
是否需要写回文件 | 经常需要 | 通常不需要,代码段只读 |
主要收益 | 减少拷贝、随机访问方便、利用 page cache | 共享代码页、按需加载、权限隔离 |
最大风险 | 持久化、一致性、SIGBUS、缺页延迟 | 重定位开销、COW、地址随机化、符号解析 |
谁主要使用 | 数据库进程自己 | 内核 + 动态链接器 ld.so |
一句话:
数据库用 mmap,是为了把“磁盘上的数据页”变成“可随机访问的内存页”;动态库用 mmap,是为了把“磁盘上的代码段”变成“可执行、可共享、可按需加载的内存页”。
8. 面试回答版本
如果面试官问:数据库、动态库加载如何使用 mmap?为何要用?
可以这样答:
mmap的本质是把文件的一段内容映射到进程虚拟地址空间。数据库可以把数据文件、索引文件或者 WAL 映射进内存,之后访问某个 page 就像访问普通内存一样,缺页时内核再从磁盘把对应页加载到 page cache 并建立页表映射。这样可以减少read/write的用户态和内核态拷贝,适合随机访问,也可以利用内核 page cache 做缓存。但它不自动保证事务持久性,修改后只是脏页,仍然要通过msync/fsync和日志机制保证掉电一致性。
动态库加载也大量依赖mmap。程序execve后,内核先映射可执行文件和动态链接器,之后ld.so根据 ELF 的 Program Header,把.so的 text、rodata、data 等 segment 分别 mmap 到进程地址空间,并设置不同权限,比如 text 段是r-x,rodata 是r--,data 是rw-。动态库一般用MAP_PRIVATE,这样只读代码页可以被多个进程共享,可写数据页在写入时触发 COW,保证每个进程有自己的私有数据。这样既节省物理内存,又支持按需加载,还能用页权限保护代码和数据。
最后补一句最加分:
所以 mmap 的价值不是“完全绕过内核”,而是把文件 IO 变成按页管理的虚拟内存访问:缺页时加载,页缓存复用,页表控制权限,必要时 COW。数据库看重随机访问和减少拷贝;动态库看重共享代码页、按需加载和权限隔离。
9. 一张总图
10. 你真正要记住的三句话
- 数据库用 mmap:把数据文件页映射成内存地址,减少拷贝和系统调用,适合随机访问,但持久化还得靠
msync/fsync + WAL。
- 动态库用 mmap:把
.so的不同 ELF 段按权限映射进进程,代码页多进程共享,数据页用 COW 私有化。
- mmap 不是魔法,也不是完全不经过内核;它只是把“文件 IO”交给虚拟内存系统按页、按需、带权限地管理。
这套专题的统一结构
- 概述:先明确这一章解决什么工程问题。
- 架构框架:用结构图建立整体认知,不让内容碎成知识点。
- 分点细节:把关键机制逐层拆开,讲清对象、约束和因果。
- 实践指南:从“知道”推进到“能做”,明确落地动作。
- 易错点分析:提前暴露高频翻车位,而不是事后补锅。
- 词汇表:统一术语口径,避免概念混用。
你真正要建立的认知框架
- 先定义映射对象:别一上来背 API,先说清楚映射的是谁。
- 再看内核如何建立映射:VMA、页属性、映射方式是执行主线。
- 把 cache 和 DMA 当系统正确性问题:它们不是附加题,而是主战场。
- 把权限边界当产品级约束:开放给用户态,就必须明确暴露面。
- 把生命周期当长期契约:映射建立后,真正的麻烦才刚开始。
- 最后沉淀成可复用方法论:能复用的设计框架,比一次性解释更有价值。
核心关系图
推荐阅读方式
- 如果你正在 设计驱动接口:按顺序完整阅读,不要跳过权限和生命周期。
- 如果你正在 排查 mmap / DMA 诡异 bug:优先看缓存属性、DMA 一致性、生命周期。
- 如果你正在 落地 V4L2 或异构共享内存方案:直接看第 8 篇,再回查第 3~6 篇。
- 如果你正在 写教程、做评审或带新人:先看第 1、2、7、8 篇,再按专题展开。
- 如果你只是想 快速建立系统框架:先读首页,再看第 1、3、4、7、8 篇。
- Author:felixfixit
- URL:http://www.felixmicrospace.top/article/linux_driver_mmap_guide
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!







