3. mmap 系统调用原理
mmap 是 Linux 中实现用户空间与内核空间高效数据交换的核心系统调用。它允许用户程序将设备内存或文件直接映射到进程的虚拟地址空间,从而避免传统的 read/write 拷贝开销。在 DMA 缓冲区共享场景中,mmap 是驱动向用户态暴露物理连续内存的标准接口。
3.1 mmap 函数原型与参数
mmap 系统调用的用户态原型定义在 <sys/mman.h> 中:
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
各参数含义如下表:
| 参数 | 类型 | 说明 |
|---|---|---|
addr |
void * |
映射起始地址建议值,通常传 NULL 由内核选择 |
length |
size_t |
映射长度(字节),必须为页大小(4KB)的整数倍 |
prot |
int |
内存保护标志:PROT_READ、PROT_WRITE、PROT_EXEC、PROT_NONE |
flags |
int |
映射类型与行为:MAP_SHARED、MAP_PRIVATE、MAP_ANONYMOUS 等 |
fd |
int |
文件描述符(文件映射时有效,匿名映射传 -1) |
offset |
off_t |
文件内偏移量,必须为页大小对齐 |
关键约束:驱动开发中,
offset 通常被驱动用于区分不同的 DMA 缓冲区或物理内存区域。用户态传入的 offset 会直接传递给驱动的 mmap 回调函数,驱动据此决定映射哪一段物理内存。
3.2 映射类型:文件映射 vs 匿名映射
mmap 根据 flags 参数分为两大类:
- 文件映射(File-backed Mapping):将文件或设备内存映射到进程地址空间。设置
fd为打开的文件描述符,offset指定文件内起始位置。驱动设备映射属于此类。 - 匿名映射(Anonymous Mapping):不关联任何文件,由内核分配零填充的物理页。设置
flags包含MAP_ANONYMOUS,fd传 -1。常用于进程间共享内存(MAP_SHARED | MAP_ANONYMOUS)或 malloc 内部实现。
| 特性 | 文件映射 | 匿名映射 |
|---|---|---|
| 关联对象 | 文件或设备 | 无(内核匿名页) |
| fd 参数 | 有效文件描述符 | -1 |
| MAP_ANONYMOUS | 不设置 | 必须设置 |
| 数据来源 | 文件内容或设备内存 | 全零页(写时拷贝) |
| 驱动场景 | DMA 缓冲区、帧缓冲、寄存器映射 | 用户态进程间共享内存 |
| 回写行为 | MAP_SHARED 时写回文件 | 无回写(内存匿名) |
驱动开发重点:设备驱动使用文件映射方式实现
mmap。用户态通过 open 设备节点获得 fd,然后调用 mmap(fd, ...) 触发驱动的 file_operations.mmap 回调。驱动在该回调中建立用户虚拟地址到物理 DMA 内存的映射关系。
3.3 VMA(虚拟内存区域)结构
每次 mmap 调用成功后,内核会在进程的虚拟地址空间中创建一个 VMA(Virtual Memory Area)。VMA 是描述一段连续虚拟内存区域的内核数据结构,定义在 linux/mm_types.h 中。
3.3.1 VMA 核心字段
| 字段 | 类型 | 说明 |
|---|---|---|
vm_start |
unsigned long |
VMA 起始虚拟地址 |
vm_end |
unsigned long |
VMA 结束虚拟地址(不含) |
vm_flags |
unsigned long |
VMA 标志位:读/写/执行/共享/私有等 |
vm_file |
struct file * |
关联的文件对象(文件映射时有效) |
vm_ops |
const struct vm_operations_struct * |
VMA 操作函数表(驱动需实现) |
vm_private_data |
void * |
驱动私有数据指针 |
3.3.2 vm_operations_struct 回调
驱动在 mmap 回调中需要设置 vma->vm_ops,指向驱动自定义的操作函数表:
struct vm_operations_struct {
void (*open)(struct vm_area_struct *vma); // VMA 被复制时调用
void (*close)(struct vm_area_struct *vma); // VMA 被销毁时调用
vm_fault_t (*fault)(struct vm_fault *vmf); // 缺页处理(核心回调)
vm_fault_t (*page_mkwrite)(struct vm_fault *vmf); // 页变为可写时调用
int (*access)(struct vm_area_struct *vma, unsigned long addr,
void *buf, int len, int write); // 内核访问用户页
};
驱动常见错误:许多初学者在
mmap 回调中直接调用 remap_pfn_range 映射物理内存,却忘记设置 vma->vm_ops。这会导致 VMA 缺少正确的生命周期管理,在进程 fork 或内存回收时出现内核崩溃。对于 DMA 缓冲区映射,至少应实现 open 和 close 回调来管理引用计数。
3.4 mmap 映射流程与 VMA 关系
下图展示了用户态调用 mmap 到驱动建立 VMA 的完整流程:
3.5 驱动 mmap 回调实现要点
驱动中 file_operations.mmap 回调的标准实现模板:
static int my_device_mmap(struct file *filp, struct vm_area_struct *vma)
{
struct my_dev *dev = filp->private_data;
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
unsigned long size = vma->vm_end - vma->vm_start;
unsigned long pfn;
// 1. 校验 offset 和 size 合法性
if (offset + size > dev->dma_buf_size)
return -EINVAL;
// 2. 设置 VMA 标志:禁止交换、禁止合并
vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;
// 3. 设置 VMA 操作函数表
vma->vm_ops = &my_vm_ops;
vma->vm_private_data = dev;
// 4. 调用 open 回调增加引用计数
my_vm_ops.open(vma);
// 5. 建立页表映射
pfn = virt_to_phys(dev->dma_buf_virt) >> PAGE_SHIFT;
pfn += (offset >> PAGE_SHIFT);
if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot))
return -EAGAIN;
return 0;
}
VMA 标志位说明:
这三个标志是 DMA 缓冲区映射的标准配置,缺一不可。
VM_IO — 标记该 VMA 映射的是 I/O 内存,内核不会对其做页缓存或回写。VM_DONTEXPAND — 禁止通过 mremap 扩展该 VMA。VM_DONTDUMP — 禁止在 core dump 中包含该 VMA 内容(DMA 缓冲区通常不需要 dump)。这三个标志是 DMA 缓冲区映射的标准配置,缺一不可。
3.6 VMA 与缺页异常
当用户进程访问映射的虚拟地址但对应页表项无效时,CPU 触发缺页异常,内核调用 VMA 的 fault 回调。对于使用 remap_pfn_range 预先建立页表映射的 DMA 缓冲区,缺页异常不会发生,因为页表项在 mmap 时已全部建立。
但对于稀疏映射或按需映射的场景(如帧缓冲设备),驱动需要实现 fault 回调:
static vm_fault_t my_vma_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct my_dev *dev = vma->vm_private_data;
unsigned long offset = (vmf->pgoff - vma->vm_pgoff) << PAGE_SHIFT;
struct page *page;
// 根据 offset 获取对应的物理页
page = my_get_dma_page(dev, offset);
if (!page)
return VM_FAULT_SIGBUS;
// 将物理页插入到 vmf 的页表
vmf->page = page;
return 0;
}
性能对比:
DMA 缓冲区共享通常使用
remap_pfn_range 方式:mmap 时一次性建立所有页表,访问时无缺页开销,适合连续物理内存。fault 回调方式:按需建立页表,节省页表内存,适合稀疏映射或内存池管理。DMA 缓冲区共享通常使用
remap_pfn_range 方式以获得最佳访问性能。
3.7 本章小结
mmap系统调用通过prot和flags控制映射权限与类型,驱动开发中主要使用文件映射 +MAP_SHARED。- VMA 是内核管理虚拟内存区域的核心结构,驱动通过
vm_ops和vm_private_data与 VMA 交互。 - 驱动
mmap回调的标准流程:校验参数 → 设置 VMA 标志 → 设置vm_ops→ 调用open→ 建立页表映射。 VM_IO | VM_DONTEXPAND | VM_DONTDUMP是 DMA 缓冲区 VMA 的标准标志组合。- 缺页异常处理(
fault回调)提供了按需映射的灵活性,但 DMA 共享场景更推荐remap_pfn_range的预映射方式。