好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 mmap 函数,它提供了一种将文件或设备映射到进程内存地址空间的方法,允许程序像访问普通内存一样直接读写文件内容,这在处理大文件或需要频繁随机访问文件时非常高效。


1. 函数介绍 見出しへのリンク

mmap (memory map) 是一个强大的 Linux 系统调用,用于在调用进程的虚拟地址空间中创建一个内存映射区域 (memory mapping)。这个映射可以关联到一个文件、一个设备,或者是一块匿名内存

简单来说,mmap 可以让你把一个文件的内容“映射”到你的程序内存里的一块区域。一旦映射成功,你就可以直接通过读写这块内存来访问或修改文件内容,而无需使用 readwrite 系统调用。内核会负责在后台处理实际的文件 I/O。

这就像把一本书(文件)的一页(部分内容)贴在你的书桌上(内存),你可以直接在桌子上阅读和修改,而不需要每次都去书架上翻找(调用 read/write)。


2. 函数原型 見出しへのリンク

#include <sys/mman.h> // 必需

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

3. 功能 見出しへのリンク

  • 创建映射: 请求内核在调用进程的地址空间中分配一段大小为 length 字节的连续虚拟内存区域。
  • 建立关联: 将这段内存区域与文件描述符 fd 指向的文件(或设备)从 offset 开始的部分关联起来。文件内容会按需(通常是按页)从磁盘加载到物理内存,并通过虚拟内存系统映射到进程的地址空间。
  • 返回地址: 如果成功,返回指向映射区域起始地址的指针。如果失败,返回 MAP_FAILED(即 (void *) -1)。
  • 访问数据: 程序可以直接通过返回的指针读写内存,就像操作普通数组或变量一样。内核会处理内存和文件之间的数据同步(根据映射类型)。

4. 参数 見出しへのリンク

  • void *addr: 指定映射区域在进程地址空间中的期望起始地址
    • 通常设置为 NULL。这告诉内核:“我不关心具体地址,你帮我选一个合适的地方。” 这是最常见和推荐的做法。
    • 如果设置为非 NULL 值,内核会尝试在该地址附近创建映射,但这可能失败(如果该地址已被占用或不满足对齐要求)。除非有特殊需求,否则不建议指定具体地址。
  • size_t length: 映射区域的长度(以字节为单位)。这是你希望映射的文件部分的大小。
  • int prot: 指定映射区域的保护模式 (protection),即进程可以对该区域进行何种操作。这是一个位掩码,由以下值按位或组合:
    • PROT_READ: 页面可读。
    • PROT_WRITE: 页面可写。
    • PROT_EXEC: 页面可执行。
    • PROT_NONE: 页面不可访问。 例如,PROT_READ | PROT_WRITE 表示映射区域可读可写。
  • int flags: 指定映射的类型和行为。必须包含以下互斥值之一:
    • MAP_SHARED: 创建一个共享映射。对映射区域的修改会反映到文件本身,并且对映射同一文件的其他进程可见。这是实现进程间共享内存的一种方式。
    • MAP_PRIVATE: 创建一个私有映射。对映射区域的修改会产生一个副本(写时复制 Copy-On-Write),不会影响原始文件,且对其他进程不可见。 还可以按位或上其他标志来修改行为,常见的包括:
    • MAP_ANONYMOUSMAP_ANON: 创建一个匿名映射,不与任何文件关联。映射区域的内容被初始化为零。常用于分配大块内存。
    • MAP_FIXED: 强制内核在 addr 指定的确切地址创建映射,如果该地址已被占用则会覆盖。使用需非常谨慎,因为它可能破坏现有映射。
  • int fd: 这是与映射相关的文件描述符。它必须是通过 open 等系统调用成功打开的。
    • 如果 flags 包含 MAP_ANONYMOUS,则 fd 参数会被忽略(通常传 0 或 -1)。
  • off_t offset: 文件映射的起始偏移量。它必须是系统页大小(通常为 4KB,在 Linux 上可以用 getpagesize() 获取)的整数倍。指定从文件的哪个字节开始映射。

5. 返回值 見出しへのリンク

  • 成功时: 返回指向映射区域起始地址的指针。这个指针可以像普通内存指针一样使用。
  • 失败时: 返回 MAP_FAILED(即 (void *) -1),并设置全局变量 errno 来指示具体的错误原因(例如 EINVAL 参数无效、EACCES 权限不足、ENOMEM 内存不足等)。

6. 相似函数,或关联函数 見出しへのリンク

  • munmap: 用于解除之前通过 mmap 创建的内存映射,释放映射区域。
  • mprotect: 修改已存在映射区域的保护模式(例如,将只读区域改为可读可写)。
  • msync: 将映射区域的修改同步(刷新)到关联的文件中。对于 MAP_SHARED 映射,在某些情况下是必要的,以确保数据写入磁盘。
  • read, write: 传统的文件 I/O 方式,与 mmap 提供的内存映射方式相对。
  • open, close: 通常在使用 mmap 映射文件时,需要先 open 文件获取 fd,映射后通常可以 closefd(映射仍然有效),但最好在 munmap 之后再 close
  • getpagesize: 获取系统的内存页大小,这对于设置 offsetlength 很有用。

7. 示例代码 見出しへのリンク

示例 1:使用 mmap 读取文件内容 見出しへのリンク

这个例子演示如何将一个文件映射到内存中并读取其内容。

#include <sys/mman.h> // mmap, munmap
#include <sys/stat.h> // fstat
#include <fcntl.h>    // open, O_RDONLY
#include <unistd.h>   // close, fstat, getpagesize
#include <stdio.h>    // perror, printf, fprintf
#include <stdlib.h>   // exit
#include <string.h>   // strchr

int main(int argc, char *argv[]) {
    int fd;
    struct stat sb;
    char *mapped;
    size_t length;
    off_t offset = 0;
    int pagesize;

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

    // 1. 打开文件
    fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 2. 获取文件大小
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        close(fd);
        exit(EXIT_FAILURE);
    }
    length = sb.st_size;

    if (length == 0) {
        printf("File is empty.\n");
        close(fd);
        exit(EXIT_SUCCESS);
    }

    // 3. 获取系统页大小 (虽然在这个例子中不严格需要,但了解它很重要)
    pagesize = getpagesize();
    printf("System page size: %d bytes\n", pagesize);

    // 4. 创建内存映射
    // addr=NULL: 让内核选择地址
    // length: 文件大小
    // prot=PROT_READ: 只读映射
    // flags=MAP_PRIVATE: 私有映射,修改不影响文件
    // fd: 文件描述符
    // offset=0: 从文件开头映射
    mapped = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, offset);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("File '%s' mapped successfully. Size: %zu bytes\n", argv[1], length);
    printf("Mapped memory starts at: %p\n", (void*)mapped);

    // 5. 直接访问映射的内存,就像访问字符数组一样
    printf("First 100 characters (or less) of the file:\n");
    size_t to_print = (length < 100) ? length : 100;
    for (size_t i = 0; i < to_print; ++i) {
        // 简单打印,不处理换行符等
        putchar(mapped[i]);
    }
    printf("\n--- End of first 100 chars ---\n");

    // 查找第一个换行符
    char *newline = strchr(mapped, '\n');
    if (newline != NULL) {
        size_t first_line_len = newline - mapped;
        printf("Length of first line: %zu characters\n", first_line_len);
        printf("First line: ");
        fwrite(mapped, 1, first_line_len, stdout);
        printf("\n");
    } else {
        printf("No newline found in the mapped region.\n");
    }


    // 6. 解除映射
    if (munmap(mapped, length) == -1) {
        perror("munmap");
        // 即使 munmap 失败,也应尝试关闭 fd
    }

    // 7. 关闭文件描述符
    if (close(fd) == -1) {
        perror("close");
        exit(EXIT_FAILURE); // 如果 close 失败,通常是很严重的问题
    }

    printf("File unmapped and closed.\n");
    return 0;
}

代码解释:

  1. 检查命令行参数。
  2. 使用 open 以只读模式打开文件。
  3. 使用 fstat 获取文件大小,因为 mmap 需要知道要映射多少字节。
  4. 调用 getpagesize() 获取系统页大小(虽然此例未严格使用,但了解很重要)。
  5. 调用 mmap
    • addr=NULL: 让内核选择地址。
    • length=sb.st_size: 映射整个文件。
    • prot=PROT_READ: 只读。
    • flags=MAP_PRIVATE: 私有映射。
    • fd: 文件描述符。
    • offset=0: 从文件开头开始映射。
  6. 检查 mmap 是否成功(返回值不为 MAP_FAILED)。
  7. 直接通过 mapped 指针访问文件内容,就像操作字符数组一样。示例打印了前 100 个字符和第一行。
  8. 使用 munmap 解除映射,释放资源。
  9. 使用 close 关闭文件描述符。

示例 2:使用 mmap 修改文件内容(MAP_SHARED) 見出しへのリンク

这个例子演示如何创建一个共享映射来修改文件内容。

#include <sys/mman.h> // mmap, munmap, msync
#include <sys/stat.h> // fstat
#include <fcntl.h>    // open
#include <unistd.h>   // close, fstat
#include <stdio.h>    // perror, printf, fprintf
#include <stdlib.h>   // exit
#include <string.h>   // strlen

int main(int argc, char *argv[]) {
    int fd;
    struct stat sb;
    char *mapped;
    size_t length;
    const char *new_content = "Content written via mmap!\n";
    size_t content_len = strlen(new_content);

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

    // 1. 以读写模式打开文件
    fd = open(argv[1], O_RDWR);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 2. 获取文件大小
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        close(fd);
        exit(EXIT_FAILURE);
    }
    length = sb.st_size;

    // 如果文件为空或太小,调整映射长度为要写入的内容长度
    // (或者可以先 truncate 文件)
    if (length < content_len) {
        printf("File is smaller than content to write. Mapping %zu bytes.\n", content_len);
        length = content_len;
        // 可选:扩展文件大小
        // if (ftruncate(fd, length) == -1) { perror("ftruncate"); close(fd); exit(EXIT_FAILURE); }
    }

    // 3. 创建共享内存映射 (可读可写)
    // prot=PROT_READ | PROT_WRITE: 可读可写
    // flags=MAP_SHARED: 共享映射,修改会写回文件
    mapped = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        close(fd);
        exit(EXIT_FAILURE);
    }

    printf("File '%s' mapped for read/write. Size: %zu bytes\n", argv[1], length);

    // 4. 修改映射内存中的内容
    // 这里简单地将新内容复制到映射区域的开头
    // 注意:确保 mapped 区域足够大
    for (size_t i = 0; i < content_len && i < length; ++i) {
        mapped[i] = new_content[i];
    }
    // 或者使用 memcpy(mapped, new_content, content_len); // 需 #include <string.h>

    printf("Wrote new content to mapped memory.\n");

    // 5. (可选但推荐) 强制将修改同步到文件
    // MS_SYNC: 同步等待 I/O 完成
    if (msync(mapped, content_len, MS_SYNC) == -1) {
        perror("msync");
        // 即使 msync 失败,也继续清理
    } else {
        printf("Changes synced to file.\n");
    }

    // 6. 解除映射
    if (munmap(mapped, length) == -1) {
        perror("munmap");
    }

    // 7. 关闭文件描述符
    if (close(fd) == -1) {
        perror("close");
        exit(EXIT_FAILURE);
    }

    printf("File unmapped, synced, and closed.\n");
    printf("Check the file content now.\n");
    return 0;
}

代码解释:

  1. 以读写模式 (O_RDWR) 打开文件。
  2. 获取文件大小。
  3. 调用 mmap
    • prot=PROT_READ | PROT_WRITE: 可读可写。
    • flags=MAP_SHARED: 共享映射。这是关键,它使得对内存的修改会反映到文件中。
  4. 直接修改 mapped 指针指向的内存区域。
  5. 调用 msync 将修改强制刷新到磁盘,确保数据持久化。MS_SYNC 会等待写入完成。
  6. 使用 munmap 解除映射。
  7. 使用 close 关闭文件描述符。

示例 3:使用 mmap 分配匿名内存 見出しへのリンク

这个例子演示如何使用 mmap 分配一块不与任何文件关联的内存(匿名映射)。

#include <sys/mman.h> // mmap, munmap
#include <stdio.h>    // perror, printf
#include <stdlib.h>   // exit
#include <string.h>   // strcpy

int main() {
    size_t length = 1024; // 分配 1KB
    char *anonymous_mem;

    // 1. 创建匿名内存映射
    // addr=NULL: 让内核选择地址
    // length: 分配大小
    // prot=PROT_READ | PROT_WRITE: 可读可写
    // flags=MAP_PRIVATE | MAP_ANONYMOUS: 私有匿名映射
    // fd=-1: 对于匿名映射,fd 被忽略
    // offset=0: 对于匿名映射,offset 被忽略
    anonymous_mem = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (anonymous_mem == MAP_FAILED) {
        perror("mmap anonymous");
        exit(EXIT_FAILURE);
    }

    printf("Allocated %zu bytes of anonymous memory at %p\n", length, (void*)anonymous_mem);

    // 2. 像使用普通 malloc 分配的内存一样使用它
    strcpy(anonymous_mem, "Hello from anonymous mmap!");
    printf("Content written: %s\n", anonymous_mem);

    // 修改内存
    anonymous_mem[17] = 'M';
    anonymous_mem[18] = 'M';
    anonymous_mem[19] = 'A';
    anonymous_mem[20] = 'P';
    printf("Content modified: %s\n", anonymous_mem);

    // 3. 解除映射 (释放内存)
    if (munmap(anonymous_mem, length) == -1) {
        perror("munmap anonymous");
        exit(EXIT_FAILURE);
    }

    printf("Anonymous memory unmapped.\n");
    return 0;
}

代码解释:

  1. 调用 mmap 分配内存:
    • flags=MAP_PRIVATE | MAP_ANONYMOUS: 指定为私有匿名映射。
    • fd=-1offset=0: 对于匿名映射,这两个参数通常被忽略。
  2. 成功后,anonymous_mem 指向一块大小为 length 的可读写内存。
  3. 可以像使用 malloc 分配的内存一样使用它(但需要 munmap 来释放,而不是 free)。
  4. 使用 munmap 释放这块内存。

mmap 是一个功能强大且高效的工具,特别适用于大文件处理、内存数据库、进程间共享内存等场景。理解其参数和不同标志的含义对于有效利用它至关重要。