内存映射基础概念
内存映射(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 的正确使用方式、理解其背后的虚拟内存机制,能帮助你编写出更高效、更可靠的系统级程序。在实际开发中,务必注意页对齐要求、信号处理和同步问题,确保映射操作的安全性。