讲解复杂繁琐的机制原理,最通俗的方法就是用模型架构的方式向读者呈现,先要在整体上了解大方向大架构,再根据大方向大架构来进行分支深入,犹如毛主席那句话“战略上蔑视敌人,战术上重视敌人”。下面我也以这种方式把各个大模型方式向大家画出,并作出简略解述。
一. 地址划分。
1. CPU地址。
CPU地址是指CPU的地址总线能寻址的范围,32bit-CPU寻址范围为4G, 这个地址是虚拟的,实际上外部物理内存是不会使用这么大的内存。
CPU虚拟地址的4G空间,通常划分为两部分,一部分为内核虚拟地址,通常为3G-4G之间,另一部分为用户虚拟地址,通常为0G-3G之间,显然,用户进程能使用的虚拟地址范围远大于内核可以使用的虚拟地址空间,但是,物理内存只有局限性的几M,几G,内核虚拟地址如何使用物理内存,用户空间如何使用物理内存,这些问题正是linux内存管理的关键。
2. 物理内存
物理内存是指外部存储数据的设备,有可以被CPU寻址到的地址总线,受到CPU的Cache 和TLB/MMU管理寻址。
需要澄清一个概念:任何代码是在CPU上运行的,而不是在物理内存上,物理内存是个设备,用于存放用户进程空间的可执行代码或者内核关键数据结构,这些代码或结构终将是要受到CPU通过MMU寻址,Cache***指令数据来获取的。
NUMA的全称是非一致性内存访问,它通常是多核访问的概念,每一个CPU核都会有一个节点对应使用一部分物理内存,对这些节点的管理附加这些数据结构:perCPU变量,list表串联各节点遍历,zone的划分,zonelist的管理等等。为了使问题更加简单化,我们只分析UMA的一个节点的情况,当然它也包含NUMA的一些数据结构特征,这个后面会有所简述。
下图是NUMA的一个简略图抽象如图2-1所示。
图2-1 NUMA多核物理内存zone示意图
3. 内核虚拟地址空间划分。
如果读者仅仅了解一些皮毛,必然认为内核的虚拟地址空间仅有逻辑地址这一说,其实这只是内存内核虚拟地址划分的一个特例,并非全部的完整表述,现在我划出完整的图形,并且改变改变对内核虚拟地址空间名称的叫法,如图2-2
图2-2 内核虚拟地址空间划分及其对物理内存的映射
下面来改改名字咯,直接映射的地址我们可以叫为内核物理直接映射地址或者逻辑地址。linux原则上只能使用虚拟空间1G中的896M,剩下的128M留作它用,所以直接映射之外的物理内存称为高端内存。128M之间的空间又划分为多个gap安全间隙,虚拟地址,固定映射和持久映射,注意这里的虚拟地址叫法通常和前述的内核虚拟地址有些混杂,后者是指CPU内核虚拟地址,是更广的概念。由于直接映射的部分有了名字叫逻辑地址,那么这里的虚拟地址空间常专指这个部分。
虚拟地址有以下用途,使用vm_struct结构体经内核管理高端内存,它可以使用kmap方式获取高端物理内存的空间;也可以不映射物理高端内存,将这段地址直接作为外部物理设备的ioremap地址,从而可以直接操纵设备,当然这也将外部设备地址空间暴露出来并且容易造成干扰,所以通常不能直接访问ioremap映射的地址而是用readb/writeb读写,而且要做好优化屏障设置并且用iounmap释放,因为映射了的设备常具有’边际效应’.
如果没有高端内存,(当然32bit的嵌入式系统通常不会使用高端内存,至少我见过的那么多关于ARM,powerPC,MIPS32的嵌入式应用都是没有使用高端内存的), 那么固定映射和持久映射也多半不会用到。固定映射可以指定长期持有物理内存某些地址页的占用,这个映射关系可以在初始阶段进行配置,而持久映射在启用时就建立了同高端内存物理页的映射关系,它在其他阶段都不会被解除。
强调的是,我这里不关心高端内存,内核的直接映射逻辑地址就可以涵盖全部物理内存。
4. 用户虚拟地址空间的划分
用户虚拟地址空间图构并不复杂,复杂的是它在虚拟内存空间中的应用,如何映射文件,如何组织区间映射,关联的进程是谁,对应的内存结构体实例是什么等等问题才是用户虚拟映射最难的地方,下面仅仅划出图示,对用户虚拟内存空间可以先有一个大了解,如图2-3。
图2-3用户空间虚拟内存布局
既然用户空间是虚拟的,那么它是怎么访问物理内存的呢,当然就是PGD,PUD,PMD,PTE,OFFSET及其TLB快表查询了,上层目录入口PUD和中间目录入口一般不考虑,考虑二级目录就可以了。从网上摘的图2-4:
图2-4 用户进程空间访问物理内存的方法
二. 伙伴系统
伙伴系统是按阶管理外界物理内存的方法,***有11阶,每一阶有一个或者多个页合并的集合并使用指针串联起来,同时在同一阶中的一个或多个页集合中形成各自的伙伴,要强调的是各个阶的伙伴都是等页个数的,用下图2-5是比较好理解的。
图2-5 伙伴系统在内存中的大致模型
当内核申请一段按页却并非按照阶数分配的内存时候,通常会使用伙伴系统原理将其按照该申请空间的***阶数分配,多出来的页按照伙伴系统算法归并到其他阶的链表当中形成其他阶的新伙伴。释放该内存空间的时候,释放的空间会尝试找到能以它为伙伴的那个阶进行连接,如果大小超过,则劈开,多余的再寻找其他可以以它为伙伴的阶。够拗口的,但还是很容易理解的,后面会有源代码呈现出来以实例详细分析。
三. 反碎片技术:
反碎片机制其实还在伙伴系统之前,它主要是将各个zone区域的物理内存分成可回收reclaimable但不可移动unmovable,可移动movable,不可移动unmovable. 这些标记按照一定得list串联起来管理,当外部条件申请物理内存导致许多碎片的时候,它可以按照这些数据结构的标志,来从新组织归类物理内存,从而减少碎片页或者孤独页。反碎片技术在嵌入式系统当中少用,绝大部分由伙伴系统占据江山了,因此不会对此做具体分析,简略过之。
四. Slab分配机制。
众所周知,操作系统使用伙伴系统管理内存,不仅会造成大量的内存碎片,同时处理效率也较低下。SLAB是一种内存管理机制,其拥有较高的处理效率,同时也有效的避免内存碎片的产生,其核心思想是预分配。其按照SIZE对内存进行分类管理的,当申请一块大小为SIZE的内存时,分配器就从SIZE集合中分配一个内存块(BLOCK)出去,当释放一个大小为SIZE的内存时,则将该内存块放回到原有集合,而不是释放给操作系统。当又要申请相同大小的内存时,可以复用之前被回收的内存块(BLOCK),从而避免了内存碎片的产生。[注:因SLAB处理过程的细节较多,在此只是做一个原理上的讲解
1. 总体结构
图1 SLAB内存结构
2.处理流程
如图1中所示:SLAB管理机制将内存大体上分为SLAB头、SLOT数组、PAGES数组、可分配空间、被浪费空间等模块进行分别管理,其中各模块的功能和作用:
SLAB头:包含SLAB管理的汇总信息,如最小分配单元(min_size)、最小分配单元对应的位移(min_shift)、页数组地址(pages)、空闲页链表(free)、可分配空间的起始地址(start)、内存块结束地址(end)等等信息(如代码1所示),在内存的管理过程中,内存的分配、回收、定位等等操作都依赖于这些数据。
SLOT数组:SLOT数组各成员分别负责固定大小的内存块(BLOCK)的分配和回收。在nginx中SLOT[0]~SLOT[7]分别负责区间在[1~8]、[9~16]、[17~32]、[33~64]、[65~128]、[129~256]、[257~512]、[513~1024]字节大小内存的分配,但为方便内存块(BLOCK)的分配和回收,每个内存块(BLOCK)的大小为各区间的上限(8、16、32、64、128、256、512、1024)。比如说:假如应用进程请求申请5个字节的空间,因5处在[1~8]的区间内,因此由SLOT[0]负责该内存的分配,但区间[1~8]的上限为8,因此即使申请5个字节,却依然分配8字节给应用进程。以此类推:假如申请12字节,12处于区间[9~16]之间,取上限16,因此由SLOT[1]分配16个字节给应用进程;假如申请50字节,50处于区间[33~64]之间,取上限64,因此由SLOT[2]分配64个字节给应用进程;假如申请84字节,84处于区间[65~128]之间,取上限128,因此由SLOT[3]分配128个字节;...;假如申请722字节,722处于区间[513~1024]之间,取上限1024,因此由SLOT[7]分配1024字节。
PAGES数组:PAGES数组各成员分别负责可分配空间中各页的查询、分配和回收,其处理流程可参考3.2节的说明。
可分配空间:SLAB在逻辑上将可分配空间划分成M个内存页,每页大小为4K。每页内存与PAGES数组成员一一对应,由PAGES数组各成员负责各内存页的分配和回收。
被浪费空间:按照每页4K的大小对空间进行划分时,满足4K的空间,将作为可分配空间被PAGES数组进行管理,而***剩余的不足4K的内存将会被舍弃,也就是被浪费了!