3. mmap系统调用原理

mmap() 是 Linux 驱动与用户空间交互的核心系统调用,它允许用户程序将设备内存或文件直接映射到进程的虚拟地址空间。理解其原型、映射类型和标志是进行驱动开发和性能调优的基础。

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 映射的长度,以字节为单位。必须是系统页大小的整数倍(通常为 4096 字节)。
prot int 映射区域的内存保护标志,决定读写执行权限。
flags int 映射类型和属性标志,决定映射的可见性和行为。
fd int 文件描述符。对于文件映射,指向被映射的文件;对于匿名映射,通常设为 -1。
offset off_t 文件映射的起始偏移量,必须是页大小的整数倍。
返回值:成功时返回映射区域的起始虚拟地址(void *);失败时返回 MAP_FAILED(即 (void *) -1),并设置 errno。

3.2 映射类型:文件映射与匿名映射

根据映射的后端存储来源,mmap() 主要分为两种类型:

3.2.1 文件映射

将磁盘文件或设备文件(如 /dev/mem、/dev/fb0)的一部分映射到进程地址空间。对映射区域的读写操作会直接反映到文件中(取决于 flags)。

3.2.2 匿名映射

不关联任何文件,由内核分配一块物理内存并映射到进程地址空间。通常用于进程间通信(共享内存)或分配大块内存。标志 MAP_ANONYMOUS 用于指定匿名映射,此时 fd 参数被忽略。

mmap 映射类型对比 文件映射 (File-backed) 匿名映射 (Anonymous) 磁盘文件 (普通文件) 设备文件 (/dev/mem, /dev/fb0) 进程间共享内存 (IPC) 大块内存分配 (malloc 底层) 关键区别 • 文件映射:数据最终同步到文件,支持 MAP_SHARED 和 MAP_PRIVATE • 匿名映射:数据仅存在于内存,通常用于共享内存或堆扩展 • 驱动开发中,设备文件映射是访问硬件寄存器和 DMA 缓冲区的标准方式

3.3 映射标志:MAP_SHARED 与 MAP_PRIVATE

flags 参数中的 MAP_SHARED 和 MAP_PRIVATE 决定了映射的可见性和写时复制行为,这是驱动开发中必须理解的核心概念。

标志 行为 写时复制 对其他进程可见 典型用途
MAP_SHARED 对映射区域的修改会直接写回底层文件或设备,并立即对其他映射同一区域的进程可见。 否 是 设备寄存器映射、共享内存、文件 I/O 优化
MAP_PRIVATE 对映射区域的修改不会写回文件,而是创建一份私有副本(写时复制)。 是 否 程序加载、数据段初始化、私有内存分配
驱动开发注意:在驱动中实现 mmap 操作时,必须根据设备特性正确设置 vma->vm_flags。对于需要共享硬件状态的设备(如帧缓冲、DMA 缓冲区),必须使用 MAP_SHARED;如果使用 MAP_PRIVATE,用户空间的写入将不会反映到硬件,导致驱动行为异常。

3.4 内存保护标志 (prot)

prot 参数指定映射区域的内存保护权限,可以组合使用:

  • PROT_READ:页面可读。
  • PROT_WRITE:页面可写。
  • PROT_EXEC:页面可执行。
  • PROT_NONE:页面不可访问。
性能提示:在驱动映射中,如果设备内存不需要执行权限,应避免设置 PROT_EXEC,以减少 TLB 缓存污染和潜在的安全风险。对于只读的硬件状态寄存器,仅设置 PROT_READ 即可。

3.5 mmap 映射流程示意图

以下 SVG 展示了从用户态调用 mmap() 到内核完成映射的完整流程:

mmap 系统调用流程 用户态 (User Space) 应用程序调用 mmap() 内核态 (Kernel Space) 1. 检查参数 (addr, length, flags) 2. 查找文件/设备 inode 3. 调用驱动 mmap 回调 (f_op->mmap) 4. 创建 vma 并建立页表映射 返回映射地址给用户程序

3.6 驱动开发中的 mmap 实现要点

在 Linux 驱动中实现 mmap 操作时,通常需要完成以下步骤:

  1. 实现 file_operations 中的 mmap 回调:int (*mmap)(struct file *filp, struct vm_area_struct *vma);
  2. 设置 vma 操作:为 vma->vm_ops 提供 open、close、fault 等回调。
  3. 建立物理地址到虚拟地址的映射:使用 remap_pfn_range() 或 io_remap_pfn_range() 函数。
  4. 设置 vma 标志:根据设备特性设置 VM_SHARED、VM_IO、VM_PFNMAP 等标志。
// 驱动 mmap 回调示例
static int my_device_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long pfn = my_device_get_phys_addr(filp);

    // 设置映射标志:IO 内存、不可换出
    vma->vm_flags |= VM_IO | VM_PFNMAP | VM_SHARED;

    // 建立物理页帧到用户虚拟地址的映射
    if (remap_pfn_range(vma, vma->vm_start, pfn,
                        vma->vm_end - vma->vm_start,
                        vma->vm_page_prot)) {
        return -EAGAIN;
    }

    return 0;
}
调试建议:使用 cat /proc/<pid>/maps 可以查看进程的映射信息,包括映射地址、权限、偏移量和设备号。这是排查 mmap 映射故障的第一手工具。