大家好,这里是物联网心球。
本文我们继续来介绍Linux内存管理,今天要讲的是内存管理中非常重要的机制伙伴系统。
为了小伙伴们能正确理解今天讲的内容,这里需要强调一下,我们今天还是围绕物理内存来讲解,请不要和虚拟内存弄混淆了。
1.伙伴系统是什么?
伙伴系统是一种内存管理算法,用于动态分配和释放物理内存页。
该算法的核心思想是将相邻且大小相等的内存页合并成一个大的内存页,从而减少内存碎片的产生和浪费。
在伙伴系统中,每个内存页都有一个伙伴,当某个内存页被分配时,系统会查找它的伙伴,如果伙伴也未被分配,则将它们合并成一个更大的内存页。
反之,当某个内存页被释放时,系统会查找它的伙伴,如果伙伴也空闲,则将它们合并成一个更大的内存页。
图片
Linux通过几个关键的数据结构实现伙伴系统:
- pglist_data:表示内存节点,内存节点详细介绍请参考文章图解Linux内存管理_整体架构。
- zone:表示内存区域,常见的区域有ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGHMEM,每个区域都对应一个伙伴系统。
- free_area:表示分配阶级,总共有MAX_ORDER(通常为11)个分配阶级,每个分配阶级对应的内存块大小为2^order个page。
- free_list:表示页类型链表头,用于内存碎片处理,本文不展开讨论。
2.内存管理区域
内存管理区域是伙伴系统一个核心的概念,内存节点被进一步划分成更小的内存区域,这个更小的内存区域称为内存管理区域(zone),zone的定义如下:
图片
2.1 为什么需要把内存节点划分成zone?
Linux系统不能把所有的内存都按相同的方式去处理,所以把内存节点划分成不同的区域,实现内存精细化管理,比如以下几种情况,需要将物理内存进行区域划分:
- 旧的工业标准体系结构(Industry Standard Architecture,ISA)总线只能直接访问 16MB 以下的内存,所以需要划分一个ZONE_DMA区域来兼容ISA标准。
- X86_32位系统内核虚拟地址空间只有1GB,其中896MB空间已经用于直接映射,剩余的128M空间需要访问3GB的物理内存空间,所以需要划分ZONE_HIGHMEM高端内存区域。
2.2 zone类型
zone主要可以划分为如下几种类型:
- ZONE_DMA:直接内存访问,是一种允许某些硬件子系统(例如磁盘驱动器或显卡)直接访问系统内存的技术。
- ZONE_DMA32:在32位系统上实现DMA传输的一种机制,主要用于允许64位或者大于16MB的DMA传输。
- ZONE_NORMAL:在Linux内存区域划分中,表示正常的内存区域,即不包含高端内存的区域。
- ZONE_HIGHMEM:在Linux内存区域划分中,表示高端内存,即不常用的内存区域,通常用于大型数据的缓存或者动态内存分配。
Linux内存节点并不一定会包含所有的内存区域类型,多数情况是只包含部分区域类型,我们以X86_32和X86_64系统来讲解。
X86_32系统物理内存划分如下:
图片
X86_32系统将物理内存划分为3个zone:
ZONE_DMA:0-16M,DMA内存区域。
ZONE_NORMAL:16M-896M,普通内存区域。
ZONE_HIGHMEM:896M-4GB,高端内存只存在于32位系统。
X86_64系统物理内存划分如下:
图片
X86_64系统将物理内存划分为3个zone:
ZONE_DMA:0-16M,DMA内存区域。
ZONE_DMA32:16M-4GB,DMA32内存区域。
ZONE_NORMAL:4GB-end,普通内存区域。
不同的CPU架构和系统支持的内存区域都会有差异,我们可以通过
dmesg | grep "mem"查看Linux系统内存区域的详细信息。
图片
3.伙伴系统初始化
Linux启动时,伙伴系统并没有实际物理内存,内核首先需要根据系统配置信息初始化各个内存区域,接下来需要将memblock(早期内存分配机制)模块的内存释放至伙伴系统,伙伴系统完成初始化后,memblock将退出历史舞台,后续的物理内存分配和回收由伙伴系统来完成。
Linux伙伴系统初始化流程如下:
start_kernel()->mm_init()->mem_init()->memblock_free_all()。
Linux内核获取设备树物理内存信息初始化memblock,由memblock负责早期的内存分配工作,memblock完成对应的工作后,将未使用的内存释放至伙伴系统,完成伙伴系统的初始化工作。
图片
4.伙伴系统内存分配
伙伴系统常见的内存分配函数如下:
图片
内存分配函数需要传入两个参数gfp_mask和order。
- gfp_mask:指定从哪个内存区域分配内存,内存区域定义如下:
图片
- order:指定分配阶级。
调用内存分配函数从伙伴系统分配内存时,通过gfg_mask和order找到对应的分配阶级,并申请2^order个page的内存块。
此时会出现两种情况:
- 情况1:指定分配阶级有空闲内存块。
该情况比较简单,只需要把该内存块分配出去即可。
- 情况2:指定分配阶级没有空闲内存块。
该情况处理起来会复杂一点,需要向高阶分配阶级申请空闲内存块,首先向order+1阶申请空闲内存块,如果order+1阶没有空闲内存块,继续向order+2阶申请空闲内存块,以此类推直到在order+n阶找到空闲内存块。
将order+n阶空闲内存块减半分裂,其中一半插入上一阶内存链表,另外一半内存块继续减半分裂,减半后其中一半内存块继续插入对应的分配阶级内存链表,另外一半内存块继续减半分裂,直至内存块符合申请内存块大小。
图片
这里介绍一下伙伴系统调试小技巧,通过cat /proc/buddyinfo可以查看伙伴系统所有分配阶级内存块情况。
5.伙伴系统内存回收
伙伴系统内存回收函数如下:
图片
调用内存回收函数同样需要指定order,指定order的目的是告知伙伴系统当前回收的内存块包含2^order个page。
伙伴系统根据order找到对应的分配阶级,伙伴系统不会把回收的内存块直接插入内存链表,而是先在分配阶级内存链表中查找内存块的伙伴。
如果没有找到内存块的伙伴,则将内存块插入分配阶级内存链表。
如果找到内存块的伙伴,则将内存块和伙伴进行合并,形成一个2倍大小的新的内存块,将新的内存块继续插入下一个分配阶级内存链表,同时也需要判断新的内存块是否存在伙伴,以此类推直到未找到内存块伙伴,并将合并后的内存块插入到最终分配阶级内存链表。
图片
总结
伙伴系统是Linux内存管理的一个重要机制,伙伴系统通过伙伴机制将小块的内存合并,在一定程度上减少了内存碎片,同时也实现了分配连续物理内存的功能。