Linux深入理解内存管理31(基于Linux6.6)---内存映射mmap介绍
一、概念
内存映射,简而言之就是将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,同样,内核空间对这段区域的修改也直接反映给用户空间,对于用户空间和内核空间两者之间需要进行大量数据传输等操作的话效率是非常高的。如下图所示:
实现这样的映射后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页到对应的文件磁盘上,就可以完成对于文件的操作,而不需要再调用read/write等系统调用函数。同时,内核空间对于这段区域的修改也可以直接反映到用户空间,从而可以实现不同进程间的文件共享。
mmap/munmap接口是常用的内存映射的系统调用接口,无论是在用户空间分配内存、读写大文件、连接动态库文件,还是多进程间共享内存,都可以看到其身影,其声明如下:
#include <sys/mman.h>
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
条件:
mmap()必须以PAGE_SIZE为单位进行映射,而内存也只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行以PAGE_SIZE的倍数大小进行映射。
参数说明:
start:映射区 的开 始地址,设置为0时表示由系统决定映射区的起始地址。
length: 映射 区的长度。//长度单位是 以字节为单位,不足一内存页按一内存页处理。
prot:期望 的 内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起。
- PROT_EXEC: 表示映射的页面是可以执行的。
- PROT_READ:表示映射的页面是可以读取的。
- PROT_WRITE :表示映射的页面是可以写入的。
- PROT_NONE :表示映射的页面是不可访问的。
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体。
- MAP_SHARED:创建一个共享映射的区域,多个进程可以通过共享映射的方式来映射一个文件,这样其他进程也可以看到映射内容的改变,修改后的内容会同步到磁盘文件。
- MAP_PRIVATE:创建一个私有的写时复制的映射,多个进程可以通过私有映射方式来映射一个文件,其他的进程不会看到映射文件内容的改变,修改后也不会同步到磁盘中。
- MAP_ANONYMOUS:创建一个匿名映射,即没有关联到文件的映射。
- MAP_FIXED:
- MAP_POPULATE:提前遇到文件内容到映射区。
fd:mmap映射释放和文件相关联,可以分为匿名映射和文件映射
文件映射:将一个普通文件的全部或者一部分映射到进程的虚拟内存中。映射后,进程就可以直接在对应的内存区域操作文件内容。匿名映射:匿名映射没有对应的文件或者对应的文件时虚拟文件(如:/dev/zero),映射后会把内存分页全部初始化为0。
offset:被映射对象内容的起点。
返回说明:成功执行时,mmap()返回被映射区的指针,munmap()返回0。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],munmap返回-1。
根据文件关联性和映射区域示范共享等属性,其分为:
私有映射 | 共享映射 | |
匿名映射 | 私有匿名映射(通常用于内存分配),当使用大于128K内存时fd=-1且flags=MAP_ANONYMOUS|MAP_PRIVATE | 共享匿名映射(通常用于父子进程间共享),fd=-1且flags=MAP_ANONYMOUS|MAP_SHARED |
文件映射 | 私有文件映射(通常用于动态库加载) | 共享文件映射(通常用于内存映射IO,进程间通信) |
1.1、内存映射的应用场景
内存映射在 Linux 中有多种应用,涵盖了文件映射、共享内存、匿名内存等多个方面。以下是一些典型的应用场景:
1、文件映射
mmap()
可以将文件的内容映射到进程的地址空间,使得文件内容在内存中可直接访问,从而避免了传统的 read()
和 write()
系统调用带来的性能瓶颈。
-
优点:
- 零拷贝:避免了内核与用户空间之间的数据拷贝,提升了 I/O 性能。
- 懒加载:文件内容只有在访问时才会被加载到内存,节省内存开销。
- 文件大小不受限制:对于大文件,
mmap()
只会映射需要的部分。
-
例子:
-
int fd = open("example.txt", O_RDWR); size_t length = 4096; char *data = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (data == MAP_FAILED) { perror("mmap failed"); exit(1); } // 可以通过 data 指针访问文件内容
2、共享内存
mmap()
还可用于进程间通信(IPC)。通过将一块内存映射到多个进程的地址空间,实现它们之间的数据共享。这是实现共享内存的一种常见方法,适用于多进程共享数据的场景。
- 例子:
-
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); ftruncate(fd, 4096); // 设置共享内存的大小 void *shared_memory = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (shared_memory == MAP_FAILED) { perror("mmap failed"); exit(1); } // 共享内存区域可以在多个进程间共享
3、匿名内存映射
匿名内存映射是不依赖于文件的映射方式。它通过 MAP_ANONYMOUS
标志来创建一个匿名内存区域。常用于分配内存或进程间共享内存。
- 例子:
void *mem = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (mem == MAP_FAILED) {
perror("mmap failed");
exit(1);
}
// mem 现在是一个匿名内存区域,可以用来存储数据
二、源码分析
查看mmap的系统调用的代码实现,其流程为sys_mmp_pg_off(),最终会调用达到do_mmap_pgoff,该函数使一个体系结构无关的代码,定义在mm/mmap.c中,
do_mmap(),是整个mmap()
的具体操作函数:
mm/mmap.c
/*
* The caller must write-lock current->mm->mmap_lock.
*/
unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, unsigned long pgoff,
unsigned long *populate, struct list_head *uf)
{
struct mm_struct *mm = current->mm;//获取该进程的memory descriptor
vm_flags_t vm_flags;
int pkey = 0;
validate_mm(mm);
*populate = 0;
//函数对传入的参数进行一系列检查, 假如任一参数出错,都会返回一个errno
if (!len)
return -EINVAL;
/*
* Does the application expect PROT_READ to imply PROT_EXEC?
*
* (the exception is when the underlying filesystem is noexec
* mounted, in which case we dont add PROT_EXEC.)
*/
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
if (!(file && path_noexec(&file->f_path)))
prot |= PROT_EXEC;
/* force arch specific MAP_FIXED handling in get_unmapped_area */
if (flags & MAP_FIXED_NOREPLACE)
flags |= MAP_FIXED;
//假如没有设置MAP_FIXED标志,且addr小于mmap_min_addr, 因为可以修改addr, 所以就需要将addr设为mmap_min_addr的页对齐后的地址
if (!(flags & MAP_FIXED))
addr = round_hint_to_min(addr);
/* Careful about overflows.. */
len = PAGE_ALIGN(len);//进行Page大小的对齐
if (!len)
return -ENOMEM;
/* offset overflow? */
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
return -EOVERFLOW;
/* Too many mappings? */
if (mm->map_count > sysctl_max_map_count)//判断该进程的地址空间的虚拟区间数量是否超过了限制
return -ENOMEM;
/* Obtain the address to map to. we verify (or select) it and ensure
* that it represents a valid section of the address space.
*/
addr = get_unmapped_area(file, addr, len, pgoff, flags);
if (IS_ERR_VALUE(addr))//检查addr是否有效
return addr;
if (flags & MAP_FIXED_NOREPLACE) {
if (find_vma_intersection(mm, addr, addr + len))
return -EEXIST;
}
if (prot == PROT_EXEC) {
pkey = execute_only_pkey(mm);
if (pkey < 0)
pkey = 0;
}
/* Do simple checking here so the lower-level routines won't have
* to. we assume access permissions have been handled by the open
* of the memory object, so we don't do any here.
*/
vm_flags = calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags) |
mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
//假如flags设置MAP_LOCKED,即类似于mlock()将申请的地址空间锁定在内存中, 检查是否可以进行lock
if (flags & MAP_LOCKED)
if (!can_do_mlock())
return -EPERM;
if (mlock_future_check(mm, vm_flags, len))
return -EAGAIN;
if (file) {
struct inode *inode = file_inode(file);// file指针不为nullptr, 即从文件到虚拟空间的映射
unsigned long flags_mask;
if (!file_mmap_ok(file, inode, pgoff, len))
return -EOVERFLOW;
flags_mask = LEGACY_MAP_MASK | file->f_op->mmap_supported_flags;
switch (flags & MAP_TYPE) { //根据标志指定的map种类,把为文件设置的访问权考虑进去
case MAP_SHARED:
/*
* Force use of MAP_SHARED_VALIDATE with non-legacy
* flags. E.g. MAP_SYNC is dangerous to use with
* MAP_SHARED as you don't know which consistency model
* you will get. We silently ignore unsupported flags
* with MAP_SHARED to preserve backward compatibility.
*/
flags &= LEGACY_MAP_MASK;
fallthrough;
case MAP_SHARED_VALIDATE:
if (flags & ~flags_mask)
return -EOPNOTSUPP;
if (prot & PROT_WRITE) {
if (!(file->f_mode & FMODE_WRITE))
return -EACCES;
if (IS_SWAPFILE(file->f_mapping->host))
return -ETXTBSY;
}
/*
* Make sure we don't allow writing to an append-only
* file..
*/
if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
return -EACCES;
vm_flags |= VM_SHARED | VM_MAYSHARE;
if (!(file->f_mode & FMODE_WRITE))
vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
fallthrough;
case MAP_PRIVATE:
if (!(file->f_mode & FMODE_READ))
return -EACCES;
if (path_noexec(&file->f_path)) {
if (vm_flags & VM_EXEC)
return -EPERM;
vm_flags &= ~VM_MAYEXEC;
}
if (!file->f_op->mmap)
return -ENODEV;
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
return -EINVAL;
break;
default:
return -EINVAL;
}
} else {
switch (flags & MAP_TYPE) {
case MAP_SHARED:
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
return -EINVAL;
/*
* Ignore pgoff.
*/
pgoff = 0;
vm_flags |= VM_SHARED | VM_MAYSHARE;
break;
case MAP_PRIVATE:
/*
* Set pgoff according to addr for anon_vma.
*/
pgoff = addr >> PAGE_SHIFT;
break;
default:
return -EINVAL;
}
}
/*
* Set 'VM_NORESERVE' if we should not account for the
* memory use of this mapping.
*/
if (flags & MAP_NORESERVE) {
/* We honor MAP_NORESERVE if allowed to overcommit */
if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
vm_flags |= VM_NORESERVE;
/* hugetlb applies strict overcommit unless MAP_NORESERVE */
if (file && is_file_hugepages(file))
vm_flags |= VM_NORESERVE;
}
//一顿检查和配置,调用核心的代码mmap_region
addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
if (!IS_ERR_VALUE(addr) &&
((vm_flags & VM_LOCKED) ||
(flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
*populate = len;
return addr;
}
do_mmap()
根据用户传入的参数做了一系列的检查,然后根据参数初始化vm_area_struct
的标志vm_flags
,vma->vm_file = get_file(file)
建立文件与vma
的映射, mmap_region()负责创建虚拟内存区域:
mm/mmap.c
unsigned long mmap_region(struct file *file, unsigned long addr,
unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
struct list_head *uf)
{
struct mm_struct *mm = current->mm;//获取该进程的memory descriptor
struct vm_area_struct *vma = NULL;
struct vm_area_struct *next, *prev, *merge;
pgoff_t pglen = len >> PAGE_SHIFT;
unsigned long charged = 0;
unsigned long end = addr + len;
unsigned long merge_start = addr, merge_end = end;
pgoff_t vm_pgoff;
int error;
MA_STATE(mas, &mm->mm_mt, addr, end - 1);
/* Check against address space limit. */ /* 检查申请的虚拟内存空间是否超过了限制 */
if (!may_expand_vm(mm, vm_flags, len >> PAGE_SHIFT)) {
unsigned long nr_pages;
/*
* MAP_FIXED may remove pages of mappings that intersects with
* requested mapping. Account for the pages it would unmap.
*/
nr_pages = count_vma_pages_range(mm, addr, end);
if (!may_expand_vm(mm, vm_flags,
(len >> PAGE_SHIFT) - nr_pages))
return -ENOMEM;
}
/* Unmap any existing mapping in the area */
if (do_mas_munmap(&mas, mm, addr, len, uf, false))
return -ENOMEM;
/*
* Private writable mapping: check memory availability
*/
if (accountable_mapping(file, vm_flags)) {
charged = len >> PAGE_SHIFT;
if (security_vm_enough_memory_mm(mm, charged))
return -ENOMEM;
vm_flags |= VM_ACCOUNT;
}
next = mas_next(&mas, ULONG_MAX);
prev = mas_prev(&mas, 0);
if (vm_flags & VM_SPECIAL)
goto cannot_expand;
/* Attempt to expand an old mapping */
/* Check next */
if (next && next->vm_start == end && !vma_policy(next) &&
can_vma_merge_before(next, vm_flags, NULL, file, pgoff+pglen,
NULL_VM_UFFD_CTX, NULL)) {
merge_end = next->vm_end;
vma = next;
vm_pgoff = next->vm_pgoff - pglen;
}
/* Check prev */
if (prev && prev->vm_end == addr && !vma_policy(prev) &&
(vma ? can_vma_merge_after(prev, vm_flags, vma->anon_vma, file,
pgoff, vma->vm_userfaultfd_ctx, NULL) :
can_vma_merge_after(prev, vm_flags, NULL, file, pgoff,
NULL_VM_UFFD_CTX, NULL))) {
merge_start = prev->vm_start;
vma = prev;
vm_pgoff = prev->vm_pgoff;
}
/* Actually expand, if possible */
if (vma &&
!vma_expand(&mas, vma, merge_start, merge_end, vm_pgoff, next)) {
khugepaged_enter_vma(vma, vm_flags);
goto expanded;
}
mas.index = addr;
mas.last = end - 1;
cannot_expand:
/*
* Determine the object being mapped and call the appropriate
* specific mapper. the address has already been validated, but
* not unmapped, but the maps are removed from the list.
*/
vma = vm_area_alloc(mm);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
vma->vm_start = addr;
vma->vm_end = end;
vma->vm_flags = vm_flags;
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;
if (file) {//假如指定了文件映射
if (vm_flags & VM_SHARED) {//映射的文件允许其他进程可见, 标记文件为可写
error = mapping_map_writable(file->f_mapping);
if (error)
goto free_vma;
}
vma->vm_file = get_file(file);//递增File的引用次数,返回File赋给vma
error = call_mmap(file, vma);//调用文件系统指定的mmap函数
if (error)
goto unmap_and_free_vma;
/*
* Expansion is handled above, merging is handled below.
* Drivers should not alter the address of the VMA.
*/
if (WARN_ON((addr != vma->vm_start))) {
error = -EINVAL;
goto close_and_free_vma;
}
mas_reset(&mas);
/*
* If vm_flags changed after call_mmap(), we should try merge
* vma again as we may succeed this time.
*/
if (unlikely(vm_flags != vma->vm_flags && prev)) {
merge = vma_merge(mm, prev, vma->vm_start, vma->vm_end, vma->vm_flags,
NULL, vma->vm_file, vma->vm_pgoff, NULL, NULL_VM_UFFD_CTX, NULL);
if (merge) {
/*
* ->mmap() can change vma->vm_file and fput
* the original file. So fput the vma->vm_file
* here or we would add an extra fput for file
* and cause general protection fault
* ultimately.
*/
fput(vma->vm_file);
vm_area_free(vma);
vma = merge;
/* Update vm_flags to pick up the change. */
vm_flags = vma->vm_flags;
goto unmap_writable;
}
}
vm_flags = vma->vm_flags;
} else if (vm_flags & VM_SHARED) {//假如标志为VM_SHARED,但没有指定映射文件,需要调用shmem_zero_setup(),实际映射的文件是dev/zero
error = shmem_zero_setup(vma);
if (error)
goto free_vma;
} else {
vma_set_anonymous(vma);
}
/* Allow architectures to sanity-check the vm_flags */
if (!arch_validate_flags(vma->vm_flags)) {
error = -EINVAL;
if (file)
goto close_and_free_vma;
else if (vma->vm_file)
goto unmap_and_free_vma;
else
goto free_vma;
}
if (mas_preallocate(&mas, vma, GFP_KERNEL)) {
error = -ENOMEM;
if (file)
goto close_and_free_vma;
else if (vma->vm_file)
goto unmap_and_free_vma;
else
goto free_vma;
}
if (vma->vm_file)
i_mmap_lock_write(vma->vm_file->f_mapping);
vma_mas_store(vma, &mas);
mm->map_count++;
if (vma->vm_file) {
if (vma->vm_flags & VM_SHARED)
mapping_allow_writable(vma->vm_file->f_mapping);
flush_dcache_mmap_lock(vma->vm_file->f_mapping);
vma_interval_tree_insert(vma, &vma->vm_file->f_mapping->i_mmap);
flush_dcache_mmap_unlock(vma->vm_file->f_mapping);
i_mmap_unlock_write(vma->vm_file->f_mapping);
}
/*
* vma_merge() calls khugepaged_enter_vma() either, the below
* call covers the non-merge case.
*/
khugepaged_enter_vma(vma, vma->vm_flags);
/* Once vma denies write, undo our temporary denial count */
unmap_writable:
if (file && vm_flags & VM_SHARED)
mapping_unmap_writable(file->f_mapping);
file = vma->vm_file;
expanded:
perf_event_mmap(vma);
vm_stat_account(mm, vm_flags, len >> PAGE_SHIFT);
if (vm_flags & VM_LOCKED) {
if ((vm_flags & VM_SPECIAL) || vma_is_dax(vma) ||
is_vm_hugetlb_page(vma) ||
vma == get_gate_vma(current->mm))
vma->vm_flags &= VM_LOCKED_CLEAR_MASK;
else
mm->locked_vm += (len >> PAGE_SHIFT);
}
if (file)
uprobe_mmap(vma);
/*
* New (or expanded) vma always get soft dirty status.
* Otherwise user-space soft-dirty page tracker won't
* be able to distinguish situation when vma area unmapped,
* then new mapped in-place (which must be aimed as
* a completely new data area).
*/
vma->vm_flags |= VM_SOFTDIRTY;
vma_set_page_prot(vma);
validate_mm(mm);
return addr;
close_and_free_vma:
if (vma->vm_ops && vma->vm_ops->close)
vma->vm_ops->close(vma);
unmap_and_free_vma:
fput(vma->vm_file);
vma->vm_file = NULL;
/* Undo any partial mapping done by a device driver. */
unmap_region(mm, mas.tree, vma, prev, next, vma->vm_start, vma->vm_end);
if (file && (vm_flags & VM_SHARED))
mapping_unmap_writable(file->f_mapping);
free_vma:
vm_area_free(vma);
unacct_error:
if (charged)
vm_unacct_memory(charged);
validate_mm(mm);
return error;
}
mmap_region()调用了call_mmap(file, vma): call_mmap根据文件系统的类型选择适配的mmap()函数,我们选择目前常用的ext4,ext4_file_mmap()是ext4对应的mmap, 功能非常简单,更新了file的修改时间(file_accessed(flie)),将对应的operation赋给vma->vm_flags`。
通过分析mmap的源码我们发现在调用mmap()的时候仅仅申请一个vm_area_struct来建立文件与虚拟内存的映射,并没有建立虚拟内存与物理内存的映射。假如没有设置MAP_POPULATE标志位,Linux并不在调用mmap()时就为进程分配物理内存空间,直到下次真正访问地址空间时发现数据不存在于物理内存空间时,触发Page Fault即缺页中断,Linux才会将缺失的Page换入内存空间。其代码流程图如下所示:
三、应用场景
对于传统的linux系统文件操作是如何的呢?首选来看看工作流是如何的,其流程如下图所示:
其特点为:
- 使用页缓存机制,提高读写效率和保护磁盘。
- 读文件时,先将文件从磁盘拷贝到缓存,由于页缓存区是在内核空间,不能被用户空间直接访问,所以需要将页缓存区数据再次拷贝到用户空间,有2次文件拷贝工作。
下面来看看使用内存映射文件读/写的流程,其流程图如下图所示:
其特点为:
- 用户空间与内核空间的交互式通过映射的区域直接交互,用内存的读取代替I/O读写,文件读写效率高。
- 数据拷贝次数少,对文件的读取操作跨过页缓存,减少了数据拷贝一次,效率提高。
- 可实现高效的大规模数据传输。
在Linux系统中,根据内存映射的本质和特点,其应用场景在于:
- 实现内存共享,如跨进程通信。
- 提高数据读/写效率:如读写操作。
对于进程间的通信,其工作流程如下图所示:
- 创建一块共享的接收区,实现地址映射关系。
- 发送进程数据到自身的虚拟内存区域,数据拷贝1次。
- 由于发送进程的虚拟地址空间与接收进程的虚拟内存地址存在映射关系,所以发送到的数据也存放到接收进程的虚拟内存中,即实现了跨进程间通信。
四、总结
内存映射的读写操作主要的过程如下:
- 创建虚拟映射区域,其在当前进程的虚拟地址空间中,寻找一段满足大小要求的虚拟地址,并且为此虚拟地址分配一个虚拟内存区域(vm_area_struct结构),初始化该虚拟内存区域,插入到进程虚拟地址区域的链表和红黑树中
- 实现地址映射关系,建立页表,该过程在mmap函数中并未实现,此时只是创建了映射关系,并不将任何文件数据拷贝至主存中,真正的数据拷贝是通过进程发起读写操作时
- 进程访问该映射空间,实现文件内容到物理内存的数据拷贝,当进程读写访问该映射地址时,如果进程写操作改变了内容,并不会立即更新,而是一定时间后系统会自动会写脏数据到对应硬盘的地址空间
使用mmap来创建文件映射,由于只建立了进程地址空间VMA,并没有马上分配page cache和建立映射关系。那么就会导致一个问题,当创建一个很大的VMA,会频繁发生缺页中断。
内存映射机制mmap是POSIX标准的系统调用,有匿名映射和文件映射两种。
- 匿名映射使用进程的虚拟内存空间,它和malloc(3)类似,实际上有些malloc实现会使用mmap匿名映射分配内存,不过匿名映射不是POSIX标准中规定的。
- 文件映射有MAP_PRIVATE和MAP_SHARED两种。前者使用COW的方式,把文件映射到当前的进程空间,修改操作不会改动源文件。后者直接把文件映射到当前的进程空间,所有的修改会直接反应到文件的page cache,然后由内核自动同步到映射文件上。
相比于IO函数调用,基于文件的mmap的一大优点是把文件映射到进程的地址空间,避免了数据从用户缓冲区到内核page cache缓冲区的复制过程;当然还有一个优点就是不需要频繁的read/write系统调用。
4.1、mmap的工作原理
mmap的工作原理可以分为以下三个阶段:
- 进程启动映射过程:在用户空间调用库函数mmap,在当前进程的虚拟地址空间中寻找一段空闲的、满足要求的连续虚拟地址。为这个虚拟区分配一个vm_area_struct结构,并对其进行初始化。然后,将这个虚拟区结构插入进程的虚拟地址区域链表或树中。
- 建立映射关系:调用内核空间的系统调用函数mmap(不同于用户空间函数),通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,链接到内核“已打开文件集”中该文件的文件结构体。通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,通过虚拟文件系统inode模块定位到文件磁盘物理地址,并通过remap_pfn_range函数建立页表,实现文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址还没有任何数据关联到主存中。
- 访问映射空间:当进程发起对这片映射空间的访问时,如果这段地址不在物理页上(因为只建立了地址映射,真正的数据还没有拷贝到内存),会引发缺页异常。缺页异常经过一系列判断后,内核发起请求调页过程,将所缺的页从文件在磁盘里的地址拷贝到物理内存。之后,进程就可以对这片主存进行读写操作。如果写操作修改了内容,一定时间后系统会自动回写脏页面到对应的磁盘地址,完成了写入到文件的过程。
4.2、mmap的特点和优势
- 直接访问:通过内存映射,进程可以像访问内存一样直接访问文件或设备的内容,消除了传统的读取和写入文件的系统调用的开销。
- 共享内存:多个进程可以将同一个文件映射到各自的地址空间中,实现文件内容的共享和进程间通信。
- 简化文件I/O操作:通过内存映射,可以将文件的内容直接映射到内存中,省去了使用read()和write()等传统的文件I/O函数的步骤。
- 高效访问:mmap使得文件的读写操作像访问内存一样高效,避免了频繁的系统调用和数据拷贝。
- 内存消耗:虽然mmap避免了一次性将整个文件读入内存,但映射的文件会占用进程的虚拟内存空间,处理大文件时可能导致内存消耗过多。