linux kernel 内存管理-内存映射

内存映射是在进程的虚拟空间中创建一个映射,分为以下两种:
(1)文件映射:文件支持的内存映射,把文件的一个区间映射到进程的虚拟地址空间,数据源是存储设备上的文件
(2)匿名映射:没有文件支持的内存映射,把物理内存映射到进程的虚拟地址空间,没有数据源。
通常把文件映射的物理页称为文件页,把匿名映射的物理页称为匿名页
根据修改是否对其他进程可见和是否传递到底层文件,内存映射分为共享映射私有映射
(1)共享映射:修改数据时映射相同区域的其他进程可以看见,如果是文件支持的映射,修改会传递到底层文件。
(2)私有映射:第一次修改数据时会从数据源复制一个副本,然后修改副本,其他进程看不见,不影响数据。
在进程的虚拟地址空间中,代码段和数据段是私有的文件映射,未初始化数据段、堆和栈是私有的匿名映射
内存映射的原理如下:
(1)创建内存时,在进程的用户虚拟地址空间中分配一个虚拟内存区域
(2)Linux内核采用延迟分配物理内存的策略,在进程第一次访问虚拟地址时,产生缺页异常。如果是文件映射,那么分配物理页,把文件指定区间的数据读到物理页中,然后在页表中把虚拟页映射到物理页;如果是匿名映射,那么分配物理页,然后在页表中把虚拟页映射到物理页。

1 应用编程接口

内存管理子系统提供了以下常用的系统调用
(1)mmap创建内存映射。

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

有以下用处:进程创建匿名映射的内存映射,把内存的物理页映射到进程的虚拟地址空间;进程把文件映射到进程的虚拟地址空间,像访问内存一样访问文件,避免用户模式和内核模式之间的切换,提高读写文件的速度;两个进程针对同一个文件创建共享的内存映射,实现共享内存
(2)mremap用来扩大或缩小已经存在的内存映射,可能同时移动。

void * mremap(void *old_address, size_t old_size , size_t new_size, int flags.../* void *new_address */);

(3)munmap用来删除内存映射。

int munmap(void *start,size_t length);

(4)brk用来设置堆的上限。
(5)remap_file_pages用来创建非线性的文件映射,即文件区间和虚拟地址空间之间的映射不是线性关系,被废弃。
(6)mprotect用来设置虚拟内存区域的访问权限。

int mprotect(void *addr, size_t len, int prot);

(7)madvise用来向内核提出内存使用的建议,应用程序告诉内核期望怎么使用指定的虚拟内存区域,以便内核可以选择合适的预读和缓存技术。

2 数据结构

2.1 虚拟内存区域

虚拟内存区域是分配给进程的一个虚拟地址范围,内核使用结构体vm_area_struct描述虚拟内存区域。

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 address
                       within 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; //红黑树节点
    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. */
    struct {
        struct rb_node rb;
        unsigned long rb_subtree_last;
    } shared; //为了支持查询一个文件区间被映射到哪些虚拟内存区域,加入address_space->i_mmap

    struct list_head anon_vma_chain; /* Serialized by mmap_sem &
                      * page_table_lock */
    struct anon_vma *anon_vma;  /* Serialized by page_table_lock */ //指向一个anon_vma实例,用来组织匿名页被映射到的所有虚拟地址空间

    /* 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_SIZE
                       units, *not* PAGE_CACHE_SIZE *///文件偏移,单位是页
    struct file * vm_file;      /* File we map to (can be NULL). */
    void * vm_private_data;     /* was vm_pte (shared mem) */

#ifndef CONFIG_MMU
    struct vm_region *vm_region;    /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
    struct mempolicy *vm_policy;    /* NUMA policy for the VMA */
#endif
    struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
};

2.1文件映射

(1)成员vm_file指向文件的一个打开实例(file)。索引节点代表一个文件,描述文件的属性
(2)成员vm_pgoff存放文件的以页为单位的偏移
(3)成员vm_ops指向虚拟内存操作集合,创建文件映射的时候调用文件操作集合中的mmap方法(file->f_ops->mmap)以注册虚拟内存操作集合。例如:假设文件属于EXT4文件系统,文件操作集合中的mmap方法是函数ext4_file_mmap,该函数吧虚拟内存区域成员vm_ops设置为ext4_file_vm_ops;

2.1.2 共享匿名映射

共享匿名映射的虚拟内存区域的实现原理和文件映射相同,区别是共享匿名映射关联的文件是内存创建的内部文件。在内存文件系统tmpfs中创建一个名为"/dev/zero"的文件,名字没有意义,创建两个共享匿名映射就会创建两个"/dev/zero"的文件,两个文件是独立的,毫无关系的。
(1)成员vm_file指向文件的一个打开实例(file)。
(2)成员vm_pgoff存放文件的以页为单位的偏移。
(3)成员vm_ops指向共享内存的虚拟内存操作集合shmem_vm_ops。

2.1.3 私有匿名映射

(1)成员vm_file没有意义,是空指针。
(2)成员vm_pgoff没有意义。
(3)成员vm_ops是空指针。

2.2 页保护位

vm_page_prot描述虚拟内存区域的访问权限。内核定义了一个保护位映射数组,把VM_READ,VM_WRITE,VM_EXEC和VM_SHARED这四个标志位转换成保护位组合。
每种处理器架构需要定义从_P000到_S111;P代表私有,S代表共享,后面的3个数字分别表示为可读、可写和可执行。

2.3 虚拟内存区域标志

(1)VM_READ、VM_WRITE、VM_EXEC和VM_SHARED分别表示可读、可写、可执行和可以被多个进程共享。
(2)VM_MAYREAD表示运行设置VM_READ,VM_MYWRITE、VM_MAYEXEC、VM_MAYSHARE同理,这四个属性用来限制系统调用mprotect可以设置的访问权限
(3)VM_GROWSDOWN表示虚拟内存可以向下(低的虚拟地址)扩展,VM_GROWSUP表示虚拟区域可以向上。
(4)VM_PFNMAP表示页帧号映射。
(5)VM_MIXEDMAP表示映射混合使用页帧号和页描述符。
(6)VM_LOCKED表示页被锁定在内存中,不允许换出到交换区
(7)VM_SEQ_READ表示进程从头到尾按顺序读一个文件VM_RAND_READ表示进程随机读一个文件。这个两个标志用来提示文件系统,如果进程按顺序读一个文件,文件系统可以预读,提高性能。
(8)VM_DONTCOPY表示调用fork以创建子进程时不把虚拟内存区域复制给子进程。
(9)VM_DONTEXPAND表示不允许使用mremmap扩大虚拟内存区域。
(10)VM_ACCOUNT表示虚拟内存区域需要记账,判断所有进程申请的虚拟内存的总和是否超过物理内存容量。
(11)VM_NORESERVE表示不需要预留物理内存。
(12)VM_HUGETLB表示虚拟内存区域使用标准巨型页。
(13)VM_ARCH_1和VM_ARCH_2由各种处理器自定义。
(14)VM_HUGEPAGE表示虚拟内存区域允许使用透明巨型页,VM_NOHUGEPAGE表示虚拟内存区域不允许使用透明巨型页。
(15)VM_MERGEABLE表示KSM(内核相同页合并)可以合并数据相同的页。

2.4 虚拟内存操作操作

struct vm_operations_struct {
    void (*open)(struct vm_area_struct * area);
    void (*close)(struct vm_area_struct * area);
    int (*mremap)(struct vm_area_struct * area);
    int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
    int (*pmd_fault)(struct vm_area_struct *, unsigned long address,
                        pmd_t *, unsigned int flags);
    void (*map_pages)(struct vm_area_struct *vma, struct vm_fault *vmf);
    int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);

    /* same as page_mkwrite when using VM_PFNMAP|VM_MIXEDMAP */
    int (*pfn_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);

    int (*access)(struct vm_area_struct *vma, unsigned long addr,
              void *buf, int len, int write);
    const char *(*name)(struct vm_area_struct *vma);

#ifdef CONFIG_NUMA
    int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);
    struct mempolicy *(*get_policy)(struct vm_area_struct *vma,
                    unsigned long addr);
#endif
    struct page *(*find_special_page)(struct vm_area_struct *vma,
                      unsigned long addr);
};

(1)open:在创建虚拟内存区域时调用open方法,通常不使用,设置为空指针。
(2)close:在删除虚拟内存区域时调用close方法,通常不使用,设置为空指针。
(3)mremap:使用系统调用mremap移动虚拟内存区域时调用mremap方法。
(4)fault访问文件映射的虚拟页时,如果没有映射到物理页,生成缺页异常,异常处理程序调用fault方法来把文件的数据读到文件的页缓存中
(5)huge_fault:和fault方法类型,区别是huge_fault方法针对使用透明巨型页的文件映射。
(6)map_pages:读文件映射的虚拟页时,如果没有映射到物理页,生成缺页异常,异常处理程序除了读入正在访问的文件页,还会预读后续的文件页,调用map_pages方法在文件的页缓存中分配物理页
(7)page_mkwrite:第一次写私有的文件映射时,生成页错误异常,异常处理时执行写时复制,调用page_mkwrite方法通知文件系统页即将变成可写,以便文件系统检查是否允许写,或者等待页进入合适的状态。
(8)pfn_mkwrite:page_mkwrite类似,区别是pfn_mkwrite方法针对页帧号映射和混合映射。

2.5 链表和树

(1)双向链表:mm_struct.mmap指向第一个vm_area_struct实例。
(2)红黑树:mm_struct.mm_rb指向红黑树的根。

3 创建内存映射

C标准库封装了函数mmap用来创建内存映射,内核提供了POSIX标准定义的系统调用mmap;

3.1 系统调用sys_mmap执行流程

(1)检查偏移是不是页的整数倍,如果偏移不是页的整数倍,返回"-EINVAL"。
(2)如果偏移是页的整数倍,那么把偏移转换成以页为单位的偏移,然后调用函数sys_mmap_pgoff。

3.2 sys_mmap_pgoff执行流程

(1)如果是创建文件映射,根据文件描述符在进程的打开文件表中找到file实例。
(2)如果是创建匿名巨型页映射,在hugetlbfs文件系统中创建文件"anon_hugepage",并且创建该文件的一个打开实例file。
(3)调用函数vm_mmap_pgoff进行处理。

3.3 vm_mmap_pgoff的执行流程

(1)以写者身份申请写信号量mm->mmap_sem。
(2)把创建内存映射的主要工作委托给函数do_mmap
(3)释放读写信号量mm->mmap_sem。
(4)如果调用者要求页锁定在内存中,或者要求填充页表并且允许阻塞,那么调用函数mm_populate分配物理页,并且在页表中把虚拟页映射到物理页
常见的情况:创建内存映射时不分配物理页,等到进程第一次访问虚拟页的时候,生成错误异常,页错误异常处理程序分配物理页,在页表中把虚拟页映射到物理页

3.4 函数do_mmap

do_mmap实现创建内存映射的主要工作,执行流程如下:
(1)调用函数get_unmapped_area从进程的虚拟地址空间分配一个虚拟地址范围。函数get_unmapped_area根据情况调用特定函数以分配虚拟地址范围。如果是创建共享的匿名映射,那么调用shmem_get_unmapped_area以分配虚拟地址范围。如果是创建私有的匿名映射,那么调用mm->get_unmapped_area以分配虚拟地址范围。ARM64架构的内核在装载程序时,如果选择传统布局,函数arch_pick_mmap_layout把mm->get_unmapped_area 设置为 arch_get_unmapped_area。
(2)计算虚拟内存标志。
(3)调用函数mmap_region以创建虚拟内存区域。

3.5 函数mmap_region

函数mmap_region负责创建虚拟内存区域,执行流程如下:
(1)调用函数may_expand_vm以检查进程申请的虚拟内存是否超过限制。
(2)如果是固定映射,调用者强制指定虚拟地址范围,可能和旧的虚拟内存区域重叠,那么需要从旧的虚拟内存区域删除重叠的部分。
(3)如果是私有的可写映射,检查所有进程申请的虚拟内存的总和是否超过物理内存的容量。
(4)如果可以和已有的虚拟内存区域合并,那么调用函数vma_merge,和已有的虚拟内存区域合并。
(5)如果不能和已有的虚拟内存区域合并,那么
1)创建新的虚拟内存区域
2)如果是文件映射,那么调用文件的文件操作中的mmap方法,mmap方法的主要功能是设置虚拟内存区域的虚拟内存操作集合
3)如果是共享的匿名映射,那么在内存文件系统tmpfs中创建一个"/dev/zero"的文件,并且创建文件的一个打开实例file,虚拟内存区域的成员vm_file指向这个打开实例,把虚拟内存操作集合设置为shmem_vm_ops。如果没有开启共享内存的配置宏CONFIG_SHMEM,shmem_vm_ops等价于generic_file_vm_ops.
4)调用函数vma_link把虚拟内存区域添加到链表和红黑树中。如果虚拟内存区域关联文件,那么把虚拟内存区域添加到文件的区间树中,文件的区间树用来跟踪文件被映射到哪些虚拟内存区域。
5)调用函数vma_set_page_prot根据虚拟内存标志(vma->vm_flags)计算页保护位(vma->vm_page_prot)

4 删除内存映射

系统调用munmap用来删除内存映射,有两个参数:起始地址和长度。流程如下:
(1)根据起始地址找到要删除的第一个虚拟内存区域vma。
(2)如果只删除虚拟内存区域vma的一部分,那么分裂虚拟内存区域vma
(3)根据结束地址找到要删除的最后一个虚拟内存区域last。
(4)如果只删除虚拟内存区域last的一部分,那么分裂虚拟内存区域last。
(5)针对所有要删除目标,如果虚拟内存区域被锁定在内存,调用函数munlock_vma_pages_all以解除锁定。
(6)调用函数detach_vmas_to_be_unmapped,把所有删除目标从进程的虚拟内存区域链表和树中删除,单独组成一条临时的链表
(7)调用函数unmap_region,针对所有删除目标,在进程的页表中删除映射,并且处理器的页表缓存中删除映射
(8)调用函数arch_unmap执行处理器架构特定的处理。
(9)调用函数remove_vma_list删除所有目标。

你可能感兴趣的:(linux kernel 内存管理-内存映射)