mmap详解
- mmap基础概念
- mmap内存映射原理
- mmap相关函数调用
- mmap的使用细节
- mmap和常规文件操作的区别
mmap基础概念
mmap
是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read
,write
等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。mmap
还可以用于实现共享内存,允许不同进程间共享数据,如下图所示:
我们知道,在进程虚拟地址空间中,内存映射部分是处于堆栈之间的,linux内核使用vm_area_struct
结构来表示一个独立的虚拟内存区域,由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct
结构来分别表示不同类型的虚拟内存区域。各个vm_area_struct
结构使用链表或者树形结构链接,方便进程快速访问,如下图所示:
mm_struct
就是进程用户空间的抽象,一个进程只有一个mm_struct
结构,当一个mm_struct
结构却可以为多个进程所共享,例如当一个进程创建一个子进程时(vfork
或clone
),子进程与父进程共享一个mm_struct
,mm_struct
的代码就如下:
struct mm_struct {struct vm_area_struct *mmap; /* list of VMAs */ //指向VMA对象的链表头struct rb_root mm_rb; //指向VMA对象的红黑树的根u64 vmacache_seqnum; /* per-thread vmacache */
#ifdef CONFIG_MMUunsigned long (*get_unmapped_area) (struct file *filp,unsigned long addr, unsigned long len,unsigned long pgoff, unsigned long flags); // 在进程地址空间中搜索有效线性地址区间的方法
#endifunsigned long mmap_base; /* base of mmap area */unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
#ifdef CONFIG_HAVE_ARCH_COMPAT_MMAP_BASES/* Base adresses for compatible mmap() */unsigned long mmap_compat_base;unsigned long mmap_compat_legacy_base;
#endifunsigned long task_size; /* size of task vm space */unsigned long highest_vm_end; /* highest vma end address */pgd_t * pgd; //指向页全局目录/*** @mm_users: The number of users including userspace.** Use mmget()/mmget_not_zero()/mmput() to modify. When this drops* to 0 (i.e. when the task exits and there are no other temporary* reference holders), we also release a reference on @mm_count* (which may then free the &struct mm_struct if @mm_count also* drops to 0).*/atomic_t mm_users; //使用计数器/*** @mm_count: The number of references to &struct mm_struct* (@mm_users count as 1).** Use mmgrab()/mmdrop() to modify. When this drops to 0, the* &struct mm_struct is freed.*/atomic_t mm_count; //使用计数器atomic_long_t nr_ptes; /* PTE page table pages */ //进程页表数
#if CONFIG_PGTABLE_LEVELS > 2atomic_long_t nr_pmds; /* PMD page table pages */
#endifint map_count; /* number of VMAs */ //VMA的个数spinlock_t page_table_lock; /* Protects page tables and some counters */struct rw_semaphore mmap_sem;struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung* together off init_mm.mmlist, and are protected* by mmlist_lock*/unsigned long hiwater_rss; /* High-watermark of RSS usage */unsigned long hiwater_vm; /* High-water virtual memory usage */unsigned long total_vm; /* Total pages mapped */ //进程地址空间的页数unsigned long locked_vm; /* Pages that have PG_mlocked set */ //锁住的页数,不能换出unsigned long pinned_vm; /* Refcount permanently increased */unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */ //数据段内存的页数unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */ //可执行内存映射的页数unsigned long stack_vm; /* VM_STACK */ //用户态堆栈的页数unsigned long def_flags;unsigned long start_code, end_code, start_data, end_data; //代码段,数据段等的地址unsigned long start_brk, brk, start_stack; //堆栈段的地址,start_stack表示用户态堆栈的起始地址,brk为堆的当前最后地址unsigned long arg_start, arg_end, env_start, env_end; //命令行参数的地址,环境变量的地址unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv *//** Special counters, in some configurations protected by the* page_table_lock, in other configurations by being atomic.*/struct mm_rss_stat rss_stat;struct linux_binfmt *binfmt;cpumask_var_t cpu_vm_mask_var;/* Architecture-specific MM context */mm_context_t context;unsigned long flags; /* Must use atomic bitops to access the bits */struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_MEMBARRIERatomic_t membarrier_state;
#endif
#ifdef CONFIG_AIOspinlock_t ioctx_lock;struct kioctx_table __rcu *ioctx_table;
#endif
#ifdef CONFIG_MEMCG/** "owner" points to a task that is regarded as the canonical* user/owner of this mm. All of the following must be true in* order for it to be changed:** current == mm->owner* current->mm != mm* new_owner->mm == mm* new_owner->alloc_lock is held*/struct task_struct __rcu *owner;
#endifstruct user_namespace *user_ns;/* store ref to file /proc/<pid>/exe symlink points to */struct file __rcu *exe_file;
#ifdef CONFIG_MMU_NOTIFIERstruct mmu_notifier_mm *mmu_notifier_mm;
#endif
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKSpgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
#ifdef CONFIG_CPUMASK_OFFSTACKstruct cpumask cpumask_allocation;
#endif
#ifdef CONFIG_NUMA_BALANCING/** numa_next_scan is the next time that the PTEs will be marked* pte_numa. NUMA hinting faults will gather statistics and migrate* pages to new nodes if necessary.*/unsigned long numa_next_scan;/* Restart point for scanning and setting pte_numa */unsigned long numa_scan_offset;/* numa_scan_seq prevents two threads setting pte_numa */int numa_scan_seq;
#endif/** An operation with batched TLB flushing is going on. Anything that* can move process memory needs to flush the TLB when moving a* PROT_NONE or PROT_NUMA mapped page.*/atomic_t tlb_flush_pending;
#ifdef CONFIG_ARCH_WANT_BATCHED_UNMAP_TLB_FLUSH/* See flush_tlb_batched_pending() */bool tlb_flush_batched;
#endifstruct uprobes_state uprobes_state;
#ifdef CONFIG_HUGETLB_PAGEatomic_long_t hugetlb_usage;
#endifstruct work_struct async_put_work;#if IS_ENABLED(CONFIG_HMM)/* HMM needs to track a few things per mm */struct hmm *hmm;
#endif
} __randomize_layout;
struct vm_area_struct
用于描述进程地址空间中的一段虚拟区域,每一个VMA都对应一个struct vm_area_struct
。
/** This struct defines a memory VMM memory area. There is one of these* per VM-area/task. A VM area is any part of the process virtual memory* space that has a special rule for the page-fault handlers (ie a shared* library, the executable area etc).*/
struct vm_area_struct {/* The first cache line has the info for VMA tree walking. */unsigned long vm_start; /* Our start address within vm_mm. */ //起始地址unsigned long vm_end; /* The first byte after our end addresswithin vm_mm. */ //结束地址,区间中不包含结束地址/* linked list of VM areas per task, sorted by address */ //按起始地址排序的链表struct vm_area_struct *vm_next, *vm_prev;struct rb_node vm_rb; //红黑树节点/** Largest free memory gap in bytes to the left of this VMA.* Either between this VMA and vma->vm_prev, or between one of the* VMAs below us in the VMA rbtree and its ->vm_prev. This helps* get_unmapped_area find a free area of the right size.*/unsigned long rb_subtree_gap;/* Second cache line starts here. */struct mm_struct *vm_mm; /* The address space we belong to. */pgprot_t vm_page_prot; /* Access permissions of this VMA. */unsigned long vm_flags; /* Flags, see mm.h. *//** For areas with an address space and backing store,* linkage into the address_space->i_mmap interval tree.*/struct {struct rb_node rb;unsigned long rb_subtree_last;} shared;/** A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma* list, after a COW of one of the file pages. A MAP_SHARED vma* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack* or brk vma (with NULL file) can only be in an anon_vma list.*/struct list_head anon_vma_chain; /* Serialized by mmap_sem &* page_table_lock */struct anon_vma *anon_vma; /* Serialized by page_table_lock *//* Function pointers to deal with this struct. */const struct vm_operations_struct *vm_ops;/* Information about our backing store: */unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZEunits */struct file * vm_file; /* File we map to (can be NULL). */ //指向文件的一个打开实例void * vm_private_data; /* was vm_pte (shared mem) */atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMUstruct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMAstruct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endifstruct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;
我们的mmap
函数在使用的过程中就是要创建一个新的vm_area_struct
,并将其与文件的物理磁盘地址相连,关系图如下图:
mmap内存映射原理
mmap内存映射的实现过程,总的来说可以分为三个阶段:
- 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
-
1. 进程在用户空间调用库函数
mmap
; -
2. 在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址;
-
3. 为此虚拟区分配一个
vm_area_struct
结构,接着对这个结构的各个域进行了初始化; -
4. 将新建的虚拟区结构(
vm_area_struct
)插入进程的虚拟地址区域链表或树中。
- 调用内核空间的系统调用函数
mmap
(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
- 5. 为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(
struct file
),每个文件结构体维护着和这个已打开文件相关各项信息。 - 6. 通过该文件的文件结构体,链接到
file_operations
模块,调用内核函数mmap
,其原型为:intmmap(struct file *filp, struct vm_area_struct *vma)
,不同于用户空间库函数。 - 7. 内核
mmap
函数通过虚拟文件系统inode
模块定位到文件磁盘物理地址。 - 8. 通过
remap_pfn_range
函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。
注意:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时,也就是接下来这个阶段。
- 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
- 9. 进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常;
- 10. 缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程;
- 11. 调页过程先在交换缓存空间(
swap cache
)中寻找需要访问的内存页,如果没有则调用nopage
函数把所缺的页从磁盘装入到主存中。 - 12. 之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
注意:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。
mmap相关函数调用
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
返回MAP_FAILED
[其值为(void *)-1
],error
被设为以下的某个值:
1 EACCES:访问出错2 EAGAIN:文件已被锁定,或者太多的内存已被锁定3 EBADF:fd不是有效的文件描述符4 EINVAL:一个或者多个参数无效5 ENFILE:已达到系统对打开文件的限制6 ENODEV:指定文件所在的文件系统不支持内存映射7 ENOMEM:内存不足,或者进程已超出最大内存映射数量8 EPERM:权能不足,操作不允许9 ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
10 SIGSEGV:试着向只读区写入
11 SIGBUS:试着访问不属于进程的内存区
参数说明:
void *addr
:一个提示地址,表示希望映射区域开始的地址。然⽽,这个地址可能会被内核忽略,特别是当我们没有足够的权限来请求特定的地址时。如果addr
是NULL
,则系统会⾃动选择⼀个合适的地址;size_t length
: 要映射到进程地址空间中的字节数。这个长度必须是系统页面大小的整数倍(通常是 4KB ,但可能因系统而异)。如果指定的length
不是页面大小的整数倍,系统可能会向上舍入到最近的页面大小边界(系统内存页大小为4KB(即4096字节),而请求的内存大小为3500字节,则按照向上舍入的原则,应分配4096字节的内存);int prot
: 指定了映射区域的内存保护属性。可以是以下值的组合(使用按位或运算符|
):
—PROT_READ
:映射区域可读。
—PROT_WRITE
:映射区域可写。
—PROT_EXEC
:映射区域可执行。
—PROT_NONE
:页不可访问int flags
: 指定了映射的类型和其他选项,可以是一下位的组合值;
1 MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。2 MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。3 MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。4 MAP_DENYWRITE //这个标志被忽略。5 MAP_EXECUTABLE //同上6 MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。7 MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。8 MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。9 MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
10 MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。
11 MAP_FILE //兼容标志,被忽略。
12 MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
13 MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
14 MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
int fd
: ⼀个有效的文件描述符,指向要映射的文件或设备。对于匿名映射,这个参数可以是-1
(在某些系统上,也可以使用MAP_ANONYMOUS
或MAP_ANON
标志来指定匿名映射,此时fd
参数会被忽略);off_t offset
: ⽂件中的起始偏移量,即映射区域的开始位置。offset
和length
一起定义了映射区域在文件中的位置和大小。
相关函数:int munmap( void * addr, size_t len )
- 成功执行时,
munmap
返回0
。失败时,munmap
返回-1
,error
返回标志和mmap
一致; - 该调用在进程地址空间中解除一个映射关系,
addr
是调用mmap
时返回的地址,len
是映射区的大小; - 当映射关系解除后,对原来映射地址的访问将导致段错误发生。
接下来我们来daemon一段代码验证一下:
写入映射
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <iostream>
#include <unistd.h>
#include <sys/mman.h>#define SIZE 4096int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << "filename" << std::endl;return 1;}std::string filename = argv[1];// 首先需要打开一个文件,要成功写入文件映射,这里打开文件的模式必须是:O_RWDRint fd = ::open(filename.c_str(), O_CREAT | O_RDWR, 0666);if (fd < 0){std::cerr << "open failed!!!" << std::endl;return 2;}// 默认文件大小是0,无法与mmap形成文件映射,这里需要手动设置文件大小::ftruncate(fd, SIZE);char *mmap_addr = (char *)::mmap(nullptr, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (mmap_addr == MAP_FAILED){perror("mmap error");return 3;}// 对文件进行操作for (int i = 0; i < SIZE; i++){mmap_addr[i] = 'a' + i % 26;}// 取消文件映射::munmap(mmap_addr, SIZE);// 关闭文件::close(fd);return 0;
}
读取映射
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <iostream>
#include <unistd.h>
#include <sys/mman.h>#define SIZE 4096int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << "filename" << std::endl;return 1;}std::string filename = argv[1];// 首先需要打开一个文件,要成功写入文件映射,这里打开文件的模式必须是:O_RWDRint fd = ::open(filename.c_str(), O_RDONLY);if (fd < 0){std::cerr << "open failed!!!" << std::endl;return 2;}// 获取真实文件大小struct stat st;::fstat(fd, &st);char *mmap_addr = (char *)::mmap(nullptr, st.st_size, PROT_READ, MAP_SHARED, fd, 0);if (mmap_addr == MAP_FAILED){perror("mmap error");return 3;}std::cout << mmap_addr << std::endl;// 取消文件映射::munmap(mmap_addr, st.st_size);// 关闭文件::close(fd);return 0;
}
mmap的使用细节
- 使用
mmap
需要注意的一个关键点是,mmap
映射区域大小必须是物理页大小(page_size
)的整倍数(32位系统中通常是4k字节)。原因是,内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作,mmap
从磁盘到虚拟地址空间的映射也必须是页; - 内核可以跟踪被内存映射的底层对象(文件)的大小,进程可以合法的访问在当前文件大小以内又在内存映射区以内的那些字节。也就是说,如果文件的大小一直在扩张,只要在映射区域范围内的数据,进程都可以合法得到,这和映射建立时文件的大小无关;
- 映射建立之后,即使文件关闭,映射依然存在。因为映射的是磁盘的地址,不是文件本身,和文件句柄无关。同时可用于进程间通信的有效地址空间不完全受限于被映射文件的大小,因为是按页映射。
场景一:一个文件的大小是 5000 字节,mmap函数从一个文件的起始位置开始,映射 5000 字节到虚拟内存中。
在32位系统下,一个物理页面所占的大小是 4KB,也就是 4096 字节,如果 mmap 需要将这5000字节的数据映射到虚拟内存当中,就需要映射 8KB 大小,也就是 8192 字节大小,也就是说,在 mmap 函数执行以后,实际上映射到虚拟内存当中大小为 8192 字节大小,对于第5000 ~ 8191字节的数据是以0来进行填充的。
此时:
- 读 / 写前 5000 个字节,也就是0 ~ 4999会返回操作文件的内容;
- 读 5000 ~ 8191 的数据,返回的是0,写 5000 ~ 8191 的数据,程序不会有任何报错,但是不会将数据写入到原文件当中;
- 读 / 写 8191 以外的部分,就会返回一个 SIGSEGV 信号。
场景二:一个文件的大小是 5000 字节,mmap函数从一个文件的起始位置开始,映射 15000 字节到虚拟内存中,即映射大小超过了原始文件的大小。
由于原文件大小为 5000 字节,在 0 ~ 8191 之间跟场景一一样,但是系统要求 mmap 映射 15000 字节大小,而文件大小只占2个物理页,所以在 8191 ~ 15000 之间的字节不能读写,会返回信号异常的错误。
此时:
- 对于 0 ~ 8191 字节之间数据操作跟场景一相同;
- 因为原文件只占两个物理页,所以对 8191 ~ 15000 字节之间的不能进行读写,否则就会返回SIGBUS信号,同样,对于 15000 字节以外的进行读写,会返回一个 SIGSEGV 信号。
场景三:一个文件初始大小为0,使用mmap操作映射了1000*4K的大小,即1000个物理页大约4M字节空间,mmap返回指针ptr
- 如果在文件建立映射之初,就直接对文件进行读写操作,因为此时文件大小为0,没有映射对应合理的物理页,就会如图场景二一样返回一个SIGBUS信号;
- 但是当映射建立完成以后,已经返回一个 ptr 指针,此时每次操作 ptr 读写之前,先增加文件的大小,那么 ptr 在文件内部操作就是合法的,比如文件扩充 4096 个字节,那么此时 ptr 就能操作
([ptr ~ (char*)ptr + 4095])
之间的数据,只要访问操作最终实在 1000 个映射空间大小范围内的。
mmap和常规文件操作的区别
- 常规的文件操作(
read / write
)这些操作,是使用了页缓存机制的,也就是说,我们在调取read
函数时,首先是会将磁盘的数据给写到页缓存当中的(内核识别到缺页异常才会有),此时就完成了一次拷贝,但是页缓存是处于内核当中的,用户又不能直接进行访问,所以又需要将这部分数据拷贝到用户空间当中,这就进行了两次拷贝;同样,write
函数也是一样的道理,首先会将数据写入到buffer当中,但是待写入的buffer并不能直接访问,所以就会将数据先写入到对应的主存当中,这就会造成一次拷贝,然后内核在选择恰当的时机将数据写入到磁盘当中,进行两次拷贝。 - 对于
mmap
来说,我们在调用mmap
函数以后,创建新的虚拟内存区域和创建虚拟内存区域与磁盘文件之间的映射关系这两步并没有进行任何的拷贝操作,而是当访问对应的的内存区域发现没有可以访问的数据时,此时会触发缺页异常,就会将对应的数据从磁盘拷贝到内存当中,然后根据对应的映射关系去进行访问即可,这期间其实也就进行了一次数据的拷贝工作,提高了对应的效率。