动态分配
系列篇将动态分配分成上下两篇,本篇为下篇,阅读之前建议翻看上篇。
- 鸿蒙内核源码分析(TLFS算法) 结合图表从理论视角说清楚 TLFS 算法
- 鸿蒙内核源码分析(内存池管理) 结合源码说清楚鸿蒙内核动态内存池实现过程,个人认为这部分代码很精彩,简洁高效,尤其对空闲节点和已使用节点的实现令人称奇。
为了便于理解源码,站长画了以下图,图中列出主要结构体,位图,分配和释放信息,逐一说明。
- 请将内存池想成一条画好了网格虚线的大白纸,会有两种角色往白纸上画东西,一个是内核画管理数据,一个外部程序画业务数据,内核先画,外部程序想画需申请大小,申请成功内核会提供个地址给外部使用,例如申请
20
个格子,成功后内核返回一个(5,8)
坐标,表示从第五行第八列开始往后的连续20
个格子你可以使用。用完了释放只需要告诉内核一个坐标(5,8)
而不需要大小,内核就知道回收多少格子。但内核凭什么知道要释放多少个格子呢 ? 一定有个格子给记录下来了对不对,实际中存大小的格子坐标就是(5,7)
。其值是在申请的时候或更早的时候填进去的。而且不一定是20
,但一定不小于20
。如果您能完全理解以上这段话,那可能已经理解了内存池的管理的方式,不用往下看了。
内存池 | OsMemPoolHead
/// 内存池头信息
struct OsMemPoolHead {
struct OsMemPoolInfo info; ///< 记录内存池的信息
UINT32 freeListBitmap[OS_MEM_BITMAP_WORDS]; ///< 空闲位图 int[7] = 32 * 7 = 224
struct OsMemFreeNodeHead *freeList[OS_MEM_FREE_LIST_COUNT];///< 空闲节点链表 32 + 24 * 8 = 224
SPIN_LOCK_S spinlock; ///< 操作本池的自旋锁,涉及CPU多核竞争,所以必须得是自旋锁
#ifdef LOSCFG_MEM_MUL_POOL
VOID *nextPool; ///< 指向下一个内存池 OsMemPoolHead 类型
#endif
};
/// 内存池信息
struct OsMemPoolInfo {
VOID *pool; ///< 指向内存块基地址,仅做记录而已,真正的分配内存跟它没啥关系
UINT32 totalSize; ///< 总大小,确定了内存池的边界
UINT32 attr; ///< 属性 default attr: lock, not expand.
#ifdef LOSCFG_MEM_WATERLINE
UINT32 waterLine; /* Maximum usage size in a memory pool | 内存吃水线*/
UINT32 curUsedSize; /* Current usage size in a memory pool | 当前已使用大小*/
#endif
};
解读
OsMemPoolInfo.pool
是整个内存池的第一个格子,里面放的是一个内存池起始虚拟地址。OsMemPoolInfo.totalSize
表示这张纸有多少个格子。OsMemPoolInfo.attr
表示池子还能不能再变大。OsMemPoolInfo.waterLine
池子水位警戒线,跟咱三峡大坝发洪水时的警戒线 175米 类似,告知上限,水一旦漫过此线就有重大风险,waterLine
一词很形象,内核很多思想真来源于生活。OsMemPoolInfo.curUsedSize
所有已分配内存大小的叠加。freeListBitmap
空闲位图,这是tlfs
算法的一二级表示,是个长度为7
的整型数组
#define OS_MEM_BITMAP_WORDS ((OS_MEM_FREE_LIST_COUNT >> 5) + 1)
#define OS_MEM_FREE_LIST_COUNT (OS_MEM_SMALL_BUCKET_COUNT + (OS_MEM_LARGE_BUCKET_COUNT << OS_MEM_SLI))
#define OS_MEM_LARGE_START_BUCKET 7 /// 大桶的开始下标
#define OS_MEM_SMALL_BUCKET_COUNT 31 ///< 小桶的偏移单位 从 4 ~ 124 ,共32级
#define OS_MEM_SLI 3 ///< 二级小区间级数,
这一坨坨的宏看着有点绕,简单说就是鸿蒙对申请大小分成两种情况
- 第一种:小桶申请** 当小于128个字节大小的需求平均分成了
([0-4],[4-8],...,[124-128])
共32
个等级,而freeListBitmap[0]
为一个UINT32
,共32
位刚好表示这32
个等级是否有空闲块。例如: 当freeListBitmap[0] = 0b...101
时,如果此时malloc(3)
到来,因101
对应的是12
,8
,4
等级,而且12
,4
位图位为1
,说明在4
的等级上有空闲内存块可以满足malloc(3)
,需要注意的是虽然malloc(3)
但因为4
等级上只有一种单位4
所以malloc(3)
最后实际得到的是4
,而如果malloc(7)
到来时,正常需要8
等级来满足,但8
等级位图位为0
表示没有空闲内存块,就需要向上找位图为1
的12
等级来申请,于是12
将被分成8
,4
两块,8
提供给malloc(7)
,剩下的4
挂入等级为4
的空闲链表上。 - 第二种:大桶申请** 将占用
freeListBitmap
的剩余6
个UINT32
整型变量,共可以表示32 * 6
=192
位 ,同时192
=24 * 8
,鸿蒙将大于128
个字节的申请按2
次幂分成24
大等级,每个等级又分成8
个小等级 即 TLFS 算法24
级对应的范围为([2^7-2^8-1],[2^8-2^9-1],...,[2^30-2^31-1])
而每大级被平均分成8
小级,
例如最小的[2^7-2^8-1]
将被分成每份递增2^4 = 16
大小的八份([2^7-2^7+2^4],[2^7+2^4-2^7+2^4*2],...,[2^7+2^4*7-2^8-1])
而最大的[2^30-2^31-1]
将被分成每份递增2^27 = 134 217 728
大小的八份,请记住2^27
这个数,后面还会说它。([2^30-2^30+2^27],[2^30+2^4-2^30+2^27*2],...,[2^30+2^4*7-2^31-1])
OsMemFreeNodeHead freeList[..]
是空闲链表数组,大小224
个,即每个freeListBitmap
等级都对应了一个链表
/// 内存池空闲节点
struct OsMemFreeNodeHead {
struct OsMemNodeHead header; ///< 内存池节点
struct OsMemFreeNodeHead *prev; ///< 前一个空闲前驱节点
struct OsMemFreeNodeHead *next; ///< 后一个空闲后继节点
};
prev
,next
,指向同级前后节点,
节点的内容在OsMemNodeHead
中,这是一个关键结构体,需单独讲。
内存池节点 | OsMemNodeHead
/// 内存池节点
struct OsMemNodeHead {
UINT32 magic; ///< 魔法数字 0xABCDDCBA
union {//注意这里的前后指向的是连续的地址节点,用于分割和合并
struct OsMemNodeHead *prev; /* The prev is used for current node points to the previous node | prev 用于当前节点指向前一个节点*/
struct OsMemNodeHead *next; /* The next is used for last node points to the expand node | next 用于最后一个节点指向展开节点*/
} ptr;
#ifdef LOSCFG_MEM_LEAKCHECK //内存泄漏检测
UINTPTR linkReg[LOS_RECORD_LR_CNT];///< 存放左右节点地址,用于检测
#endif
UINT32 sizeAndFlag; ///< 数据域大小
};
/// 已使用内存池节点
struct OsMemUsedNodeHead {
struct OsMemNodeHead header;///< 已被使用节点
#if OS_MEM_FREE_BY_TASKID
UINT32 taskID; ///< 使用节点的任务ID
#endif
};
解读
magic
魔法数字多次提高,内核很多模块都用到了它,比如 栈顶 ,存在的意义是防止越界,栈溢出栈顶元素就一定会被修改。同理使用了大于申请的内存会导致紧挨着的内存块魔法数字被修改,从而判定为内存溢出。- 出现一个联合体,其中的
prev
,是指向前节点的 虚拟地址 或者叫 线性地址 也可以叫 逻辑地址, 这些地址是 连续 的,注意 连续性 很重要,它是内存块合并和分割的前提,回到图中的0x1245
,0x12A5
,0x1305
来看,三个内存块节点的地址是逻辑地址相连的,内存块节点由头体两部分组成,头部放的是该节点的信息,体是 malloc(…) 的返回地址,所以当释放 free(0xXXX) 某块内存时很容易知道本节点的起始地址是多少,但向前合并就得知道前节点prev
的地址,而后节点next
的地址可通过0xXXX + sizeAndFlag - 头部 = next
计算得到。既然不需要next
那联合体出现在的next
有什么意思呢? 这个next
是指该块内存的尾节点的意思,当内存池允许扩展大小时,新旧两块内存之间就会产生一个连接处,它们的线性地址是不可能连续的,所以不存在合并的问题,prev
于它而言没有意义,需要记录下一个内存块的地址,这个工作就交给了联合体中的next
。 - 一个内存池可以由多个内存块组成,每个内存块都有独立的尾节点,指向下一块内存的开始地址,最后一个内存块的尾节点也称为哨兵节点,它像个哨兵一样为整个内存池站岗,风餐露宿,固守边疆。当扩大版图之后它又跑到下一站,一个内存池只有一个哨兵,它是最可爱的人,此处应有掌声。
linkReg
用于检测内存泄漏,这部分内容在 鸿蒙内核源码分析(模块监控) 已有详细说明,此处不再赘述。UINT32 sizeAndFlag
,表示总大小 包括(头部和体部)和 标签 ,上面已经让大家记住2^27
这个数,这是动态内存能分配的最大的尺寸。UINT32
中留28
位给它足以,剩下的高4
位就留给Flag
。每位又分别表示以下含义
#define OS_MEM_NODE_USED_FLAG 0x80000000U ///< 已使用标签
#define OS_MEM_NODE_ALIGNED_FLAG 0x40000000U ///< 对齐标签
#define OS_MEM_NODE_LAST_FLAG 0x20000000U /* Sentinel Node | 哨兵节点标签,最后一个节点*/
#define OS_MEM_NODE_ALIGNED_AND_USED_FLAG (OS_MEM_NODE_USED_FLAG | OS_MEM_NODE_ALIGNED_FLAG | OS_MEM_NODE_LAST_FLAG)
- 从联合体和
sizeAndFlag
可以看出鸿蒙的设计思想,充分利用空间,准确区分概念,一张卫生纸擦完嘴还要接着擦地,节俭之家必有余粮啊,这是非常有必要的,因为内存资源太稀缺了。在实际运行过程中,分配节点常数以万计,每个能省一个UINT32
,就是一万个UINT32
,约等于39KB
,非常可观。 这也是为什么站长始终觉得鸿蒙是个大宝藏的原因。 OsMemUsedNodeHead.taskID
已使用节点比空闲节点头部多了一个使用该节点任务的标记,由开关宏OS_MEM_FREE_BY_TASKID
控制,默认是关闭的。
代码实现
有了这么长的铺垫,再来看鸿蒙内核动态内存管理的代码简直就是易如反掌,此处拆解 节点切割 ,节点合并 ,内存池扩展 三段代码。
节点切割 | OsMemSplitNode
/// 切割节点
STATIC INLINE VOID OsMemSplitNode(VOID *pool, struct OsMemNodeHead *allocNode, UINT32 allocSize)
{
struct OsMemFreeNodeHead *newFreeNode = NULL;
struct OsMemNodeHead *nextNode = NULL;
newFreeNode = (struct OsMemFreeNodeHead *)(VOID *)((UINT8 *)allocNode + allocSize);//切割后出现的新空闲节点,在分配节点的右侧
newFreeNode->header.ptr.prev = allocNode;//新节点指向前节点,说明是从左到右切割
newFreeNode->header.sizeAndFlag = allocNode->sizeAndFlag - allocSize;//新空闲节点大小
allocNode->sizeAndFlag = allocSize;//分配节点大小
nextNode = OS_MEM_NEXT_NODE(&newFreeNode->header);//获取新节点的下一个节点
if (!OS_MEM_NODE_GET_LAST_FLAG(nextNode->sizeAndFlag)) {//如果下一个节点不是哨兵节点(末尾节点)
nextNode->ptr.prev = &newFreeNode->header;//下一个节点的前节点为新空闲节点
if (!OS_MEM_NODE_GET_USED_FLAG(nextNode->sizeAndFlag)) {//如果下一个节点也是空闲的
OsMemFreeNodeDelete(pool, (struct OsMemFreeNodeHead *)nextNode);//删除下一个节点信息
OsMemMergeNode(nextNode);//下一个节点和新空闲节点 合并成一个新节点
}
}
OsMemFreeNodeAdd(pool, newFreeNode);//挂入空闲链表
}
节点合并 | OsMemMergeNode
/// 合并节点,和前面的节点合并 node 消失
STATIC INLINE VOID OsMemMergeNode(struct OsMemNodeHead *node)
{
struct OsMemNodeHead *nextNode = NULL;
node->ptr.prev->sizeAndFlag += node->sizeAndFlag; //前节点长度变长
nextNode = (struct OsMemNodeHead *)((UINTPTR)node + node->sizeAndFlag); // 下一个节点位置
if (!OS_MEM_NODE_GET_LAST_FLAG(nextNode->sizeAndFlag)) {//不是哨兵节点
nextNode->ptr.prev = node->ptr.prev;//后一个节点的前节点变成前前节点
}
}
内存池扩展
/// 内存池扩展实现
STATIC INLINE INT32 OsMemPoolExpandSub(VOID *pool, UINT32 size, UINT32 intSave)
{
UINT32 tryCount = MAX_SHRINK_PAGECACHE_TRY;
struct OsMemPoolHead *poolInfo = (struct OsMemPoolHead *)pool;
struct OsMemNodeHead *newNode = NULL;
struct OsMemNodeHead *endNode = NULL;
size = ROUNDUP(size + OS_MEM_NODE_HEAD_SIZE, PAGE_SIZE);//圆整
endNode = OS_MEM_END_NODE(pool, poolInfo->info.totalSize);//获取哨兵节点
RETRY:
newNode = (struct OsMemNodeHead *)LOS_PhysPagesAllocContiguous(size >> PAGE_SHIFT);//申请新的内存池 | 物理内存
if (newNode == NULL)
return -1;
newNode->sizeAndFlag = (size - OS_MEM_NODE_HEAD_SIZE);//设置新节点大小
newNode->ptr.prev = OS_MEM_END_NODE(newNode, size);//新节点的前节点指向新节点的哨兵节点
OsMemSentinelNodeSet(endNode, newNode, size);//设置老内存池的哨兵节点信息,其实就是指向新内存块
OsMemFreeNodeAdd(pool, (struct OsMemFreeNodeHead *)newNode);//将新节点加入空闲链表
endNode = OS_MEM_END_NODE(newNode, size);//获取新节点的哨兵节点
(VOID)memset(endNode, 0, sizeof(*endNode));//清空内存
endNode->ptr.next = NULL;//新哨兵节点没有后续指向,因为它已成为最后
endNode->magic = OS_MEM_NODE_MAGIC;//设置新哨兵节的魔法数字
OsMemSentinelNodeSet(endNode, NULL, 0); //设置新哨兵节点内容
OsMemWaterUsedRecord(poolInfo, OS_MEM_NODE_HEAD_SIZE);//更新内存池警戒线
return 0;
}
经常有很多小伙伴抱怨说:不知道学习鸿蒙开发哪些技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?
为了能够帮助到大家能够有规划的学习,这里特别整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。
《鸿蒙 (Harmony OS)开发学习手册》(共计892页)
如何快速入门?
1.基本概念
2.构建第一个ArkTS应用
3.……
开发基础知识:
1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……
基于ArkTS 开发
1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……
鸿蒙开发面试真题(含参考答案)
OpenHarmony 开发环境搭建
《OpenHarmony源码解析》
- 搭建开发环境
- Windows 开发环境的搭建
- Ubuntu 开发环境搭建
- Linux 与 Windows 之间的文件共享
- ……
- 系统架构分析
- 构建子系统
- 启动流程
- 子系统
- 分布式任务调度子系统
- 分布式通信子系统
- 驱动子系统
- ……
OpenHarmony 设备开发学习手册
写在最后
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
- 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
- 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。