Administrator
发布于 2025-09-09 / 17 阅读
0
0

mmap 底层原理及内存申请时机

mmap(Memory Map)是 Linux/Unix 系统提供的一种将文件或设备映射到进程虚拟地址空间的机制,其底层原理和内存申请时机涉及 虚拟内存管理缺页异常(Page Fault)内核交互。以下是详细分析:

1. mmap 的底层原理

(1) 核心流程

  1. 虚拟地址分配

    • 调用 mmap 时,内核仅在进程的虚拟地址空间中 预留一段连续的虚拟内存区域(VMA, Virtual Memory Area),并记录映射的权限(读/写/执行)和文件/设备信息。

    • 此时并未分配物理内存,仅修改进程的页表(Page Table)和虚拟内存结构(如 vm_area_struct)。

  2. 物理内存申请(延迟分配)

    • 当进程首次访问映射区域的某个地址时,触发 缺页异常(Page Fault),内核才会按需分配物理内存页(Page)。

    • 如果是文件映射,内核还会从磁盘读取文件内容到物理内存(若未缓存)。

  3. 同步到文件(可选)

    • 对于共享文件映射(MAP_SHARED),修改后的内容会由内核定期或通过 msync() 写回磁盘。

(2) 关键数据结构

  • vm_area_struct
    内核为每个 mmap 映射维护一个 VMA 结构,记录虚拟地址范围、权限、文件偏移等。

  • 页表(Page Table)
    虚拟地址到物理地址的映射关系,初始时页表项(PTE)标记为 不存在(Not Present),触发缺页异常后填充。

2. 内存实际申请时机

(1) 匿名映射(MAP_ANONYMOUS

  • 场景:用于申请动态内存(类似 malloc),不与文件关联。

  • 内存分配时机

    • 首次访问时:通过缺页异常分配物理页,并初始化为零(Zero Page)。

    • 例外:若指定 MAP_POPULATEmlock(),则立即分配物理内存。

(2) 文件映射(非匿名)

  • 场景:将文件内容映射到内存(如加载动态库)。

  • 内存分配时机

    • 首次访问时:内核分配物理页,并从磁盘读取文件内容(若页缓存中不存在)。

    • 预读优化:内核可能预加载后续文件内容(取决于访问模式)。

(3) 共享内存(MAP_SHARED

  • 场景:进程间共享内存或文件。

  • 内存分配时机

    • 与匿名/文件映射相同,但修改会同步到其他进程或文件。


3. 触发物理内存分配的操作

以下行为会迫使内核实际分配物理内存:

  1. 首次读写映射区域(触发缺页异常)。

  2. 显式调用:

    • mlock():锁定内存,强制分配物理页。

    • MAP_POPULATE 标志:在 mmap 时立即预分配内存。

  3. 写入共享文件映射(需将脏页写回磁盘)。

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); // 仅当前用户可读写



评论