mmap munmap接口介绍

mmapmunmap 是 Unix/Linux 系统编程中用于管理内存映射的核心系统调用。它们提供了在进程的虚拟地址空间和资源(通常是文件)之间建立直接关联的机制,从而高效地进行文件 I/O 或创建匿名内存区域。

1. mmap - 建立内存映射

1
2
3
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • 功能: 将文件或设备的一部分(或匿名内存)映射到调用进程的虚拟地址空间中的一段连续区域。
  • 参数:
    • addr (建议地址):进程希望映射开始的建议起始地址。通常设置为 NULL,让内核自动选择合适地址。如果指定非 NULL 值,内核会尽量满足(需满足对齐等约束),但不保证。
    • length (映射长度):要映射的字节数。必须大于 0。
    • prot (保护权限):指定映射区域的访问权限。使用位掩码 | 组合:
      • PROT_READ:页面可读。
      • PROT_WRITE:页面可写。
      • PROT_EXEC:页面可执行(包含机器代码)。
      • PROT_NONE:页面不可访问(极少单独使用)。
    • flags (映射标志):控制映射的类型和行为。使用位掩码 | 组合:
      • MAP_SHARED共享映射。对映射区域的修改会写回到底层文件(或共享内存对象),对其他映射了同一文件的进程可见。文件修改时间通常也会更新。用于文件 I/O 和进程间通信 (IPC)。
      • MAP_PRIVATE私有映射(写时复制)。对映射区域的修改不会写回文件。进程会获得原文件内容的私有副本,修改只影响该副本。其他进程不可见。常用于加载程序代码/只读数据或创建临时内存副本。
      • MAP_ANONYMOUS / MAP_ANON:创建匿名映射。映射区域不与任何文件关联,初始化为零。fd 参数被忽略,通常设为 -1。offset 应为 0。用于动态分配大块内存(如 malloc 底层可能使用)、进程间共享内存 (IPC) 或 BSS 段初始化。
      • MAP_FIXED强制使用指定地址。如果 addr 不为 NULL,则内核必须将映射精确放在该地址。如果该地址已被占用或不符合对齐要求,调用会失败。谨慎使用,可能导致地址冲突。
      • MAP_POPULATE (Linux 特定):预读页表项。对于文件映射,尝试在 mmap 返回前预读文件内容(预填充页表),减少后续缺页异常。对于匿名映射,会预先分配物理内存(零填充)。可能阻塞。
      • MAP_NORESERVE不预留交换空间。告诉内核不要为这个映射预留后备的交换空间。如果物理内存耗尽,访问未分配的页面可能导致 SIGSEGV。常用于 MAP_PRIVATE | MAP_ANONYMOUS 映射,确保内存耗尽时立即失败。
      • MAP_LOCKED (Linux 特定):锁定内存。尝试将映射的页面锁定在物理内存中,防止被换出到交换空间(需要 CAP_IPC_LOCK 能力或 RLIMIT_MEMLOCK 限制足够)。
      • MAP_HUGETLB (Linux 特定):使用大页。使用巨页 (Huge Pages) 进行映射,减少页表开销,提高大内存区域性能。
    • fd (文件描述符):要映射的文件的描述符。对于 MAP_ANONYMOUS 映射,必须设为 -1。
    • offset (文件偏移):从文件开头算起的偏移量,表示映射应从文件的哪个位置开始。必须是系统页大小 (sysconf(_SC_PAGE_SIZE)) 的整数倍。通常为 0 表示映射整个文件(或从开头)。
  • 返回值:
    • 成功:返回映射区域的起始地址(一个指针)。
    • 失败:返回 MAP_FAILED (通常是 (void *) -1),并设置 errno (如 EACCES, EBADF, EINVAL, ENFILE, ENODEV, ENOMEM, EPERM, ETXTBSY 等)。
  • 工作原理:
    1. 内核在进程的虚拟地址空间中寻找一段足够大的、连续的、满足 addr 建议和权限要求的空闲区域。
    2. 在内核的页表中创建映射条目,将这些虚拟页面对应到物理内存页面(或即将对应)。此时尚未分配物理页面
    3. 当进程首次访问映射区域中的某个页面时,触发**缺页异常 (Page Fault)**。
    4. 内核处理缺页异常:
      • **文件映射 (MAP_SHARED/MAP_PRIVATE)**:将文件 offset 对应位置的数据读取到物理内存页面。
      • **匿名映射 (MAP_ANONYMOUS)**:分配一个物理页面(通常清零)。
      • **MAP_PRIVATE**:如果是写访问,内核会为该页面创建一份私有副本(写时复制 - Copy-on-Write)。
    5. 内核更新页表,建立虚拟地址到物理地址的映射。
    6. 进程恢复执行,访问成功。

2. munmap - 解除内存映射

1
2
3
#include <sys/mman.h>

int munmap(void *addr, size_t length);
  • 功能: 解除之前由 mmap 建立的映射关系。
  • 参数:
    • addr:要解除映射区域的起始地址。必须是之前 mmap 返回的地址值
    • length:要解除映射的字节数。不需要精确匹配原始 mmap 的长度,但必须包含在原始映射范围内且起始地址对齐到页边界。通常直接使用 mmap 返回的地址和原始长度。
  • 返回值:
    • 成功:返回 0。
    • 失败:返回 -1,并设置 errno (通常是 EINVAL,表示地址无效或未对齐)。
  • 工作原理:
    1. 内核查找覆盖地址范围 [addr, addr + length - 1] 的所有映射。
    2. 移除这些映射对应的页表项。
    3. 释放与这些映射关联的进程虚拟地址空间
    4. 释放物理资源:
      • 对于 MAP_PRIVATE 映射或匿名映射:如果这些物理页面被当前进程的该映射引用(且未被锁定),则释放物理内存(或标记为可回收)。
      • 对于 MAP_SHARED 文件映射:物理页面可能被其他映射引用(如其他进程或内核缓存),因此不会立即释放。脏页面(被修改过)会由内核异步写回文件(除非使用 msync 强制同步)。
    5. 进程后续访问该地址范围会导致 SIGSEGV (段错误)。

关键点与常见用途

  1. 高效文件 I/O:

    • MAP_SHARED: 提供对文件内容的直接内存访问,避免了 read/write 的系统调用开销和用户/内核空间之间的数据拷贝。对大文件随机访问或频繁读写局部区域特别高效。修改自动反映到文件(通过内核脏页回写机制)。
    • MAP_PRIVATE: 高效创建文件内容的只读或私有可写副本,无需显式 malloc + read。常用于加载程序代码/库。
  2. 进程间通信 (IPC):

    • MAP_SHARED | MAP_ANONYMOUS: 创建不依赖文件的匿名共享内存区域。父进程映射后 fork,子进程自动继承映射,父子进程可通过该区域通信。也可用于无亲缘关系进程(需配合其他 IPC 机制传递地址/描述符,如 shm_open + ftruncate + mmap 创建 POSIX 共享内存对象)。
    • MAP_SHARED 文件映射:多个进程映射同一文件(或部分)到一个共享内存区域进行通信。
  3. 动态内存分配:

    • MAP_PRIVATE | MAP_ANONYMOUS: 分配大块内存的底层机制之一(如 malloc 在分配超大块时可能使用 mmap)。分配的内存初始化为零。释放时调用 munmap
  4. 特殊用途:

    • 映射设备内存到用户空间进行直接访问(需要驱动支持)。
    • 实现零拷贝网络/存储 (Zero-Copy)。

重要注意事项

  1. 对齐: offset 必须是页大小的整数倍。addrlengthmunmap 时最好也保持页对齐。
  2. 资源管理: mmap 分配的是虚拟地址空间。物理内存按需分配(缺页异常)。munmap 是释放映射的唯一正确方式。
  3. 同步 (msync):MAP_SHARED 映射,修改不会立即写回磁盘(由内核异步处理)。如果需要确保数据落盘,需调用 msync(addr, length, MS_SYNC)
  4. 文件描述符 (fd): mmap 之后立即 close(fd) 通常是安全的,映射依然有效(内核内部维护引用)。
  5. 地址空间碎片: 频繁 mmap/munmap 不同大小的区域可能导致虚拟地址空间碎片化,影响后续大块内存映射。
  6. 错误处理: 务必检查 mmap 返回值是否为 MAP_FAILED 并处理 errno。检查 munmap 返回值。
  7. 权限冲突: protflags 的设置必须与打开文件 fd 时的模式兼容(如 O_RDONLY 打开的文件不能 PROT_WRITE)。

简单示例 (C)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
exit(1);
}

int fd = open(argv[1], O_RDONLY);
if (fd == -1) {
perror("open");
exit(1);
}

struct stat sb;
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
exit(1);
}

// Map the entire file read-only
char *mapped = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapped == MAP_FAILED) {
perror("mmap");
close(fd);
exit(1);
}

// File descriptor can be closed now
close(fd);

// Access the file content via 'mapped' pointer
printf("First 10 bytes: ");
for (int i = 0; i < 10 && i < sb.st_size; i++) {
printf("%02x ", (unsigned char)mapped[i]);
}
printf("\n");

// Unmap the region
if (munmap(mapped, sb.st_size) == -1) {
perror("munmap");
exit(1);
}

return 0;
}

这个示例打开一个文件,将其整个内容映射到进程的地址空间进行只读访问,打印前 10 个字节,然后解除映射。注意错误处理和资源清理 (close, munmap)。

理解 mmapmunmap 对于进行高效系统编程、文件操作和进程间通信至关重要。