linux的内存管理
linux的内存管理是一个非常复杂的过程,主要分成两个大的部分:内核的内存管理和进程虚拟内存。内核的内存管理是linux内存管理的核心,所以我们先对内核的内存管理进行简介。
一、物理内存模型
物理内存模型主要分为两种:uma(uniform memory access)和numa(non-uniform memory access)。
uma模型是指物理内存是连续的,smp系统中的每个处理器访问各个内存区都是同样快的;而numa模型则是指smp中的每个cpu都有自己的物理内存区,虽然cpu可以访问其他cpu的内存区,但是要比方位自己的内存区慢得多。我们一般使用的物理模型都是uma模型。为了numa模型,linux提供了三种可能的内存布局配置:flat memory, sparse memory, discontiguous memory。
flat memory就是简单的线性组织物理内存,一般没有内存空洞的uma架构都采用这种配置。对于numa模型,一般只能采用后两者,而后两者的区别主要在于:sparse memory配置一般认为是试验性的,不是那么稳定,但是有一些新的功能和性能优化,而discontiguous memory配置一般认为是稳定的,但不具有内存热插拔之类的新特性。
二、物理内存组织
物理内存的组织主要分为两个部分:节点(node)和内存与内存域(zone)。 node主要针对numa设计,在numa的smp系统中,每个处理器都有一个自己的node,而在uma模型中则只有一个node。对于每个node中的内存,linux分成了若干内存域,定义在mmzone.h的zone_type中,常用的有zone_dma、zone_dma32、zone_normal、zone_highmem和zone_movable。其中zone_normal是最为常用的,表示内核能够直接映射的一般内存区域;zone_dma表示dma内存区;zone_dma32表示64位系统中对于32位dma设备使用的内存;zone_highmem表示在32位系统中,高地址内存的区域;zone_movable与伙伴系统的内存碎片消除有关。后文会详细介绍相关部分。 在物理内存管理过程中有一些名词: page frame(页帧,或称页框):是系统内存管理的最小单位,系统中每个页框都是struct page的一个实例。ia-32系统的页框大小是4kb。 hot-n-code pages(冷热页):是指内存管理中对页框的分类,访问较多的或者近期访问的为热页,否则为冷页。该标记主要与内存换出(memory swap)相关。 page table(页表):是内存寻址过程中的辅助数据结构。层次化的页表对于大地之空间的快速、高效管理很有意义。linux一般支持四级页表:pgd(page global directory)、pud(page upper directory)、pmd(page middle directory)和pte(page table entry)。ia-32体系中默认只是用了两级分页系统,即只有pgd和pte。 三、x86架构下的内存布局
内核在内存中的布局
linux的内核在初始化的时候会被加载到内存区的固定位置(在此我们不讨论可重定位内核的情况),而内核所占用的内存区域的布局是固定的,如图:
内存第一个页框不实用,主要被bios用来初始化;之后的连续640kb内存也不被内核使用,主要用来映射各种rom(通常是bios和显卡rom);再之后的空间是闲置的,原因是内核要被放在连续的内存空间。在0x100000开始为内核部分,分别是代码段、数据段和附加段。
ia-32架构的布局
ia-32架构可以访问4gb的地址空间(不考虑pae),常规情况下会将4gb线性空间划分成3:1的两部分:低地址的3/4部分为用户空间,而高地址的1gb是内核空间,即内核地址空间从偏移量0xc0000000开始,每个虚拟地址x都对应于物理地址x-0xc0000000。这样的设计加快了内核空间寻址的速度(简单的减法操作)。在进程切换的过程中,只有用户空间的低3gb内存对应的页表会被切换,高地址空间会公用内核页表。
ia-32架构的这种设计存在着一个问题:既然内核只能处理1gb的空间(事实上,内核处理的空间还不足1gb,后面会详细说明),那么如果物理内存大于1gb,剩下的内存将如何处理?这种情况下,内核将无法直接映射全部物理内存,这样就用到了上面所说的高地址内存域(zone_highmem)。具体的内存分配如下图:
能够看到内核区域的映射从__page_offset(0xc00000)开始,即3gib位置开始映射到4gib,开始的一段用来直接映射,而后面有128mb的vmalloc空间(这部分空间的使用后文将讲到),再之后有永久映射和固定映射的空间(从pkmap_base开始)。所以事实上物理内存能够直接映射的空间为1gb-vmalloc-固定映射-永久映射,所以真正大约只有850mb多一点,也就是说,物理内存中只有前850多mb是可以直接映射到内核空间的,对于超过的部分来说,将作为高地址空间(highmem)。高地址空间可以在vmalloc、永久映射和固定映射部分使用到。
到这里可能会有这样一个疑问:如果内核只能处理896mb的空间,那么如果内存很大(比如3gb),剩下的空间的利用率和利用效率岂不是很低?对于这个问题我们需要注意:这里我们所讲述的:1、这里的内存都是内核在内核区的1gb空间里对物理内存的访问,用户对物理内存的访问不是通过直接映射来访问的,还有另外一套机制;2、这里的内存仅仅是通过直接映射得到的内存,内核还可以通过其他的方式访问到较高地址的内存。
还有一个普遍的疑问就是:内核直接映射占用了800多mb的空间,那么如果我们又3gb的物理内存,是不是只有2gb多一点的实际可用内存呢?这个说法是错误的,上图所描述的只是内核在线性地址空间的分布情况,其中的任何区域如果没有真正的物理内存与之映射的话是不会真正占用物理内存的,而物理内存在分配的过程中(用户申请内存、vmalloc部分等),更倾向于先分配高地址内存,在高地址内存耗尽的情况下才会使用低850mb内存。
amd64架构的布局
amd64架构采用了与ia-32完全不同的布局模式。由于64位的寻址空间的64位长,而在真正的实现过程中64位长寻址会造成较大的开销,所以linux目前仅适用了48位长的地址空间,但是为了向后兼容仍然适用64位地址空间表示。在布局方面考虑,如果单纯采用48位类似ia-32的布局方式的话,则很难保证向后兼容性。所以amd64架构下的内存布局linux采用了一种特殊的方式,如图:
linux将内存分成了高地址部分和低地址部分两部分,即下半部空间0~0x0000 7fff ffff ffff和上半部空间0xffff 8000 0000 0000~0xffff ffff ffff ffff。可以看到虚拟地址的低47位,即[0,46]为有效位,[47,63]的值总是相同的:或者全为0或者全为1。除此之外的值都是无效的。这样在虚拟内存空间中就将内存分成了两个部分:内存空间的下半部和上半部。下半部为用户空间,上半部为内核空间。我们考虑内核空间部分,下半部的前maxmem大小(64tb)为直接映射地址,之后有一个空洞,主要目的是处理内存访问越界;再之后是大小为32tb的vmalloc空间,在之后是vmmemmap空间、kernel text段空间以及modules空间。
在这里我们不仔细讲述amd64架构的布局,以后的部分则主要关注于ia-32架构。
四、启动过程期间的内存管理
在启动过程中,尽管内存管理尚未初始化,但内核仍然需要分配内存以创建各种数据结构。bootmem分配器用于在启动阶段早期分配内存。由于对这部分内存分配集中于简单性方面而不是性能和通用性,因此使用的是最先适配(first-fit)分配器。该分配器使用一个位图来管理页,位图中的1表示页已使用,0表示未使用。在需要分配内存时,分配器扫描位图,直到找到一个能够提供足够连续页的为之,即最先最佳(first-best)或最先适配位置。
在这个分配过程中,需要处理一些不可分配的页面,如ia-32系统中的0页。另外对于ia-32系统,bootmem仅仅使用了低地址部分,对于高地址部分的操作过于麻烦,所以在这里被放弃了。
在这个部分有一个很有意思的事情。我们在编写内核模块的时候,对于模块的初始化函数会使用__init标记或者__init_data标记。对于被这两个关键字标记的函数和数据,是只有在初始化阶段才用到的,在bootmem退出的时候会全部被回收。而这部分代码和数据再内核链接的过程中将会被放在.init.text段和.init.data段,并统一放在内核的尾部,在启动结束后便于回收。
五、物理内存的管理
1、伙伴系统
物理内存管理中伙伴系统是最为重要的一个系统,伙伴系统也基于一种相对简单然而令人吃惊的强大算法,到目前已经使用了几乎40年。伙伴系统在这里不再赘述,简单谷歌一下就可以查到该算法的描述(实在是很简单)。在这里主要讲一下linux kernel的伙伴系统以及在2.6.24之后版本的系统中对伙伴系统的改良。
在上文中已经说到,物理内存的惯例分为若干个node,每个node中又有若干个zone。对于每个zone,都会有对应的伙伴系统,如下图所示:
上图中的fallback list指的是:在多个node的系统中,如果某个node的内存空间不够,则会在fallback list中指定的node中分配内存。
我们可以执行cat /proc/buddyinfo,能够看到大约如下所示的信息:
/proc/buddyinfo:wolfgang@meitner> cat /proc/buddyinfonode 0, zone dma 3 5 7 4 6 3 3 3 1 1 1node 0, zone dma32 130 546 695 271 107 38 2 2 1 4 479node 0, zone normal 23 6 6 8 1 4 3 0 0 0 0 显示的三个域则是我们使用到的内存域。
伙伴系统会出现一个很常见的问题:在系统使用较长时间之后,内存中经常出现较多碎片。对于这种情况,内核将内存页面分成五种类型:
migrate_unmovable
migrate_reclaimable
migrate_reserve
migrate_movable
migrate_isolate
其中migrate_reserve所表示的内存是被系统保留以备急用的;migrate_unmovable是不可移动的,如bios信息页;migrate_reclaimable在swap系统中使用;migrate_isolate表示不能从这里分配的内存;migrate_movable表示可以移动的内存。对于内核来说,migrate_movable部分的内存可以采用某种算法来进行移动,使得内存中的碎片减少。另外内核还维护了一个fallback list,来表示如果在某个类型中分配页面未成功,会在哪些类型的页面中来分配。
具体的信息可以在/proc/pagetypeinfo中看到
2、伙伴系统的内存分配api
基本上从如下两张图就能够看出来:
对于其中的函数命名基本都是自明的,主要的差别在于:对于双下划线开头的函数(__get_free_page, __free_page等)返回值或者参数为struct page *,而其他的函数返回值为unsigned long,即线性地址地址。
3、内核中不连续页的分配
根据上文的讲述,我们知道物理上连续的映射对内核是最好的,但并不是总能成功的使用。所以内核提供了类似用户空间访问内存一样的机制(vmalloc)来进行对内核中不连续页的分配。这一部分就是上文中所说的vmalloc区域。这部分主要是一个vmalloc函数:
void *vmalloc(unsigned long size); 在该函数的实现过程中,需要先申请一部分虚拟内存空间vm_area,然后将这部分空间映射到vmalloc区域中。对于映射的物理内存,内核更倾向于使用高地址空间(zone_highmem),来节省宝贵的地地址空间。对于不同vmalloc调用申请的vm_area之间,会有一个hole来隔离,以避免越界访问。
注意vmalloc系统底层也是使用伙伴系统来分配内存,所以申请内存的大小只能是整页的(页大小对齐)。
在这部分有一个有意思的事情:vmalloc区域在ia-32中预设的大小是128mb,这部分内存一般会被内核模块使用。vmalloc区域的大小是可以定制的,在新版内核中可以在内核启动选项中加入vmalloc=xxxmb的方式来修改,或者修改内核代码对应的宏:
unsigned int __vmalloc_reserve = 128
flush_tlb_page(struct vm_area_struct *vma, unsigned long page) 和flush_cache_page(vma, page)刷出虚拟地址在[page, page + page_size]范围内所有的tlb/高速缓存项。
update_mmu_cache(struct vm_area_struct *vma, unsigned long address, pte_t pte)在处理页失效之后调用。它在处理器的内存管理单元mmu中加入信息,是的虚拟地址address由页表项pte描述。仅当存在外部mmu时才需要该函数,通常mmu集成在处理器内部。
此外,flush_cache_和flush_tlb_函数常常成对出现,例如,在使用fork进程复制进程的地址空间时,则:1、刷出高速缓存,2、操作内存,3、刷出tlb。这个顺序很重要,因为
如果顺序相反,那么在tlb刷出之后,正确信息提供之前,多处理器系统中的另一个cpu可能从进程的页表项取得错误的信息。
在刷出高速缓存时,某些体系结构需要依赖tlb中的“虚拟->物理”转换规则。flush_tlb_mm必须在flush_cache_mm之后执行以确保这一点。
小结
这部分东西实在是太多,简单的总结一下就已经这么多了。在这里对以上的内容进行一个简单的概括。
在内核进入正常运行之后,内存管理分为两个层次:伙伴系统负责物理页框的管理。在伙伴系统之上,所有的内存管理都基于此,主要分为:slab分配器处理小块内存;vmalloc模块为不连续物理页框提供映射;永久映射区域和固定映射区域提供对高地址物理页框的访问。
内存管理的初始化很具有挑战性,内核通过引入一个非常简单的自举内存分配器(bootmem)解决了该问题,该分配器在正式的分配机制(伙伴系统)启用后停用。
神了!三摄手机竟有两个摄像头是装饰
成立电池研究小组 巴西启动电动车研发项目
努比亚推出专业电竞旗舰手机——红魔3 颠覆我们对传统手机的认知
太阳能不间断电源,Solar power supply
数说宪法:宪法宣传电子屏
Linux的内存管理是什么,Linux的内存管理详解
Plexus新产品,VR触觉和传感器手套即将上市
5G时代下:RF-SOI市场前景良好,预计规模未来5年将翻番
华为发布两款昇腾系列AI芯片,其野心远不止如此
时隔五年,万众期待多年的10nm终于来了!
STM32的ADC转换最常见的方式
PID控制原理详解
第三代半导体材料,或将成为我国半导体产业发展弯道超车机会
关于索尼CMOS图像传感器芯片IMX400的三层堆叠结构
新小米手机可能会以Mi 10T Lite的形式正式发布
ONT/ONU如何工作
低成本永续可再生太阳能电池曝光
红米4高配版上手评测:千元机顶配!
OPPO为什么那么火?是产品还是渠道
如何在Cadence Allegro软件中制作过孔