mmap()函数是一个用于在用户空间和内核空间之间进行文件映射的系统调用。它允许文件在物理内存中的特定区域被映射到进程的地址空间中,从而允许进程通过内存访问操作来读取和写入文件。
#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数说明:
addr
:映射的起始地址,一般为NULL,让内核自动选择。
length
:映射的长度,可以是文件的长度或者是页的整数倍。
prot
:内存保护标志,用于指定内存的保护方式,如读、写、执行等。
flags
:控制映射的标志,如映射是共享的还是私有的。
fd
:打开的文件描述符,指定要映射的文件。
offset
:文件映射的偏移量,指定映射文件开始的位置。
addr
:
如果addr
为NULL
,内核会自动选择一个起始地址作为映射的起点。
如果addr
不为NULL
,则内核会尝试将映射放置在指定的起始地址处。如果指定的地址无效或与已有映射冲突,则mmap()
调用会失败。
通常情况下,最常用的做法是将addr
设置为NULL
,由内核选择一个适当的地址。
length
:
length
参数指定要映射的长度,可以是文件的长度或者是页的整数倍。对于文件映射,length
一般为文件大小。
对齐到页面大小可以获得最佳性能,因为页面是内存管理的最小单位。
prot
:
prot
参数指定内存的保护标志,用于指定内存的访问权限。常见的标志有PROT_READ
(可读)、PROT_WRITE
(可写)和PROT_EXEC
(可执行)。
这些标志可以按位或运算进行组合,以指定多个访问权限。
flags
:
flags
参数用于控制映射的标志,用于指定映射是共享的还是私有的,以及一些其他的映射特性。
常见的标志有MAP_SHARED
(共享映射)和MAP_PRIVATE
(私有映射)。
共享映射允许多个进程对映射进行读写操作,而私有映射则会对映射的页进行复制,每个进程都有自己的私有拷贝。
fd
:
fd
参数是打开的文件描述符,用于指定要映射的文件。
如果fd
为-1,则表示创建一个匿名映射(anonymous mapping),不与任何文件关联,而是在内存中分配一段连续的空间。
匿名映射通常用于进程间的共享内存或者用作临时存储空间。
offset
:
offset
参数指定从文件的哪个位置开始进行映射。一般情况下,可以将offset
设置为0,表示从文件的开头位置开始映射。
offset
必须是页面大小的整数倍。
mmap()函数返回一个指向映射区域的指针,如果映射失败则返回MAP_FAILED。
私有映射(Private Mapping):对映射的修改不会影响到原始文件,映射的页会被复制到进程的私有页中。
共享映射(Shared Mapping):多个进程可以对映射进行读写操作,任何一个进程对映射的修改都会影响到其他进程。
文件映射是一种将文件内容直接映射到进程地址空间的技术,它允许进程通过对内存的直接操作来读取或写入文件。文件映射具有以下特性:
零拷贝(Zero-Copy): 文件映射可以实现零拷贝操作,即数据可以直接从文件系统读取到映射的内存区域,而无需在用户空间和内核空间之间进行数据复制。这可以显著提高I/O操作的效率。
内存操作: 通过文件映射,进程可以使用内存访问指令(如读写操作)来直接访问文件内容,而不是使用传统的文件I/O(如read()
和write()
)。这使得文件操作看起来像是对内存的直接操作,简化了编程模型。
共享访问: 当使用共享映射(MAP_SHARED
标志)时,多个进程可以访问同一文件映射区域。这意味着任何进程对映射区域的修改都会立即对其他所有共享该映射的进程可见,反之亦然。
私有访问: 私有映射(MAP_IVATE
标志)则不允许其他进程访问该映射区域。进程对私有映射的修改不会影响其他进程,也不会影响原始文件。
懒惰写入(Lazy Write): 默认情况下,文件映射采用懒惰写入策略。这意味着对映射区域的修改不会立即写入文件系统,而是在映射区域被回收时或者显式调用msync()
函数时才进行写入。这可以减少不必要的磁盘I/O操作,提高性能。
同步机制: 文件映射提供了同步机制,例如msync()
函数可以用来强制将映射区域的修改写入文件系统,或者将文件系统中的修改同步到映射区域。
内存保护: 文件映射允许通过prot
参数来设置内存保护标志,如PROT_READ
、PROT_WRITE
和PROT_EXEC
,以确保映射区域的安全性。
映射类型: 文件映射可以是匿名映射(不与任何文件关联)或文件映射(与特定文件关联)。匿名映射通常用于进程间通信,而文件映射则用于访问文件系统中的数据。
页对齐: 文件映射的起始地址和长度通常需要对页面大小进行对齐。页面大小通常是4KB,这取决于系统的内存管理配置。
mmap内存映射的实现过程,总的来说可以分为三个阶段:
(一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
1、进程在用户空间调用库函数mmap
2、在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
3、为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
4、将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中
(二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
5、为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
6、通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。
7、内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
8、通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。
(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。
9、进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
10、缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
11、调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
12、之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟(写回策略中有介绍),可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。
对齐要求: mmap()
函数通常会要求映射区域的起始地址和长度是页面大小(通常为4KB)的整数倍。因此,在使用mmap()
函数时,需要确保起始地址和长度符合对齐要求,否则可能导致映射失败。
长度与文件大小的关系: 如果将文件映射到内存中,通常需要保证映射的长度与文件的大小相匹配,以确保可以访问到文件的所有内容。否则,如果访问超出映射长度范围的数据,将会引发段错误(Segmentation Fault)。
文件描述符的打开方式: 在创建文件映射时,需要使用正确的文件描述符打开方式。如果需要对映射进行读写操作,应使用O_RDWR
标志;如果只需要读取文件内容,应使用O_RDONLY
标志;如果只需要写入文件内容,应搭配O_CREAT | O_TRUNC
等标志使用。
内存保护标志的设置: 需要根据需求正确设置内存保护标志(prot
参数),以决定映射区域是否可以读取、写入或执行。不正确的设置可能导致访问权限不足的问题。
共享映射的同步: 如果使用共享映射(MAP_SHARED
标志),需要注意在多个进程之间对映射区域的同步操作。在多进程并发访问映射区域时,需要使用其他机制(如信号量、文件锁等)来保证数据的一致性和互斥访问。
文件的改变对映射的影响: 如果映射的文件发生变化(如被其他进程修改、截断等),映射区域的行为是未定义的。为了避免这种情况,可以在映射区域上使用madvise()
函数,并使用MADV_DONTNEED
标志来告知系统底层页不再需要,从而避免无效访问。
取消映射和关闭文件描述符: 在不再使用映射区域后,需要使用munmap()
函数释放映射内存,同时关闭相关的文件描述符。如果忘记取消映射和关闭文件描述符,可能会导致内存泄漏和资源泄漏。
munmap()
函数是用于取消映射区域的系统调用,它将释放由mmap()
函数创建的内存映射。
以下是munmap()
函数的重点介绍:
函数原型:
#include
int munmap(void *addr, size_t length);
参数说明:
addr
:指向要取消映射的起始地址。
length
:映射区域的长度。
函数返回值: munmap()
函数返回0表示成功,返回-1表示失败。失败的原因可能是无效的起始地址或长度,或者由于权限不足。
功能: munmap()
函数用于取消映射区域,将内核中的虚拟内存释放回操作系统。取消映射后,该区域不再可访问。
注意事项:
munmap()
函数必须以mmap()
函数返回的起始地址和长度作为参数进行调用,否则会出现未定义的行为。
取消映射后,该区域的访问将引发段错误(Segmentation Fault)。
取消映射并不会自动关闭与映射区域关联的文件描述符,需要通过调用close()
函数来显式关闭。
msync()
函数是用于将内存中的改动同步到文件系统中的函数,确保数据的持久化。它可以用于确保对映射区域的修改写入文件系统,或者将文件系统中的修改同步到映射区域。
以下是msync()
函数的重点介绍:
函数原型:
#include
int msync(void *addr, size_t length, int flags);
参数说明:
addr
:指向映射区域的起始地址。
length
:映射区域的长度。
flags
:用于指定同步操作的标志。常见的标志有MS_ASYNC
(异步写入)、MS_SYNC
(同步写入)和MS_INVALIDATE
(使缓存无效)。
函数返回值: msync()
函数返回0表示成功,返回-1表示失败。失败的原因可能是无效的起始地址或长度,或者由于权限不足。
功能: msync()
函数用于将进程内存中对映射区域的修改写入到文件系统中,或者将文件系统中的修改同步到映射区域。这样可以确保数据的持久化,保证文件的一致性。
同步模式: msync()
函数提供了两种同步模式:
MS_ASYNC
:异步写入模式。这种模式下,msync()
函数会立即返回,而不会等待写入操作的完成。这样可以提高性能,但不能保证写入的数据立即持久化到磁盘上。
MS_SYNC
:同步写入模式。这种模式下,msync()
函数会等待所有的写入操作完成,然后返回。这确保了对映射区域的修改已经持久化到磁盘上。
缓存处理: msync()
函数的flags
参数中的MS_INVALIDATE
标志可用于使缓存无效,即强制从文件系统重新读取映射区域的数据。这样可以确保映射区域的数据与文件系统的数据一致。
#include
#include
#include
#include
#include
int main() {
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
struct stat sb;
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
return 1;
}
char *addr = (char *)mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
for (off_t i = 0; i < sb.st_size; ++i) {
putchar(addr[i]);
}
munmap(addr, sb.st_size);
close(fd);
return 0;
}
在这个示例中,我们打开一个名为example.txt
的文件并获取它的文件描述符。然后,使用fstat()
函数获取文件的元数据信息,包括文件大小。
接下来,我们使用mmap()
函数将文件映射到内存中。PROT_READ
标志表示映射的内存可读。MAP_PRIVATE
标志表示创建一个私有映射,对映射的修改不会影响原始文件。
然后,我们可以通过访问addr
指针来读取映射的文件内容,并使用sb.st_size
来确定要读取的字节数。
最后,使用munmap()
函数取消映射,并关闭文件描述符。
#include
#include
#include
#include
#include
#include
int main() {
const char *data = "Hello, World!";
size_t length = strlen(data);
int fd = open("output.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
return 1;
}
if (ftruncate(fd, length) == -1) {
perror("ftruncate");
close(fd);
return 1;
}
char *addr = (char*)mmap(NULL, length, PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
memcpy(addr, data, length);
if (msync(addr, length, MS_SYNC) == -1) {
perror("msync");
close(fd);
return 1;
}
munmap(addr, length);
close(fd);
return 0;
}
在这个示例中,我们创建了一个名为output.txt
的文件,并将字符串"Hello, World!"写入到文件中。
首先,我们打开文件并使用ftruncate()
函数设置文件大小为要写入的数据的长度。然后,使用mmap()
函数将文件映射到内存中。PROT_WRITE
标志表示映射的内存可写入。MAP_SHARED
标志表示创建一个共享映射,对映射的修改将影响到原始文件。
接下来,我们使用memcpy()
函数将数据从源data
复制到映射的内存区域。
然后,我们使用msync()
函数将对映射区域的修改同步到文件系统,并确保写入了磁盘。
最后,我们取消映射,并关闭文件描述符。
#include
#include
#include
#include
#include
#include
#include
int main()
{
const int SIZE = 4096;
const char *name = "shared_memory";
int fd = shm_open(name, O_CREAT | O_RDWR, 0666);
ftruncate(fd, SIZE);
void *addr = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED)
{
perror("mmap");
return 1;
}
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
return 1;
}
else if (pid == 0)
{
strncpy((char*)addr, "Hello from child", strlen("Hello from child"));
return 0;
}
else
{
wait(NULL);
printf("Parent received: %s\n", (char *)addr);
shm_unlink(name);
return 0;
}
}
这个示例展示了如何使用mmap
函数进行进程间共享内存。父进程创建了一个共享内存区域,然后fork出一个子进程。子进程写入数据到共享内存,父进程读取并打印共享内存中的数据