3. mmap 系统调用详解:参数 prot、flags 与 offset 对齐要求
mmap 是 Linux 内核提供的核心内存映射系统调用,它允许用户态程序直接操作文件或匿名内存页。理解其三个核心参数——prot(保护标志)、flags(映射类型)和 offset(偏移对齐)——是掌握 DMA 缓冲区共享技术的基础。本节将逐一拆解这些参数的语义、组合规则和底层约束。
3.1 参数 prot:内存保护标志
prot 参数定义映射区域的内存访问权限,由以下标志按位或组合而成:
| 标志 | 值 | 含义 | 典型场景 |
|---|---|---|---|
PROT_READ |
0x1 | 允许读取映射区域 | 只读文件映射、代码段 |
PROT_WRITE |
0x2 | 允许写入映射区域 | DMA 缓冲区、共享内存 |
PROT_EXEC |
0x4 | 允许执行映射区域中的指令 | 动态库加载、JIT 编译 |
PROT_NONE |
0x0 | 禁止任何访问 | 守卫页、地址空间预留 |
关键规则: 对于 DMA 缓冲区共享场景,必须同时设置
PROT_READ | PROT_WRITE。如果只设置 PROT_READ,写入操作会触发 SIGSEGV 段错误。内核在建立页表时会根据 prot 设置页表项的 R/W 位,硬件 MMU 直接执行该检查。
3.2 参数 flags:映射类型与行为控制
flags 参数决定映射的共享属性、内存来源和内核行为。以下是最关键的三个标志:
3.2.1 MAP_SHARED vs MAP_PRIVATE
| 标志 | 写时复制 | 对文件的可见性 | 适用场景 |
|---|---|---|---|
MAP_SHARED |
否 | 写入立即同步回文件(或 DMA 设备) | DMA 缓冲区、进程间共享内存、文件 I/O 加速 |
MAP_PRIVATE |
是 | 写入仅对当前进程可见,不写回文件 | 程序加载、调试断点、只读数据私有副本 |
DMA 共享陷阱: 如果使用
MAP_PRIVATE 映射 DMA 缓冲区,写入操作会触发缺页异常并分配新的物理页(写时复制),导致用户态写入的地址与 DMA 设备实际操作的物理地址不一致,造成数据损坏。DMA 场景必须使用 MAP_SHARED。
3.2.2 MAP_ANONYMOUS
MAP_ANONYMOUS(或 MAP_ANON)创建一个不关联任何文件的匿名映射。此时 fd 参数应设为 -1,offset 被忽略。内核分配初始化为零的物理页。典型用途:
- 分配大块连续虚拟地址空间(如 malloc 底层使用)
- 进程间共享匿名内存(配合
MAP_SHARED和fork()) - DMA 缓冲区分配(通过
/dev/dma_heap等驱动)
3.2.3 其他重要 flags
| 标志 | 作用 |
|---|---|
MAP_FIXED |
强制在指定地址映射,若地址已被占用则覆盖 |
MAP_POPULATE |
预分配物理页,避免首次访问时缺页 |
MAP_LOCKED |
锁定映射页,防止被换出(类似 mlock) |
MAP_NORESERVE |
不预留交换空间,用于大块稀疏映射 |
3.3 offset 对齐要求:页边界约束
offset 参数指定映射在文件中的起始位置。内核要求 offset 必须是系统页大小的整数倍(通常为 4096 字节,即 PAGE_SIZE)。
底层原因: 内核的页缓存(page cache)和 MMU 页表均以页为单位管理。如果 offset 不对齐,内核无法将文件中的某个页直接映射到进程地址空间,因为文件页的起始位置与虚拟页的起始位置不匹配。内核会返回
EINVAL 错误。
对于 DMA 缓冲区场景,offset 通常设为 0,表示从文件(设备)的起始地址开始映射。如果设备驱动支持多缓冲区,offset 可设置为 n * PAGE_SIZE 来映射第 n 个缓冲区。
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("/dev/dma_buf", O_RDWR);
if (fd < 0) return -1;
size_t length = 4 * 1024 * 1024; // 4MB DMA 缓冲区
// 正确:offset 为 0(页对齐)
void *addr = mmap(NULL, length,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd, 0);
if (addr == MAP_FAILED) {
perror("mmap failed");
close(fd);
return -1;
}
// 错误示例:offset 不是页对齐
// void *bad = mmap(NULL, length,
// PROT_READ | PROT_WRITE,
// MAP_SHARED,
// fd, 1234); // 1234 % 4096 != 0 → EINVAL
// 使用映射区域...
munmap(addr, length);
close(fd);
return 0;
}
3.4 参数组合与内核校验流程
下图展示了内核在 mmap 系统调用入口处对 prot、flags 和 offset 的校验流程:
3.5 参数组合速查表
以下表格总结了不同场景下 prot 和 flags 的推荐组合:
| 场景 | prot | flags | fd | offset |
|---|---|---|---|---|
| DMA 缓冲区共享 | PROT_READ | PROT_WRITE |
MAP_SHARED |
设备 fd | 0 或页对齐 |
| 进程间匿名共享 | PROT_READ | PROT_WRITE |
MAP_SHARED | MAP_ANONYMOUS |
-1 | 0 |
| 只读文件映射 | PROT_READ |
MAP_PRIVATE |
文件 fd | 0 |
| 可执行代码加载 | PROT_READ | PROT_EXEC |
MAP_PRIVATE |
ELF fd | 页对齐 |
| 大块匿名内存 | PROT_READ | PROT_WRITE |
MAP_PRIVATE | MAP_ANONYMOUS |
-1 | 0 |
调试技巧: 使用
strace -e mmap 跟踪进程的 mmap 调用,可以观察实际传入的 prot、flags 和 offset 值,帮助排查参数错误。例如:mmap(NULL, 4194304, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0) = 0x7f1234560000
3.6 常见错误与排查
- EINVAL:offset 不是页对齐,或 prot/flags 包含非法组合(如
PROT_NONE | PROT_READ) - EACCES:文件未以正确的权限打开(如
PROT_WRITE但 fd 是只读打开的) - ENOMEM:无法分配足够的虚拟地址空间或物理内存
- ENODEV:文件系统不支持
mmap(如某些伪文件系统)