C语言内存映射与mmap完全指南

系统编程内存映射

内存映射基础概念

内存映射(Memory Mapping)是操作系统提供的一种机制,将文件或设备映射到进程的虚拟地址空间,使得程序可以通过指针直接访问文件内容,而无需使用传统的 read/write 系统调用。这种方式不仅简化了文件操作,还能显著提升 I/O 性能,是现代系统编程中的重要技术。

传统 I/O vs 内存映射

  • 传统 read/write:数据从内核缓冲区拷贝到用户缓冲区,涉及两次数据拷贝(磁盘→内核→用户)
  • mmap 内存映射:文件直接映射到进程地址空间,访问映射区域触发缺页中断,由内核按需加载,仅一次拷贝

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:映射的起始地址,通常设为 NULL,由内核自动选择
  • length:映射区域的字节数
  • prot:内存保护标志,指定映射区域的访问权限
  • flags:映射类型标志,决定映射的行为特性
  • fd:文件描述符(匿名映射时设为 -1)
  • offset:文件偏移量,必须是页大小的整数倍

保护标志(prot)

  • PROT_READ:允许读取映射区域
  • PROT_WRITE:允许写入映射区域
  • PROT_EXEC:允许执行映射区域
  • PROT_NONE:不允许访问映射区域

映射标志(flags)

  • MAP_SHARED:共享映射,写入内容会反映到文件和其他映射该文件的进程
  • MAP_PRIVATE:私有映射,写入触发写时复制(COW),不影响原文件
  • MAP_ANONYMOUS:匿名映射,不基于文件,fd 设为 -1,内容初始化为零
  • MAP_FIXED:强制在指定地址映射,不推荐使用,可能覆盖已有映射
  • MAP_HUGETLB:使用大页(Huge Pages)映射,减少 TLB 缺失

文件映射实践

基本文件读取

#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 <file>\n", argv[0]);
        return 1;
    }

    /* 打开文件 */
    int fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    /* 获取文件大小 */
    struct stat st;
    if (fstat(fd, &st) == -1) {
        perror("fstat");
        close(fd);
        return 1;
    }

    size_t size = st.st_size;

    /* 内存映射 */
    void *map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (map == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    /* 通过指针直接访问文件内容 */
    printf("File size: %zu bytes\n", size);
    printf("First 80 chars:\n%.80s\n", (char *)map);

    /* 统计行数 */
    size_t lines = 0;
    for (size_t i = 0; i < size; i++) {
        if (((char *)map)[i] == '\n')
            lines++;
    }
    printf("Total lines: %zu\n", lines);

    /* 解除映射 */
    munmap(map, size);
    close(fd);
    return 0;
}

可写文件映射与同步

/* 修改映射文件内容 */
int fd = open("data.bin", O_RDWR);
if (fd == -1) { perror("open"); return 1; }

struct stat st;
fstat(fd, &st);

/* MAP_SHARED: 修改会反映到文件 */
char *map = mmap(NULL, st.st_size,
                   PROT_READ | PROT_WRITE,
                   MAP_SHARED, fd, 0);
if (map == MAP_FAILED) { perror("mmap"); return 1; }

/* 修改映射内容 */
map[0] = 'H';  /* 直接写入,自动同步到文件 */

/* 手动同步到磁盘(可选,内核会定期自动同步) */
if (msync(map, st.st_size, MS_SYNC) == -1) {
    perror("msync");
}

munmap(map, st.st_size);
close(fd);

msync() 的标志参数:

  • MS_SYNC:同步等待写入完成,确保数据落盘
  • MS_ASYNC:异步写入,仅标记需要写回,立即返回
  • MS_INVALIDATE:使映射区域的其他缓存副本失效

匿名映射

匿名映射不基于任何文件,映射区域的内容初始化为零,常用于分配大块内存或实现进程间共享内存。

分配大块内存

/* 使用 mmap 分配 64MB 内存(替代 malloc) */
size_t size = 64 * 1024 * 1024;  /* 64 MB */

void *mem = mmap(NULL, size,
                   PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS,
                   -1, 0);
if (mem == MAP_FAILED) {
    perror("mmap");
    return 1;
}

/* 使用分配的内存 */
int *arr = (int *)mem;
for (int i = 0; i < 1000; i++) {
    arr[i] = i * i;
}

/* 释放 */
munmap(mem, size);

相比 malloc,mmap 匿名映射的优势:

  • 分配的内存可以通过 munmap 立即归还给操作系统,而 free 不一定立即释放
  • 适合大块内存分配(通常 > 128KB 时 glibc 的 malloc 内部也会使用 mmap)
  • 映射区域页对齐,没有内存碎片

父子进程共享内存

#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    /* MAP_SHARED: fork 后父子进程共享同一映射 */
    int *shared = mmap(NULL, sizeof(int),
                         PROT_READ | PROT_WRITE,
                         MAP_SHARED | MAP_ANONYMOUS,
                         -1, 0);
    if (shared == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    *shared = 0;

    pid_t pid = fork();
    if (pid == 0) {
        /* 子进程 */
        for (int i = 0; i < 100000; i++) {
            (*shared)++;
        }
        printf("Child done, shared = %d\n", *shared);
    } else {
        /* 父进程 */
        for (int i = 0; i < 100000; i++) {
            (*shared)++;
        }
        waitpid(pid, NULL, 0);
        printf("Parent done, shared = %d\n", *shared);
        /* 注意:无同步时结果可能小于200000 */
    }

    munmap(shared, sizeof(int));
    return 0;
}

共享内存与进程间通信

基于 mmap 的共享内存是最快的 IPC 方式,因为进程间无需内核中转,直接读写同一块物理内存。

使用 shm_open 创建共享内存

/* 生产者进程:创建共享内存并写入数据 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define SHM_NAME "/my_shm_demo"
#define SHM_SIZE 4096

typedef struct {
    int ready;         /* 数据就绪标志 */
    int count;         /* 数据条数 */
    char data[4000];  /* 数据缓冲区 */
} shm_data_t;

int main(void) {
    /* 创建共享内存对象 */
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (fd == -1) { perror("shm_open"); return 1; }

    /* 设置大小 */
    if (ftruncate(fd, SHM_SIZE) == -1) {
        perror("ftruncate");
        close(fd);
        return 1;
    }

    /* 映射共享内存 */
    shm_data_t *shm = mmap(NULL, SHM_SIZE,
                              PROT_READ | PROT_WRITE,
                              MAP_SHARED, fd, 0);
    if (shm == MAP_FAILED) { perror("mmap"); return 1; }

    close(fd);  /* 映射后可关闭 fd */

    /* 写入数据 */
    shm->ready = 0;
    shm->count = 1;
    strncpy(shm->data, "Hello from producer!", sizeof(shm->data) - 1);

    /* 内存屏障:确保写入对其他进程可见 */
    __sync_synchronize();
    shm->ready = 1;

    printf("Producer: data written, waiting...\n");
    sleep(5);

    /* 清理 */
    munmap(shm, SHM_SIZE);
    shm_unlink(SHM_NAME);
    return 0;
}

消费者进程

/* 消费者进程:读取共享内存中的数据 */
int main(void) {
    int fd = shm_open(SHM_NAME, O_RDWR, 0666);
    if (fd == -1) { perror("shm_open"); return 1; }

    shm_data_t *shm = mmap(NULL, SHM_SIZE,
                              PROT_READ | PROT_WRITE,
                              MAP_SHARED, fd, 0);
    if (shm == MAP_FAILED) { perror("mmap"); return 1; }

    close(fd);

    /* 等待数据就绪(忙等待,实际应使用信号量) */
    while (!shm->ready) {
        __sync_synchronize();
        usleep(1000);
    }

    printf("Consumer: count=%d, data=%s\n",
           shm->count, shm->data);

    munmap(shm, SHM_SIZE);
    return 0;
}

mremap 与映射调整

mremap() 允许调整已有映射的大小或位置,类似于 realloc 对内存的重分配。

#include <sys/mman.h>

void *mremap(void *old_address, size_t old_size,
             size_t new_size, int flags, ... /* void *new_address */);
/* 扩展映射区域 */
size_t old_size = 4096;
void *map = mmap(NULL, old_size,
                   PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

/* 扩展到 8192 字节 */
size_t new_size = 8192;
void *new_map = mremap(map, old_size, new_size, MREMAP_MAYMOVE);
if (new_map == MAP_FAILED) {
    perror("mremap");
} else {
    map = new_map;  /* 地址可能改变 */
    /* 新扩展的区域内容为零 */
}

标志说明:

  • MREMAP_MAYMOVE:允许内核将映射移动到新的虚拟地址,如果不设此标志且原地址无法扩展则失败

实战:高性能文件复制

使用 mmap 实现文件复制,相比传统 read/write 减少一次数据拷贝。

/* mmap_copy.c - 基于内存映射的高效文件复制 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int mmap_copy(const char *src_path, const char *dst_path) {
    /* 打开源文件 */
    int src_fd = open(src_path, O_RDONLY);
    if (src_fd == -1) { perror("open src"); return -1; }

    struct stat st;
    if (fstat(src_fd, &st) == -1) {
        perror("fstat");
        close(src_fd);
        return -1;
    }
    size_t size = st.st_size;

    /* 创建目标文件 */
    int dst_fd = open(dst_path, O_RDWR | O_CREAT | O_TRUNC, st.st_mode);
    if (dst_fd == -1) {
        perror("open dst");
        close(src_fd);
        return -1;
    }

    /* 设置目标文件大小 */
    if (ftruncate(dst_fd, size) == -1) {
        perror("ftruncate");
        close(src_fd);
        close(dst_fd);
        return -1;
    }

    /* 映射源文件 */
    void *src_map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, src_fd, 0);
    if (src_map == MAP_FAILED) {
        perror("mmap src");
        close(src_fd); close(dst_fd);
        return -1;
    }

    /* 映射目标文件 */
    void *dst_map = mmap(NULL, size, PROT_WRITE, MAP_SHARED, dst_fd, 0);
    if (dst_map == MAP_FAILED) {
        perror("mmap dst");
        munmap(src_map, size);
        close(src_fd); close(dst_fd);
        return -1;
    }

    /* 内存拷贝(内核会优化为 DMA 传输) */
    memcpy(dst_map, src_map, size);

    /* 同步到磁盘 */
    msync(dst_map, size, MS_SYNC);

    /* 清理 */
    munmap(src_map, size);
    munmap(dst_map, size);
    close(src_fd);
    close(dst_fd);

    return 0;
}

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

实战:基于 mmap 的键值存储

/* mmap_kv.c - 基于内存映射的简单键值存储 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

#define MAX_ENTRIES 1024
#define KEY_SIZE 32
#define VAL_SIZE 64

typedef struct {
    char key[KEY_SIZE];
    char value[VAL_SIZE];
    int used;
} kv_entry_t;

typedef struct {
    int count;
    kv_entry_t entries[MAX_ENTRIES];
} kv_store_t;

static kv_store_t *store = NULL;

int kv_open(const char *path) {
    int fd = open(path, O_RDWR | O_CREAT, 0666);
    if (fd == -1) return -1;

    size_t size = sizeof(kv_store_t);
    if (ftruncate(fd, size) == -1) {
        close(fd);
        return -1;
    }

    store = mmap(NULL, size,
                   PROT_READ | PROT_WRITE,
                   MAP_SHARED, fd, 0);
    close(fd);

    if (store == MAP_FAILED) {
        store = NULL;
        return -1;
    }

    /* 首次初始化 */
    if (store->count == 0 && !store->entries[0].used) {
        memset(store, 0, size);
    }

    return 0;
}

int kv_put(const char *key, const char *value) {
    if (!store) return -1;

    /* 查找已有 key 或空槽 */
    for (int i = 0; i < MAX_ENTRIES; i++) {
        if (store->entries[i].used &&
            strcmp(store->entries[i].key, key) == 0) {
            /* 更新已有 key */
            strncpy(store->entries[i].value, value, VAL_SIZE - 1);
            msync(store, sizeof(kv_store_t), MS_ASYNC);
            return 0;
        }
    }

    /* 插入新 key */
    for (int i = 0; i < MAX_ENTRIES; i++) {
        if (!store->entries[i].used) {
            strncpy(store->entries[i].key, key, KEY_SIZE - 1);
            strncpy(store->entries[i].value, value, VAL_SIZE - 1);
            store->entries[i].used = 1;
            store->count++;
            msync(store, sizeof(kv_store_t), MS_ASYNC);
            return 0;
        }
    }

    return -1;  /* 存储已满 */
}

const char *kv_get(const char *key) {
    if (!store) return NULL;

    for (int i = 0; i < MAX_ENTRIES; i++) {
        if (store->entries[i].used &&
            strcmp(store->entries[i].key, key) == 0) {
            return store->entries[i].value;
        }
    }
    return NULL;
}

void kv_close(void) {
    if (store) {
        msync(store, sizeof(kv_store_t), MS_SYNC);
        munmap(store, sizeof(kv_store_t));
        store = NULL;
    }
}

错误处理与注意事项

信号处理

访问无效的映射区域会触发 SIGSEGV 信号,需要妥善处理:

#include <signal.h>
#include <setjmp.h>

static sigjmp_buf jmp_env;

static void segv_handler(int sig) {
    siglongjmp(jmp_env, 1);
}

int safe_access(void *addr, size_t len) {
    struct sigaction sa, old_sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = segv_handler;
    sa.sa_flags = 0;
    sigaction(SIGSEGV, &sa, &old_sa);

    if (sigsetjmp(jmp_env, 1) == 0) {
        /* 尝试访问内存 */
        volatile char *p = (char *)addr;
        for (size_t i = 0; i < len; i += 4096) {
            (void)p[i];
        }
        sigaction(SIGSEGV, &old_sa, NULL);
        return 0;  /* 访问成功 */
    } else {
        sigaction(SIGSEGV, &old_sa, NULL);
        return -1;  /* 访问失败 */
    }
}

常见陷阱

  • 映射大小与文件大小:映射长度可以超过文件大小,但访问超出文件的部分会触发 SIGBUS
  • offset 必须页对齐:offset 参数必须是系统页大小(通常 4096 字节)的整数倍
  • MAP_PRIVATE 的写时复制:写入不会修改原文件,而是在内存中创建副本
  • 文件描述符可以关闭:mmap 之后 close(fd) 不影响映射,映射仍有效
  • 信号安全:访问映射区域可能触发信号(SIGSEGV/SIGBUS),需要在信号处理器中小心处理

获取页大小

long page_size = sysconf(_SC_PAGESIZE);
printf("Page size: %ld\n", page_size);

/* 计算对齐后的偏移和长度 */
off_t raw_offset = 5000;
off_t aligned_offset = raw_offset & ~(page_size - 1);
size_t adjust = raw_offset - aligned_offset;

总结

  • mmap/munmap - 内存映射的核心接口,将文件或匿名内存映射到进程地址空间
  • 保护标志 - PROT_READ/PROT_WRITE/PROT_EXEC 控制映射区域的访问权限
  • 映射类型 - MAP_SHARED 共享写入反映到文件,MAP_PRIVATE 写时复制不影响原文件
  • 匿名映射 - MAP_ANONYMOUS 不基于文件,适合大块内存分配和进程间共享
  • msync - 手动同步映射内容到磁盘,MS_SYNC 同步等待,MS_ASYNC 异步写回
  • 共享内存 - shm_open + mmap 实现最快的 IPC 方式,需配合同步机制使用
  • mremap - 动态调整映射大小,MREMAP_MAYMOVE 允许地址移动

内存映射是C语言系统编程中的一项强大技术,从高效的文件访问到进程间共享内存,mmap 都有着不可替代的优势。掌握 mmap 的正确使用方式、理解其背后的虚拟内存机制,能帮助你编写出更高效、更可靠的系统级程序。在实际开发中,务必注意页对齐要求、信号处理和同步问题,确保映射操作的安全性。