icache dcache tlb 运存 固态 内存条 机械硬盘 malloc,名目繁多。
page fault主要是用户态进程建立页表的机制,但是有些页表的建立是直接建立映射,不走page fault机制。比如内核态使用的vmalloc,比如内核态用来映射设备地址空间的ioremap
mmap内存映射的实现过程,总的来说可以分为三个阶段
(一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
(二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。
利用 mmap() 替换 read(),配合 write() 调用的整个流程如下:
当你访问相关内存地址时,才会进行真正的 write、read 等系统调用。CPU 会通过陷入缺页异常的方式来将磁盘上的数据加载到物理内存中,此时才会发生真正的物理内存分配
虚拟内存带来了种种好处,但是一个最大的问题在于所有进程的虚拟内存大小总和可能大于物理内存总大小,因此当操作系统物理内存不够用时,就会把一部分内存 swap 到磁盘上
内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作,mmap从磁盘到虚拟地址空间的映射也必须是页。
在内存映射的过程中,并没有实际的数据拷贝,文件没有被载入内存,只是逻辑上被放入了内存,具体到代码,就是建立并初始化了相关的数据结构(struct address_space),这个过程有系统调用mmap()实现,所以建立内存映射的效率很高。
既然建立内存映射没有进行实际的数据拷贝,那么进程又怎么能最终直接通过内存操作访问到硬盘上的文件呢?那就要看内存映射之后的几个相关的过程了。
mmap()会返回一个指针ptr,它指向进程逻辑地址空间中的一个地址,这样以后,进程无需再调用read或write对文件进行读写,而只需要通过ptr就能够操作文件。但是ptr所指向的是一个逻辑地址,要操作其中的数据,必须通过MMU将逻辑地址转换成物理地址,这个过程与内存映射无关。
前面讲过,建立内存映射并没有实际拷贝数据,这时,MMU在地址映射表中是无法找到与ptr相对应的物理地址的,也就是MMU失败,将产生一个缺页中断,缺页中断的中断响应函数会在swap中寻找相对应的页面,如果找不到(也就是该文件从来没有被读入内存的情况),则会通过mmap()建立的映射关系,从硬盘上将文件读取到物理内存中,如图1中过程3所示。这个过程与内存映射无关。
如果在拷贝数据时,发现物理内存不够用,则会通过虚拟内存机制(swap)将暂时不用的物理页面交换到硬盘上,这个过程也与内存映射无关。
mmap内存映射的实现过程:
???CPU 进行一次磁盘读写操作涉及的数据量至少是 4KB,但是进行一次内存操作涉及的数据量是基于地址的,也就是通常的 64bit
linux使用vm_area_struct来表示一个独立的虚拟内存区域,一个进程可以使用多个vm_area_struct来表示不用类型的虚拟内存区域(如堆,栈,代码段,MMAP区域等)
2023 clk内核开发者大会上,oppo工程师绍了他们现在的64k动态大页的方案及效果。感兴趣的可以到下面链接听一下,。https://live.csdn.net/room/wl5875/vsWNFfGP
内核架构视角(连续应用启动),内核资源分布
每次缺页异常的处理都会经历漫长的流程,其中还会包括系统异常等级切换,c语言漫长处理流程,持锁,内存分配,甚至内存清零和内存拷贝,睡眠等等,是非常耗时的。tlb miss虽然只是cpu去查询页表,比page fault快很多,但是数量确比page fault大两个数量级。这两个对系统性能会产生较大影响,因此针对这两项的优化将是很有价值的。
内核大页技术降低TLB miss和page fault的产生,对内存访问产生显著的优化。如下,将一段连续2M物理内存直接映射到一个pmd entry,这样2M内存只占一个tlb entry,只产生一次page fault
优点:
降低TLB miss。如果采用大小为 2MB 的 huge page,一个 huge page 只需要对应一项 TLB 条目;同样大小的内存,换成使用 4KB 大小的页,则需要 512 项 TLB 条目。huge page可以限制降低TLB miss,提高访问性能;
减少了页表级数,节省内存,也可以减少查找页表的时间。大页使得页表级数减少,节省建立页表所使用的内存,加快页表查找时间;
减少缺页异常(page fault)的发生次数。假设应用程序需要 2MB 的内存,如果操作系统以 4KB 作为分页的单位,则需要512 次缺页异常才能将 2MB 应用程序空间全部映射到物理内存;然而当采用 2MB 作为分页时,只需要一次缺页异常就能完成;
因减少了缺页异常的处理,系统也将减少执行的指令;
因访问连续内存几率增加等因素,间接也能降低一下cache miss
内核中访问的内存,在初始化时采用线性映射的方式将内存映射到内核地址空间,除了某些内存机制的二次映射(如vamlloc等)外,初始化后不需要再建立页表,也不会因缺乏页表产生pagefault
一般所讲内核大页,除了huge vmalloc和huge ioremap之外,都是针对用户态进程,通过mmap、page fault等方式建立的页表
内核启动时在将memblock中主要内存映射到内核中,如果条件合适将映射成1G的大页,这样内核tlb miss会比较少,页表基址保存在swapper_pg_dir中
透明大页THP目前只支持2M大小,不需要预留内存,在产生page fault时,自动判断是否合适应用THP,同时将进程加入THP链表,后台进程khugepaged将扫描加入THP的进程的地址空间,并在时机合适的时候将其中映射的4k页面合并成大页
当THP配置之后,向应用层提供了接口
/sys/kernel/mm/transparent_hugepage/enabled有三个选项,代表透明大页的两种使用方式:
将enabled设为always,表示THP打开,并最大程度上对所有的区域都尽可能使用透明,自适应使用,对应用层透明;
将enabled设为madvise ,表示THP打开,但只有应用层调用madvise MADV_HUGEPAGE,将某一段地址范围加入THP后,才对这一段地址范围使用THP,但是这样就失去了透明,用户无感知的优点;
对于参数defrag,如果写入always,内核在分配2M大页时,如果没有连续2M内存,则会进行内存回收和内存规整;
THP有一个后台线程khugepaged,会扫描加入到mm_slot_cache链表中的mm,有些vma中由于没有分配到大页,或调用madvise的原因,还是4K页映射,这个线程会扫描mm_slot_cache链表,将条件合适的4K页映射合并成2M页映射。后台进程最终调用到collapse_huge_page,进行小页合并成大页的最后操作
首先预留内存:通过启动参数或sysfs接口设定预留内存
$ ls /sys/kernel/mm/hugepages/hugepages-2048kB/ free_hugepages nr_hugepages nr_hugepages_mempolicy nr_overcommit_hugepages resv_hugepages surplus_hugepages
内核有一个struct hstate结构体的全局变量hstates,当使用sysfs接口或启动参数预留内存时,内核将分配设定好大小和数量的大页挂到全局变量上,在使用时,从hstates所保存的大页内存池中分配
mount一个特殊的 hugetlbfs 文件系统,在上面创建文件,然后用mmap() 进行访问
hugetlbfs_file_mmap中会将vma设置VM_HUGETLB标志,
在产生page fault后检查vma如果有VM_HUGETLB标志,将走hugetlb大页映射流程:
需要提前预留内存,这些内存在使用中将不再释放回系统,不参与内存回收、内存交换和内存迁移等机制,好处是效率比较高,比较稳定,可以做DMA等操作,坏处是会浪费内存
因为透明大页的通用性非常好,支持内核的内存回收、反向映射等机制,在有些情况下必须将大页分离成4k的页面映射,比如以下的几种情况:
内存紧张时,shrink_slab调用deferred_split_shrinker,将大页分裂成4k页,回收空闲的页面:
THP注册了一个shrink_slab的接口deferred_split_shrinker, 在内存紧张时,内存回收通过shrink_slab流程调用deferred_split_scan,将大页分裂成4K页,回收空闲内存,缓解内存紧张;
阿里云提交给社区的一个patch,在透明大页的框架下,优化对代码段的支持
代码大页主要为大代码段业务服务,您可以通过该功能将应用程序和动态链接库的可执行部分放入到大页(Huge Pages)中,降低程序的iTLB miss,提升CPU的2 MB iTLB利用率,从而提升程序性能。 参考:https://help.aliyun.com/zh/alinux/user-guide/hugetext
对用户态进程的支持,依赖于page fault机制,page fault主要是用户态进程建立页表的机制,但是有些页表的建立是直接建立映射,不走page fault机制。比如内核态使用的vmalloc,比如内核态用来映射设备地址空间的ioremap,比如大多数的dmabuf,都采用直接建立页表的方式,即在调用函数vmalloc,ioremap的时候,就会分配虚拟地址和物理地址,然后建立好页表。
vmalloc原理 使用连续的虚拟地址去映射不连续的物理页面,一般分配大内存时使用
分配一段空闲虚拟地址空间;
根据长度分配多个物理页面,这个物理页面从buddy了一个一个page分配,所以可以不连续;
将虚拟地址空间和物理页面按照pte页表4k、4k映射
如果配置了VM_ALLOW_HUGE_VMAP,CONFIG_HAVE_ARCH_HUGE_VMALLOC,在分配物理页面时不是4k、4k分配,而是按照大页的order大小,分配一个一个连续的大块内存(64K、2M等);
将一个一个大块的连续page,按照大页映射
static int vmap_pmd_range(pud_t *pud, unsigned long addr, unsigned long end
static int vmap_try_huge_pmd(pmd_t *pmd, unsigned long addr, unsigned long end
static int vmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end
ioremap主要用于映射io设备地址空间,在设备驱动中常用,一般不直接映射内存。
ioremap原理:分配一段空闲虚拟地址空间,将物理地址空间按照4k的pte entry建立页表映射
因为ioremap是映射的物理地址空间,不是内存,所以不产生内存碎片化等负收益,因此当配置了CONFIG_HAVE_ARCH_HUGE_VMAP 时,尽量使用大页映射,因此也没有独立接口
优点:huge vmalloc和huge ioremap的收益主要来自于减少内存访问tlb miss和减少页表级数带来的收益。而且huge ioremap因为是映射io设备空间,不存在内存碎片化、大内存分配压力等负收益。
缺点:vmalloc一般用来分配大的,物理地址不连续的内存,但是huge vmalloc是要求映射的每一个大页物理地址连续的(当然大页和大页之间还是可以不连续),还有就是大页导致内存碎片化问题和分配大页产生的内存压力。
dma-buf内存框架广泛应用于多媒体系统,因为多媒体系统中,经常需要在不同进程之间,进程和设备之间传递数据,为了减少因不同进程之间,进程和设备之间内存的多次拷贝而产生的不必要的开销所引入的一套内存框架。
DMA-BUF框架实现了一个dma-buf的文件系统(filesystem),这个文件系统直接通过调用挂载接口挂载到了内核(没有提供挂载节点),每个dma-buf实例,会在文件系统中创建出inode及file,并将fd返回给调用者。调用者通过对file的ioctrl、map等操作,来操作dma-buffer。同时,不同进程之间,不同驱动之间,进程和设备之间,通过与dma-buf绑定的file传递,来实现dma-buf内存共享问题。
SMMU主要作用如下:
地址空间转换:传统DMA必须采用物理地址在设备和内存间搬运数据,SMMU和IOMMU的出现,可以像MMU一样,将物理地址映射成一个虚拟地址给DMA使用,典型的应用场景一个是虚拟机的设备透传,一个是用户态驱动;
映射非连续物理内存:传统DMA使用物理地址和长度进行搬运数据,要求物理内存必须连续,SMMU和IOMMU可以将连续的虚拟地址映射到不连续的物理内存片段,DMA使用映射后的虚拟地址即可;
地址空间扩展:32位转换成64位。现在很多系统是64位的,但是有些Master还是32位的,只能访问低4GB空间,如果访问更大的地址空间需要软硬件参与交换memory,实现起来比较复杂,也可以通过SMMU来解决,Master发出来的32位的地址,通过SMMU转换成64位,就很容易访问高地址空间
以上讲的几种大页技术,都是在4K基础页的基础上,利用各种大页机制,在一定范围里利用大页带来的好处。但是随着cpu性能越来越高,系统内存越来越大,希望使用更大的基础页以提升系统整体性能。比如,苹果ios目前就是使用的16K页面。Google目前打算在Android15中推动16K基础页。经过Google测算,不同场景系统性能可提升2%~10%,但memory会增加9%+,还会增加一定的内存碎片化问题
原来很多程序是默认4K page大小,因为改变了基础页的大小,需要第三方程序重新适配。比如,原来mmap时,默认是4K地址对齐,改成16K基础页后,需修改为16K地址对齐,否则就可能出问题。这就要求,推动Soc厂商,OEM厂商,第三方app开发商等系统方方面面的适配
轻松突破文件IO瓶颈:内存映射mmap技术 - 知乎