最清楚的mmap()详解与源码分析

内核版本: 4.1

函数原型

void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);

这是mmap的函数原型,而系统调用的接口在mm/map.c中的:

unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len,
                              unsigned long prot, unsigned long flags,
                              unsigned long fd, unsigned long pgoff);

虚拟内存管理

这里我们先介绍两个关于虚拟内存的数据结构。虚拟内存概念的相关资料网上已经足够的丰富,这里我们从内核的角度来分析。虚拟空间的管理是以进程为基础的,每个进程都有各自的虚存空间,除此之外,每个进程的“内核空间”是为所有的进程所共享的。一个进程的虚拟地址空间主要由两个数据结构来描述: mm_struct 和vm_area_struct。

The Memory Descriptor

mm_struct包括进程中虚拟地址空间的所有信息,mm_struct定义在include/linux/mm_types.h:

struct mm_struct {
        struct {
            struct vm_area_struct *mmap;            /* vm_area_struct的链表 */
            pgd_t * pgd;			    /* 指向进程的页目录 */
            
            /* ... */
            int map_count;			    /* vm_area_struct数量 */
            /* ... */
            unsigned long total_vm;		    /* 映射的Page数量 */
	    /* ... */
	    unsigned long start_code, end_code, start_data, end_data;	  /* 代码段起始结束位置,数据段起始结束位置 */
	    unsigned long start_brk, brk, start_stack;			  /* 堆的起始结束位置, 栈因为其性质,只有起始位置 */
	    unsigned long arg_start, arg_end, env_start, env_end;	  /* 参数段,环境段的起始结束位置 */
            /* ... */
        }
        
}

结合mm_struct和下图32位系统典型的虚拟地址空间分布更能直观的理解(来自《深入理解计算机系统》):

virtual_address_space

Virtual Memory Area

virtual_memory

vm_area_struct描述了虚拟地址空间的一个区间, 一个进程的虚拟空间中可能有多个虚拟区间, vm_area_struct同样定义在include/linux/mm_types.h:

/*
 * 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;         /* 在虚拟地址空间的起始位置 */
        unsigned long vm_end;           /* 在虚拟地址空间的结束位置*/

        /* 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. */

    	/* Function pointers to deal with this struct. */
        const struct vm_operations_struct *vm_ops;	/* 对这个区间进行操作的函数 */
    
        struct mm_struct *vm_mm;        /* vma所属的虚拟地址空间 */
        pgprot_t vm_page_prot;          /* Access permissions of this VMA. */
        unsigned long vm_flags;         /* Flags, see mm.h. */
    	struct file * vm_file;          /* 映射的文件,匿名映射即为nullptr*/
}

下图是某个进程的虚拟内存简化布局以及相应的几个数据结构之间的关系:

process_address_space

mmap是什么?

相信做过Linux开发的兄弟都或多或少用过,或者听说过mmap,但可能并不完全了解mmap的作用。

mmap字面上是内存映射的意思,听起来比较抽象,其用法有很多,但总结起来,主要是如下两个用途:

  1. 将文件内容映射到进程用户态的虚拟地址空间中,如此,进程就可以通过读写相应的虚拟地址空间内容,而直接读写相应文件中的内容。如此映射最大的好处是,进程可以直接从用户态访问文件中的数据,而不需要用户态和内核态之间的内存拷贝操作(正常流程下,如果想要向文件中写数据,需要将数据从用户态拷贝到内核态,然后再从内核态写入文件),相当于少了一次内存拷贝操作,这也是人们常说的零拷贝技术之一。当然,这里的文件不限于普通文件,Unix环境中,一切皆文件嘛,这里的文件完全可能是特殊文件,比如设备文件,那相应的mmap操作就需要单独的驱动实现了。
  2. 分配内存。当mmap中传入的fd为空时,其作用就是分配内存,类似于malloc(其实malloc的glibc实现中就使用了mmap来分配内存),俗称“匿名映射”,匿名的意思就是fd为空,名字很抽象,本质不复杂:就是在进程的虚拟地址空间中分配一段虚拟内存(用vma表示),物理内存在缺页异常中分配,并修改相应页表。

mmap基本原理

如前面所述,mmap主要有两种用途,其中第一种用途分两种情况(普通文件和特殊文件),这里分别描述相关原理:

1.普通文件的mmap基本原理为:每个文件(file)都定义了相应的文件操作数据结构(file_operations),该结构中定义了mmap操作,比如ext3文件系统文件对应的文件操作为:ext3_file_operations,对应的mmap操作接口为:generic_file_mmapgeneric_file_mmap中就是创建(或查找利用现有的)vma,然后设置相应的成员,包括缺页异常对应的处理钩子,最后返回相应的虚拟地址。当进程访问写相应的虚拟地址时,硬件会触发缺页异常(磁盘页未缓存到屋里内存中),此时会进入缺页异常流程(do_page_fault),然后会进入之前设置的缺页异常钩子,该钩子会触发文件系统的写入操作,最终会将数据写入到文件中。

2.特殊文件(以设备文件为例)的mmap的基本原理与上述普通文件类似,主要差别在于:其定义的文件操作不同,对应的接口不同;其实现取决于具体的驱动,流程与普通文件实现可以完全不同,这里不详述。

3.匿名映射的基本原理:由于没有具体的fd,没有对应的文件,匿名映射没有对应的文件操作,其流程比较直接,主要还是创建(或查找利用现有的)vma,然后设置相应的成员,返回相应的虚拟地址。当进程访问写相应的虚拟地址时,硬件会触发缺页异常(因为相应的页表项还没有创建),此时会进入缺页异常流程(do_page_fault),然后会进入匿名映射对应的流程,主要就是为虚拟地址范围创建相应的页表,本质上,就是分配了相应的物理内存。

代码流程

mmap流程

mmap有相应的系统调用接口,从系统调用开始的大致流程如下(代码不是很好找,需要仔细看看):

SYSCALL_DEFINE6(mmap_pgoff, ...
  sys_mmap_pgoff	
    SYSCALL_DEFINE6(mmap_pgoff, ...
	  vm_mmap_pgoff
	    do_mmap_pgoff
		  mmap_region
			file->f_op->mmap() //不同文件(驱动)自己定义的mmap钩子,比如ext3文件系          统对应为`generic_file_mmap`

文件缺页异常流程

do_page_fault
  __do_page_fault
	handle_pte_fault
  	  __handle_mm_fault
		handle_pte_fault
		  do_fault
			do_shared_fault
			  __do_fault
				vma->vm_ops->fault()  //文件系统或驱动注册的缺页异常钩子,如ext4文件对应为filemap_fault

匿名映射缺页异常流程

do_page_fault
  __do_page_fault
	handle_pte_fault
  	  __handle_mm_fault
		handle_pte_fault
		  do_anonymous_page
		    mk_pte //创建页表项
			set_pte_at

普通文件映射与匿名文件映射的用法

用法举例1

字符设备文件的驱动代码编写

总结

1.对于普通文件的mmap,在执行后仅仅是分配了一个可用的虚拟地址以及缺页处理函数,访问的时候按照缺页处理将文件从磁盘上读入,修改后的内存页最终会回写到磁盘上

2.以字符设备为例,设备文件的大小为0;设备文件的mmap需要自己定义驱动函数来返回一个映射地址。

发布了48 篇原创文章 · 获赞 4 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/m0_37313888/article/details/97921907
今日推荐