13. 内存管理

13.1. 内存管理的基本概念

在计算系统中,变量、中间数据一般存放在系统存储空间中,只有在实际使用时才将它们从存储空间调入到中央处理器内部进行运算。通常存储空间可以分为两种:内部存储空间和外部存储空间。内部存储空间访问速度比较快,能够按照变量地址随机地访问,也就是我们通常所说的RAM(随机存储器),或电脑的内存;而外部存储空间内 所保存的内容相对来说比较固定,即使掉电后数据也不会丢失,可以把它理解为电脑的硬盘。在这一章中我们主要讨论内部存储空间(RAM)的管理——内存管理。

在嵌入式系统设计中,内存分配应该是根据所设计系统的特点来决定选择使用动态内存分配还是静态内存分配算法,一些可靠性要求非常高的系统应选择使用静态的,而普通的业务系统可以使用动态来提高内存使用效率。静态可以保证设备的可靠性但是需要考虑内存上限,内存使用效率低,而动态则是相反。

uCOS的内存管理是采用内存池的方式进行管理,也就是创建一个内存池,静态划分一大块连续空间作为内存管理的空间,里面划分为很多个内存块,我们在使用的时候就从这个内存池中获取一个内存块,使用完毕的时候用户可以将其放回内存池中,这样子就不会导致内存碎片的产生。

uCOS内存管理模块管理用于系统中内存资源,它是操作系统的核心模块之一,主要包括内存池的创建、分配以及释放。

很多人会有疑问,什么不直接使用C标准库中的内存管理函数呢?在电脑中我们可以用 malloc()和 free()这两个函数动态的分配内存和释放内存。但是,在嵌入式实时操作系统中,调用 malloc()和 free()却是危险的,原因有以下几点:

  • 这些函数在小型嵌入式系统中并不总是可用的,小型嵌入式设备中的RAM不足。

  • 它们的实现可能非常的大,占据了相当大的一块代码空间。

  • 他们几乎都不是安全的。

  • 它们并不是确定的,每次调用这些函数执行的时间可能都不一样。

  • 它们有可能产生碎片。

  • 这两个函数会使得链接器配置得复杂。

  • 如果允许堆空间的生长方向覆盖其他变量占据的内存,它们会成为debug的灾难。

在一般的实时嵌入式系统中,由于实时性的要求,很少使用虚拟内存机制。所有的内存都需要用户参与分配,直接操作物理内存,所分配的内存不能超过系统的物理内存,所有的系统堆栈的管理,都由用户自己管理。

同时,在嵌入式实时操作系统中,对内存的分配时间要求更为苛刻,分配内存的时间必须是确定的。一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面,而寻找这样一个空闲内存块所耗费的时间是不确定的,因此对于实时系统来说,这就是不可接受的,实时系统必须 要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定。

在嵌入式系统中,内存是十分有限而且是十分珍贵的,用一块内存就少了一块内存,而在分配中随着内存不断被分配和释放,整个系统内存区域会产生越来越多的碎片,因为在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块,它们地址不连续,不能够作为一整块的大内存分配出去,所以一定会在某个时 间,系统已经无法分配到合适的内存了,导致系统瘫痪。其实系统中实际是还有内存的,但是因为小块的内存的地址不连续,导致无法分配成功,所以我们需要一个优良的内存分配算法来避免这种情况的出现。所以uCOS提供的内存分配算法是只允许用户分配固定大小的内存块,当使用完成就将其放回内存池中,这样子分配效率极高,时 间复杂度是O(1),也就是一个固定的时间常数,并不会因为系统内存的多少而增加遍历内存块列表的时间,并且还不会导致内存碎片的出现,但是这样的内存分配机制会导致内存利用率的下降以及申请内存大小的限制。

13.2. 内存管理的运作机制

内存池(Memory Pool)是一种用于分配大量大小相同的内存对象的技术,它可以极大加快内存分配/释放的速度。

在系统编译的时候,编译器就静态划分了一个大数组作为系统的内存池,然后在初始化的时候将其分成大小相等的多个内存块,内存块直接通过链表连接起来(此链表也称为空闲内存块列表)。每次分配的时候,从空闲内存块列表中取出表头上第一个内存块,提供给申请者。物理内存中允许存在多个大小不同的内存池,每一个内存池又由多 个大小相同的空闲内存块组成。我们必须先创建内存池才能去使用内存池里面的内存块,在创建的时候,我们必须定义一个内存池控制块,然后进行相关初始化,内存控制块的参数包括内存池名称,内存池起始地址,内存块大小,内存块数量等信息,在以后需要从内存池取出内存块或者释放内存块的时候,我们只需根据内存控制块的信息就 能很轻易做到,内存控制块的数据结构具体见 代码清单27-1。内存池一旦创建完成,其内部的内存块大小将不能再做调整,具体见 图27-1

代码清单‑1内存控制块数据结构
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    struct os_mem
    {
            OS_OBJ_TYPE          Type;                      (1)
    void                *AddrPtr;           (2)
            CPU_CHAR            *NamePtr;           (3)
    void                *FreeListPtr;               (4)
            OS_MEM_SIZE          BlkSize;           (5)
            OS_MEM_QTY           NbrMax;            (6)
            OS_MEM_QTY           NbrFree;           (7)
    #if OS_CFG_DBG_EN > 0u
            OS_MEM              *DbgPrevPtr;
            OS_MEM              *DbgNextPtr;
    #endif
    };

代码清单27-1 (1):内核对象类型。

代码清单27-1 (2):内存池的起始地址。

代码清单27-1 (3):内存池名称。

代码清单27-1 (4):空闲内存块列表。

代码清单27-1 (5):内存块大小。

代码清单27-1 (6):内存池中内存块的总数量。

代码清单27-1 (7):空闲内存块数量。

图片没有找到

注意:内存池中的内存块是通过单链表连接起来的,类似于消息池,内存池在创建的时候内存块地址是连续的,但是经过多次申请以及释放后,空闲内存块列表的内存块在地址上不一定是连续的。

13.3. 内存管理的应用场景

首先,在使用内存分配前,必须明白自己在做什么,这样做与其他的方法有什么不同,特别是会产生哪些负面影响,在自己的产品面前,应当选择哪种分配策略。

内存管理的主要工作是动态划分并管理用户分配好的内存区间,主要是在用户需要使用大小不等的内存块的场景中使用,当用户需要分配内存时,可以通过操作系统的内存申请函数索取指定大小内存块,一旦使用完毕,通过动态内存释放函数归还所占用内存,使之可以重复使用(heap_1.c的内存管理除外)。

例如我们需要定义一个float型数组:floatArr[];

但是,在使用数组的时候,总有一个问题困扰着我们:数组应该有多大?在很多的情况下,你并不能确定要使用多大的数组,可能为了避免发生错误你就需要把数组定义得足够大。即使你知道想利用的空间大小,但是如果因为某种特殊原因空间利用的大小有增加或者减少,你又必须重新去修改程序,扩大数组的存储范围。这种分配固定大小 的内存分配方法称之为静态内存分配。这种内存分配的方法存在比较严重的缺陷,在大多数情况下会浪费大量的内存空间,在少数情况下,当你定义的数组不够大时,可能引起下标越界错误,甚至导致严重后果。

uCOS将系统静态分配的大数组作为内存池,然后进行内存池的初始化,然后分配固定大小的内存块。

注意:uCOS也不能很好解决这种问题,因为内存块的大小是固定的,无法解决这种弹性很大的内存需求,只能按照最大的内存块进行分配。但是uCOS的内存分配能解决内存利用率的问题,在不需要使用内存的时候,将内存释放到内存池中,让其他任务能正常使用该内存块。

13.4. 内存管理函数接口讲解

13.4.1. 内存池创建函数

在使用内存池的时候首先要创建一个内存池,需要用户静态分配一个数组空间作为系统的内存池,且用户还需定义一个内存控制块。创建内存池后, 任务才可以通过系统的内存申请、释放函数从内存池中申请或释放内存,uCOS提供内存池创建函数OSMemCreate(),内存池创建函数源码具体见 代码清单27-2

代码清单‑2静态内存创建函数rt_mp_create()源码
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
    void  OSMemCreate (OS_MEM       *p_mem,         (1)     //内存池控制块
                                    CPU_CHAR     *p_name,           (2)     //命名内存池
    void         *p_addr,           (3)     //内存池首地址
                                    OS_MEM_QTY    n_blks,           (4)     //内存块数目
                                    OS_MEM_SIZE   blk_size,         (5)     //内存块大小(单位:字节
                                    OS_ERR       *p_err)            (6)     //返回错误类型
    {
    #if OS_CFG_ARG_CHK_EN > 0u
            CPU_DATA       align_msk;
    #endif
            OS_MEM_QTY     i;
            OS_MEM_QTY     loops;
            CPU_INT08U    *p_blk;
    void         **p_link;               //二级指针,存放指针的指针
            CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必需该宏,该宏声明和
    //定义一个局部变量,用于保存关中断前的 CPU 状态寄存器
    // SR(临界段关中断只需保存SR),开中断时将该值还原。

    #ifdef OS_SAFETY_CRITICAL//如果使能了安全检测
    if (p_err == (OS_ERR *)0)            //如果错误类型实参为空
            {
                    OS_SAFETY_CRITICAL_EXCEPTION();  //执行安全检测异常函数
    return;                          //返回,停止执行
            }
    #endif

    #ifdef OS_SAFETY_CRITICAL_IEC61508//如果使能了安全关键
    if (OSSafetyCriticalStartFlag == DEF_TRUE)
            {
                    *p_err = OS_ERR_ILLEGAL_CREATE_RUN_TIME;//错误类型为“非法创建内核对
    return;                                  //返回,停止执行
            }
    #endif

    #if OS_CFG_CALLED_FROM_ISR_CHK_EN > 0u//如果使能了中断中非法调用检测
    if (OSIntNestingCtr > (OS_NESTING_CTR)0)   //如果该函数是在中断中被调用
            {
                    *p_err = OS_ERR_MEM_CREATE_ISR;         //错误类型为“在中断中创建对
    return;                                //返回,停止执行
            }
    #endif

    #if OS_CFG_ARG_CHK_EN > 0u//如果使能了参数检测
    if (p_addr == (void *)0)                (7)//如果 p_addr 为空
            {
                    *p_err   = OS_ERR_MEM_INVALID_P_ADDR;    //错误类型为“内存池地址非法
    return;                                        //返回,停止执行
            }
    if (n_blks < (OS_MEM_QTY)2)             (8)//如果内存池的内存块数目少于2
            {
                    *p_err = OS_ERR_MEM_INVALID_BLKS;         //错误类型为“内存块数目非
    return;                                        //返回,停止执行
            }
    if (blk_size <sizeof(void *))          (9)//如果内存块空间小于指针的
            {
                    *p_err = OS_ERR_MEM_INVALID_SIZE;          //错误类型为“内存空间非
    return;                                        //返回,停止执行
            }
            align_msk = sizeof(void *) - 1u;        (10)//开始检查内存地址是否对齐
    if (align_msk > 0u)
            {
    if (((CPU_ADDR)p_addr & align_msk) != 0u)  //如果首地址没对齐
                    {
                            *p_err = OS_ERR_MEM_INVALID_P_ADDR;   //错误类型为“内存池地址非
    return;                                    //返回,停止执行
                    }
    if ((blk_size & align_msk) != 0u)   (11)//如果内存块地址没对齐
                    {
                            *p_err = OS_ERR_MEM_INVALID_SIZE;     //错误类型为“内存块大小非
    return;                                    //返回,停止执行
                    }
            }
    #endif
    /* 将空闲内存块串联成一个单向链表 */
            p_link = (void **)p_addr;              (12)//内存池首地址转为二级指针
            p_blk  = (CPU_INT08U *)p_addr;         (13)//首个内存块地址
            loops  = n_blks - 1u;
    for (i = 0u; i < loops; i++)           (14)//将内存块逐个串成单向链表
            {
                    p_blk +=  blk_size;                            //下一内存块地址
                    *p_link = (void  *)p_blk;
    //在当前内存块保存下一个内存块地址
                    p_link = (void **)(void *)p_blk;
    //下一个内存块的地址转为二级指针
            }
            *p_link             = (void *)0;       (15)//最后一个内存块指向空

            OS_CRITICAL_ENTER();                             //进入临界段
            p_mem->Type        = OS_OBJ_TYPE_MEM;  (16)//设置对象的类型
            p_mem->NamePtr     = p_name;           (17)//保存内存池的命名
            p_mem->AddrPtr     = p_addr;           (18)//存储内存池的首地址
            p_mem->FreeListPtr = p_addr;           (19)//初始化空闲内存块池的首地址
            p_mem->NbrFree     = n_blks;          (20)//存储空闲内存块的数目
            p_mem->NbrMax      = n_blks;           (21)//存储内存块的总数目
            p_mem->BlkSize     = blk_size;         (22)//存储内存块的空间大小

    #if OS_CFG_DBG_EN > 0u//如果使能了调试代码和变量
            OS_MemDbgListAdd(p_mem);      //将内存管理对象插入内存管理双向调试列表
    #endif

            OSMemQty++;             (23)//内存管理对象数目加1

            OS_CRITICAL_EXIT_NO_SCHED();  //退出临界段(无调度)
            *p_err = OS_ERR_NONE;          //错误类型为“无错误”
    }

代码清单27-2 (1):内存池控制块指针。

代码清单27-2 (2):内存池名字。

代码清单27-2 (3):内存池首地址。

代码清单27-2 (4):内存块数目。

代码清单27-2 (5):内存块大小(单位:字节)。

代码清单27-2 (6):返回的错误类型。

代码清单27-2 (7):如果使能了参数检测,在编译的时候回包含参数检测相关代码,如果 p_addr 为空,返回错误类型为“内存池地址非法”的错误代码。

代码清单27-2 (8):如果内存池的内存块数目少于2,返回错误类型为“内存块数目非法”错误代码。

代码清单27-2 (9):如果内存块空间小于一个指针的大小(在i.MX RT上是4字节),返回错误类型为“内存空间非法”的错误代码。sizeof(void *)是求出 CPU 指针的字节大小,i.MX RT是 32 位单片机,求出的指针所占字节大小是 4,减去 1 后就是 3,3 的二进制数是 11(B)。如果一个地址或者内存块字节大小是4 字节对齐的,那么用二进制表示地址或内存块大小最低两位都是 0,比如 11100(B)、101010100(B)这些 4 字节对齐的都最低 2 位都是 0,那么 11(B)与上一个低两位字节都是0 的数结果肯定为 0,不为 0 说明不是 4 字节对齐。同理可以检测内存块的大小是否是 4的倍数。

代码清单27-2 (10):开始检查内存地址是否对齐,如果内存池首地址没对齐,返回错误类型为“内存池地址非法”的错误代码。

代码清单27-2 (11):如果内存块地址没对齐,返回错误类型为“内存块大小非法”的错误代码。

代码清单27-2 (12):程序执行到这里,就表示传递进来的参数都是正确的,下面开始初始化内存池以及内存控制块的信息,将内存池首地址转为二级指针保存在p_link变量中。

代码清单27-2 (13):获取内存池中首个内存块地址。

代码清单27-2 (14):将空闲内存块逐个连接成一个单向链表,根据内存块起始地址与内存块大小获取下一个内存块的地址,然后在当前内存块中保存下一个内存块的地址,再将下一个内存块的地址转为二级指针,将这些内存块连接成一个单链表,也就是空闲内存块链表。

一个内存块的操作是先计算是下一个内存块的地址,因为此时数组元素的地址是连续的,所以开始的时候只要在前一个内存块的首地址加上内存块字节大小即可得到下一个内存块的首地址,然后把下一个内存块的首地址放在前一个内存块中,就将他们串起来了,如此循环反复即可串成空闲内存块列表。

代码清单27-2 (15):然后将最后一个内存块存储的地址为空,表示到达空闲内存块列表尾部,连接完成的示意图具体见 图27-2

图片没有找到

代码清单27-2 (16):设置对象的类型。

代码清单27-2 (17):保存内存池的名称。

代码清单27-2 (18):保存内存池的首地址。

代码清单27-2 (19):初始化空闲内存块列表的首地址,指向下一个可用的内存块。

代码清单27-2 (20):保存空闲内存块的数目。

代码清单27-2 (21):保存内存块的总数目。

代码清单27-2 (22):保存内存块的空间大小。

代码清单27-2 (23):创建完成,内存管理对象数目加1。

整个内存池创建完成示意图具体见 图27-3

图片没有找到

内存池创建函数的使用实例具体见 代码清单27-3

代码清单‑3 OSMemCreate()使用实例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    OS_MEM  mem;                    //声明内存管理对象
    uint8_t ucArray [ 3 ] [ 20 ];   //声明内存池大小

    OS_ERR      err;
    /* 创建内存管理对象 mem */
    OSMemCreate ((OS_MEM      *)&mem,             //指向内存管理对象
                            (CPU_CHAR    *)"Mem For Test",   //命名内存管理对象
                            (void        *)ucArray,          //内存池的首地址
                            (OS_MEM_QTY   )3,                //内存池中内存块数目
                            (OS_MEM_SIZE  )20,               //内存块的字节数目
                            (OS_ERR      *)&err);            //返回错误类型

13.4.2. 内存申请函数OSMemGet()

这个函数用于申请固定大小的内存块,从指定的内存池中分配一个内存块给用户使用,该内存块的大小在内存池初始化的时候就已经决定的。如果内存池中有可用的内存块,则从内存池的空闲内存块列表上取下一个内存块并且返回对应的内存地址;如果内存池中已经没有可用内存块,则返回0与对应的错误代码OS_ERR_MEM_NO _FREE_BLKS,其源码具体见代码清单27‑4。

代码清单‑4OSMemGet()源码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    void  *OSMemGet (OS_MEM  *p_mem,        (1)     //内存管理对象
                                    OS_ERR  *p_err)         (2)     //返回错误类型
    {
    void    *p_blk;
            CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必需该宏,该宏声明和
    //定义一个局部变量,用于保存关中断前的 CPU 状态寄存器
    // SR(临界段关中断只需保存SR),开中断时将该值还原。

    #ifdef OS_SAFETY_CRITICAL//如果使能了安全检测
    if (p_err == (OS_ERR *)0)            //如果错误类型实参为空
            {
                    OS_SAFETY_CRITICAL_EXCEPTION();  //执行安全检测异常函数
    return ((void *)0);              //返回0(有错误),停止执行
            }
    #endif

    #if OS_CFG_ARG_CHK_EN > 0u//如果使能了参数检测
    if (p_mem == (OS_MEM *)0)              //如果 p_mem 为空
            {
                    *p_err  = OS_ERR_MEM_INVALID_P_MEM; //错误类型为“内存池非法”
    return ((void *)0);                //返回0(有错误),停止执行
            }
    #endif

            CPU_CRITICAL_ENTER();                    //关中断
    if (p_mem->NbrFree == (OS_MEM_QTY)0) (3)//如果没有空闲的内存块
            {
                    CPU_CRITICAL_EXIT();                 //开中断
                    *p_err = OS_ERR_MEM_NO_FREE_BLKS;     //错误类型为“没有空闲内存块”
    return ((void *)0);                  //返回0(有错误),停止执行
            }
            p_blk  = p_mem->FreeListPtr;    (4)     //如果还有空闲内存块,就获取它
            p_mem->FreeListPtr = *(void **)p_blk;(5)//调整空闲内存块指针
            p_mem->NbrFree--;                   (6)//空闲内存块数目减1
            CPU_CRITICAL_EXIT();                     //开中断
            *p_err = OS_ERR_NONE;                     //错误类型为“无错误”
    return (p_blk);                      (7)//返回获取到的内存块
    }

代码清单27-4 (1):指定内存池对象。

代码清单27-4 (2):保存返回的错误类型。

代码清单27-4 (3):判断一下内存池控制块中NbrFree的值,如果没有空闲的内存块,就没法申请内存,保存错误类型为“没有空闲内存块”的错误代码,返回0表示没申请到内存块。

代码清单27-4 (4):如果内存池中还有空闲内存块,就获取它,获取的过程就是从空闲内存块中取出一个内存块,并且返回该内存块的地址。

代码清单27-4 (5):调整内存池控制块的空闲内存块指针,指向下一个可用的内存块。

代码清单27-4 (6):内存池中空闲内存块数目减1。

代码清单27-4 (7):返回获取到的内存块地址。

假设我们在内存池创建完成后就调用OSMemGet()函数申请一个内存块,那么申请完毕后的内存块示意图具体见 图27-4,被申请出去的内存块会脱离空闲内存块列表,并且内存控制块中的NbrFree变量会减一。

图片没有找到

OSMemGet()函数的使用实例具体见 代码清单27-5

代码清单‑5OSMemGet()使用实例
1
2
3
4
5
    OS_MEM  mem;                    //声明内存管理对象
    OS_ERR      err;
    /* 向 mem 获取内存块 */
    p_mem_blk = OSMemGet ((OS_MEM      *)&mem,              //指向内存管理对象
                                            (OS_ERR      *)&err);             //返回错误类型

13.4.3. 内存释放函数

嵌入式系统的内存对我们来说是十分珍贵的,任何内存块使用完后都必须被释放,否则会造成内存泄露,导致系统发生致命错误。uCOS提供了OSMemPut()函数进行内存的释放管理, 使用该函数接口时,根据指定的内存控制块对象,将内存块插入内存池的空闲内存块列表中,然后增加该内存池的可用内存块数目,其源码具体见 代码清单27-6

代码清单‑6 OSMemPut()源码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    void  OSMemPut (OS_MEM  *p_mem,         (1)     //内存管理对象
    void    *p_blk,         (2)     //要退回的内存块
                                    OS_ERR  *p_err)         (3)     //返回错误类型
    {
            CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必需该宏,该宏声明和
    //定义一个局部变量,用于保存关中断前的 CPU 状态寄存器
    // SR(临界段关中断只需保存SR),开中断时将该值还原。

    #ifdef OS_SAFETY_CRITICAL//如果使能了安全检测
    if (p_err == (OS_ERR *)0)            //如果错误类型实参为空
            {
                    OS_SAFETY_CRITICAL_EXCEPTION();  //执行安全检测异常函数
    return;                          //返回,停止执行
            }
    #endif

    #if OS_CFG_ARG_CHK_EN > 0u//如果使能了参数检测
    if (p_mem == (OS_MEM *)0)               //如果 p_mem 为空
            {
                    *p_err  = OS_ERR_MEM_INVALID_P_MEM;  //错误类型为“内存池非法”
    return;                             //返回,停止执行
            }
    if (p_blk == (void *)0)                 //如果内存块为空
            {
                    *p_err  = OS_ERR_MEM_INVALID_P_BLK;  //错误类型为"内存块非法"
    return;                             //返回,停止执行
            }
    #endif

            CPU_CRITICAL_ENTER();                   //关中断
    if (p_mem->NbrFree >= p_mem->NbrMax)  (4)//如果内存池已满
            {
                    CPU_CRITICAL_EXIT();                 //开中断
                    *p_err = OS_ERR_MEM_FULL;             //错误类型为“内存池已满”
    return;                              //返回,停止执行
            }
            *(void **)p_blk = p_mem->FreeListPtr; (5)//把内存块插入空闲内存块链表
            p_mem->FreeListPtr = p_blk;           (6)//内存块退回到链表的最前端
            p_mem->NbrFree++;                     (7)//空闲内存块数目加1
            CPU_CRITICAL_EXIT();                  //开中断
            *p_err              = OS_ERR_NONE;        //错误类型为“无错误”
    }

代码清单27-6 (1):内存控制块指针,指向要操作的内存池。

代码清单27-6 (2):要释放的内存块。

代码清单27-6 (3):保存返回的错误类型。

代码清单27-6 (4):如果内存池已经满了,那是无法进行释放的,返回错误类型为“内存池已满”的错误代码。

代码清单27-6 (5):如果内存池没满,那么释放内存块到内存池中,把内存块插入空闲内存块列表。

代码清单27-6 (6):内存块退回到链表的最前端。

代码清单27-6 (7):空闲内存块数目加1。

我们在释放一个内存块的时候,我们会将内存插入内存池中空闲内存块列表的首部,然后增加内存池中空闲内存块的数量,该函数的使用实例具体见 代码清单27-7

代码清单‑7 OSMemPut()使用实例
1
2
3
4
5
6
7
8
    OS_MEM  mem;                    //声明内存管理对象

    OS_ERR      err;

    /* 释放内存块 */
    OSMemPut ((OS_MEM  *)&mem,                        //指向内存管理对象
                    (void    *)pMsg,                        //内存块的首地址
                    (OS_ERR  *)&err);                       //返回错误类型

至此uCOS常用的内存管理函数就讲解完,需要注意的是:我们想要使用内存管理相关的函数时,需要将os_cfg.h中的OS_CFG_MEM_EN宏定义配置为1;OSMemCreate()只能在任务级被调用,但是OSMemGet()和OSMemPut()可以在中断中被调用。

13.5. 内存管理实验

本次的实验例程采用消息队列进行发送与接收消息,只不过存放消息的地方是在内存块中,在获取完消息的时候,就进行释放内存块,反复使用内存块,具体见 代码清单27-8 高亮部分。

代码清单‑8内存管理实验
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
    *
    ***********************************************************************
    * @file    main.c
    * @author  fire
    * @version V1.0
    * @date    2019-xx-xx
    * @brief   内存管理
    ***********************************************************************
    * @attention
    *
    * 实验平台:野火  i.MXRT1052开发板
    * 论坛    :http://www.firebbs.cn
    * 淘宝    :http://firestm32.taobao.com
    *
    ***********************************************************************
    */
    /*
    *************************************************************************
    *                                            包含的文件
    *************************************************************************
    */
    //ucosiii系统相关
    #include  <cpu.h>
    #include  <lib_mem.h>
    #include  <os.h>
    #include  <bsp_os.h>
    #include  <bsp_clk.h>
    #include  <bsp_int.h>
    #include"os_app_hooks.h"
    #include"app_cfg.h"
    //板级驱动
    #include"bsp.h"
    #include"fsl_debug_console.h"
    #include"board.h"
    #include"pin_mux.h"
    #include"clock_config.h"

    /*
    *************************************************************************
    *                                            LOCAL DEFINES
    *************************************************************************
    */

    OS_MEM  mem;                    //声明内存管理对象
    uint8_t ucArray [ 3 ] [ 20 ];   //声明内存分区大小

    /*
    *************************************************************************
    *                                         任务控制块TCB
    *************************************************************************
    */
    static  OS_TCB   AppTaskStartTCB;    //任务控制块

    static  OS_TCB   AppTaskPostTCB;
    static  OS_TCB   AppTaskPendTCB;


    /*
    *************************************************************************
    *                                            任务堆栈
    *************************************************************************
    */
    static  CPU_STK  AppTaskStartStk[APP_TASK_START_STK_SIZE];//任务堆栈

    static  CPU_STK  AppTaskPostStk [ APP_TASK_POST_STK_SIZE ];
    static  CPU_STK  AppTaskPendStk [ APP_TASK_PEND_STK_SIZE ];
    /*
    *************************************************************************
    *                                            函数原型
    *************************************************************************
    */

    static  void  AppTaskStart  (void *p_arg);               //任务函数声明

    static  void  AppTaskPost   ( void * p_arg );
    static  void  AppTaskPend   ( void * p_arg );

    /*
    *************************************************************************
    * 函数名 : main
    * 描述   : 标准的C函数入口
    * 形参   : 无
    * 返回值 : 无
    *************************************************************************
    */
    int  main (void)
    {
            OS_ERR  err;


            BSP_ClkInit();/* 初始化系统时钟       */
            BSP_IntInit();/* 初始化RAM中断向量表. */
            BSP_OS_TickInit();/* 初始化内核计时器     */

            Mem_Init();/* 初始化内存管理模块   */
            CPU_IntDis();/* 禁用所有中断         */
            CPU_Init();/* 初始化uC/CPU相关     */

            OSInit(&err);/*初始化uC / OS-III     */
    if (err != OS_ERR_NONE) {
    while (1);
            }

            App_OS_SetAllHooks();
    置所有应用程序钩子函数

    /* 创建起始任务 */
            OSTaskCreate((OS_TCB     *)&AppTaskStartTCB,//任务控制块地址
                                    (CPU_CHAR   *)"App Task Start",//任务名称
                                    (OS_TASK_PTR ) AppTaskStart,//任务函数
                                    (void       *) 0,//传递给任务函数(形参p_arg)的实参
                                    (OS_PRIO     ) APP_TASK_START_PRIO,//任务的优先级
                                    (CPU_STK    *)&AppTaskStartStk[0],//任务堆栈的基地址
                                    (CPU_STK_SIZE) APP_TASK_START_STK_SIZE / 10,//任务堆栈空
    下1/10时限制其增长
                                    (CPU_STK_SIZE) APP_TASK_START_STK_SIZE,//任务堆栈空间(单
    sizeof(CPU_STK))
                                    (OS_MSG_QTY  ) 5u,//任务可接收的最大消息数
                                    (OS_TICK     ) 0u,//任务的时间片节拍数(0表默认值
    fg_TickRate_Hz/10)
                                    (void       *) 0,//任务扩展(0表不扩展)
                                    (OS_OPT      )(OS_OPT_TASK_STK_CHK |
    OPT_TASK_STK_CLR), //任务选项
                                    (OS_ERR     *)&err);
    if (err != OS_ERR_NONE) {
    while (1);
            }

            OSStart(&err);//启动多任务管理(交由uC/OS-III控制)

    while (DEF_ON) {
                    ;
            }
    }


    /*

    *************************************************************************
    **********
    * 函数名:AppTaskStart
    * 描述   : 这是一个启动任务,在多任务系统启动后,必须初始化滴答计数器(在 BSP_Init
    现)
    * 形参   : p_arg   是OSTaskCreate()在创建该任务时传递过来的形参。
    * 返回值 : 无
    * 注意   : 1) 第一行代码 (void)p_arg; 是为了防止编译器报错,因为形参p_arg并没有


    *************************************************************************
    **********
    */

    static  void  AppTaskStart (void *p_arg)
    {
            OS_ERR  err;

            (void)p_arg;

            OS_TRACE_INIT();//初始化uC / OS-III跟踪记录器

            BSP_OS_TickEnable();//启用滴答计时器和中断
            BSP_Init();//板级初始化

    #if OS_CFG_STAT_TASK_EN > 0u
            OSStatTaskCPUUsageInit(&err);//无需任务运行即可计算CPU容量
    #endif

    #ifdef CPU_CFG_INT_DIS_MEAS_EN
            CPU_IntDisMeasMaxCurReset();
    #endif

    /* 创建内存管理对象 mem */
            OSMemCreate ((OS_MEM      *)&mem,//指向内存管理对象
                                    (CPU_CHAR    *)"Mem For Test",//命名内存管理对象
                                    (void        *)ucArray,//内存分区的首地址
                                    (OS_MEM_QTY   )3,//内存分区中内存块数目
                                    (OS_MEM_SIZE  )20,//内存块的字节数目
                                    (OS_ERR      *)&err);//返回错误类型


    /* 创建 AppTaskPost 任务 */
            OSTaskCreate((OS_TCB     *)&AppTaskPostTCB,//任务控制块地址
                                    (CPU_CHAR   *)"App Task Post",//任务名称
                                    (OS_TASK_PTR ) AppTaskPost,//任务函数
                                    (void       *) 0,//传递给任务函数(形参p_arg)的实参
                                    (OS_PRIO     ) APP_TASK_POST_PRIO,//任务的优先级
                                    (CPU_STK    *)&AppTaskPostStk[0],//任务堆栈的基地址
                                    (CPU_STK_SIZE) APP_TASK_POST_STK_SIZE / 10,//任务堆栈空间
    1/10时限制其增长
                                    (CPU_STK_SIZE) APP_TASK_POST_STK_SIZE,//任务堆栈空间(单位:
    eof(CPU_STK))
                                    (OS_MSG_QTY  ) 5u,//任务可接收的最大消息数
                                    (OS_TICK     ) 0u,//任务的时间片节拍数(0表默认值
    fg_TickRate_Hz/10)
                                    (void       *) 0,//任务扩展(0表不扩展)
                                    (OS_OPT      )(OS_OPT_TASK_STK_CHK |
    OPT_TASK_STK_CLR), //任务选项
                                    (OS_ERR     *)&err);//返回错误类型

    /* 创建 AppTaskPend 任务 */
            OSTaskCreate((OS_TCB     *)&AppTaskPendTCB,//任务控制块地址
                                    (CPU_CHAR   *)"App Task Pend",//任务名称
                                    (OS_TASK_PTR ) AppTaskPend,//任务函数
                                    (void       *) 0,//传递给任务函数(形参p_arg)的实参
                                    (OS_PRIO     ) APP_TASK_PEND_PRIO,//任务的优先级
                                    (CPU_STK    *)&AppTaskPendStk[0],//任务堆栈的基地址
                                    (CPU_STK_SIZE) APP_TASK_PEND_STK_SIZE / 10,//任务堆栈空间
    1/10时限制其增长
                                    (CPU_STK_SIZE) APP_TASK_PEND_STK_SIZE,//任务堆栈空间(单位:
    eof(CPU_STK))
                                    (OS_MSG_QTY  ) 50u,//任务可接收的最大消息数
                                    (OS_TICK     ) 0u,//任务的时间片节拍数(0表默认值
    fg_TickRate_Hz/10)
                                    (void       *) 0,//任务扩展(0表不扩展)
                                    (OS_OPT      )(OS_OPT_TASK_STK_CHK |
    OPT_TASK_STK_CLR), //任务选项
                                    (OS_ERR     *)&err);//返回错误类型

            OSTaskDel ( & AppTaskStartTCB, & err );//删除起始任务本身,该任务不再运行


    }


    /*
    ************************************************************************
    *                                          POST TASK
    ************************************************************************
    */
    static  void  AppTaskPost ( void * p_arg )
    {
            OS_ERR      err;

    char *   p_mem_blk;
    uint32_t ulCount = 0;

            (void)p_arg;


    while (DEF_TRUE) {//任务体
    /* 向 mem 获取内存块 */
                    p_mem_blk = OSMemGet ((OS_MEM      *)&mem,//指向内存管理对象
                                                            (OS_ERR      *)&err);//返回错误类型

                    sprintf ( p_mem_blk, "%d", ulCount ++ );//向内存块存取计数值

    /* 发布任务消息到任务 AppTaskPend */
                    OSTaskQPost ((OS_TCB      *)&AppTaskPendTCB,//目标任务的控制块
                                            (void        *)p_mem_blk,//消息内容的首地址
                                            (OS_MSG_SIZE  )strlen ( p_mem_blk ),//消息长度
                                            (OS_OPT       )OS_OPT_POST_FIFO,//发布到任务消息队列的
                                            (OS_ERR      *)&err);//返回错误类型

                    OSTimeDlyHMSM ( 0, 0, 1, 0, OS_OPT_TIME_DLY, & err );//每20ms发

            }

    }


    /*

    *************************************************************************
    *                                          PEND TASK

    *************************************************************************
    */
    static  void  AppTaskPend ( void * p_arg )
    {
            OS_ERR         err;
            OS_MSG_SIZE    msg_size;
            CPU_TS         ts;
            CPU_INT32U     cpu_clk_freq;

    char * pMsg;


            (void)p_arg;


            cpu_clk_freq = BSP_ClkFreqGet(kCLOCK_CpuClk);//获取CPU时钟,时间戳是以
    钟计数


    while (DEF_TRUE) {//任务体
    /* 阻塞任务,等待任务消息 */
                    pMsg = OSTaskQPend ((OS_TICK        )0,//无期限等待
                                                            (OS_OPT         )OS_OPT_PEND_BLOCKING, //没有
    就阻塞任务
                                                            (OS_MSG_SIZE   *)&msg_size,//返回消息长度
                                                            (CPU_TS        *)&ts,//返回消息被发布的时间戳
                                                            (OS_ERR        *)&err);//返回错误类型

                    ts = OS_TS_GET() - ts;//计算消息从发布到被接收的时间差

                    LED1_TOGGLE;//切换LED1的亮灭状态
    //进入临界段,避免串口打印被打断

                    PRINTF ( "\r\n接收到的消息的内容为:%s,长度是:%d字节。",
                                    pMsg, msg_size );

                    PRINTF ( "\r\n任务消息从被发布到被接收的时间差是%dus\r\n",
                                    ts / ( cpu_clk_freq / 1000000 ) );


    /* 退还内存块 */
                    OSMemPut ((OS_MEM  *)&mem,//指向内存管理对象
                                    (void    *)pMsg,//内存块的首地址
                                    (OS_ERR  *)&err);//返回错误类型

            }

    }

13.6. 内存管理实验现象

程序编译好,用USB线连接电脑和开发板的USB接口(对应丝印为USB转串口),用DAP仿真器把配套程序下载到野火i.MX RT系列开发板(具体型号根据你买的板子而定,每个型号的板子都配套有对应的程序),在电脑上打开串口调试助手,然后复位开发板就可以在调试助手中看到串口的打印信息与运行结果,具体见 图27-5

图片没有找到