内存映射:小块内存申请brk和申请大块内存的Mmap分析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了内存映射:小块内存申请brk和申请大块内存的Mmap分析相关的知识,希望对你有一定的参考价值。

参考技术A

本文为内存部分最后一篇,介绍内存映射。内存映射不仅是物理内存和虚拟内存间的映射,也包括将文件中的内容映射到虚拟内存空间。这个时候,访问内存空间就能够访问到文件里面的数据。而仅有物理内存和虚拟内存的映射,是一种特殊情况。本文首先分析用户态在堆中申请小块内存的brk和申请大块内存的mmap,之后会分析内核态的内存映射机制vmalloc,kmap_atomic,swapper_pg_dir以及内核态缺页异常。

用户态调用malloc()会分配堆内存空间,而实际上则是完成了一次用户态的内存映射,根据分配空间的大小,内存映射对应的系统调用主要有brk()和mmap()(当然我们也可以直接调用mmap()来映射文件)。对小块内存(小于 128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。而大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。这两种方式,自然各有优缺点。

brk()方式的缓存,可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。

mmap() 方式分配的内存,会在释放时直接归还系统,所以每次 mmap() 都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是 malloc() 只对大块内存使用 mmap() 的原因。

brk()系统调用为sys_brk()函数,其参数brk是新的堆顶位置,而mm->brk是原堆顶位置。该函数主要逻辑如下

在 do_brk_flags() 中,调用 find_vma_links() 找到将来的 vm_area_struct 节点在红黑树的位置,找到它的父节点、前序节点。接下来调用 vma_merge(),看这个新节点是否能够和现有树中的节点合并。如果地址是连着的,能够合并,则不用创建新的 vm_area_struct 了,直接跳到 out,更新统计值即可;如果不能合并,则创建新的 vm_area_struct,添加到anon_vma_chain 链表中,也加到红黑树中。

大块内存的申请通过mmap系统调用实现,mmap既可以实现虚拟内存向物理内存的映射,也可以映射文件到自己的虚拟内存空间。映射文件时,实际是映射虚拟内存到物理内存再到文件。

这里主要调用ksys_mmap_pgoff()函数,这里逻辑如下:

vm_mmap_pgoff()函数调用do_mmap_pgoff(),实际调用do_mmap()函数。这里get_unmapped_area()函数负责寻找可映射的区域,mmap_region()负责映射该区域。

更多Linux内核视频教程文本资料免费领取后台私信【 内核大礼包 】自行获取。

首先来看看寻找映射区的函数get_unmapped_area()。

mmap_region()首先会再次检测地址空间是否满足要求,然后清除旧的映射,校验内存的可用性,在一切均满足的情况下调用vma_link()将新创建的vm_area_struct结构挂在mm_struct内的红黑树上。

vma_link()本身是__vma_link()和__vma_link_file()的包裹函数

其中__vma_link()主要是链表和红黑表的插入,这属于基本数据结构操作,不展开讲解。

而__vma_link_file()会对文件映射进行处理,在file结构体中成员f_mapping指向address_space结构体,该结构体中存储红黑树i_mmap挂载vm_area_struct。

至此,我们完成了用户态内存的映射,但是此处仅在虚拟内存中建立了新的区域,尚未真正访问物理内存。物理内存的访问只有在调度到该进程时才会真正分配,即发生缺页异常时分配。

三. 用户态缺页异常   一旦开始访问虚拟内存的某个地址,如果我们发现,并没有对应的物理页,那就触发缺页中断,调用 do_page_fault()。这里的逻辑如下

find_vma()为红黑树查找操作,在此不做展开描述,下面重点看看handle_mm_fault()。这里经过一系列校验之后会根据是否是大页而选择调用hugetlb_fault()或者__handle_mm_fault()

__handle_mm_fault()完成实际上的映射操作。这里涉及到了由pgd, p4g, pud, pmd, pte组成的五级页表,页表索引填充完后调用handle_pte_fault()创建页表项。

handle_pte_fault()处理以下三种情况:

对于匿名页映射,流程如下:

映射文件do_fault()函数调用了fault函数,该函数实际会根据不同的文件系统调用不同的函数,如ext4文件系统中vm_ops指向ext4_file_vm_ops,实际调用ext4_filemap_fault()函数,该函数会调用filemap_fault()完成实际的文件映射操作。

file_map_fault()主要逻辑为:

前文提到了我们会通过主动回收或者被动回收的方式将物理内存已映射的页面回收至硬盘中,当数据再次访问时,我们又需要通过do_swap_page()将其从硬盘中读回来。do_swap_page() 函数逻辑流程如下:查找 swap 文件有没有缓存页。如果没有,就调用 swapin_readahead()将 swap 文件读到内存中来形成内存页,并通过 mk_pte() 生成页表项。set_pte_at 将页表项插入页表,swap_free 将 swap 文件清理。因为重新加载回内存了,不再需要 swap 文件了。

通过以上步骤,用户态的缺页异常就处理完毕了。物理内存中有了页面,页表也建立好了映射。接下来,用户程序在虚拟内存空间里面可以通过虚拟地址顺利经过页表映射的访问物理页面上的数据了。页表一般都很大,只能存放在内存中。操作系统每次访问内存都要折腾两步,先通过查询页表得到物理地址,然后访问该物理地址读取指令、数据。为了加快映射速度,我们引入了 TLB(Translation Lookaside Buffer),我们经常称为快表,专门用来做地址映射的硬件设备。它不在内存中,可存储的数据比较少,但是比内存要快。所以我们可以想象,TLB 就是页表的 Cache,其中存储了当前最可能被访问到的页表项,其内容是部分页表项的一个副本。有了 TLB 之后,我们先查块表,块表中有映射关系,然后直接转换为物理地址。如果在 TLB 查不到映射关系时,才会到内存中查询页表。

和用户态使用malloc()类似,内核态也有相应的内存映射函数:vmalloc()可用于分配不连续物理页(使用伙伴系统),kmem_cache_alloc()和kmem_cache_create()使用slub分配器分配小块内存,而kmalloc()类似于malloc(),在分配大内存的时候会使用伙伴系统,分配小内存则使用slub分配器。分配内存后会转换为虚拟地址,保存在内核页表中进行映射,有需要时直接访问。由于vmalloc()会带来虚拟连续页和物理不连续页的映射,因此一般速度较慢,使用较少,相比而言kmalloc()使用的更为频繁。而kmem_cache_alloc()和kmem_cache_create()会分配更为精准的小内存块用于特定任务,因此也比较常用。

相对于用户态,内核态还有一种特殊的映射:临时映射。内核态高端内存地区为了节省空间会选择临时映射,采用kmap_atomic()实现。如果是 32 位有高端地址的,就需要调用 set_pte 通过内核页表进行临时映射;如果是 64 位没有高端地址的,就调用 page_address,里面会调用 lowmem_page_address。其实低端内存的映射,会直接使用 __va 进行临时映射。

kmap_atomic ()发现没有页表的时候会直接创建页表进行映射。而 vmalloc ()只分配了内核的虚拟地址。所以访问它的时候,会产生缺页异常。内核态的缺页异常还是会调用 do_page_fault(),最终进入vmalloc_fault()。在这里会实现内核页表项的关联操作,从而完成分配,整体流程和用户态相似。

至此,我们分析了内存物理地址和虚拟地址的映射关系,结合前文页的分配和管理,内存部分的主要功能就算是大致分析清楚了,最后引用极客时间中的一幅图作为总结,算是全部知识点的汇总。

1、brk

2、mmap

3、page_fault

私有内存的申请与释放

1.私有内存指的是物理页自己用,无法共享

2.同样一块内存被多个进程共享时,则为共享内存Map

  


一、申请内存的两种方式:

  1. 通过VirtualAlloc/VirtualAllocEx申请的:Private Memory  //底层的实现完全一样
  2. 通过CreateFileMapping映射的:Mapped Memory

 

二、内存的申请与释放:

LPVOID VirtualAlloc
LPVOID lpAddress, // 要分配的内存区域的地址
DWORD dwSize, // 分配的大小
DWORD flAllocationType, // 分配的类型
DWORD flProtect // 该内存的初始保护属性
;
 
BOOL VirtualFree(
LPVOID lpAddress, // 区域地址
SIZE_T dwSize, // 区域大小,字节
DWORD dwFreeType // 类型);
// 私有内存申请.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <windows.h>

int main()

    LPVOID P = VirtualAlloc(
        NULL, // 要分配的内存区域的地址
        0x1000 * 2, // 分配的大小
        MEM_COMMIT, // 分配的类型 MEM_COMMIT占住并分配物理页
        PAGE_READWRITE // 该内存的初始保护属性  可读可写
    );
    //释放申请的内存
    VirtualFree(P, 0x1000 * 2, MEM_DECOMMIT);
    //VirtualFree(P, 0, MEM_RELEASE);//彻底释放

new的本质就是malloc;但是他们不是真正申请内存,只是从已有内存中,再申请一小块自己用(堆内存)

//malloc -》 HeapAlloc -》 没有进内核

 

 

以上是关于内存映射:小块内存申请brk和申请大块内存的Mmap分析的主要内容,如果未能解决你的问题,请参考以下文章

简单内存池与定长内存池

简单内存池与定长内存池

malloc和free,brk和sbrk和mmap和munmap的使用和关系以及内存分配的原理

内存池

开胃菜-定长内存池

开胃菜-定长内存池