4. mmap系统调用原理:虚拟内存区域(VMA)概念、mmap在内核中的执行路径、do_mmap与remap_pfn_range
mmap 是 Framebuffer 驱动实现用户空间直接显存访问的核心系统调用。本章深入剖析 mmap 的底层原理,包括虚拟内存区域(VMA)的数据结构、系统调用在内核中的完整执行路径,以及驱动开发者最关心的 do_mmap 与 remap_pfn_range 两个关键函数的工作机制。
4.1 虚拟内存区域(VMA)概念
VMA(Virtual Memory Area)是 Linux 内核用于描述进程地址空间中一段连续虚拟内存区域的核心数据结构。每个 VMA 对应一组具有相同权限和映射属性的虚拟页。
4.1.1 VMA 数据结构
内核使用 struct vm_area_struct 描述 VMA,其关键字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
vm_start |
unsigned long | VMA 起始虚拟地址(包含) |
vm_end |
unsigned long | VMA 结束虚拟地址(不包含) |
vm_flags |
unsigned long | VMA 标志位(读/写/执行/共享/私有等) |
vm_file |
struct file * | 映射的文件对象(匿名映射为 NULL) |
vm_ops |
const struct vm_operations_struct * | VMA 操作函数集(open/close/fault/access) |
vm_pgoff |
unsigned long | 映射的文件偏移(以页为单位) |
vm_private_data |
void * | 驱动私有数据指针 |
关键理解: 在 Framebuffer 驱动中,
vm_private_data 通常指向 struct fb_info 结构体,使 VMA 操作函数能够访问帧缓冲设备的私有数据。
4.1.2 VMA 与进程地址空间的关系
每个进程的 mm_struct 维护一棵红黑树(mmap 字段)和一个链表(mmap_cache),用于快速查找和管理所有 VMA。当用户调用 mmap 时,内核在进程地址空间中创建一个新的 VMA 节点。
4.2 mmap 在内核中的执行路径
用户态调用 mmap() 后,内核经历一系列函数调用,最终到达驱动注册的 mmap 方法。完整路径如下:
4.2.1 系统调用入口
// 用户态调用
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 内核入口: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)
{
return ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
}
4.2.2 核心执行路径
| 步骤 | 函数 | 位置 | 说明 |
|---|---|---|---|
| 1 | ksys_mmap_pgoff |
mm/mmap.c | 参数校验,获取文件描述符对应的 struct file |
| 2 | vm_mmap_pgoff |
mm/util.c | 获取 mmap 锁,调用 do_mmap_pgoff |
| 3 | do_mmap_pgoff |
mm/mmap.c | 调用 do_mmap,处理标志位和偏移 |
| 4 | do_mmap |
mm/mmap.c | 核心函数:分配 VMA,调用文件系统的 mmap 方法 |
| 5 | call_mmap |
include/linux/fs.h | 调用 file->f_op->mmap(),即驱动注册的 mmap 函数 |
| 6 | fb_mmap |
drivers/video/fbdev/core/fbmem.c | Framebuffer 通用 mmap 实现,调用驱动自定义 mmap 或默认行为 |
4.3 do_mmap 函数深度解析
do_mmap 是 mmap 系统调用的核心实现,位于 mm/mmap.c。它负责将文件或设备映射到进程地址空间。
4.3.1 do_mmap 主要工作流程
- 参数校验与调整:检查 len、prot、flags 合法性,对齐长度到页边界。
- 查找虚拟地址:调用
get_unmapped_area在进程地址空间中查找空闲区域。 - 创建 VMA:分配
struct vm_area_struct,初始化起始地址、结束地址、标志位。 - 调用文件 mmap:通过
call_mmap调用驱动注册的 mmap 方法。 - 插入 VMA:将新 VMA 插入进程的红黑树和链表。
- 返回地址:返回映射的起始虚拟地址。
驱动开发者关注点: do_mmap 在步骤 4 调用驱动 mmap 方法时,VMA 已经创建但尚未建立页表映射。驱动 mmap 方法的核心任务就是调用
remap_pfn_range 或 io_remap_pfn_range 建立物理地址到虚拟地址的映射。
4.3.2 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 pgoff)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma;
unsigned long vm_flags;
// 1. 参数校验
if (!len)
return -EINVAL;
len = PAGE_ALIGN(len);
// 2. 计算 vm_flags
vm_flags = calc_vm_prot_bits(prot, 0) | calc_vm_flag_bits(flags);
vm_flags |= mm->def_flags;
// 3. 查找空闲虚拟地址
addr = get_unmapped_area(file, addr, len, pgoff, flags);
if (IS_ERR_VALUE(addr))
return addr;
// 4. 分配并初始化 VMA
vma = vm_area_alloc(mm);
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;
vma->vm_file = get_file(file);
// 5. 调用文件系统的 mmap
if (file) {
error = call_mmap(file, vma);
if (error)
goto unmap_and_free_vma;
}
// 6. 插入 VMA 到进程地址空间
vma_link(mm, vma, prev, rb_link, rb_parent);
return addr;
}
4.4 remap_pfn_range 函数详解
remap_pfn_range 是驱动 mmap 方法中最核心的函数,负责将连续的物理页帧号(PFN)映射到 VMA 指定的虚拟地址范围。
4.4.1 函数原型与参数
// include/linux/mm.h
int remap_pfn_range(struct vm_area_struct *vma,
unsigned long addr, // 目标虚拟起始地址
unsigned long pfn, // 物理页帧号起始
unsigned long size, // 映射大小(字节)
pgprot_t prot); // 页保护属性
| 参数 | 说明 |
|---|---|
vma |
当前正在处理的 VMA 结构体指针 |
addr |
映射的目标虚拟地址,必须与 vma->vm_start 一致 |
pfn |
物理页帧号,通过 virt_to_phys() >> PAGE_SHIFT 或 dma_alloc_coherent 返回的 DMA 地址转换得到 |
size |
映射区域大小,必须是 PAGE_SIZE 的整数倍 |
prot |
页表项的保护位,通常使用 vma->vm_page_prot |
4.4.2 工作原理
remap_pfn_range 遍历 VMA 覆盖的每个虚拟页,为每个页创建页表项(PTE),将虚拟地址映射到指定的物理页帧。它不分配物理内存,而是建立现有物理内存的映射。
4.4.3 Framebuffer 驱动中的典型用法
// drivers/video/fbdev/core/fbmem.c
static int fb_mmap(struct file *file, struct vm_area_struct *vma)
{
struct fb_info *info = file->private_data;
unsigned long mmio_pgoff;
unsigned long start;
u32 len;
// 计算物理页帧号
start = info->fix.smem_start; // 帧缓冲物理地址
len = info->fix.smem_len; // 帧缓冲大小
mmio_pgoff = PAGE_ALIGN((start & ~PAGE_MASK) + len) >> PAGE_SHIFT;
// 判断映射类型:显存 vs MMIO
if (vma->vm_pgoff >= mmio_pgoff) {
// 映射 MMIO 寄存器区域
vma->vm_pgoff -= mmio_pgoff;
start = info->fix.mmio_start;
len = info->fix.mmio_len;
} else {
// 映射帧缓冲显存
start += (vma->vm_pgoff << PAGE_SHIFT);
}
// 设置页属性(禁止缓存,保证显存访问一致性)
vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
// 核心映射调用
return remap_pfn_range(vma, vma->vm_start,
start >> PAGE_SHIFT,
len, vma->vm_page_prot);
}
重要注意事项:
remap_pfn_range只能映射连续的物理页帧,不适用于分散的 DMA 缓冲区。- 对于 Framebuffer,必须使用
pgprot_writecombine或