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 节点。

进程地址空间与 VMA 结构 进程 mm_struct mmap (红黑树根) mmap_cache (链表) VMA 1 vm_start=0x7f000000 vm_end=0x7f001000 VMA 2 (Framebuffer) vm_start=0x7f002000 vm_pgoff=0, vm_file=fbdev VMA 3 vm_start=0x7f003000 vm_end=0x7f005000 VMA 关键特征 • 红黑树:快速查找 • 链表:遍历所有 VMA • 每个 VMA 独立权限 • 驱动通过 vm_ops 介入

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 或默认行为
mmap 系统调用内核执行路径 用户态 mmap() ksys_mmap_pgoff() vm_mmap_pgoff() do_mmap_pgoff() do_mmap() — 核心函数 分配 VMA、检查权限、调用文件 mmap call_mmap() → f_op->mmap() 路径关键点 1. 参数校验与转换 2. 获取 mmap 写锁 3. 查找空闲虚拟地址 4. 创建 VMA 结构 5. 调用驱动 mmap 方法 6. 建立页表映射 7. 返回用户态虚拟地址

4.3 do_mmap 函数深度解析

do_mmap 是 mmap 系统调用的核心实现,位于 mm/mmap.c。它负责将文件或设备映射到进程地址空间。

4.3.1 do_mmap 主要工作流程

  1. 参数校验与调整:检查 len、prot、flags 合法性,对齐长度到页边界。
  2. 查找虚拟地址:调用 get_unmapped_area 在进程地址空间中查找空闲区域。
  3. 创建 VMA:分配 struct vm_area_struct,初始化起始地址、结束地址、标志位。
  4. 调用文件 mmap:通过 call_mmap 调用驱动注册的 mmap 方法。
  5. 插入 VMA:将新 VMA 插入进程的红黑树和链表。
  6. 返回地址:返回映射的起始虚拟地址。
驱动开发者关注点: 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),将虚拟地址映射到指定的物理页帧。它不分配物理内存,而是建立现有物理内存的映射。

remap_pfn_range 映射原理 虚拟地址空间 VMA vm_start VMA vm_start + PAGE VMA vm_start + 2*PAGE VMA vm_end - PAGE 页表 (PTE) PTE → PFN 0x100 PTE → PFN 0x101 PTE → PFN 0x102 PTE → PFN 0x10N 物理内存 (Framebuffer) PFN 0x100 PFN 0x101 PFN 0x102 PFN 0x10N remap_pfn_range 逐页建立虚拟地址 → 页表 → 物理页帧的映射,不分配新物理内存

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);
}
重要注意事项: