目录
一、引言
接下来说一下实时操作系统的动态内存管理。对于实时操作系统来说,动态内存分配的执行时间必须是可确定的。ucosii对malloc()函数和free()函数这两个动态内存分配和释放内存函数进行了改进,使得它们可以对大小固定的内存块进行操作,使得其执行时间可以确定,满足实时要求。
二、内存控制块
先来说一下内存分区和内存块的概念;操作系统以分区为单位来管理动态内存,而任务以内存块为单位来管理内存。而内存分区和内存块的使用情况是由内存控制块来进行记录的,内存分区和内存块以及内存控制块的关系图如下所示:
从上图我们可以看到,内存分区是包含内存块的,多个大小相等的内存块组成内存分区。因此OS定义了一个U16的二维数组Intmenbuff[10][10]来存放内存分区,这个数组表示有10个内存块,每一个内存块的长度为10,10个内存块构成分区。同TCB和ECB一样,为了方便管理内存分区,OS也定义了一个内存控制块OS_MEM,接下来我们看看这个结构体里边的内容:
从上图可以看出OSmemFreeList用于充当链表指针,这也代表着内存控制块也和TCB类似,构成了一个内存控制块链表。每当系统需要创建一个内存分区时,就会从内存控制块链表摘取一个内存控制块(这里很重要也就是说先从内存控制块链表中取出一个内存控制块来充当内存分区,如果这里不理解,对接下来的分区创建函数源码里边定义了一个二维指针会很茫然),当用完这个内存控制块释放时,又将这个内存控制块归还内存控制块链表,用于新的内存分配。同样的,这一步链接成链表的操作也是在系统调用OSInit()函数时候完成的,也是定义了一个数组OSMemTb1[]数组来存放链表的各个节点,因为在OSInit()函数内部会调用OSMemInit()函数,该函数用于将OSMemTb1[]数组的各个元素链接成链表,并且各个节点进行初始化;接下来看看这个函数的源码:
上述源码都是链表的简单操作,学过链表的都知道,链表中的节点存放着本身的元素和下一个节点的地址,因此,这里也是将每个数组的元素的节点链接起来。首先利用OS_MemClr函数将OSMemTb1[]数组里的内容全部清空,也相当于是都初始化成0;然后根据内存分区中内存块数目的多少去判断有多少节点,并根据节点数进行链表的连接,也就是将第二个节点的地址赋值给第一个元素的链表指针。可以看到这里是用了一个for循环,循环各个节点(也就是OSMemTb1数组的各个元素),用一个指针变量去获取头指针,然后依次将下一元素的地址赋给前一个节点。接下来pmem=&OSMemTb1[i]代表的是链表中的最后一个元素的地址,接下来将链表的最后一个节点的链表指针赋值NULL代表链表结束。最后将链表的首元素赋给头指针,用作链表的起始。
三、内存管理的实现源码
1.动态内存分区创建函数
老规矩,我们来看一下内存创建函数OSMemCreate()的源码,先看一下这个函数里边的各个参数;
其关键源码如下:
上述代码是去判断各个参数是否满足要求,首先去判断内存分区的起始地址是否为空,如果为空,返回错误信息,创建内存分区失败。再去判断addr指针变量是否按照指针大小对齐(对齐的目的是为了更快的访问内存数据),也即是if(addr&(sizeof(void *)-1))这行代码。接下来对其做详细分析,首先什么是对齐?对齐代表的是数据在内存中的起始地址必须是某个特定值的倍数。接下来看到sizeof(void *),sizeof是一个操作符,返回一个数据类型或者变量类型的大小,以字节为单位,这里参数是void*,代表返回的是指针类型的大小,而在32位系统中,指针占4个字节,而在8位系统中,指针占8个字节。而我们一般用的是32单片机,因此这里实际上就是去判断这个addr指针变量是否是4字节对齐的?也就是说只要addr这个内存分区起始地址是4的倍数,则对齐。而只要地址是关于4字节对齐的,它的最后两位地址是00,因此只需要将addr与sizeof(void *)-1=3(0x03)按位与,就可以知道addr是否对齐。紧接着判断内存块的数目是否大于两块,必须要求内存块的数目两块以上,否则创建失败。最后再去判断每一个内存块的长度是否在4字节以上。
在做完一系列的条件判断后,接下来我们看上述源码,主要做了三件事情,第一是从内存控制块链表中摘取一个内存控制块来作为内存分区;第二是将这个内存分区中的各个内存块链接成一个链表;第三是填充这个内存分区的基本信息。我们来看一下每一件事情具体是怎么做的。首先是定义一个指针变量pmem用来存放空内存控制块的链表指针OSMemFreeList,表示摘取一个空内存控制块当作内存分区,然后去判断这个内存控制块链表指针是否为0,也就是说链表是否结束,还有没有下一个内存控制块,如果不为空,那么就更新空内存控制块链表指针,使其指向下一个空内存控制块。最后再去检测是否成功获得内存分区。
接下来我们来看第二件事情是怎么做的;首先定义了一个二维指针变量plink,这是为什么要这样定义呢?刚刚在上边我已经提到了,OSMemFreeList是一个内存控制块链表指针,指向的是一个空内存控制块,然后将这个空的内存控制块作为一个内存分区。那么既然是内存分区,而内存分区由多个内存块组成,那么我们就需要将这些内存块构建成链表链接起来,那需要链接,自然也就需要链表指针;而内存分区就是一个指针,我们还要在内存分区里边定义一个链表指针,因此就变成了指向指针的指针了。明白了这一点,接下来就很好解释了,和前文提到的链表基本链接操作一样,首先是将plink初始化为*addr代表的是头指针,然后把pblk定义为addr表示的是当前内存块的地址。然后我们进入for循环,以内存块数目进行遍历,pblk+=blksize;这行代码表示的是将pblk指向为下一个内存块的地址再赋给pblk,因为每一个内存块都是由长度的,blksize代表的是每一个内存块的长度。*plink=(void *)pblk;这行代码代表的是将当前内存块的指针指向下一个内存块;而plink=(void **)pblk;代表的是把plink更新为下一个内存块的地址;此处举一个例子用以说明:
假设我们有一个内存区域,起始地址为 0x1000,每个内存块大小为 4 字节,包含一个指针大小。我们有三个内存块。地址如下:
- `0x1000`: 第一块
- `0x1004`: 第二块
- `0x1008`: 第三块
**链表创建过程**:
1. 初始化:
- `plink = 0x1000`
- `pblk = 0x1000`
- `loops = 2`(三个块减去一)
2. 第一次循环:
- `pblk += 4` -> `pblk = 0x1004`
- `*plink = 0x1004` -> 在 `0x1000` 位置写入 `0x1004`,表示第一个块指向第二个块,相当于是在当前节点写入下一个节点的地址,
- `plink = 0x1004`//将链表指针更新为下一节点的地址,用于下一次循环
3. 第二次循环:
- `pblk += 4` -> `pblk = 0x1008`
- `*plink = 0x1008` -> 在 `0x1004` 位置写入 `0x1008`,表示第二个块指向第三个块
- `plink = 0x1008`
4. 结束:
- `*plink = 0` -> 在 `0x1008` 位置写入 `0`,表示第三个块指向 `NULL`。
通过这种方式,链表就创建完成了,`0x1000` -> `0x1004` -> `0x1008` -> `NULL`。
最后,第三件事情登记信息完成以后,返回pmem这个内存分区指针。
2.获得一个内存块函数
可以调用OSMemGet(OSMem *pmem,u8 *err)函数来向某内存分区获得一个内存控制块,函数返回所请求成功的内存控制块指针。接下来看这个函数的关键源码:
从上述源码我们可以看到,首先会检查这个内存分区里边是否有空余的内存块数,OSMemNFree文章在一开始就提到代表的是分区内剩余的可分配内存块数。如果有剩余,那么这个内存分区内的内存控制块链表的一个内存控制块地址赋给指针变量pblk,然后更新当前的链表指针指向下一个内存控制块。接下来将剩余的可分配内存块数目减一,代表已经分配出去一个内存控制块了,最后返回这个内存控制块的地址。
3.释放内存块函数
可以调用OSMemPut(OSMem *pmem,void *pblk)函数来释放一个内存块,函数中的第一个参数代表的是需要释放的内存块的内存分区指针,第二个参数代表的是需要释放的内存块指针。其源码如下:
从上述源码可以看到,先去判断当前内存池里边可分配的剩余内存块数目是否大于等于内存池里边本身的内存块数目,如果是的话,代表这个内存池满了,不需要再释放内存块,返回一个错误信息。如果内存池未满的话,将要释放的内存块指针pblk转换为void **类型,再将其指向内存控制块链表的头指针,相当于是在链表的头部插入一个指针作为新头指针。接下来将这个插入的头指针作为新的链表头指针;最后将分区的可分配内存块数自增代表已释放内存控制块,最后返回一个成功的指示信息。
标签:控制,ucosii,pblk,分区,链表,内存,------,指针 From: https://blog.csdn.net/weixin_52247452/article/details/139388799