鸿蒙轻内核M核源码分析系列七 动态内存Dynamic Memory 原创 精华
鸿蒙轻内核M核源码分析系列七 动态内存Dynamic Memory
内存管理模块管理系统的内存资源,它是操作系统的核心模块之一,主要包括内存的初始化、分配以及释放。
在系统运行过程中,内存管理模块通过对内存的申请/释放来管理用户和OS
对内存的使用,使内存的利用率和使用效率达到最优,同时最大限度地解决系统的内存碎片问题。
鸿蒙轻内核的内存管理分为静态内存管理和动态内存管理,提供内存初始化、分配、释放等功能。
-
动态内存:在动态内存池中分配用户指定大小的内存块。
- 优点:按需分配。
- 缺点:内存池中可能出现碎片。
-
静态内存:在静态内存池中分配用户初始化时预设(固定)大小的内存块。
- 优点:分配和释放效率高,静态内存池中无碎片。
- 缺点:只能申请到初始化预设大小的内存块,不能按需申请。
上一系列分析了静态内存,我们开始分析动态内存。动态内存管理主要用于用户需要使用大小不等的内存块的场景。当用户需要使用内存时,可以通过操作系统的动态内存申请函数索取指定大小的内存块,一旦使用完毕,通过动态内存释放函数归还所占用内存,使之可以重复使用。
OpenHarmony LiteOS-M动态内存在TLSF
算法的基础上,对区间的划分进行了优化,获得更优的性能,降低了碎片率。动态内存核心算法框图如下:
根据空闲内存块的大小,使用多个空闲链表来管理。根据内存空闲块大小分为两个部分:[4, 127]和[2^7, 2^31],如上图size class所示:
-
对[4,127]区间的内存进行等分,如上图绿色部分所示,分为31个小区间,每个小区间对应内存块大小为4字节的倍数。每个小区间对应一个空闲内存链表和用于标记对应空闲内存链表是否为空的一个比特位,值为1时,空闲链表非空。[4,127]区间的内存使用1个32位无符号整数位图标记。
-
大于127字节的空闲内存块,按照2的次幂区间大小进行空闲链表管理。总共分为24个小区间,每个小区间又等分为8个二级小区间,见上图蓝色的Size Class和Size SubClass部分。每个二级小区间对应一个空闲链表和用于标记对应空闲内存链表是否为空的一个比特位。总共24*8=192个二级小区间,对应192个空闲链表和192/32=6个32位无符号整数位图标记。
例如,当有40字节的空闲内存需要插入空闲链表时,对应小区间[40,43],第10个空闲链表,位图标记的第10比特位。把40字节的空闲内存挂载第10个空闲链表上,并判断是否需要更新位图标记。当需要申请40字节的内存时,根据位图标记获取存在满足申请大小的内存块的空闲链表,从空闲链表上获取空闲内存节点。如果分配的节点大于需要申请的内存大小,进行分割节点操作,剩余的节点重新挂载到相应的空闲链表上。当有580字节的空闲内存需要插入空闲链表时,对应二级小区间[2^9, 2^9 + 2^6],第31+2*8=47个空闲链表,第2个位图标记的第17比特位。把580字节的空闲内存挂载第47个空闲链表上,并判断是否需要更新位图标记。当需要申请580字节的内存时,根据位图标记获取存在满足申请大小的内存块的空闲链表,从空闲链表上获取空闲内存节点。如果分配的节点大于需要申请的内存大小,进行分割节点操作,剩余的节点重新挂载到相应的空闲链表上。如果对应的空闲链表为空,则向更大的内存区间去查询是否有满足条件的空闲链表,实际计算时,会一次性查找到满足申请大小的空闲链表。
动态内存管理结构如下图所示:
- 内存池池头部分
内存池池头部分包含内存池信息和位图标记数组和空闲链表数组。内存池信息包含内存池起始地址及堆区域总大小,内存池属性。位图标记数组有7个32位无符号整数组成,每个比特位标记对应的空闲链表是否挂载空闲内存块节点。空闲内存链表包含223个空闲内存头节点信息,每个空闲内存头节点信息维护内存节点头和空闲链表中的前驱、后继空闲内存节点。
- 内存池节点部分
包含3种类型节点,未使用空闲内存节点,已使用内存节点,尾节点。每个内存节点维护一个前序指针,指向内存池中上一个内存节点,维护大小和使用标记,标记该内存节点的大小和是否使用等。空闲内存节点和已使用内存节点后面的数据域,尾节点没有数据域。
本文通过分析动态内存模块的源码,帮助读者掌握动态内存的使用。本文中所涉及的源码,以OpenHarmony LiteOS-M
内核为例,均可以在开源站点https://gitee.com/openharmony/kernel_liteos_m 获取。接下来,我们看下动态内存的结构体,动态内存初始化,动态内存常用操作的源代码。
1、动态内存结构体定义和常用宏定义
1.1 动态内存结构体定义
动态内存的结构体有动态内存池信息结构体OsMemPoolInfo
,动态内存池头结构体OsMemPoolHead
、动态内存节点头结构体OsMemNodeHead
,已使用内存节点结构体OsMemUsedNodeHead
,空闲内存节点结构体OsMemFreeNodeHead
。这些结构体定义在文件kernel\src\mm\los_memory.c
中,下文会结合上文的动态内存管理结构示意图对各个结构体的成员变量进行说明。
1.1.1 动态内存池池头相关结构体
动态内存池信息结构体OsMemPoolInfo
维护内存池的开始地址和大小信息。三个主要的成员是内存池开始地址.pool
,内存池大小.poolSize
和内存值属性.attr
。如果开启宏LOSCFG_MEM_WATERLINE
,还会维护内存池的水线数值。
动态内存池头结构体OsMemPoolHead
源码如下,除了动态内存池信息结构体struct OsMemPoolInfo info
,还维护2个数组,一个是空闲内存链表位图数组freeListBitmap[]
,一个是空闲内存链表数组freeList[]
。宏定义OS_MEM_BITMAP_WORDS
和OS_MEM_FREE_LIST_COUNT
后文会介绍。
1.1.2 动态内存池内存节点相关结构体
先看下动态内存节点头结构体OsMemNodeHead
的定义,⑴处如果开启内存节点完整性检查的宏LOSCFG_BASE_MEM_NODE_INTEGRITY_CHECK
,会维护魔术字.magic
进行校验。⑵处如果开启内存泄漏检查的宏,会维护链接寄存器数组linkReg[]
。⑶处的成员变量是个指针组合体,内存池中的每个内存节点头维护指针执行上一个内存节点。⑷处维护内存节点的大小和标记信息。
接着看下已使用内存节点结构体OsMemUsedNodeHead
,该结构体比较简单,直接以动态内存节点头结构体OsMemNodeHead
作为唯一的成员。
我们再看下空闲内存节点结构体OsMemFreeNodeHead
,除了动态内存节点头结构体OsMemNodeHead
成员,还包含2个指针分别指向上一个和下一个空闲内存节点。
1.2 动态内存核心算法相关的宏和函数
动态内存中还提供了一些和TLSF
算法相关的宏定义和内联函数,这些宏非常重要,在分析源代码前需要熟悉下这些宏的定义。可以结合上文的动态内存核心算法框图进行学习。⑴处的宏对处于[2n,2(n+1)],其中(n=7,8,…30)区间的大内存块进行2^3=8等分。⑵处的宏,定义处于[4,127]区间的小内存块划分为31个,即4,8,12,…,124。⑶处定义小内存的上界值,考虑内存对齐和粒度,最大值只能取到124。
⑷处的宏表示处于[2n,2(n+1)],其中(n=7,8,…30)区间的大内存分为24个小区间,其中n=7 就是⑺处定义的宏OS_MEM_LARGE_START_BUCKET
。⑻处对应空闲内存链表的长度。⑼处是空闲链表位图数组的长度,31个小内存使用1个位图字,所以需要加1。⑽处定义位图掩码,每个位图字是32位无符号整数。
继续看下内联函数。⑾处函数查找位图字中的第一个1的比特位,这个实现的功能类似内建函数__builtin_ctz
。该函数用于获取空闲内存链表对应的位图字中,第一个挂载着空闲内存块的空闲内存链表。⑿处获取位图字中的最后一个1的比特位,(从32位二进制数值从左到右依次第0,1,…,31位)。⒀处函数名称中的Log
是对数英文logarithm
的缩写,函数用于计算以2为底的对数的整数部分。⒁处获取内存区间的大小级别编号,对于小于128字节的,有31个级别,对处于[2n,2(n+1)],其中(n=7,8,…30)区间的内存,有24个级别。⒂处根据内存大小,内存区间一级编号获取获取二级小区间的编号,对处于[2n,2(n+1)],其中(n=7,8,…30)区间的内存,有8个二级小区间。
2、动态内存常用操作
动态内存管理模块为用户提供初始化和删除内存池、申请、释放动态内存等操作,我们来分析下接口的源代码。在分析下内存操作接口之前,我们先看下一下常用的内部接口。
2.1 动态内存内部接口
2.1.1 设置和清理空闲内存链表标记位
⑴处函数OsMemSetFreeListBit
需要2个参数,一个是内存池池头head
,一个是空闲内存链表索引index
。当空闲内存链表上挂载有空闲内存块时,位图字相应的位需要设置为1。⑴处函数OsMemClearFreeListBit
做相反的操作,当空闲内存链表上不再挂载空闲内存块时,需要对应的比特位清零。
2.1.2 合并内存节点
函数VOID OsMemMergeNode(struct OsMemNodeHead *node)
用于合并给定节点struct OsMemNodeHead *node
和它前一个空闲节点。⑴处把前一个节点的大小加上要合入节点的大小。⑵处获取给定节点的下一个节点,然后执行⑶把它的前一个节点指向给定节点的前一个节点,完成节点的合并。其中宏OS_MEM_NODE_GET_LAST_FLAG
用于判断是否最后一个节点,默认为0,可以自行查看下该宏的定义。
2.1.3 分割内存节点
函数VOID OsMemSplitNode(VOID *pool, struct OsMemNodeHead *allocNode, UINT32 allocSize)
用于分割内存节点,需要三个参数。VOID *pool
是内存池起始地址,struct OsMemNodeHead *allocNode
表示从该内存节点分配出需要的内存,UINT32 allocSize
是需要分配的内存大小。分割之后剩余的部分,如果下一个节点是空闲节点,则合并一起。分割剩余的节点会挂载到空闲内存链表上。
⑴处表示newFreeNode
是分配之后剩余的空闲内存节点,设置它的上一个节点为分配的节点,并设置剩余内存大小。⑵处调整分配内存的大小,⑶处获取下一个节点,然后执行⑷下一个节点的前一个节点设置为新的空闲节点newFreeNode
。⑸处判断下一个节点是否被使用,如果没有使用,则把下一个节点从链表中删除,然后和空闲节点newFreeNode
合并。⑹处分割剩余的空闲内存节点挂载到链表上。
2.1.4 重新申请内存
OsMemReAllocSmaller()
函数用于从一个大的内存块里重新申请一个较小的内存,他需要的4个参数分别是:VOID *pool
是内存池起始地址,UINT32 allocSize
是重新申请的内存的大小,struct OsMemNodeHead *node
是当前需要重新分配内存的内存节点,UINT32 nodeSize
是当前节点的大小。⑴设置内存节点selfNode.sizeAndFlag
为去除标记后的实际大小,⑵按需分割节点,⑶分割后的节点设置已使用标记,完成完成申请内存。
2.1.5 合并节点重新申请内存
最后,再来看下函数函数OsMemMergeNodeForReAllocBigger()
,用于合并内存节点,重新分配更大的内存空间。它需要5个参数,VOID *pool
是内存池起始地址,UINT32 allocSize
是重新申请的内存的大小,struct OsMemNodeHead *node
是当前需要重新分配内存的内存节点,UINT32 nodeSize
是当前节点的大小,struct OsMemNodeHead *nextNode
是下一个内存节点。⑴处设置内存节点的大小为去除标记后的实际大小,⑵把下一个节点从链表上删除,然后合并节点。⑶处如果合并后的节点大小超过需要重新分配的大小,则分割节点。⑷处把申请的内存节点标记为已使用,完成完成申请内存
2.1.6 空闲内存链表相关操作
动态内存提供了针对空闲内存链表的几个操作,我们依次分析下这些操作的代码。首先看下函数OsMemFreeListIndexGet
,根据内存节点大小获取空闲内存链表的索引。⑴处先获取一级索引,⑵处获取二级索引,然后计算空闲链表的索引并返回。
接着看下函数OsMemListAdd
,如何把空闲内存节点插入空闲内存链表。⑴处获取空闲链表的第一个节点,如果节点不为空,则把这个节点的前驱节点设置为待插入节点node
。⑵处设置待插入节点的前驱、后继节点,然后把该节点赋值给空闲链表pool->freeList[listIndex]
。最后执行⑶处代码,把设置空闲链表位图字,并设置魔术字。
最后,分析下函数OsMemListDelete
如何从空闲内存链表删除指定的空闲内存节点。⑴处如果删除的节点是空闲内存链表的第一个节点,则需要把空闲链表执行待删除节点的下一个节点。如果下一个节点为空,需要执行⑵清除空闲链表的位图字。否则执行⑶把下一个节点的前驱节点设置为空。如果待删除节点不是空闲链表的第一个节点,执行⑷把待删除节点的前驱节点的后续节点设置为待删除节点的后继节点。如果待删除节点不为最后一个节点,需要执行⑸把待删除节点的后继节点的前驱节点设置为待删除节点的前驱节点。最后需要设置下魔术字。
2.1.7 空闲内存节点相关操作
动态内存提供了针对空闲内存的几个操作,如OsMemFreeNodeAdd
、OsMemFreeNodeDelete
、OsMemFreeNodeGet
。
函数OsMemFreeNodeAdd
用于把一个空闲内存节点加入相应的空闲内存链表上。⑴处调用函数获取空闲内存链表的索引,然后执行⑵把空闲内存节点加入空闲链表。
函数OsMemFreeNodeDelete
用于把一个空闲内存节点从相应的空闲内存链表上删除。代码较简单,获取空闲内存链表的索引,然后调用函数OsMemListDelete
进行删除。
函数OsMemFreeNodeGet
根据内存池地址和需要的内存大小获取满足大小条件的空闲内存块。⑴处调用函数获取满足大小条件的内存块,然后执行⑵把获取到的内存块从空闲内存链表删除,返回内存节点地址。
最后,分析下函数OsMemFindNextSuitableBlock
。⑴处根据需要的内存块大小获取一级区间编号,如果申请的内存处于[4,127]区间,执行⑵处记录空闲内存链表索引。如果需要申请的是大内存,执行⑶处代码。先获取二级区间索引,然后计算出空闲内存链表的索引值index
。这样计算出来的空闲内存链表下可能并没有挂载空闲内存块,调用⑷处函数OsMemNotEmptyIndexGet
获取挂载空闲内存块的空闲内存链表索引值。如果成功获取到满足大小的空闲内存块,返回空闲链表索引值,否则继续执行后续代码。⑹处对空闲链表位图字进行遍历,循环中的自增变量index
对应一级区间编号。如果位图字不为空,执行⑺获取这个位图字对应的最大的空闲内存链表的索引。
如果执行到⑻处,说明没有匹配到合适的内存块,返回空指针。⑼处表示存在满足大小的空闲内存链表,调用函数OsMemFindCurSuitableBlock
获取合适的内存块并返回。⑽处标签表示获取到合适的空闲内存链表索引,返回空闲内存链表。
我们再详细分析下函数OsMemNotEmptyIndexGet
的源码。⑴处根据空闲内存链表索引获取位图字,⑵处判断空闲内存链表索引对应的一级内存区间对应的二级小内存区间是否存在满足条件的空闲内存块。其中index & OS_MEM_BITMAP_MASK
对索引只取低5位后,可以把索引值和位图字中的比特位关联起来,比如index
为39时,index & OS_MEM_BITMAP_MASK
等于7,对应位图字的第7位。表达式~((1 << (index & OS_MEM_BITMAP_MASK)) - 1)
则用于表示大于空闲内存链表索引index
的索引值对应的位图字。⑵处的语句执行后,mask
就表示空闲链表索引值大于index
的链表索引对应的位图字的值。当mask
不为0时,表示存在满足内存大小的空闲内存块,则执行⑶处代码,其中OsMemFFS(mask)
获取位图字中第一个为1的比特位位数,该位对应着挂载空闲内存块的链表。(index & ~OS_MEM_BITMAP_MASK)
对应链表索引的高位,加上位图字位数就计算出挂载着满足申请条件的空闲内存链表的索引值。
最后,再看下函数OsMemFindCurSuitableBlock
。⑴处循环遍历空闲内存链表上挂载的内存块,如果遍历到的内存块大小大于需要的大小,则执行⑵返回该空闲内存块。否则返回空指针。
2.2 初始化动态内存池
我们分析下初始化动态内存池函数UINT32 LOS_MemInit(VOID *pool, UINT32 size)
的代码。我们先看看函数参数,VOID *pool
是动态内存池的起始地址,UINT32 size
是初始化的动态内存池的总大小,size
需要小于等于*pool
开始的内存区域的大小,否则会影响后面的内存区域,还需要大于动态内存池的最小值OS_MEM_MIN_POOL_SIZE
。[pool, pool + size]
不能和其他内存池冲突。
我们看下代码,⑴处对传入参数进行校验,⑵处对传入参数进行是否内存对齐校验,如果没有内存对齐会返回错误码。⑶处调用函数OsMemPoolInit()
进行内存池初始化,这是初始化内存的核心函数。⑷处开启宏LOSCFG_MEM_MUL_POOL
多内存池支持时,才会执行。
我们继续看下函数OsMemPoolInit()
。⑴处设置动态内存池信息结构体struct OsMemPoolHead *poolHead
的起始地址和大小,⑵处设置内存池属性设置为锁定、不可扩展。⑶处获取内存池的第一个内存控制节点,然后设置它的大小,该节点大小等于内存池总大小减去内存池池头大小和一个内存节点头大小。然后再设置该内存节点的上一个节点为内存池的最后一个节点OS_MEM_END_NODE(pool, size)
。
⑷处调用宏给节点设置魔术字,然后把内存节点插入到空闲内存链表中。⑸处获取内存池的尾节点,设置魔术字,然后执行⑹设置尾节点大小为0和设置上一个节点,并设置已使用标记。如果开启调测宏LOSCFG_MEM_WATERLINE
,还会有些其他操作,自行阅读即可。
2.3 申请动态内存
初始化动态内存池后,我们可以使用函数VOID *LOS_MemAlloc(VOID *pool, UINT32 size)
来申请动态内存,下面分析下源码。
⑴处对参数进行校验,内存池地址不能为空,申请的内存大小不能为0。⑵处判断申请的内存大小是否已标记为使用或内存对齐。⑶处调用函数OsMemAlloc(poolHead, size, intSave)
申请内存块。
我们继续分析函数OsMemAlloc()
。⑴处对申请内存大小加上头结点大小的和进行内存对齐,⑵处从空闲内存链表中获取一个满足申请大小的空闲内存块,如果申请失败,则打印错误信息。⑶处如果找到的内存块大于需要的内存大小,则执行分割操作。⑷处把已分配的内存节点标记为已使用,更新水线记录。⑸返回内存块的数据区的地址,这个是通过内存节点地址加1
定位到数据区内存地址实现的。申请内存完成,调用申请内存的函数中可以使用申请的内存了。
2.4 按指定字节对齐申请动态内存
我们还可以使用函数VOID *LOS_MemAllocAlign(VOID *pool, UINT32 size, UINT32 boundary)
,从指定动态内存池中申请长度为size
且地址按boundary
字节对齐的内存。该函数需要3个参数,VOID *pool
为内存池起始地址,UINT32 size
为需要申请的内存大小,UINT32 boundary
内存对齐数值。当申请内存后得到的内存地址VOID *ptr
,对齐后的内存地址为VOID *alignedPtr
,二者的偏移值使用UINT32 gapSize
保存。因为已经按OS_MEM_ALIGN_SIZE
内存对齐了,最大偏移值为boundary - OS_MEM_ALIGN_SIZE
。下面分析下源码。
⑴处对参数进行校验,内存池地址不能为空,申请的内存大小不能为0,对齐字节boundary
不能为0,还需要是2的幂。申请的内存大小必须大于最小的申请值OS_MEM_MIN_ALLOC_SIZE
。⑵处校验下对齐内存后是否会数据溢出。⑶处计算对齐后需要申请的内存大小,然后判断内存大小数值没有已使用或已对齐标记。⑷处调用函数申请到内存VOID *ptr
,然后计算出对齐的内存地址VOID *alignedPtr
,如果二者相等则返回。⑸处计算出对齐内存的偏移值,⑹处获取申请到的内存的头节点,设置已对齐标记。⑺对偏移值设置对齐标记,然后把偏移值保存在内存VOID *alignedPtr
的前4个字节里。⑻处重新定向要返回的指针,完成申请对齐的内存。
2.5 释放动态内存
对申请的内存块使用完毕,我们可以使用函数UINT32 LOS_MemFree(VOID *pool, VOID *ptr)
来释放动态态内存,需要2个参数,VOID *pool
是初始化过的动态内存池地址。VOID *ptr
是需要释放的动态内存块的数据区的起始地址,注意这个不是内存控制节点的地址。下面分析下源码,⑴处对传入的参数先进行校验。⑵处获取校准内存对齐后的真实的内存地址,然后获取内存节点头地址。⑶处调用函数OsMemFree(pool, ptr)
完成内存的释放。
我们回过头来,继续看下函数OsGetRealPtr()
。⑴获取内存对齐的偏移值,⑵如果偏移值同时标记为已使用和已对齐,则返回错误。⑶如果偏移值标记为已对齐,则执行⑷去除对齐标记,获取不带标记的偏移值。然后执行⑸,获取内存对齐之前的数据区内存地址。
2.6 重新申请动态内存
我们还可以使用函数VOID *LOS_MemRealloc(VOID *pool, VOID *ptr, UINT32 size)
,按指定size
大小重新分配内存块,并将原内存块内容拷贝到新内存块。如果新内存块申请成功,则释放原内存块。该函数需要3个参数,VOID *pool
为内存池起始地址,VOID *ptr
为之前申请的内存地址,UINT32 size
为重新申请的内存大小。返回值为新内存块地址,或者返回NULL。下面分析下源码。
⑴处对参数进行校验,内存池地址不能为空,内存大小不能含有已使用、已对齐标记。⑵处如果传入的内存地址为空,则等价于LOS_MemAlloc()
函数。⑶如果传入size
为0,等价于函数LOS_MemFree()
。⑷处保证申请的内存块大小至少为系统允许的最小值OS_MEM_MIN_ALLOC_SIZE
。⑸处获取内存对齐之前的内存地址,上文已分析该函数OsGetRealPtr()
。⑹处由数据域内存地址计算出内存控制节点node
的内存地址,然后执行⑺处函数重新申请内存。
继续分析下函数OsMemRealloc
。⑴处处理重新申请的内存小于等于现有的内存的情况,需要调用函数OsMemReAllocSmaller()
进行分割,分割完毕返回(VOID *)ptr
即可。如果重新申请更大的内存,则执行⑵处代码获取下一个节点,然后执行⑶处理下一个节点可用且两个节点大小之和大于等于重新申请内存的大小allocSize
。执行⑷处的函数,合并节点重新分配内存。
如果连续的节点的大小不满足重新申请内存的大小,则执行⑸处函数重新申请内存。申请成功后,执行⑹把之前内存的数据复制到新申请的内存区域,复制失败的话,则把新申请的内存释放掉,并返回NULL,退出函数。如果复制成功,继续执行⑺释放掉之前的节点。
小结
本文带领大家一起剖析了鸿蒙轻内核的静态内存模块的源代码,包含动态内存的结构体、动态内存池初始化、动态内存申请、释放等。感谢阅读,如有任何问题、建议,都可以留言给我们: https://gitee.com/openharmony/kernel_liteos_m/issues 。为了更容易找到鸿蒙轻内核代码仓,建议访问 https://gitee.com/openharmony/kernel_liteos_m ,关注Watch
、点赞Star
、并Fork
到自己账户下,谢谢。
图和文字描述都很详细,学习了。
学习代码 比较好的方式是动画图解涉及的数据结构,这个做起来 比较耗时,希望以后我有时间也做一下。:)
已经更新2群二维码 欢迎加入OHOS南向轻内核&HDF技术交流2群。
👍👍👍
自己看的时候一头雾水,看完本文以后,清晰了。
真的吗?\(^o^)/~ 感觉你的鼓励哈
[27, 231] 是不是应该写成[2^7 ~ 2^31]
“对应二级小区间[29,29+2^6]” 浏览器显示有点问题。
文中这样的现象还很多 连接符号加个空格就好了 找时间改下 感谢反馈