3、用户空间 mmap 系统调用:函数原型、参数详解与返回值

在嵌入式 Linux 驱动开发中,mmap 是用户空间与内核空间共享内存的核心系统调用。它允许用户程序将设备内存或内核缓冲区直接映射到进程的虚拟地址空间,从而避免昂贵的 read/write 数据拷贝,是实现高性能 DMA 传输的关键桥梁。本节将深入剖析 mmap 的函数原型、每个参数的含义、返回值及常见错误码。

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);

该函数请求内核在调用进程的虚拟地址空间中创建一个新的内存映射。映射的源可以是文件描述符 fd 引用的文件或设备,也可以是匿名内存(flags 中指定 MAP_ANONYMOUS)。

3.2 参数详解

下表列出了 mmap 的六个参数及其详细说明:

参数 类型 含义与说明
addr void * 映射的起始虚拟地址建议值。通常传入 NULL,由内核自动选择合适的地址。若指定非 NULL 值,内核会尽量在附近分配,但仅当 flags 包含 MAP_FIXED 时才强制使用该地址。
length size_t 映射的字节长度。必须是系统页大小(通常 4096 字节)的整数倍。若不足一页,内核会向上取整到页边界,但多出的部分会被填充为零。
prot int 映射区域的内存保护标志。使用位掩码组合,决定该区域是否可读、可写、可执行。
flags int 映射类型与行为标志。控制映射的可见性、更新传播方式、是否共享等核心属性。
fd int 被映射的文件或设备文件描述符。对于匿名映射,此参数应设为 -1,且 flags 需包含 MAP_ANONYMOUS。
offset off_t 文件或设备中映射的起始偏移量。必须是系统页大小的整数倍。通常设为 0,表示从文件/设备开头开始映射。

3.2.1 prot 参数:内存保护标志

prot 参数可以是以下值的按位或组合:

  • PROT_READ (0x1):映射区域可读。
  • PROT_WRITE (0x2):映射区域可写。
  • PROT_EXEC (0x4):映射区域可执行(通常用于代码段映射)。
  • PROT_NONE (0x0):映射区域不可访问。访问该区域将触发 SIGSEGV 信号。
驱动开发提示: 对于 DMA 缓冲区映射,通常使用 PROT_READ | PROT_WRITE,允许用户空间同时读取和写入共享缓冲区。切勿随意添加 PROT_EXEC,除非映射的是可执行代码。

3.2.2 flags 参数:映射类型与行为

flags 参数是 mmap 最复杂的部分,它决定了映射的语义。常用标志如下:

标志 值 说明
MAP_SHARED 0x01 共享映射。对映射区域的写操作会直接反映到被映射的文件/设备中,并且对其他映射了同一区域的进程可见。驱动中映射 DMA 缓冲区时必须使用此标志。
MAP_PRIVATE 0x02 私有映射。对映射区域的写操作会触发写时复制(Copy-on-Write),不会写回原文件/设备。主要用于进程私有数据段。
MAP_ANONYMOUS 0x20 匿名映射。不依赖文件描述符,映射的内存初始化为零。常用于分配大块内存或进程间共享内存(配合 MAP_SHARED)。
MAP_FIXED 0x10 强制使用 addr 指定的地址。若该地址区域已被占用,内核会覆盖原有映射。使用需谨慎,容易破坏已有映射。
MAP_POPULATE 0x8000 预分配并填充页表,避免后续访问时产生缺页中断。适用于实时性要求高的场景。
MAP_NONBLOCK 0x10000 与 MAP_POPULATE 配合使用,仅预分配已驻留内存的页,不触发 I/O。
关键约束: 在驱动 mmap 实现中,用户空间必须使用 MAP_SHARED 标志,否则内核的 vm_ops->fault 或 remap_pfn_range 可能无法正确同步写回操作。使用 MAP_PRIVATE 映射设备内存会导致写操作仅停留在匿名页副本上,无法真正写入硬件寄存器或 DMA 缓冲区。

3.3 返回值与错误码

mmap 的返回值提供了映射是否成功的明确指示:

  • 成功: 返回映射区域的起始虚拟地址(void *)。该地址是页对齐的,用户程序可直接通过指针访问映射内容。
  • 失败: 返回 MAP_FAILED(即 (void *) -1),并设置全局变量 errno 以指示具体错误原因。

下表列出了常见的 errno 错误码及其含义:

错误码 值 含义与典型触发场景
EACCES 13 权限不足。例如,文件描述符 fd 以只读方式打开,但 prot 指定了 PROT_WRITE。
EAGAIN 11 资源暂时不可用。文件已被锁定,或映射所需的内存资源不足。
EBADF 9 无效的文件描述符。非匿名映射时 fd 不是有效的打开文件描述符。
EINVAL 22 参数无效。常见原因:length 为 0;offset 不是页对齐;flags 同时包含 MAP_SHARED 和 MAP_PRIVATE;或驱动 mmap 回调返回了负值。
ENODEV 19 设备不存在。文件系统不支持映射,或底层驱动未实现 mmap 操作。
ENOMEM 12 内存不足。内核无法分配足够的虚拟地址空间或页表结构。
EPERM 1 操作不允许。例如,在 MAP_SHARED 映射上请求 PROT_EXEC,但文件系统以 noexec 方式挂载。

3.4 mmap 系统调用流程示意图

下图展示了用户空间调用 mmap 后,参数如何传递至内核,并最终通过 VFS 层到达设备驱动的 file_operations.mmap 回调的完整流程:

mmap 系统调用参数传递与内核处理流程 用户空间应用程序 调用 mmap(addr, length, prot, flags, fd, offset) 例如:mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0) 系统调用入口 (SYSCALL_DEFINE6(mmap, ...)) 参数校验:检查 addr 对齐、length 非零、flags 合法性 VFS 层 (do_mmap / mmap_region) 查找 vma、分配虚拟地址区间、调用 file->f_op->mmap 设备驱动 file_operations.mmap 回调 调用 remap_pfn_range() 建立页表映射 将物理地址 (DMA 缓冲区) 映射到用户虚拟地址空间 返回映射起始虚拟地址 或 MAP_FAILED + errno 参数关键约束 • addr: 通常传 NULL • length: 页大小整数倍 • offset: 页对齐 • flags: 必须 MAP_SHARED • fd: 设备文件描述符

3.5 典型调用示例

以下代码片段展示了用户空间如何调用 mmap 映射一个 DMA 设备缓冲区:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>

#define DMA_BUF_SIZE  (4 * 1024 * 1024)  /* 4 MB DMA 缓冲区 */

int main(void)
{
    int fd;
    void *map_base;
    off_t dev_offset = 0;  /* 从设备内存起始地址映射 */

    /* 1. 打开设备文件 */
    fd = open("/dev/dma_device", O_RDWR);
    if (fd < 0) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    /* 2. 调用 mmap 建立映射 */
    map_base = mmap(NULL,                    /* 内核选择起始地址 */
                    DMA_BUF_SIZE,            /* 映射长度 */
                    PROT_READ | PROT_WRITE,  /* 读写权限 */
                    MAP_SHARED,              /* 共享映射,必须用于驱动 */
                    fd,                      /* 设备文件描述符 */
                    dev_offset);             /* 偏移量 */

    if (map_base == MAP_FAILED) {
        fprintf(stderr, "mmap failed, errno = %d\n", errno);
        close(fd);
        exit(EXIT_FAILURE);
    }

    /* 3. 直接通过指针访问 DMA 缓冲区 */
    printf("DMA buffer mapped at: %p\n", map_base);
    /* 例如:写入数据到缓冲区 */
    *(volatile unsigned int *)map_base = 0xA5A5A5A5;

    /* 4. 解除映射 */
    munmap(map_base, DMA_BUF_SIZE);
    close(fd);

    return 0;
}
调试要点: 当 mmap 返回 MAP_FAILED 时,务必检查 errno。在驱动开发中,最常见的错误是 EINVAL(参数无效),通常由 offset 未页对齐或驱动 mmap 回调中 remap_pfn_range 参数错误导致。使用 strace 工具可以捕获系统调用的精确返回值与错误码。

3.6 参数对齐与页边界约束

内核要求 mmap 的 offset 和 length 必须遵循页对齐规则。下表总结了这些约束及其原因:

参数 对齐要求 违反后果 内核处理方式
offset 必须是 PAGE_SIZE 的整数倍 mmap 返回 EINVAL 内核在 sys_mmap_pgoff 中直接校验,不通过则拒绝
length 不必严格对齐,但内核会向上取整到页边界 多出的部分被填充为零,访问可能产生意外行为 内核在 do_mmap 中调用 round_up(len, PAGE_SIZE)
addr (仅 MAP_FIXED) 必须是 PAGE_SIZE 的整数倍 mmap 返回 EINVAL 内核在 get_unmapped_area 中校验