1、liteos 内存管理源码以及架构分析
对于操作系统来说主要有如下基础模块:
1)内存管理,任务调度,任务之间的通信和互斥,这几个是一个操作系统最核心的模块;
2)其次比较重要的就是文件系统,网络协议栈等一些比较重要的模块;
3)再下来就是操作系统根据各种类型的设备定义的一些驱动的框架,比如字符设备,块设备,等各种类型的设备的管理框架;
4)再具体下来就是具体的设备驱动程序这些是和具体的设备相关的。
内存管理应该属于操作系统最基础的模块了,因为其他的模块基本都会使用该模块的接口去分配和释放内存。
主要数据结构如下,后面再从代码层面依次详细讲解:
typedef struct
{
VOID *pPoolAddr; /*内存池起始地址*/
UINT32 uwPoolSize; /**<内存池大小*/
} LOS_MEM_POOL_INFO;
LOS_MEM_POOL_INFO 该结构体是内存池的起始地址和内存池的大小,位于内存池的开头。
typedef struct tagLOS_MEM_DYN_NODE
{
LOS_DL_LIST stFreeNodeInfo; /**<没有使用的内存节点链表*/
struct tagLOS_MEM_DYN_NODE *pstPreNode; /*前一个内存节点*/
UINT32 uwSizeAndFlag; /*当前节点的管理内存的大小,最高位表示内存是否已经被分配*/
}LOS_MEM_DYN_NODE;
LOS_MEM_DYN_NODE 该结构体内存管理的基本单元,每分配一次内存就是在找大小合适的节点,没有使用的内存也是通过这样的node一个个组织起来的。
typedef struct LOS_DL_LIST
{
struct LOS_DL_LIST *pstPrev; /*指向链表的前一个节点*/
struct LOS_DL_LIST *pstNext; /*指向链表的后一个节点*/
} LOS_DL_LIST;
LOS_DL_LIST 这个是一个双向链表,用来阻止没有使用的内存。
typedef struct
{
LOS_DL_LIST stListHead[OS_MULTI_DLNK_NUM]; // 链表数组
} LOS_MULTIPLE_DLNK_HEAD;
注意:
LOS_MULTIPLE_DLNK_HEAD该结构体是内存管理模块一个比较重要的结构体,该数组中的每一个元素都是一个链表,主要用来管理没有使用的内存,并且按照2的幂次方大小范围管理,每次分配的内存的是否都是找到该数组中合适的链表,然后找到链表中合适的元素。
liteos内存管理结构图:
上次中的第一行是整个内存池结构,第二行和第三行分别对应第一行蓝色线放大对应的具体结构,笔直的红色线代表sFreeNodeInfo是数组中某一个链表的一个node,用来管理没有使用的内存。
红色笔话的线以及标记的uwSizeAndFlag表示箭头对应的长度,注意这个flag中最高位表示该节点是否使用,liteos就是使用这样的节点表示一个内存块,然后再list数组中管理没有使用的内存。
2、liteos 内存管理源码 los_memory.c 源码分析
内存池的初始化:
UINT32 LOS_MemInit(VOID *pPool, UINT32 uwSize)
{
LOS_MEM_DYN_NODE *pstNewNode = (LOS_MEM_DYN_NODE *)NULL;
LOS_MEM_DYN_NODE *pstEndNode = (LOS_MEM_DYN_NODE *)NULL;
LOS_MEM_POOL_INFO *pstPoolInfo = (LOS_MEM_POOL_INFO *)NULL;
UINTPTR uvIntSave;
LOS_DL_LIST *pstListHead = (LOS_DL_LIST *)NULL;
if ((pPool == NULL) || (uwSize < (OS_MEM_MIN_POOL_SIZE)))
{
return OS_ERROR;
}
pstPoolInfo = (LOS_MEM_POOL_INFO *)pPool;
pstPoolInfo->pPoolAddr = pPool;//pool的起始地址就是内存数组的起始地址
pstPoolInfo->uwPoolSize = uwSize;//pool的大小就是内存数组的大小
LOS_DLnkInitMultiHead(OS_MEM_HEAD_ADDR(pPool));//内存地址除去poolinfo开始DLINK_HEAD地址,也就是list数组的起始地址
pstNewNode = OS_MEM_FIRST_NODE(pPool);//除去poolinfo 和DLINK_HEAD的list数组开始第一个node
//大小除去第info和list数组和第一个node剩下的大小
pstNewNode->uwSizeAndFlag = ((uwSize - ((UINT32)pstNewNode - (UINT32)pPool)) - OS_MEM_NODE_HEAD_SIZE);
pstNewNode->pstPreNode = (LOS_MEM_DYN_NODE *)NULL;
pstListHead = OS_MEM_HEAD(pPool, pstNewNode->uwSizeAndFlag);//找出list中的第log(size)-4个节点,这里是数学中的对数
if (NULL == pstListHead)
{
printf("%s %d\n", __FUNCTION__, __LINE__);
return OS_ERROR;
}
LOS_ListTailInsert(pstListHead,&(pstNewNode->stFreeNodeInfo));
pstEndNode = (LOS_MEM_DYN_NODE *)OS_MEM_END_NODE(pPool, uwSize);//最后一个node
(VOID)memset(pstEndNode, 0 ,sizeof(*pstEndNode));
pstEndNode->pstPreNode = pstNewNode;
pstEndNode->uwSizeAndFlag = OS_MEM_NODE_HEAD_SIZE;
OS_MEM_NODE_SET_USED_FLAG(pstEndNode->uwSizeAndFlag);//标记为已经使用,这个可以防止越界,后面会用到。
osMemSetMagicNumAndTaskid(pstEndNode);
return LOS_OK;
}
该函数的参数可以看成是一个数组的其实地址和数组的大小,事实上liteos中这两个参数确实是一个数组,该数组就是被管理的内存池。其实上面的那个图的第一行就是表示这个数组,元素的关系就是图上的关系。
主要做了一下几个重要操作:
1)分配和初始化 pstPoolInfo;
2)数组开头跳过pstPoolInfo的大小分配和初始化链表数组;
3)跳过pstPoolInfo和链表数组开始的是第一个内存管理节点pstNewNode;
4)在链表数组中找到合适的位置将pstNewNode插入链表;
5)数组的末尾也是一个内存管理节点pstEndNode,所以可以分配的初始化好之后,可以分配的内存就位于pstNewNode和pstEndNode之间。
上面五个步骤第三步是很关键的,下面详细详解。
第三步首先调用OS_MEM_HEAD这个宏定义函数,在list数组中找到合适的链表,然后将pstNewNode插入链表。
宏定义依次为:
#define OS_MEM_HEAD(pPool, uwSize) OS_DLnkHead(OS_MEM_HEAD_ADDR(pPool), uwSize)
//在内存池数组开头跳过LOS_MEM_POOL_INFO得到list数组结构体的地址
#define OS_MEM_HEAD_ADDR(pPool) ((VOID *)((UINT32)(pPool) + sizeof(LOS_MEM_POOL_INFO)))
#define OS_DLnkHead LOS_DLnkMultiHead
LOS_DL_LIST *LOS_DLnkMultiHead(VOID *pHeadAddr, UINT32 uwSize)
{
LOS_MULTIPLE_DLNK_HEAD *head = (LOS_MULTIPLE_DLNK_HEAD *)pHeadAddr;
UINT32 idx = LOS_Log2(uwSize);//这里就不再贴代码里,这里只要求uSize的二进制表示法最高为1的为在第几位,如LOS_Log2(1024)=10,LOS_Log2(2047)=10
if(idx > OS_MAX_MULTI_DLNK_LOG2)
{
return (LOS_DL_LIST *)NULL;
}
if(idx <= OS_MIN_MULTI_DLNK_LOG2)
{
idx = OS_MIN_MULTI_DLNK_LOG2;
}
//链表数组的第几个元素
return head->stListHead + (idx - OS_MIN_MULTI_DLNK_LOG2);
}
上面这个函数中的LOS_Log2()是一个数学中的以2为底的对数函数,只不过下取整如:
LOS_Log2(1024)结果为10,LOS_Log2(2047)也为10,但是LOS_Log2(2048)就是11,所以该函数也就是求解uwSize的二进制表示方式中1的最高位。
所以也不难理解LOS_DLnkMultiHead函数就是在list数组中根据uwSize的大小找到一个合适的list,所以该list数组是按照2的指数倍一次组织内存的。所有没有使用的内存都使用该list数组来组织。
求出合适的位置也就是找到了list之后,将pstNewNode插入list中,也就是 LOS_ListTailInsert(pstListHead,&(pstNewNode->stFreeNodeInfo));。
这样内存池的初始化基本就完成了,后面就可以分配内存使用了,其实就是在内存数组中找到合适的内存节点,
3、内存分配
VOID *LOS_MemAlloc (VOID *pPool, UINT32 uwSize)
{
VOID *pPtr = NULL;
do
{
if ((pPool == NULL) || (uwSize == 0))
{
break;
}
//判断如果uwSize的最最高位为1直接跳出返回
if (OS_MEM_NODE_GET_USED_FLAG(uwSize))
{
break;
}
//实际在内存池分配内存
pPtr = osMemAllocWithCheck(pPool, uwSize);
} while (0);
return pPtr;
}
static inline VOID *osMemAllocWithCheck(VOID *pPool, UINT32 uwSize)
{
LOS_MEM_DYN_NODE *pstAllocNode = (LOS_MEM_DYN_NODE *)NULL;
UINT32 uwAllocSize;
//因为liteos是使用node节点管理内存的所以这一需要添加node结构体长度,然后四字节对齐。
uwAllocSize = OS_MEM_ALIGN(uwSize + OS_MEM_NODE_HEAD_SIZE, OS_MEM_ALIGN_SIZE);
//我们在上面说过没有使用的内存都在list数组中按照内存的大小以2的指数级插入链表管理的。
pstAllocNode = osMemFindSuitableFreeBlock(pPool, uwAllocSize);
if (pstAllocNode == NULL)
{
printf("[%s] No suitable free block, require free node size: 0x%x\n", __FUNCTION__, uwAllocSize);
return NULL;
}
//如果找到的节点分配之后还可以分出一个节点,那么就将该节点分出来
if ((uwAllocSize + OS_MEM_NODE_HEAD_SIZE + OS_MEM_ALIGN_SIZE) <= pstAllocNode->uwSizeAndFlag)
{
osMemSpitNode(pPool, pstAllocNode, uwAllocSize);
}
//从链表中删除该节点
LOS_ListDelete(&(pstAllocNode->stFreeNodeInfo));
osMemSetMagicNumAndTaskid(pstAllocNode);
OS_MEM_NODE_SET_USED_FLAG(pstAllocNode->uwSizeAndFlag);
OS_MEM_ADD_USED(OS_MEM_NODE_GET_SIZE(pstAllocNode->uwSizeAndFlag));
//返回给用户的是用户可以使用的内存,所以这里需要跳过node
return (pstAllocNode + 1);
}
注意:
osMemFindSuitableFreeBlock该函数是遍历我们的list数组找到根据大小找到合适的节点,正如前面所说,所有没有使用的内存都在该数组链表里面保存着。
找到之后如果找到的节点还足够分出一个节点那么就将该节点分出来将剩下的分出来的节点挂入list数组
分节点函数:
static inline VOID osMemSpitNode(VOID *pPool,
LOS_MEM_DYN_NODE *pstAllocNode, UINT32 uwAllocSize)
{
LOS_MEM_DYN_NODE *pstNewFreeNode = (LOS_MEM_DYN_NODE *)NULL;
LOS_MEM_DYN_NODE *pstNextNode = (LOS_MEM_DYN_NODE *)NULL;
LOS_DL_LIST *pstListHead = (LOS_DL_LIST *)NULL;
//分出新的节点
pstNewFreeNode = (LOS_MEM_DYN_NODE *)((UINT8 *)pstAllocNode + uwAllocSize);
pstNewFreeNode->pstPreNode = pstAllocNode;
pstNewFreeNode->uwSizeAndFlag = pstAllocNode->uwSizeAndFlag - uwAllocSize;
pstAllocNode->uwSizeAndFlag = uwAllocSize;
//新节点的下一个节点
pstNextNode = OS_MEM_NEXT_NODE(pstNewFreeNode);
pstNextNode->pstPreNode = pstNewFreeNode;
/*判断新节点的下一个节点是否使用,如果没有使用那么将新节点和下一个节点合并,这也就是为什么在初始化的时候最后一个节点要标记为已经使用,还有就是我们可能多次的分配释放,所以下一个节点有可能是没有使用的,所以这里要添加判断*/
if (!OS_MEM_NODE_GET_USED_FLAG(pstNextNode->uwSizeAndFlag))
{
LOS_ListDelete(&(pstNextNode->stFreeNodeInfo));
osMemMergeNode(pstNextNode);
}
//根据大小在list数组中找到合适的list
pstListHead = OS_MEM_HEAD(pPool, pstNewFreeNode->uwSizeAndFlag);
if (NULL == pstListHead)
{
printf("%s %d\n", __FUNCTION__, __LINE__);
return;
}
//将分出来的节点加入list
LOS_ListAdd(pstListHead,&(pstNewFreeNode->stFreeNodeInfo));
}
以上就是内存的分配,当分配内存是首先从list数组中找到合适的node节点,如果该node节点管理的内存大于需要分配的内存,并且可以再分出一个节点,那么就将找到的节点分割,一部分是返回给分配的用户,一部分是没有使用的,然后看看这个没有使用的节点的下一个节点是否使用,如果没有那么将该节点和下一个节点合并,然后插入list数组,如果易已经使用,那么直接插入list数组。
最后就是跳过分配的节点结构体返回用户可以使用的内存起始地址
4、内存释放
UINT32 LOS_MemFree(VOID *pPool, VOID *pMem)
{
UINT32 uwRet = LOS_NOK;
UINT32 uwGapSize = 0;
do
{
LOS_MEM_DYN_NODE *pstNode = (LOS_MEM_DYN_NODE *)NULL;
if ((pPool == NULL) || (pMem == NULL))
{
break;
}
//字节对齐处理,由于有字节对齐分配,我们没有这里就不分析了
uwGapSize = *((UINT32 *)((UINT32)pMem - 4));
if (OS_MEM_NODE_GET_ALIGNED_FLAG(uwGapSize))
{
uwGapSize = OS_MEM_NODE_GET_ALIGNED_GAPSIZE(uwGapSize);
pMem = (VOID *)((UINT32)pMem - uwGapSize);
}
//由于返回给用户的是用户可以使用的,还记得刚才说的跳过node吗,所以这里需要调整,找到node
pstNode = (LOS_MEM_DYN_NODE *)((UINT32)pMem - OS_MEM_NODE_HEAD_SIZE);
//检查node使用使用
uwRet = osMemCheckUsedNode(pPool, pstNode);
if (uwRet == LOS_OK)
{
osMemFreeNode(pstNode, pPool);
}
} while(0);
return uwRet;
}
//释放函数
static inline VOID osMemFreeNode(LOS_MEM_DYN_NODE *pstNode, VOID *pPool)
{
LOS_MEM_DYN_NODE *pstNextNode = (LOS_MEM_DYN_NODE *)NULL;
LOS_DL_LIST *pstListHead = (LOS_DL_LIST *)NULL;
OS_MEM_REDUCE_USED(OS_MEM_NODE_GET_SIZE(pstNode->uwSizeAndFlag));
pstNode->uwSizeAndFlag = OS_MEM_NODE_GET_SIZE(pstNode->uwSizeAndFlag);
//如果node的前一个节点不为空,且前一个节点没有使用,那么
if ((pstNode->pstPreNode != NULL) &&
(!OS_MEM_NODE_GET_USED_FLAG(pstNode->pstPreNode->uwSizeAndFlag)))
{
LOS_MEM_DYN_NODE *pstPreNode = pstNode->pstPreNode;
//和前一个节点合并
osMemMergeNode(pstNode);
pstNextNode = OS_MEM_NEXT_NODE(pstPreNode);
//后一个节点也没有使用,那么也和后一个节点合并,
/*也就是说如果需要释放的节点位于两个都内有使用的节点之间那么前后都合并,防止内存碎片*/
if (!OS_MEM_NODE_GET_USED_FLAG(pstNextNode->uwSizeAndFlag))
{
LOS_ListDelete(&(pstNextNode->stFreeNodeInfo));
osMemMergeNode(pstNextNode);
}
LOS_ListDelete(&(pstPreNode->stFreeNodeInfo));
pstListHead = OS_MEM_HEAD(pPool, pstPreNode->uwSizeAndFlag);
if (NULL == pstListHead)
{
printf("%s %d\n", __FUNCTION__, __LINE__);
return;
}
LOS_ListAdd(pstListHead,&(pstPreNode->stFreeNodeInfo));
}
else
{
pstNextNode = OS_MEM_NEXT_NODE(pstNode);
//如果下一个节点没有使用,和下一个节点合并
if (!OS_MEM_NODE_GET_USED_FLAG(pstNextNode->uwSizeAndFlag))
{
LOS_ListDelete(&(pstNextNode->stFreeNodeInfo));
osMemMergeNode(pstNextNode);
}
//按照新节点的大小在list数组中找到合适的位置
pstListHead = OS_MEM_HEAD(pPool, pstNode->uwSizeAndFlag);
if (NULL == pstListHead)
{
printf("%s %d\n", __FUNCTION__, __LINE__);
return;
}
将释放的内存和合并之后的生成的新节点插入合适的list
LOS_ListAdd(pstListHead,&(pstNode->stFreeNodeInfo));
}
}
主要就是内存回收以及合并内存节点防止内存过于颗粒化出现大量碎片。
5、liteos 内存分配策略的优缺点
liteos 内存分配策略优点是简单,使用链表数组按照2的幂次方级数关系管理内存,缺点也很明显,容易出现内存碎片。
举例如下:
如果使用者每次都是分配–>释放—>分配—>释放,那么这样的方式是不会产生内存碎片的,但是如果按照分配—>分配—>释放—>释放,这样不是交替的方式的话那么极容易产生内存碎片,即使没有使用的内存足够,但是如果没有使用的内存过于分散,那么用户分配还是会失败。
liteos内存分配代码,在平时工作中帮助解决bug
liteos的内存分配相对比较简单,所以我们自己的代码中也经常使用malloc等函数分配内存,有时候会忘记释放,或者其他的问题,引起系统出问题,可以使用liteos的LOS_MemAlloc替换掉我们的malloc这样很容易方便的定位问题。
refer:
https://blog.csdn.net/tiantangniaochao/article/details/78446989