文章目录
- 前言
- 一、mmap()函数简介
- 二、代码演示
- 2.1 mmap使用场景
- 2.2 私有匿名映射
- 2.3 私有文件映射
- 2.4 共享匿名映射
- 2.5 共享文件映射
- 参考
前言
NAME
mmap, munmap - map or unmap files or devices into memory
SYNOPSIS
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);
mmap函数用于将文件或设备映射到内存中。
mmap函数是一种内存映射文件的方法,它可以将一个文件或设备映射到进程的地址空间中,使得进程可以像访问内存一样访问文件或设备。
一、mmap()函数简介
mmap()函数在调用进程的虚拟地址空间中创建一个新的映射:
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
RETURN VALUE
On success, mmap() returns a pointer to the mapped area.
通过这种方式,文件内容可以通过指针直接访问addr,就像访问普通的内存数组一样,这极大地提高了文件操作的效率和直观性。
以下是关于其行为的一些关键点:
(1)参数addr指定了新映射的起始地址。如果addr为NULL,内核会选择一个(页对齐的)地址来创建映射;这是创建新映射的最便携方法。如果addr不为NULL,内核将其作为放置映射的提示;对于Linux系统,内核会选择一个靠近的页边界(但始终大于或等于/proc/sys/vm/mmap_min_addr指定的值)并尝试在那里创建映射。如果该地址已经存在其他映射,内核会选择一个新的地址,可能与提示相关或不相关。新映射的地址将作为调用的结果返回。
(2)参数length指定映射的长度,必须大于0。
(3)对于文件映射(与匿名映射相对应,参见MAP_ANONYMOUS),映射的内容使用文件描述符fd引用的文件(或其他对象)中的从偏移量offset开始的length字节进行初始化。offset必须是sysconf(_SC_PAGE_SIZE)返回的页面大小的倍数。
总结一下,mmap()函数在调用进程的虚拟地址空间中创建一个新的映射,内核根据提供的地址或提示选择一个合适的地址来确定映射的起始位置。对于文件映射,映射的内容从文件中的指定偏移量处开始进行初始化。
在mmap()调用返回后,文件描述符fd可以立即关闭而不会使映射失效。
参数prot描述了映射的期望内存保护方式(不能与文件的打开模式冲突)。它可以是PROT_NONE,或者是以下标志位的按位或:
PROT_EXEC:页面可执行。
PROT_READ:页面可读取。
PROT_WRITE:页面可写入。
PROT_NONE:页面不可访问。
参数flags确定对映射的更新是否对其他映射同一区域的进程可见,以及是否将更新传递到底层文件。这个行为是通过在flags中包含以下值中的一个来确定的:
(1)MAP_SHARED:共享映射。对映射的更新对其他映射同一区域的进程可见,并且(对于基于文件的映射而言)会传递到底层文件。(要精确控制何时将更新传递到底层文件,需要使用msync(2)函数。)
使用MAP_SHARED标志可以实现共享内存,让多个进程可以共享同一区域的映射,并且对映射的更新可以相互可见。对于基于文件的映射,更新也会被传递到底层文件。需要注意的是,要精确控制更新何时传递到底层文件,可以使用msync(2)函数。
NAME
msync - synchronize a file with a memory map
(2)MAP_PRIVATE:用于创建私有的写时复制(copy-on-write)映射。对映射的更新对于其他映射同一文件的进程不可见,并且不会传递到底层文件。在mmap()调用后对文件进行的更改是否在映射的区域中可见是未指定的。
使用MAP_PRIVATE标志可以创建一个独立的映射副本,对该映射的写入操作会在需要时进行写时复制,即只有在修改映射的页面时才会复制相应的页面内容,以确保每个进程都有自己的私有副本。这样,对映射的更新不会影响其他进程的映射,并且不会对底层文件进行实际的修改。
(3)MAP_ANONYMOUS:用于创建一个不由任何文件支持的映射,其内容被初始化为零。fd参数会被忽略;但是,一些实现要求如果指定了MAP_ANONYMOUS(或MAP_ANON),则fd必须为-1,因此可移植的应用程序应确保这一点。offset参数应为零。只有在Linux内核2.4及更高版本上,才支持将MAP_ANONYMOUS与MAP_SHARED结合使用。
使用MAP_ANONYMOUS标志创建的映射不与任何文件相关联,其内容被初始化为零。这种映射通常用于实现匿名内存,用于共享数据或作为临时存储。由于没有与文件的关联,对映射的更改不会影响任何文件,并且不需要指定文件描述符(fd参数被忽略)。
如下图所示:
Memory mmaping segment 就属于内存映射区。
二、代码演示
2.1 mmap使用场景
物理内存页主要分为两种:一种是匿名页,另一种是文件页。
根据物理内存页的类型分类,内存映射自然也分为两种:一种是虚拟内存对匿名物理内存页的映射,另一种是虚拟内存对文件页的映射。
(1)匿名页(Anonymous Pages):匿名页是一种没有与之关联的文件的内存页。它们通常用于存储进程的堆栈、堆分配的内存以及共享内存等。匿名页的内容在映射时可以初始化为零或未初始化状态,不会与任何文件进行关联。
(2)文件页(File Pages):文件页是与文件关联的内存页。它们用于将文件的内容映射到进程的地址空间,允许进程通过内存访问文件的内容,而无需直接进行读取和写入操作。文件页可以用于读取文件的内容,也可以用于将修改的数据写回文件。
(1)私有匿名映射:malloc分配大内存在glibc中对应的mmap()实现,以及BSS 段,堆,栈。
(2)私有文件映射:映射动态库,文件的text、data段。
(3)共享匿名映射:用于进程间(父子进程)共享内存。
(4)共享文件映射:用于进程间(不同的进程)共享内存,通信。
(5)其他,比如大页内存。
2.2 私有匿名映射
私有匿名映射使用一下标志位:
MAP_PRIVATE | MAP_ANONYMOUS
其中fd = -1,与文件没有关联。
#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define SIZE 4096
int main() {
// 创建一个私有匿名映射
void* addr = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
perror("mmap failed");
exit(1);
}
// 在映射的内存中进行读写操作
int* data = (int*)addr;
*data = 42;
printf("Value at mapped memory: %d\n", *data);
// 取消映射
if (munmap(addr, SIZE) == -1) {
perror("munmap failed");
exit(1);
}
return 0;
}
# ./a.out
Value at mapped memory: 42
私有匿名映射(mmap/brk/malloc)申请的内存是一段虚拟地址空间,当没有在这段虚拟地址空间写入的时候,没有对应的物理内存,只有在这段虚拟地址空间写入的时候,就会发生缺页异常,然后分配对应的物理地址,建立虚拟地址空间和物理地址的影映射关系。
从上图我们可以看到进程虚拟内存空间中的 BSS 段,堆,栈这些虚拟内存区域都是私有匿名映射区域,glibc 中的 malloc函数当申请比较大的内存时,也使用私有匿名映射区域。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
size_t size = 256 * 1024; // 128K内存的大小
getpid();
// 使用malloc分配内存
char* buffer = (char*)malloc(size);
if (buffer == NULL) {
perror("malloc");
exit(1);
}
getpid();
// 内存分配成功,可以使用buffer指针访问分配的内存
// 这里可以进行读取、写入或处理数据的操作
// 释放内存
free(buffer);
return 0;
}
# strace ./a.out
......
getpid() = 101479
brk(NULL) = 0x55a399f12000
brk(0x55a399f33000) = 0x55a399f33000
mmap(NULL, 266240, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fec8b5ea000
getpid() = 101479
munmap(0x7fec8b5ea000, 266240) = 0
mmap(NULL, 266240, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fec8b5ea000
2.3 私有文件映射
私有文件映射标志位:
MAP_PRIVATE
其中fd与文件有关联。
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#define FILE_PATH "example.txt"
#define SIZE 4096
int main() {
// 打开文件
int fd = open(FILE_PATH, O_RDWR);
if (fd == -1) {
perror("open failed");
exit(1);
}
// 获取文件大小
struct stat st;
if (fstat(fd, &st) == -1) {
perror("fstat failed");
exit(1);
}
off_t file_size = st.st_size;
// 创建私有文件映射
void* addr = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) {
perror("mmap failed");
exit(1);
}
// 在映射的内存中进行读取操作
char* data = (char*)addr;
printf("Content of the file:%s\n", data);
// 在映射的内存中进行写入操作
sprintf(data, "Hello, World!");
printf("Content of the file:%s\n", data);
// 取消映射
if (munmap(addr, file_size) == -1) {
perror("munmap failed");
exit(1);
}
// 关闭文件
if (close(fd) == -1) {
perror("close failed");
exit(1);
}
return 0;
}
example.txt 文件的内容是 111。
读取其内容,然后写入:
# cat example.txt
111
# ./a.out
Content of the file:111
Content of the file:Hello, World!
# cat example.txt
111
可以看到对私有文件映射区域的修改不会修改实际的文件。
私有文件映射允许多个进程将文件的内容映射到各自的虚拟内存空间中,但对映射的修改只反映到各自的文件页上,而不会影响其他进程的文件页。这种方式可以用于加载二进制可执行文件的代码段和数据段到进程的虚拟内存空间中以及加载动态库。
从上图我们可以看到进程虚拟内存空间中的 text 段,data 段和.so动态库这些虚拟内存区域都是私有文件映射区域。
2.4 共享匿名映射
私有匿名映射使用一下标志位:
MAP_SHARED | MAP_ANONYMOUS
其中fd = -1,与文件没有关联。
#include <sys/mman.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#define SIZE 4096
int main() {
// 创建共享匿名映射
void* addr = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
perror("mmap failed");
exit(1);
}
// 创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
exit(1);
} else if (pid == 0) {
// 子进程写入数据到共享内存
char* data = (char*)addr;
sprintf(data, "Hello from the child process!");
// 子进程结束
exit(0);
} else {
// 等待子进程结束
wait(NULL);
// 父进程读取共享内存中的数据
char* data = (char*)addr;
printf("Content of shared memory: %s\n", data);
// 解除映射
if (munmap(addr, SIZE) == -1) {
perror("munmap failed");
exit(1);
}
}
return 0;
}
# ./a.out
Content of shared memory: Hello from the child process!
共享匿名映射在父子进程之间共享内存和实现进程间通信时非常有用。它是一种特殊的共享文件映射,不需要依赖具体的文件,而是将映射的内存区域与进程间共享。
父子进程通信:父进程可以创建一个共享匿名映射,并将其传递给子进程。子进程可以访问并修改映射的内存区域,从而与父进程进行通信。这种方法常用于进程间共享数据或传递消息。
父进程和子进程其页表项是相同的。只要父子进程中的一个发生了缺页中断,就给分配物理内存,建立其虚拟内存和物理内存之间的映射,由于父子进程的页表项是相同的,且共享内存,那么另一个发生缺页中断时,对应页表项已经建立了到物理地址的映射关系。
2.5 共享文件映射
共享文件映射标志位:
MAP_SHARED
其中fd与文件有关联。
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#define SIZE 4096
#define FILE_NAME "shared_memory"
int main() {
// 创建共享文件
int fd = open(FILE_NAME, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open failed");
exit(1);
}
// 设置共享文件大小
if (ftruncate(fd, SIZE) == -1) {
perror("ftruncate failed");
exit(1);
}
// 创建共享文件映射
void* addr = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
perror("mmap failed");
exit(1);
}
// 创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
exit(1);
} else if (pid == 0) {
// 子进程写入数据到共享内存
char* data = (char*)addr;
sprintf(data, "Hello from the child process!");
// 将修改刷新到文件
if (msync(addr, SIZE, MS_SYNC) == -1) {
perror("msync failed");
exit(1);
}
// 子进程结束
exit(0);
} else {
// 等待子进程结束
wait(NULL);
// 父进程读取共享内存中的数据
char* data = (char*)addr;
printf("Content of shared memory: %s\n", data);
// 解除映射
if (munmap(addr, SIZE) == -1) {
perror("munmap failed");
exit(1);
}
// 关闭文件
if (close(fd) == -1) {
perror("close failed");
exit(1);
}
// 删除共享文件
if (unlink(FILE_NAME) == -1) {
perror("unlink failed");
exit(1);
}
}
return 0;
}
# ./a.out
Content of shared memory: Hello from the child process!
共享文件映射在多进程之间共享内存、实现进程间通信,并且避免写时复制的场景中非常常见。
在这种情况下,多个进程可以通过将同一个文件映射到它们的地址空间来实现共享内存。这意味着它们可以直接读取和写入映射的内存区域,而无需进行复制操作。
共享文件映射的优势在于,多个进程可以通过将同一个文件映射到它们的地址空间来共享数据,而无需进行复制。这对于需要频繁读写共享数据的场景非常有用,因为它避免了写时复制带来的性能开销。
需要注意的是,共享文件映射使用文件作为底层存储介质,因此对于共享内存的读写操作会反映到文件中。这也意味着共享文件映射在进程终止后依然存在,并且可以被其他进程访问。因此,需要小心处理共享文件映射的生命周期和访问权限,以确保数据的一致性和安全性。
参考
https://mp.weixin.qq.com/s/AUsgFOaePwVsPozC3F6Wjw