mmap(Memory Map)是 Linux/Unix 系统提供的一种将文件或设备映射到进程虚拟地址空间的机制,其底层原理和内存申请时机涉及 虚拟内存管理、缺页异常(Page Fault) 和 内核交互。以下是详细分析:
1. mmap 的底层原理
(1) 核心流程
虚拟地址分配
调用
mmap时,内核仅在进程的虚拟地址空间中 预留一段连续的虚拟内存区域(VMA, Virtual Memory Area),并记录映射的权限(读/写/执行)和文件/设备信息。此时并未分配物理内存,仅修改进程的页表(Page Table)和虚拟内存结构(如
vm_area_struct)。
物理内存申请(延迟分配)
当进程首次访问映射区域的某个地址时,触发 缺页异常(Page Fault),内核才会按需分配物理内存页(Page)。
如果是文件映射,内核还会从磁盘读取文件内容到物理内存(若未缓存)。
同步到文件(可选)
对于共享文件映射(
MAP_SHARED),修改后的内容会由内核定期或通过msync()写回磁盘。
(2) 关键数据结构
vm_area_struct
内核为每个mmap映射维护一个 VMA 结构,记录虚拟地址范围、权限、文件偏移等。页表(Page Table)
虚拟地址到物理地址的映射关系,初始时页表项(PTE)标记为 不存在(Not Present),触发缺页异常后填充。
2. 内存实际申请时机
(1) 匿名映射(MAP_ANONYMOUS)
场景:用于申请动态内存(类似
malloc),不与文件关联。内存分配时机:
首次访问时:通过缺页异常分配物理页,并初始化为零(Zero Page)。
例外:若指定
MAP_POPULATE或mlock(),则立即分配物理内存。
(2) 文件映射(非匿名)
场景:将文件内容映射到内存(如加载动态库)。
内存分配时机:
首次访问时:内核分配物理页,并从磁盘读取文件内容(若页缓存中不存在)。
预读优化:内核可能预加载后续文件内容(取决于访问模式)。
(3) 共享内存(MAP_SHARED)
场景:进程间共享内存或文件。
内存分配时机:
与匿名/文件映射相同,但修改会同步到其他进程或文件。
3. 触发物理内存分配的操作
以下行为会迫使内核实际分配物理内存:
首次读写映射区域(触发缺页异常)。
显式调用:
mlock():锁定内存,强制分配物理页。MAP_POPULATE标志:在mmap时立即预分配内存。
写入共享文件映射(需将脏页写回磁盘)。
4. 性能优化与注意事项
(1) 延迟分配的优势
节省内存:未访问的映射区域不占用物理内存。
快速启动:大文件映射(如数据库)无需等待全部加载。
(2) 需要避免的问题
频繁缺页异常:密集访问新映射区域可能导致性能下降(需优化访问模式或预加载)。
内存超售(OOM):过度依赖延迟分配可能触发系统 OOM Killer。
(3) 手动控制内存分配
// 立即分配物理内存(Linux 特有)
mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
// 锁定内存(防止被换出)
mlock(addr, size);
5、memset 能否触发 mmap 申请物理内存?
1. 核心机制
mmap 默认采用 延迟分配(Lazy Allocation) 策略,仅当进程首次访问映射的虚拟内存时,内核才会通过 缺页异常(Page Fault) 分配物理内存。memset 的作用是向内存写入数据,因此:
如果
memset操作的地址位于mmap映射的虚拟地址范围内,且该区域尚未分配物理内存,则会触发缺页异常,内核分配物理页。如果物理内存已分配(例如之前访问过),
memset仅修改内存内容,不会触发新的分配。
2. 具体场景分析
(1) 匿名映射(MAP_ANONYMOUS)
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memset(addr, 0, size); // 首次访问,触发物理内存分配!
行为:
mmap仅保留虚拟地址空间,物理内存未分配。memset首次写入时,内核捕获缺页异常,分配物理页并初始化为零(Zero Page)。
(2) 文件映射(非匿名)
int fd = open("file.txt", O_RDWR);
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
memset(addr, 0, size); // 触发物理内存分配,并可能从磁盘加载文件内容!
行为:
若文件内容未缓存到内存,
memset触发缺页异常,内核分配物理页并从磁盘读取文件数据。若文件已缓存,直接修改内存中的页缓存。
(3) 已预分配内存的情况
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
memset(addr, 0, size); // 物理内存已提前分配,不会触发缺页异常!
行为:
MAP_POPULATE标志强制mmap调用时立即分配物理内存,memset不会触发缺页异常。
6.文件映射实战总结
std::string strBinaryName = stVideoRoutingInfo.strStreamPath + ".data";
DLOG_DEBUG("Open file data RoutingCode end = %s, CameraCode = %s",
stVideoRoutingInfo.strVideoRoutingCode.c_str(),
stVideoRoutingInfo.oCameraList.getAllCameraCode().c_str());
// 使用 RAII 方式管理文件描述符
int fd = open(strBinaryName.c_str(), O_RDWR | O_CREAT, 00777);
if (fd == -1) {
DLOG_ERR("Failed to open file %s, error: %s",
strBinaryName.c_str(), strerror(errno));
return NMS_COMMON_ERROR;
}
// 确保在函数返回前关闭文件描述符
std::unique_ptr<int, decltype(&close)> fdGuard(&fd, [](int* fd) {
if (*fd != -1) {
close(*fd);
}
});
const int pageSize = getpagesize();
const size_t sMmapStreamBufLenTmp =
stStreamInfoTmp.stOriStreamInfo.iTotalStreamNum *
stStreamInfoTmp.stOriStreamInfo.iStreamStepLen;
// 计算页面对齐的缓冲区大小
const size_t sAlignedBufLen = (sMmapStreamBufLenTmp + pageSize - 1) & ~(pageSize - 1);
// 使用 ftruncate 扩展文件大小
if (ftruncate(fd, sAlignedBufLen) == -1) {
DLOG_ERR("Failed to truncate file %s to size %zu, error: %s",
strBinaryName.c_str(), sAlignedBufLen, strerror(errno));
return NMS_COMMON_ERROR;
}
// 建立内存映射
stStreamInfoTmp.pStreamBuf = static_cast<char*>(
mmap(nullptr, sAlignedBufLen, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
if (stStreamInfoTmp.pStreamBuf == MAP_FAILED) {
DLOG_ERR("Failed to mmap file %s, size %s, error: %s",
strBinaryName.c_str(),
CStringUtil::ByteAutoConver(sAlignedBufLen).c_str(),
strerror(errno));
stStreamInfoTmp.pStreamBuf = nullptr;
return NMS_COMMON_ERROR;
}
DLOG_INFO("Open file data RoutingCode end = %s, CameraCode = %s",
stVideoRoutingInfo.strVideoRoutingCode.c_str(),
stVideoRoutingInfo.oCameraList.getAllCameraCode().c_str());
// 初始化内存区域
memset(stStreamInfoTmp.pStreamBuf, 0, sAlignedBufLen);
stStreamInfoTmp.sMmapStreamBufLen = sAlignedBufLen;
// 注意:fdGuard 会在退出作用域时自动关闭文件描述符1. 文件未扩展导致访问越界
问题描述
你使用
open创建文件时,文件初始大小为 0,而mmap只是建立了虚拟内存映射,并未实际分配磁盘空间。如果后续直接访问
pDecodeFrameBuf,可能会触发SIGBUS信号(访问非法内存),因为文件大小不足以容纳映射的内存范围。
解决方案
在
mmap之前,使用ftruncate扩展文件大小:c复制代码
ftruncate(fd, stVideoRoutingInfo.stFrameInfo.sDecodeFrameBufLen); // 确保文件足够大
否则,访问超出文件实际大小的内存区域会导致崩溃。
2. 多进程/线程竞争导致数据不一致
问题描述
使用
MAP_SHARED时,多个进程或线程可能同时修改映射内存,导致 数据竞争(Race Condition)。如果其他进程修改了文件内容,当前进程的映射内存可能不会自动更新(除非调用
msync或重新映射)。
解决方案
如果需要强一致性,使用 同步机制(如互斥锁、信号量)或调用
msync:c复制代码
msync(pDecodeFrameBuf, bufLen, MS_SYNC); // 强制同步到文件
3. 文件被删除后访问导致崩溃
问题描述
如果文件在
mmap后被其他进程删除(unlink),当前进程仍能通过pDecodeFrameBuf访问内存,但:所有修改 不会写入磁盘(因文件已删除)。
可能导致 资源泄漏(磁盘空间未被释放,直到进程结束)。
解决方案
确保文件生命周期受控,或使用 匿名映射(
MAP_ANONYMOUS)替代文件映射。
4. 内存对齐问题
问题描述
mmap要求映射的 地址和大小按页对齐(通常 4KB)。如果sDecodeFrameBufLen不是页大小的整数倍,可能导致:访问末尾时越界。
性能下降(因跨页访问)。
解决方案
手动对齐大小:
c复制代码
size_t alignedSize = (bufLen + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); // 向上对齐
5. 未正确处理 munmap 导致资源泄漏
问题描述
代码中未调用
munmap释放映射内存,可能导致:虚拟内存泄漏(进程地址空间被占用)。
文件描述符泄漏(虽然调用了
close(fd),但映射仍存在)。
解决方案
在不再需要访问
pDecodeFrameBuf时,显式释放:c复制代码
munmap(pDecodeFrameBuf, bufLen);
6. 权限问题(00777 可能不安全)
问题描述
文件权限
00777(所有人可读写执行)可能存在安全隐患:其他用户可能篡改文件内容。
建议使用更严格的权限(如
00600,仅所有者可读写)。
解决方案
限制文件权限:
c复制代码
open(filename, O_RDWR | O_CREAT, 00600); // 仅当前用户可读写