4. mmap 返回值与错误处理

mmap() 系统调用的返回值是判断映射是否成功的关键依据。与普通系统调用返回 -1 表示失败不同,mmap() 在失败时返回一个特殊的宏常量 MAP_FAILED(通常为 (void *) -1),成功时返回映射区的起始虚拟地址。理解返回值的含义以及常见 errno 的触发场景,是编写健壮内存映射程序的基础。

核心原则: 每次调用 mmap() 后,必须立即检查返回值是否等于 MAP_FAILED,并通过 errno 获取具体错误原因,避免对无效地址进行操作导致段错误。

4.1 返回值语义

返回值 类型 含义 后续操作
有效地址 void * 映射成功,返回映射区域的起始虚拟地址 可对该地址进行读写(根据权限)
MAP_FAILED void *(通常为 (void *)-1) 映射失败,不分配任何内存 检查 errno 确定失败原因
#include <sys/mman.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

void *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

if (addr == MAP_FAILED) {
    fprintf(stderr, "mmap failed: %s (errno=%d)\n",
            strerror(errno), errno);
    // 根据 errno 执行不同的恢复策略
    return -1;
}

// 映射成功,正常使用 addr
printf("Mapping at %p\n", addr);

4.2 常见 errno 及触发场景

不同的参数组合和系统状态会导致不同的 errno。下表列出了 mmap() 中最常见的四种错误码及其典型触发原因:

errno 含义 典型触发场景 排查方向
EINVAL 无效参数
  • length 为 0
  • offset 不是系统页大小(sysconf(_SC_PAGE_SIZE))的整数倍
  • flags 中同时指定了 MAP_SHARED 和 MAP_PRIVATE
  • fd 对应的文件不支持映射(如 tty 设备)
检查参数对齐和合法性
ENOMEM 内存不足
  • 进程虚拟地址空间耗尽(32 位系统常见)
  • 系统物理内存 + 交换空间不足
  • 超过 vm.max_map_count 限制(映射段数过多)
  • MAP_FIXED 指定的地址区域已被占用且无法覆盖
检查系统内存、ulimit 和映射数量限制
EACCES 权限不足
  • 以 PROT_WRITE 映射只读文件(文件权限为 r--)
  • 以 PROT_EXEC 映射不可执行的文件系统(如 noexec 挂载点)
  • 使用 MAP_SHARED 但文件描述符 fd 不可写
检查文件权限和挂载选项
EBADF 无效文件描述符
  • fd 不是有效的打开文件描述符
  • 使用 MAP_ANONYMOUS 时 fd 未设置为 -1
检查 fd 的来源和生命周期
调试技巧: 使用 perror("mmap") 或 strerror(errno) 可以快速输出人类可读的错误描述。在开发阶段,建议将 errno 的数值也打印出来,方便查阅内核文档。

4.3 错误处理流程图

下图展示了 mmap() 调用后的完整错误处理决策路径:

mmap() 错误处理决策流程 调用 mmap() addr == MAP_FAILED ? 检查 errno 使用映射地址 errno 分类处理 EINVAL → 修正参数 ENOMEM → 释放/扩容 EACCES → 检查权限 ⚠ 注意 MAP_FIXED 失败时也返回 MAP_FAILED,errno 为 EINVAL

4.4 典型错误处理模式

在实际工程中,推荐使用以下结构化错误处理模式,确保每个 mmap() 调用都有对应的错误恢复路径:

void *safe_mmap(void *addr, size_t length, int prot, int flags,
                int fd, off_t offset) {
    // 1. 前置校验:length 不能为 0
    if (length == 0) {
        errno = EINVAL;
        return MAP_FAILED;
    }

    // 2. 调用 mmap
    void *mapped = mmap(addr, length, prot, flags, fd, offset);

    // 3. 失败处理
    if (mapped == MAP_FAILED) {
        switch (errno) {
        case EINVAL:
            fprintf(stderr, "Invalid parameters: check length alignment and flags\n");
            break;
        case ENOMEM:
            fprintf(stderr, "Out of memory: check system resources\n");
            // 可尝试缩小映射大小后重试
            break;
        case EACCES:
            fprintf(stderr, "Permission denied: check file permissions\n");
            break;
        case EBADF:
            fprintf(stderr, "Bad file descriptor\n");
            break;
        default:
            fprintf(stderr, "Unexpected mmap error: %d\n", errno);
            break;
        }
    }

    return mapped;
}
常见陷阱:
  • 不要直接比较 addr == -1:应始终使用 addr == MAP_FAILED,因为 MAP_FAILED 是 (void *) -1,类型安全。
  • 不要忽略 EINTR:虽然 mmap() 通常不会被信号中断,但在某些特殊内核配置下可能返回 EINTR,建议在循环中重试。
  • MAP_FIXED 的地址冲突:使用 MAP_FIXED 时,如果指定地址区域已被占用且无法覆盖,返回 MAP_FAILED 且 errno 为 EINVAL。

4.5 错误码与系统资源关系

下表总结了 mmap() 错误码与底层系统资源限制之间的对应关系,帮助开发者快速定位问题根源:

资源类型 相关限制 触发 errno 查看命令
虚拟地址空间 进程地址空间大小(32 位约 3GB) ENOMEM ulimit -v
物理内存 + 交换 系统可用内存不足 ENOMEM free -h
映射段数量 vm.max_map_count(默认 65530) ENOMEM cat /proc/sys/vm/max_map_count
文件权限 文件读/写/执行权限 EACCES ls -l
文件描述符 进程打开文件数限制 EBADF ulimit -n
页对齐 offset 必须是页大小整数倍 EINVAL getconf PAGE_SIZE

通过系统化地理解返回值语义和 errno 分类,开发者可以在 mmap() 失败时快速定位问题,并采取合适的恢复策略,从而构建出更稳定、更可靠的内存映射程序。