4. mmap 系统调用原理:原型、映射类型与映射标志
mmap 是 Linux 内核提供给用户空间最强大的内存管理接口之一。它允许用户程序将设备内存、文件内容或匿名内存直接映射到进程的虚拟地址空间,从而绕过传统的 read/write 系统调用,实现零拷贝的高效数据访问。理解 mmap 的原型、映射类型和映射标志,是编写 mmap 驱动的基础。
4.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 |
映射区域的字节长度 | 设备寄存器区域大小 |
| prot | int |
映射区域的内存保护标志 | PROT_READ | PROT_WRITE |
| flags | int |
映射类型和共享属性 | MAP_SHARED 或 MAP_PRIVATE |
| fd | int |
文件描述符(文件映射时使用) | 设备文件 fd 或 -1 |
| offset | off_t |
文件内的偏移量(页对齐) | 物理地址偏移 |
驱动开发要点: 在设备驱动中,
fd 对应通过 open() 打开的设备文件,offset 通常设置为物理地址的页内偏移。内核驱动通过 mmap 文件操作来响应这个调用。
4.2 映射类型:文件映射 vs 匿名映射
mmap 根据映射的数据来源分为两种基本类型:
4.2.1 文件映射
- 定义: 将文件或设备内存的某一段映射到进程地址空间。
- 关键参数:
fd必须是一个有效的文件描述符,offset指定文件内的起始位置。 - 驱动视角: 设备驱动通过实现
file_operations中的.mmap方法,将设备物理地址映射给用户空间。 - 典型应用: 帧缓冲设备、DMA 缓冲区、硬件寄存器访问。
4.2.2 匿名映射
- 定义: 映射一块不与任何文件关联的物理内存。
- 关键参数:
fd设置为-1,flags中必须包含MAP_ANONYMOUS。 - 驱动视角: 匿名映射通常用于用户空间进程间通信(IPC),驱动中较少直接使用。
- 典型应用:
malloc()内部实现、共享内存shmget、栈自动扩展。
驱动开发重点: 设备驱动 mmap 属于文件映射类型。用户空间调用 mmap 时传入设备文件 fd,内核最终调用驱动注册的
.mmap 回调函数,由驱动负责将物理地址映射到用户虚拟地址空间。
4.3 映射标志:MAP_SHARED vs MAP_PRIVATE
映射标志决定了多个进程映射同一区域时的可见性和写传播行为,这是 mmap 语义的核心。
4.3.1 MAP_SHARED(共享映射)
- 行为: 对映射区域的写操作会直接修改底层文件或设备内存,并且对其他映射了同一区域的进程可见。
- 内核机制: 页表项设置为可写且共享,缺页时建立的物理页直接关联到文件页缓存或设备内存。
- 驱动要求: 设备驱动 mmap 必须使用
MAP_SHARED,否则用户空间的写操作无法到达硬件寄存器。 - 同步保证: 写操作对其他进程立即可见(取决于缓存一致性)。
4.3.2 MAP_PRIVATE(私有映射)
- 行为: 对映射区域的写操作会触发“写时复制”(Copy-on-Write),创建一份私有副本,修改不写回底层文件。
- 内核机制: 初始时页表项设置为只读,第一次写操作触发缺页中断,内核分配新物理页并复制内容。
- 驱动限制: 设备驱动 mmap 禁止使用
MAP_PRIVATE,因为写时复制会导致用户空间操作的不是原始物理地址。 - 典型用途: 程序加载、动态库映射(避免修改影响其他进程)。
驱动开发陷阱: 用户空间调用 mmap 时如果错误地使用了
MAP_PRIVATE,驱动虽然可能成功返回,但后续用户对映射区域的写操作将不会反映到硬件寄存器上。驱动开发者应在 .mmap 回调中检查 vma->vm_flags,确保包含 VM_SHARED 标志,否则返回错误。
4.4 映射标志组合速查表
| 标志组合 | 映射类型 | 写可见性 | 写回后端 | 驱动适用性 |
|---|---|---|---|---|
MAP_SHARED |
文件映射 | 所有进程可见 | 立即写回 | ✅ 必须使用 |
MAP_PRIVATE |
文件映射 | 仅本进程可见 | 不写回 | ❌ 禁止使用 |
MAP_SHARED | MAP_ANONYMOUS |
匿名共享 | 所有进程可见 | N/A | IPC 场景 |
MAP_PRIVATE | MAP_ANONYMOUS |
匿名私有 | 仅本进程可见 | N/A | malloc 实现 |
4.5 驱动开发中的 mmap 标志检查
在设备驱动的 .mmap 回调函数中,必须验证用户空间传递的映射标志是否符合要求。以下是一个典型的内核驱动检查代码片段:
static int my_device_mmap(struct file *filp, struct vm_area_struct *vma)
{
// 检查映射标志:必须为共享映射
if (!(vma->vm_flags & VM_SHARED)) {
pr_err("mmap: MAP_PRIVATE is not supported\n");
return -EINVAL;
}
// 检查保护标志:至少需要读权限
if (!(vma->vm_flags & VM_READ)) {
pr_err("mmap: PROT_READ is required\n");
return -EACCES;
}
// 设置映射区域为 IO 内存(禁止缓存)
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
// 调用 remap_pfn_range 建立页表映射
if (remap_pfn_range(vma, vma->vm_start,
phys_addr >> PAGE_SHIFT,
vma->vm_end - vma->vm_start,
vma->vm_page_prot)) {
pr_err("mmap: remap_pfn_range failed\n");
return -EAGAIN;
}
return 0;
}
关键点总结:
- 用户空间 mmap 的
flags参数中的MAP_SHARED对应内核 VMA 的VM_SHARED标志。 - 驱动必须检查
VM_SHARED,拒绝MAP_PRIVATE请求。 - 物理地址映射使用
remap_pfn_range()或io_remap_pfn_range()完成。 - IO 内存需要设置
pgprot_noncached()禁止 CPU 缓存,确保对硬件寄存器的读写直接生效。