mmap()
是一个强大的系统调用,用于在进程的虚拟地址空间中创建内存映射。它可以将文件或设备映射到内存,或者创建匿名内存区域用于进程间通信(IPC)。下面将详细解释 mmap()
的三个主要用途,并提供相应的示例代码。
(1)进程创建匿名的内存映射
把内存的物理页映射到进程的虚拟地址空间
详细解释
匿名内存映射是一种不与任何文件关联的内存映射。它通常用于动态分配内存或在父子进程间共享内存。使用匿名内存映射时,操作系统会为映射区域分配物理内存页,并将其映射到进程的虚拟地址空间中。这种方法比传统的 malloc()
更灵活,尤其适用于需要在进程间共享数据的场景。
主要特点:
-
无文件关联:匿名内存映射不依赖于任何文件,适用于动态内存分配。
-
进程间共享:通过在父子进程之间共享映射,可以实现高效的进程间通信。
-
灵活性:可以控制映射区域的大小和权限。
示例代码
以下示例展示了如何使用 mmap()
创建一个匿名内存映射,并在父子进程间共享数据。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/mman.h> #include <string.h> #include <sys/wait.h> int main() { // 创建匿名内存映射,大小为一个整数 int *shared_mem = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if (shared_mem == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); } // 初始化共享内存 *shared_mem = 0; printf("Initial value: %d\n", *shared_mem); pid_t pid = fork(); if (pid < 0) { perror("fork"); munmap(shared_mem, sizeof(int)); exit(EXIT_FAILURE); } else if (pid == 0) { // 子进程:修改共享内存的值 *shared_mem = 42; printf("Child process updated value to: %d\n", *shared_mem); // 解除映射并退出 munmap(shared_mem, sizeof(int)); exit(EXIT_SUCCESS); } else { // 父进程:等待子进程结束 wait(NULL); // 读取共享内存的值 printf("Parent process reads value: %d\n", *shared_mem); // 解除映射 munmap(shared_mem, sizeof(int)); } return 0; }
代码解释
-
创建匿名内存映射:
int *shared_mem = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
-
NULL
:让操作系统选择映射的起始地址。 -
sizeof(int)
:映射区域的大小。 -
PROT_READ | PROT_WRITE
:映射区域的保护标志,允许读写。 -
MAP_SHARED | MAP_ANONYMOUS
:共享映射且不关联任何文件。 -
-1
和0
:文件描述符和偏移量,在匿名映射中忽略。
-
-
初始化和修改共享内存:
-
父进程初始化共享内存的值为
0
。 -
子进程将其值修改为
42
。
-
-
进程同步:
-
父进程使用
wait(NULL)
等待子进程结束,然后读取共享内存的值,验证子进程的修改。
-
输出示例
Initial value: 0 Child process updated value to: 42 Parent process reads value: 42
在使用 fork()
函数时,父进程和子进程会并行执行,从 fork()
调用点开始分叉为两个独立的进程。每个进程会根据 fork()
返回值进入不同的分支:
-
父进程:
fork()
返回子进程的 PID(大于 0),因此会进入else
分支。 -
子进程:
fork()
返回0
,因此会进入else if (pid == 0)
分支。 -
错误情况:如果
fork()
返回值小于0
,表示创建子进程失败,会进入第一个if
分支。
因此,父进程和子进程会分别执行不同的代码块,但它们都是从同一个 fork()
调用点继续执行。这就是为什么看起来像是同时执行了 else if
和 else
分支,实际上它们是在不同的进程中独立执行的。
(2)进程把文件映射到进程的虚拟地址空间
可以像访问内存一样访问文件,不需要调用系统调用 read()
和 write()
访问文件,从而避免用户模式和内核模式之间的切换,提高读写文件的速度
详细解释
文件内存映射允许将文件内容直接映射到进程的虚拟地址空间。这使得进程可以像访问内存一样访问文件数据,无需使用 read()
和 write()
系统调用。这种方法具有以下优点:
-
性能提升:减少了用户空间和内核空间之间的上下文切换,尤其适用于频繁访问文件的场景。
-
简化编程:可以直接通过指针访问文件数据,代码更加简洁。
-
内存共享:多个进程可以共享同一个文件的映射,便于数据共享和协作。
示例代码
以下示例展示了如何使用 mmap()
将一个文件映射到内存,并通过指针访问和修改文件内容。
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/mman.h> #include <unistd.h> #include <string.h> #include <sys/stat.h> int main() { const char *filename = "example.txt"; int fd = open(filename, O_RDWR | O_CREAT, 0666); if (fd < 0) { perror("open"); exit(EXIT_FAILURE); } // 定义要写入文件的数据 const char *text = "Hello, mmap!"; size_t text_size = strlen(text) + 1; // 包括终止符 // 调整文件大小以适应映射的数据 if (ftruncate(fd, text_size) == -1) { perror("ftruncate"); close(fd); exit(EXIT_FAILURE); } // 创建文件内存映射 char *map = mmap(NULL, text_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (map == MAP_FAILED) { perror("mmap"); close(fd); exit(EXIT_FAILURE); } // 关闭文件描述符,因为映射已经建立 close(fd); // 写入数据到映射区域,相当于写入文件 memcpy(map, text, text_size); printf("Written to file via mmap: %s\n", map); // 修改映射区域中的数据 strcpy(map, "Hi, mmap!"); printf("Modified mapping: %s\n", map); // 解除映射 if (munmap(map, text_size) == -1) { perror("munmap"); exit(EXIT_FAILURE); } // 重新打开文件读取内容 fd = open(filename, O_RDONLY); if (fd < 0) { perror("open for reading"); exit(EXIT_FAILURE); } char buffer[50]; ssize_t bytes = read(fd, buffer, sizeof(buffer) - 1); if (bytes < 0) { perror("read"); close(fd); exit(EXIT_FAILURE); } buffer[bytes] = '\0'; printf("File content after modification: %s\n", buffer); close(fd); return 0; }
代码解释
-
打开文件:
int fd = open(filename, O_RDWR | O_CREAT, 0666);
-
以读写模式打开或创建文件
example.txt
,权限为rw-rw-rw-
。
-
-
调整文件大小:
ftruncate(fd, text_size);
-
使用
ftruncate()
调整文件大小,以确保映射区域有足够的空间存放数据。
-
-
创建文件内存映射:
char *map = mmap(NULL, text_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
-
将文件映射到内存,允许读写操作。
-
MAP_SHARED
:对映射区域的更改会写回文件,并且对其他映射该文件的进程可见。
-
-
写入和修改映射区域:
memcpy(map, text, text_size); strcpy(map, "Hi, mmap!");
-
通过指针直接修改映射区域,相当于对文件内容进行操作。
-
-
解除映射并验证文件内容:
munmap(map, text_size);
-
解除映射后,重新打开文件并读取其内容,验证修改是否生效。
-
运行结果
执行上述程序后,example.txt
的内容将被写入和修改:
Written to file via mmap: Hello, mmap! Modified mapping: Hi, mmap! File content after modification: Hi, mmap!
(3)两个进程针对同一个文件创建共享的内存映射,实现共享内存
详细解释
共享内存是一种高效的进程间通信(IPC)机制,允许多个进程访问同一块内存区域。通过 mmap()
映射同一个文件,多个进程可以共享内存数据,无需使用管道、消息队列等其他 IPC 方法。共享内存的优点包括:
-
高性能:直接在内存中进行数据交换,速度快。
-
简便性:多个进程可以像访问普通内存一样访问共享数据。
-
灵活性:适用于需要频繁交换大量数据的应用场景。
示例代码
以下示例展示了如何使用 mmap()
将一个文件映射到两个独立进程的内存空间,实现共享内存通信。
步骤概述:
-
父进程创建并映射一个文件到内存。
-
父进程创建子进程。
-
子进程映射同一个文件到内存。
-
父子进程通过共享内存交换数据。
示例代码
共享内存的父进程代码:
// parent.c #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/mman.h> #include <unistd.h> #include <string.h> #include <sys/wait.h> #define SHARED_FILE "shared_mem.txt" #define SHARED_SIZE 1024 int main() { // 打开或创建共享文件 int fd = open(SHARED_FILE, O_RDWR | O_CREAT, 0666); if (fd < 0) { perror("open"); exit(EXIT_FAILURE); } // 调整文件大小 if (ftruncate(fd, SHARED_SIZE) == -1) { perror("ftruncate"); close(fd); exit(EXIT_FAILURE); } // 映射共享内存 char *shared_mem = mmap(NULL, SHARED_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (shared_mem == MAP_FAILED) { perror("mmap"); close(fd); exit(EXIT_FAILURE); } // 关闭文件描述符 close(fd); // 写入数据到共享内存 const char *message = "Hello from parent!"; strncpy(shared_mem, message, SHARED_SIZE); printf("Parent wrote: %s\n", message); // 创建子进程 pid_t pid = fork(); if (pid < 0) { perror("fork"); munmap(shared_mem, SHARED_SIZE); exit(EXIT_FAILURE); } else if (pid == 0) { // 子进程 // 打开共享文件 int child_fd = open(SHARED_FILE, O_RDWR, 0666); if (child_fd < 0) { perror("child open"); exit(EXIT_FAILURE); } // 映射共享内存 char *child_shared_mem = mmap(NULL, SHARED_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, child_fd, 0); if (child_shared_mem == MAP_FAILED) { perror("child mmap"); close(child_fd); exit(EXIT_FAILURE); } // 关闭文件描述符 close(child_fd); // 读取父进程写入的数据 printf("Child read: %s\n", child_shared_mem); // 修改共享内存中的数据 const char *child_message = "Hello from child!"; strncpy(child_shared_mem, child_message, SHARED_SIZE); printf("Child wrote: %s\n", child_message); // 解除映射并退出 munmap(child_shared_mem, SHARED_SIZE); exit(EXIT_SUCCESS); } else { // 父进程等待子进程结束 wait(NULL); // 读取子进程写入的数据 printf("Parent reads: %s\n", shared_mem); // 解除映射 munmap(shared_mem, SHARED_SIZE); } // 删除共享文件 unlink(SHARED_FILE); return 0; }
代码解释
-
创建和映射共享文件:
int fd = open(SHARED_FILE, O_RDWR | O_CREAT, 0666); ftruncate(fd, SHARED_SIZE); char *shared_mem = mmap(NULL, SHARED_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
-
创建或打开一个名为
shared_mem.txt
的文件。 -
使用
ftruncate()
调整文件大小,以容纳共享内存。 -
使用
mmap()
将文件映射到内存,设置为可读写且共享。
-
-
父进程写入数据:
const char *message = "Hello from parent!"; strncpy(shared_mem, message, SHARED_SIZE); printf("Parent wrote: %s\n", message);
-
创建子进程并映射同一文件:
pid_t pid = fork(); if (pid == 0) { // 子进程代码 }
-
子进程重新打开并映射同一个共享文件。
-
-
子进程读取并修改共享内存:
printf("Child read: %s\n", child_shared_mem); const char *child_message = "Hello from child!"; strncpy(child_shared_mem, child_message, SHARED_SIZE);
-
父进程读取子进程写入的数据:
printf("Parent reads: %s\n", shared_mem);
-
清理资源:
-
父进程解除映射并删除共享文件。
-
运行结果
执行上述程序后,输出可能如下:
Parent wrote: Hello from parent! Child read: Hello from parent! Child wrote: Hello from child! Parent reads: Hello from child!
注意事项
-
同步机制:上述示例没有实现同步机制。在实际应用中,应该使用信号量、互斥锁或其他同步方法来防止数据竞争。
-
错误处理:示例代码中基本的错误处理已包含,但在生产环境中应更加详细。
-
资源管理:确保所有映射区域在不再使用时被正确解除,并且文件描述符被关闭。
总结
mmap()
是一个功能强大的系统调用,广泛应用于各种内存管理和进程间通信的场景。通过匿名内存映射,可以高效地在进程间共享数据;通过文件内存映射,可以简化文件操作并提升性能;通过将同一文件映射到多个进程的地址空间,实现高效的共享内存通信。
理解并熟练使用 mmap()
能够显著提升程序的性能和灵活性,尤其是在需要高效内存管理和进程间协作的复杂应用中。