内存映射(mmap)
概述
内存映射(mmap)是一种将文件或设备映射到进程地址空间的机制。通过内存映射,进程可以像访问普通内存一样访问文件,也可以用于进程间通信。mmap提供了高效的文件I/O和进程间通信方式。
通信原理
基本概念
内存映射的特点:
- 文件映射:将文件映射到进程地址空间
- 共享映射:多个进程可以共享同一映射区域
- 零拷贝:直接内存访问,减少数据拷贝
- 虚拟内存:利用虚拟内存机制,按需加载
- 同步机制:可以自动同步到文件
实现机制
文件映射
-
创建映射:
- 使用
mmap()将文件映射到进程地址空间 - 返回映射区域的虚拟地址
- 可以指定映射大小、权限、标志等
- 使用
-
访问数据:
- 进程通过指针直接访问映射区域
- 访问时触发页错误,内核按需加载文件内容
- 修改数据时,根据标志决定是否写回文件
-
同步:
msync():手动同步映射区域到文件munmap():取消映射,自动同步
-
数据流向:
进程A ──┐ ├──> [共享映射区域] <──> [文件/设备] 进程B ──┤ 进程C ──┘
匿名映射(进程间通信)
-
创建匿名映射:
- 使用
mmap()创建匿名映射(不关联文件) - 使用MAP_SHARED标志,多个进程可以共享
- 通过fork()或显式共享实现进程间通信
- 使用
-
共享机制:
- 父子进程通过fork()共享映射区域
- 任意进程可以通过文件描述符共享(需要特殊处理)
API说明
mmap()
c
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
- 功能:创建内存映射
- 参数 :
addr:建议的映射地址(通常为NULL,让系统选择)length:映射长度(字节)prot:保护标志(PROT_READ可读,PROT_WRITE可写,PROT_EXEC可执行)flags:映射标志(MAP_SHARED共享,MAP_PRIVATE私有,MAP_ANONYMOUS匿名等)fd:文件描述符(匿名映射时为-1)offset:文件偏移量(通常为0)
- 返回值:成功返回映射地址,失败返回MAP_FAILED
munmap()
c
int munmap(void *addr, size_t length);
- 功能:取消内存映射
- 参数 :
addr:映射地址length:映射长度
- 返回值:成功返回0,失败返回-1
msync()
c
int msync(void *addr, size_t length, int flags);
- 功能:同步映射区域到文件
- 参数 :
addr:映射地址length:同步长度flags:同步标志(MS_SYNC同步写,MS_ASYNC异步写,MS_INVALIDATE使缓存无效)
- 返回值:成功返回0,失败返回-1
示例代码
示例: 使用 mmap 读写文件
c
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
const char *path = "demo.txt";
const char *msg = "hello mmap\n";
int fd = open(path, O_RDWR | O_CREAT, 0666);
if (fd < 0) { perror("open"); return 1; }
// 确保文件大小 >= 要写入的长度
size_t len = strlen(msg);
if (ftruncate(fd, len) == -1) { perror("ftruncate"); close(fd); return 1; }
// 建立映射: 读写, 共享写回文件
void *addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) { perror("mmap"); close(fd); return 1; }
// 写入数据
memcpy(addr, msg, len);
// 同步到文件
if (msync(addr, len, MS_SYNC) == -1) { perror("msync"); }
// 读取验证
write(STDOUT_FILENO, addr, len);
// 清理
munmap(addr, len);
close(fd);
return 0;
}
示例: 多线程共享 mmap 区域并安全更新
c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define THREADS 4
#define PER_THREAD 100000
struct shared {
pthread_mutex_t lock;
int counter;
};
void *worker(void *arg) {
struct shared *sh = (struct shared *)arg;
for (int i = 0; i < PER_THREAD; i++) {
pthread_mutex_lock(&sh->lock);
sh->counter++;
pthread_mutex_unlock(&sh->lock);
}
return NULL;
}
int main() {
const char *path = "shared.dat";
int fd = open(path, O_RDWR | O_CREAT, 0666);
if (fd < 0) { perror("open"); return 1; }
// 准备文件长度
if (ftruncate(fd, sizeof(struct shared)) == -1) { perror("ftruncate"); close(fd); return 1; }
// 建立映射
struct shared *sh = mmap(NULL, sizeof(struct shared),
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (sh == MAP_FAILED) { perror("mmap"); close(fd); return 1; }
// 初始化锁和计数器
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE); // 同一进程线程间共享
pthread_mutex_init(&sh->lock, &attr);
sh->counter = 0;
// 创建线程
pthread_t th[THREADS];
for (int i = 0; i < THREADS; i++) {
pthread_create(&th[i], NULL, worker, sh);
}
for (int i = 0; i < THREADS; i++) {
pthread_join(th[i], NULL);
}
printf("Final counter = %d (expect %d)\n", sh->counter, THREADS * PER_THREAD);
// 清理
pthread_mutex_destroy(&sh->lock);
munmap(sh, sizeof(struct shared));
close(fd);
return 0;
}
说明:
- 使用 MAP_SHARED 将文件映射为共享区域
- 通过 pthread_mutex_t 保证多线程更新的原子性
- 如果要在多进程间共享, 将
PTHREAD_PROCESS_PRIVATE改为PTHREAD_PROCESS_SHARED, 并确保映射为 MAP_SHARED 且 mutex 存放于共享内存中
示例: 多进程(父子进程)共享 mmap 区域并安全更新
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>
#include <errno.h>
#define PROCESSES 4
#define PER_PROCESS 100000
// 共享数据结构, 包含互斥锁和计数器
struct shared {
pthread_mutex_t lock; // 进程间共享的互斥锁
int counter; // 共享计数器
int initialized; // 初始化标志, 确保只初始化一次
};
int main() {
const char *path = "shared_mmap.dat";
int fd;
struct shared *sh;
pid_t pids[PROCESSES];
int i;
// 创建或打开共享文件
fd = open(path, O_RDWR | O_CREAT, 0666);
if (fd < 0) {
perror("open");
return 1;
}
// 调整文件大小以容纳共享数据结构
if (ftruncate(fd, sizeof(struct shared)) == -1) {
perror("ftruncate");
close(fd);
return 1;
}
// 建立共享内存映射
// MAP_SHARED 确保多个进程共享同一块物理内存
sh = (struct shared *)mmap(NULL, sizeof(struct shared),
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (sh == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
// 父进程负责初始化共享数据结构
if (sh->initialized == 0) {
// 初始化进程间共享的互斥锁属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
// 关键: 设置为进程间共享, 这样多个进程可以使用同一个互斥锁
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
// 初始化互斥锁
if (pthread_mutex_init(&sh->lock, &attr) != 0) {
perror("pthread_mutex_init");
munmap(sh, sizeof(struct shared));
close(fd);
return 1;
}
pthread_mutexattr_destroy(&attr);
// 初始化计数器
sh->counter = 0;
// 设置初始化标志, 防止子进程重复初始化
sh->initialized = 1;
printf("Parent: Initialized shared memory\n");
}
// 创建多个子进程
for (i = 0; i < PROCESSES; i++) {
pids[i] = fork();
if (pids[i] < 0) {
perror("fork");
// 清理已创建的子进程
for (int j = 0; j < i; j++) {
kill(pids[j], SIGTERM);
}
munmap(sh, sizeof(struct shared));
close(fd);
return 1;
} else if (pids[i] == 0) {
// 子进程: 执行计数器递增操作
printf("Child %d (PID %d): Starting increments\n", i, getpid());
for (int j = 0; j < PER_PROCESS; j++) {
// 加锁保护临界区
pthread_mutex_lock(&sh->lock);
sh->counter++;
pthread_mutex_unlock(&sh->lock);
}
printf("Child %d (PID %d): Completed increments\n", i, getpid());
// 子进程退出, munmap 会自动清理映射
exit(0);
}
}
// 父进程等待所有子进程完成
printf("Parent: Waiting for all children to complete...\n");
for (i = 0; i < PROCESSES; i++) {
int status;
waitpid(pids[i], &status, 0);
if (WIFEXITED(status)) {
printf("Parent: Child %d (PID %d) exited with status %d\n",
i, pids[i], WEXITSTATUS(status));
}
}
// 验证最终结果
printf("Parent: Final counter = %d (expected %d)\n",
sh->counter, PROCESSES * PER_PROCESS);
if (sh->counter == PROCESSES * PER_PROCESS) {
printf("Parent: Success! Counter is correct.\n");
} else {
printf("Parent: Error! Counter mismatch (race condition detected).\n");
}
// 清理资源
pthread_mutex_destroy(&sh->lock);
munmap(sh, sizeof(struct shared));
close(fd);
// 可选: 删除共享文件
// unlink(path);
return 0;
}
说明:
- 使用
MAP_SHARED标志创建共享内存映射, 多个进程可以访问同一块物理内存 - 使用
PTHREAD_PROCESS_SHARED属性的互斥锁, 确保多个进程可以正确同步 - 父进程负责初始化共享数据结构(互斥锁和计数器)
- 多个子进程并发更新共享计数器, 通过互斥锁保证原子性
- 所有进程通过
fork()共享父进程的映射区域, 无需额外文件操作
示例: 多个独立进程通过文件共享 mmap 区域
c
// writer.c: 写入进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
struct shared {
pthread_mutex_t lock;
char message[256];
int ready;
};
int main() {
const char *path = "shared_mmap.dat";
int fd = open(path, O_RDWR | O_CREAT, 0666);
if (fd < 0) { perror("open"); return 1; }
if (ftruncate(fd, sizeof(struct shared)) == -1) {
perror("ftruncate"); close(fd); return 1;
}
struct shared *sh = (struct shared *)mmap(NULL, sizeof(struct shared),
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (sh == MAP_FAILED) { perror("mmap"); close(fd); return 1; }
// 初始化互斥锁(仅第一次)
if (sh->ready == 0) {
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&sh->lock, &attr);
pthread_mutexattr_destroy(&attr);
sh->ready = 1;
}
// 写入数据
pthread_mutex_lock(&sh->lock);
snprintf(sh->message, sizeof(sh->message), "Hello from writer (PID %d)", getpid());
printf("Writer: Wrote message\n");
pthread_mutex_unlock(&sh->lock);
sleep(2); // 等待读取进程读取
munmap(sh, sizeof(struct shared));
close(fd);
return 0;
}
c
// reader.c: 读取进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
struct shared {
pthread_mutex_t lock;
char message[256];
int ready;
};
int main() {
const char *path = "shared_mmap.dat";
int fd = open(path, O_RDWR, 0666);
if (fd < 0) { perror("open"); return 1; }
struct shared *sh = (struct shared *)mmap(NULL, sizeof(struct shared),
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (sh == MAP_FAILED) { perror("mmap"); close(fd); return 1; }
// 等待写入进程初始化
while (sh->ready == 0) {
usleep(100000); // 100ms
}
// 读取数据
pthread_mutex_lock(&sh->lock);
printf("Reader: Read message: %s\n", sh->message);
pthread_mutex_unlock(&sh->lock);
munmap(sh, sizeof(struct shared));
close(fd);
return 0;
}
说明:
- 两个独立的程序通过同一个文件共享 mmap 映射区域
- 写入进程负责初始化互斥锁和数据结构
- 读取进程等待初始化完成后读取数据
- 两个进程可以独立编译和运行, 通过文件系统协调
性能评价
优点
- 高效:零拷贝,直接内存访问
- 虚拟内存:利用虚拟内存机制,按需加载
- 大文件处理:适合处理大文件,无需一次性加载
- 进程间通信:可以用于进程间共享数据
- 灵活性:可以映射文件或创建匿名映射
缺点
- 需要同步:多进程访问需要同步机制
- 复杂性:API相对复杂,需要理解虚拟内存
- 系统限制:受虚拟内存大小限制
- 文件依赖:文件映射依赖文件系统
性能特点
- 延迟:低(直接内存访问)
- 吞吐量:高(零拷贝,适合大文件)
- CPU占用:低(利用虚拟内存机制)
- 内存占用:按需加载,节省内存
适用场景
- ✅ 大文件处理
- ✅ 需要高效文件I/O的场景
- ✅ 进程间共享数据
- ✅ 需要零拷贝的场景
- ✅ 数据库、缓存等应用
- ❌ 小文件简单读写(普通I/O更简单)
- ❌ 不需要共享的场景
注意事项
- 同步机制:多进程访问共享映射时,需要同步机制(如信号量)
- 页大小对齐:注意系统页大小,某些操作需要页对齐
- 文件大小:映射大小不能超过文件大小(除非扩展文件)
- 权限设置:注意prot和flags的权限设置
- 错误处理:注意处理MAP_FAILED返回值
- 资源清理:使用完毕后调用munmap()取消映射
- 同步时机:根据需要使用msync()同步数据到文件
- 匿名映射:匿名映射用于进程间通信时,需要特殊机制共享
扩展阅读
man 2 mmapman 2 munmapman 2 msyncman 7 shm_overview