4. mmap 系统调用流程:从用户态到内核的完整路径
mmap 系统调用是用户空间与内核空间之间建立直接内存映射的桥梁。理解其完整调用流程,对于编写正确的驱动 mmap 实现至关重要。本章将沿着用户态 mmap() → 内核 sys_mmap → do_mmap → 驱动 mmap 回调这条主线,逐层剖析每个阶段的核心职责与数据流转。
4.1 用户态 mmap 调用:入口与参数
用户态程序通过 C 库封装的 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 |
建议起始虚拟地址 | 通常传 NULL,由内核选择 |
length |
映射区域大小(字节) | 必须是页大小(4KB)的整数倍 |
prot |
内存保护标志 | PROT_READ、PROT_WRITE、PROT_EXEC 的组合 |
flags |
映射类型与行为 | MAP_SHARED 或 MAP_PRIVATE,驱动常用 MAP_SHARED |
fd |
文件描述符 | 指向通过 open() 打开的驱动设备文件 |
offset |
文件偏移量 | 驱动中通常为 0,或表示物理页帧号 |
offset 参数在驱动 mmap 回调中会以 vma->vm_pgoff 的形式出现。很多驱动利用这个字段传递物理页帧号(PFN),从而让用户态选择映射哪一页物理内存。
用户态调用 mmap() 后,C 库会执行 syscall 指令,陷入内核,进入系统调用处理程序。在 x86-64 架构上,系统调用号 9 对应 sys_mmap。
4.2 内核 sys_mmap:系统调用入口
内核中 sys_mmap 是 mmap 系统调用的入口函数。在较新内核版本中,它通常定义为 SYSCALL_DEFINE6(mmap, ...)。其核心工作是将用户态参数转换为内核内部结构,并调用 do_mmap()。
// 简化后的 sys_mmap 逻辑(arch/x86/kernel/sys_x86_64.c)
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
unsigned long, prot, unsigned long, flags,
unsigned long, fd, unsigned long, off)
{
// 1. 参数合法性检查(长度对齐、权限校验等)
if (offset_in_page(off))
return -EINVAL;
// 2. 通过 fd 获取 struct file 指针
struct fd f = fdget(fd);
struct file *file = f.file;
// 3. 调用 do_mmap 完成核心映射工作
unsigned long ret = do_mmap(file, addr, len, prot, flags, off);
fdput(f);
return ret;
}
sys_mmap 的关键职责可以归纳为三点:
- 参数转换:将用户态传入的
fd(整数)转换为内核的struct file *指针。 - 合法性校验:检查偏移量是否页对齐、长度是否合理、权限是否冲突。
- 委托执行:将实际工作交给
do_mmap(),自身不做映射决策。
sys_mmap 的实现略有差异。64 位系统通常使用 SYSCALL_DEFINE6 直接定义,而 32 位系统可能通过 sys_mmap2 处理偏移量以字节为单位的差异。
4.3 do_mmap:虚拟地址空间分配
do_mmap() 是 mmap 流程中最重要的内核函数之一,位于 mm/mmap.c。它负责在进程的虚拟地址空间中分配一块合适的区域,并创建对应的 struct vm_area_struct(VMA)结构。
// do_mmap 核心流程(mm/mmap.c,高度简化)
unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, unsigned long offset)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma;
// 1. 获取地址空间写锁
if (mmap_write_lock_killable(mm))
return -EINTR;
// 2. 查找或分配虚拟地址区间
addr = get_unmapped_area(file, addr, len, offset, flags);
// 3. 分配并初始化 VMA 结构
vma = vm_area_alloc(mm);
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags);
vma->vm_file = get_file(file);
vma->vm_pgoff = offset >> PAGE_SHIFT;
// 4. 调用文件操作中的 mmap 回调(关键步骤)
if (file) {
ret = call_mmap(file, vma);
if (ret)
goto unmap_and_free_vma;
}
// 5. 将 VMA 插入进程的红黑树和链表
vma_link(mm, vma, prev, rb_link, rb_parent);
mmap_write_unlock(mm);
return addr;
}
do_mmap() 的执行流程可以概括为以下五个阶段:
- 地址空间锁定:获取进程内存描述符
mm的写锁,防止并发修改。 - 地址分配:调用
get_unmapped_area()在虚拟地址空间中查找空闲区间。 - VMA 创建:分配并初始化 VMA 结构,设置起始地址、结束地址、标志位和文件信息。
- 驱动回调:如果映射关联了文件(设备驱动),调用
file->f_op->mmap()。 - VMA 注册:将 VMA 插入进程的 VMA 红黑树和双向链表,完成地址空间管理。
do_mmap() 的第 4 步中,内核会调用 call_mmap(file, vma),这个宏最终会执行 file->f_op->mmap(file, vma)。这就是驱动 mmap 回调被调用的时刻。如果驱动没有实现 mmap 回调,内核会返回 -ENODEV。
4.4 文件操作中的 mmap 回调:驱动的舞台
当 do_mmap() 调用 call_mmap() 时,控制权转移到驱动实现的 file_operations 中的 mmap 方法。这是驱动开发者唯一需要关注的函数,其原型为:
int (*mmap)(struct file *filp, struct vm_area_struct *vma);
驱动 mmap 回调接收两个参数:
filp:指向已打开的设备的struct file结构,驱动可以通过filp->private_data获取设备私有数据。vma:指向内核已分配好的虚拟内存区域结构,驱动需要在这个 VMA 上建立物理内存映射。
驱动 mmap 回调的标准实现模式如下:
static int my_device_mmap(struct file *filp, struct vm_area_struct *vma)
{
unsigned long pfn; // 物理页帧号
unsigned long size; // 映射大小
struct my_device *dev = filp->private_data;
// 1. 获取映射大小(页对齐)
size = vma->vm_end - vma->vm_start;
// 2. 确定要映射的物理页帧号
// 可以从 vma->vm_pgoff 获取,也可以从设备结构获取
pfn = virt_to_phys(dev->buffer) >> PAGE_SHIFT;
// 3. 设置 VMA 的操作函数集(可选,用于处理缺页中断)
vma->vm_ops = &my_device_vm_ops;
// 4. 建立页表映射(核心操作)
if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot))
return -EAGAIN;
return 0;
}
驱动 mmap 回调的核心职责可以总结为一张表格:
| 职责 | 说明 | 常用 API / 方法 |
|---|---|---|
| 确定物理页帧 | 决定将哪块物理内存映射给用户态 | virt_to_phys()、__pa()、dma_alloc_coherent() |
| 建立页表映射 | 在进程页表中插入物理地址到虚拟地址的映射 | remap_pfn_range()、io_remap_pfn_range() |
| 设置 VMA 操作 | 注册缺页处理、关闭通知等回调 | vma->vm_ops 赋值 |
| 设置 VMA 标志 | 标记映射类型(可锁定、可混合映射等) | vma->vm_flags 位操作 |
4.5 完整调用流程 SVG 架构图
下图展示了从用户态 mmap() 到驱动 mmap 回调的完整调用链,以及各层之间的数据传递关系:
4.6 流程中的关键数据结构
在整个 mmap 调用链中,有几个核心数据结构贯穿始终。理解它们之间的关系,是掌握 mmap 流程的关键:
| 数据结构 | 所在层级 | 核心字段(与 mmap 相关) | 作用 |
|---|---|---|---|
struct file |
内核 VFS 层 | f_op、private_data |
代表打开的设备文件,提供操作函数表 |
struct vm_area_struct |
内核内存管理 | vm_start、vm_end、vm_pgoff、vm_flags、vm_ops |
描述一段连续的虚拟地址空间 |
struct mm_struct |
内核内存管理 | mmap(红黑树根)、mmap_lock |
进程级内存描述符,管理所有 VMA |
struct vm_operations_struct |
驱动层 | open、close、fault |
VMA 的生命周期回调,驱动可自定义 |
mmap() → sys_mmap 将 fd 转为 struct file → do_mmap 创建 struct vm_area_struct → 驱动 mmap 回调通过 remap_pfn_range() 在 VMA 上建立物理映射。驱动开发者只需关注最后一个环节,但理解前三个环节有助于调试和性能优化。
4.7 常见问题与避坑指南
- mmap 回调未被调用:检查
file_operations结构体中.mmap字段是否正确定义,且设备文件已通过open()打开。 - remap_pfn_range 返回 -EINVAL:通常是因为
vma->vm_start或size未页对齐,或者pfn超出了有效物理地址范围。 - 用户态访问映射区域触发 SIGBUS:驱动 mmap 回调中未正确建立页表映射,或者物理内存已被释放。
- 映射后写入不生效:检查
vma->vm_flags是否包含VM_WRITE标志,以及用户态prot参数是否包含PROT_WRITE。