3. munmap与内存同步:解除映射、同步机制与共享模式
在 Linux 内存映射的生命周期中,解除映射(munmap) 和 内存同步(msync) 是决定数据完整性与资源释放的关键环节。本章深入分析 munmap 的底层行为、msync 的同步策略,以及 MAP_SHARED 与 MAP_PRIVATE 在写回机制上的本质差异。
3.1 munmap:解除映射的完整流程
munmap 系统调用用于删除指定地址区间的所有页表项,并释放对应的虚拟内存区域(VMA)。其原型如下:
#include <sys/mman.h>
int munmap(void *addr, size_t length);
解除映射时,内核执行以下关键步骤:
- VMA 查找与校验:根据 addr 在进程的 VMA 红黑树中定位对应的 vma 结构体,校验区间合法性。
- 页表解除:遍历该区间内的所有页表项,清除 PTE 中的 present 位,并刷新 TLB。
- 物理页引用计数递减:对于 MAP_SHARED 映射,递减物理页的引用计数;若计数归零且页为脏,则触发回写。
- VMA 分裂或合并:若解除区间位于 VMA 中间,内核将原 VMA 分裂为两个独立的 VMA。
- 文件映射的脏页回写:对于 MAP_SHARED 的文件映射,内核在 munmap 时自动将脏页写回文件(但不等同于 msync 的强制同步)。
关键陷阱:munmap 不会自动调用 msync。对于 MAP_SHARED 映射,如果 munmap 前没有显式调用 msync,脏页可能尚未写回磁盘。系统崩溃或意外断电会导致数据丢失。
3.2 msync:内存同步的精细控制
msync 系统调用用于将内存映射区域中的修改同步回底层文件,是保证 MAP_SHARED 映射数据持久化的核心手段。
#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);
flags 参数控制同步行为,三种模式对比如下:
| 标志 | 行为描述 | 阻塞行为 | 典型场景 |
|---|---|---|---|
| MS_ASYNC | 调度脏页写回,立即返回 | 非阻塞 | 批量写入后异步落盘,不阻塞业务 |
| MS_SYNC | 等待所有脏页写回完成 | 阻塞直到 I/O 完成 | 关键数据持久化,确保断电安全 |
| MS_INVALIDATE | 使其他映射的缓存行失效 | 非阻塞(常与 MS_SYNC 组合) | 多进程共享文件映射的一致性维护 |
最佳实践:在 munmap 之前,对 MAP_SHARED 映射调用
msync(addr, length, MS_SYNC) 确保数据落盘。对于数据库或日志系统,建议使用 MS_SYNC 而非 MS_ASYNC,避免系统崩溃导致数据丢失。
3.3 MAP_SHARED 与 MAP_PRIVATE 的本质差异
两种映射模式在写回策略、内存开销和进程间可见性上存在根本区别。下表从多个维度进行对比:
| 对比维度 | MAP_SHARED | MAP_PRIVATE |
|---|---|---|
| 写回行为 | 修改直接写入物理页,脏页可写回文件 | 触发写时复制(COW),修改在私有副本上进行 |
| 进程间可见性 | 所有映射同一文件的进程可见修改 | 修改仅对当前进程可见 |
| msync 效果 | 将脏页写回文件,持久化修改 | msync 无实际效果(私有副本不关联文件) |
| munmap 行为 | 脏页可能自动回写(非保证) | 私有副本直接丢弃,不写回文件 |
| 内存开销 | 共享同一物理页,节省内存 | 写时复制产生额外物理页,内存开销增大 |
| 典型用途 | 进程间共享内存、文件 I/O 加速 | 加载可执行文件、动态库、配置读取 |
3.4 调试案例:共享内存同步丢失
以下是一个典型的调试场景,展示未正确使用 msync 导致的数据丢失问题:
/* 错误示例:未调用 msync 直接 munmap */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
int main() {
int fd = open("/tmp/shared.dat", O_RDWR | O_CREAT, 0644);
ftruncate(fd, 4096);
char *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
/* 写入数据 */
sprintf(addr, "Critical log entry #%d", 42);
/* 错误:没有 msync 直接 munmap */
munmap(addr, 4096);
close(fd);
/* 此时数据可能尚未落盘,系统崩溃将丢失 */
return 0;
}
/* 正确示例:munmap 前显式调用 msync */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
int main() {
int fd = open("/tmp/shared.dat", O_RDWR | O_CREAT, 0644);
ftruncate(fd, 4096);
char *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
/* 写入数据 */
sprintf(addr, "Critical log entry #%d", 42);
/* 正确:强制同步到磁盘 */
if (msync(addr, 4096, MS_SYNC) == -1) {
perror("msync failed");
}
munmap(addr, 4096);
close(fd);
return 0;
}
调试技巧:使用
strace -e mmap,munmap,msync ./program 跟踪系统调用序列,确认 msync 是否被调用以及返回状态。结合 cat /proc/[pid]/maps 查看映射区域的权限和偏移。
3.5 内核视角:VMA 与页表操作
从内核源码角度看,munmap 的核心路径位于 mm/mmap.c 中的 do_munmap() 函数。关键操作包括:
- vma 查找:通过
find_vma()在红黑树中定位起始 VMA。 - 页表遍历:调用
unmap_region()遍历页表,清除 PTE 并释放页表页。 - 脏页处理:对于 MAP_SHARED 映射,
filemap_write_and_wait()尝试回写脏页,但不等同于 msync 的强制等待。 - VMA 操作:
split_vma()和remove_vma()维护 VMA 链表和红黑树。
msync 在内核中对应 mm/msync.c 的 sys_msync(),其核心逻辑:
/* 内核 msync 简化流程 */
SYSCALL_DEFINE3(msync, unsigned long, start, size_t, len, int, flags)
{
/* 1. 校验地址区间合法性 */
/* 2. 遍历区间内所有 VMA */
/* 3. 对 MAP_SHARED 的 VMA 调用 filemap_write_and_wait_range() */
/* 4. 若 flags 包含 MS_INVALIDATE,调用 invalidate_mapping_pages() */
/* 5. 返回 0 或错误码 */
}
关键结论:MAP_SHARED + msync(MS_SYNC) 是保证数据持久化的唯一可靠组合。MAP_PRIVATE 的修改永远不会写回文件,munmap 时直接丢弃。在驱动与用户空间共享内存的场景中,务必使用 MAP_SHARED 并在 munmap 前调用 msync。