IOT-OS之RT-Thread(四)--- 时钟管理与内存管理

操作系统要想协调好各线程的调度管理,离不开时间与空间的管理:时间管理主要靠时钟节拍systick实现,时钟节拍是特定的周期性中断,这个中断可以看做是系统心跳(心跳频率一般设为10-1000HZ,时钟节拍率越快,系统额外开销越大),供系统处理所有和时间有关的事件,如线程的延时、线程的时间片轮转调度以及定时器超时等;空间管理主要是对可用存储空间的管理,一般MCU的存储空间可分为片内RAM与片内FLASH / ROM两种,片内RAM可由内存管理器动态分配/释放内部缓存空间(如Linux虚拟内存管理系统),片内FLASH可由文件系统进行分区分目录以文件为单位管理数据存储空间。

一、定时器对象管理

1.1 时钟节拍

时钟节拍定时器systick的配置和SysTick_Handler中断处理函数在前一篇CPU架构与BSP移植时已经简单介绍过了,下面再看看SysTick_Handler调用的rt_tick_increase函数代码:

// rt-thread-4.0.1\src\clock.c
static rt_tick_t rt_tick = 0;

/**
 * This function will notify kernel there is one tick passed. Normally,
 * this function is invoked by clock ISR.
 */
void rt_tick_increase(void)
{
    struct rt_thread *thread;

    /* increase the global tick */
#ifdef RT_USING_SMP
    rt_cpu_self()->tick ++;
#else
    ++ rt_tick;
#endif

    /* check time slice */
    thread = rt_thread_self();

    -- thread->remaining_tick;
    if (thread->remaining_tick == 0)
    {
        /* change to initialized tick */
        thread->remaining_tick = thread->init_tick;

        /* yield */
        rt_thread_yield();
    }

    /* check timer */
    rt_timer_check();
}

/**
 * This function will return current tick from operating system startup
 * @return current tick
 */
rt_tick_t rt_tick_get(void)
{
    /* return the global tick */
    return rt_tick;
}
RTM_EXPORT(rt_tick_get);

/**
 * This function will set current tick
 */
void rt_tick_set(rt_tick_t tick)
{
    rt_base_t level;

    level = rt_hw_interrupt_disable();
    rt_tick = tick;
    rt_hw_interrupt_enable(level);
}

/**
 * This function will calculate the tick from millisecond.
 * @param ms the specified millisecond
 *           - Negative Number wait forever
 *           - Zero not wait
 *           - Max 0x7fffffff
 * @return the calculated tick
 */
rt_tick_t rt_tick_from_millisecond(rt_int32_t ms)
{
    rt_tick_t tick;

    if (ms < 0)
    {
        tick = (rt_tick_t)RT_WAITING_FOREVER;
    }
    else
    {
        tick = RT_TICK_PER_SECOND * (ms / 1000);
        tick += (RT_TICK_PER_SECOND * (ms % 1000) + 999) / 1000;
    }
    /* return the calculated tick */
    return tick;
}
RTM_EXPORT(rt_tick_from_millisecond);

时钟节拍每次中断除了系统节拍全局变量rt_tick(rt_tick 的值表示了系统从启动开始总共经过的时钟节拍数,即系统时间)自增外,还调用了软件定时器检查函数rt_timer_check()来检查软件定时器链表中是否有定时器超时,若有则执行该定时器的超时函数,rt_timer_check()代码如下:

// rt-thread-4.0.1\src\timer.c

/**
 * This function will check timer list, if a timeout event happens, the
 * corresponding timeout function will be invoked.
 *
 * @note this function shall be invoked in operating system timer interrupt.
 */
void rt_timer_check(void)
{
    struct rt_timer *t;
    rt_tick_t current_tick;
    register rt_base_t level;

    RT_DEBUG_LOG(RT_DEBUG_TIMER, ("timer check enter\n"));

    current_tick = rt_tick_get();

    /* disable interrupt */
    level = rt_hw_interrupt_disable();

    while (!rt_list_isempty(&rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1]))
    {
        t = rt_list_entry(rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1].next,
                          struct rt_timer, row[RT_TIMER_SKIP_LIST_LEVEL - 1]);

        /*
         * It supposes that the new tick shall less than the half duration of
         * tick max.
         */
        if ((current_tick - t->timeout_tick) < RT_TICK_MAX / 2)
        {
            RT_OBJECT_HOOK_CALL(rt_timer_enter_hook, (t));

            /* remove timer from timer list firstly */
            _rt_timer_remove(t);

            /* call timeout function */
            t->timeout_func(t->parameter);

            /* re-get tick */
            current_tick = rt_tick_get();

            RT_OBJECT_HOOK_CALL(rt_timer_exit_hook, (t));
            RT_DEBUG_LOG(RT_DEBUG_TIMER, ("current tick: %d\n", current_tick));

            if ((t->parent.flag & RT_TIMER_FLAG_PERIODIC) &&
                (t->parent.flag & RT_TIMER_FLAG_ACTIVATED))
            {
                /* start it */
                t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
                rt_timer_start(t);
            }
            else
            {
                /* stop timer */
                t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
            }
        }
        else
            break;
    }

    /* enable interrupt */
    rt_hw_interrupt_enable(level);

    RT_DEBUG_LOG(RT_DEBUG_TIMER, ("timer check leave\n"));
}

rt_inline void _rt_timer_remove(rt_timer_t timer)
{
    int i;

    for (i = 0; i < RT_TIMER_SKIP_LIST_LEVEL; i++)
    {
        rt_list_remove(&timer->row[i]);
    }
}

中断中的 rt_timer_check() 用于检查系统硬件定时器链表,如果有定时器超时,将调用相应的超时函数。且所有定时器在定时超时后都会从定时器链表中被移除,而周期性定时器会在它再次启动时被加入定时器链表。了解了软件定时器的超时检查过程和超时函数调用点,下面再看软件定时器的数据结构和接口函数。

1.2 定时器对象管理

定时器,是指从指定的时刻开始,经过一定的指定时间后触发一个事件,例如定个时间提醒第二天能够按时起床。定时器有硬件定时器和软件定时器之分:

  • 硬件定时器:是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式;
  • 软件定时器:是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受数目限制的定时器服务。

RT-Thread 的定时器提供两类定时器机制:第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动停止。第二类是周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动的停止,否则将永远持续执行下去。

另外,根据超时函数执行时所处的上下文环境,RT-Thread 的定时器可以分为 HARD_TIMER 模式与 SOFT_TIMER 模式,如下图:
RT-Thread定时器模式

  • HARD_TIMER 模式:定时器超时函数在中断上下文环境中执行(执行时间应该尽量短,执行时不应导致当前上下文挂起、等待),可以在初始化 / 创建定时器时使用参数 RT_TIMER_FLAG_HARD_TIMER 来指定,也是RT-Thread 定时器默认的方式;
  • SOFT_TIMER 模式:通过宏定义 RT_USING_TIMER_SOFT 来决定是否启用该模式。该模式被启用后,系统会在初始化时创建一个 timer 线程,然后 SOFT_TIMER 模式的定时器超时函数在都会在 timer 线程的上下文环境中执行。可以在初始化 / 创建定时器时使用参数 RT_TIMER_FLAG_SOFT_TIMER 来指定设置 SOFT_TIMER 模式。

在 RT-Thread 操作系统中,定时器控制块由结构体 struct rt_timer 定义并形成定时器内核对象,再链接到内核对象容器中进行管理。它是操作系统用于管理定时器的一个数据结构,会存储定时器的一些信息,例如初始节拍数,超时时的节拍数,也包含定时器与定时器之间连接用的链表结构,超时回调函数等。rt_timer的数据结构定义如下:

// rt-thread-4.0.1\include\rtdef.h

/**
 * timer structure
 */
struct rt_timer
{
    struct rt_object parent;                            /**< inherit from rt_object */

    rt_list_t        row[RT_TIMER_SKIP_LIST_LEVEL];

    void (*timeout_func)(void *parameter);              /**< timeout function */
    void            *parameter;                         /**< timeout function's parameter */

    rt_tick_t        init_tick;                         /**< timer timeout tick */
    rt_tick_t        timeout_tick;                      /**< timeout tick */
};
typedef struct rt_timer *rt_timer_t;

/**
 * clock & timer macros
 */
#define RT_TIMER_FLAG_DEACTIVATED       0x0             /**< timer is deactive */
#define RT_TIMER_FLAG_ACTIVATED         0x1             /**< timer is active */
#define RT_TIMER_FLAG_ONE_SHOT          0x0             /**< one shot timer */
#define RT_TIMER_FLAG_PERIODIC          0x2             /**< periodic timer */

#define RT_TIMER_FLAG_HARD_TIMER        0x0             /**< hard timer,the timer's callback function will be called in tick isr. */
#define RT_TIMER_FLAG_SOFT_TIMER        0x4             /**< soft timer,the timer's callback function will be called in timer thread. */

#ifndef RT_TIMER_SKIP_LIST_LEVEL
#define RT_TIMER_SKIP_LIST_LEVEL          1
#endif

// rt-thread-4.0.1\src\timer.c
/* hard timer list */
static rt_list_t rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL];
/* soft timer list */
static rt_list_t rt_soft_timer_list[RT_TIMER_SKIP_LIST_LEVEL];

结构体rt_timer的第一个成员即是前面介绍过的抽象内核对象结构rt_object,此时rt_timer.parent.type为RT_Object_Class_Timer,rt_timer.parent.list以双向链表形式保存了所有初始化或创建的定时器对象,rt_timer.parent.flag则用以标识前面介绍过的定时器工作模式,现根据宏定义汇总如下表所示:

flag位 0 1 备注
bit0 RT_TIMER_FLAG_DEACTIVATED:定时器未激活,即初始化值 RT_TIMER_FLAG_ACTIVATED:定时器激活,当定时器start后将会置为此状态 激活/非激活状态
bit1 RT_TIMER_FLAG_ONE_SHOT:单次触发定时器,即定时器时间一到自动失效 RT_TIMER_FLAG_PERIODIC:周期触发定时器,即时间一到自动开始下一次定时 单次定时器/周期定时器
bit2 RT_TIMER_FLAG_HARD_TIMER:HARD_TIMER模式,即在中断环境中执行超时函数 RT_TIMER_FLAG_SOFT_TIMER:SOFT_TIMER模式,即在线程环境中执行超时函数 硬件时钟/软件时钟标志

看完了定时器rt_timer继承的抽象对象rt_object此时各成员的含义,下面看定时器rt_timer私有扩展成员的含义。

rt_list_t类型的row[RT_TIMER_SKIP_LIST_LEVEL]算是一个结构体数组,看到这里可能会疑惑,rt_list_t本身已经是一个双向链表结构,怎么还以数组形式出现呢?我们先看看这里rt_list_t单个数组元素的作用,row[0]可以用来管理已激活的定时器,并且按照超时时间排序的方式插入到rt_timer_list 链表中(对于SOFT_TIMER模式则是rt_soft_timer_list链表),超时时间rt_timer.timeout_tick是由启动定时器时的系统时间rt_tick与设置的定时时长(暂存在rt_timer.init_tick中,如果定时时长是毫秒数应先换算为节拍数,换算过程参照前面rt_tick_from_millisecond代码)相加获得的,定时器链表的组织形式如下:
rt_timer_list结构图示
上图中启动timer4时的系统时间rt_tick=30,设置timer4的定时时长为300个tick,在启动timer4时会先计算出其超时时间timeout=330,按从小到大的顺序遍历rt_timer_list 链表并找到合适的插入位置(如上图中timeout在120与520之间的位置)。

定时器rt_timer既然使用单个元素row[0]就可以达到管理已激活定时器链表的目的,为了还使用一个数组来管理定时器链表呢?岂不是增加了编程难度和复杂性?既然采用更复杂的定时器链表管理方式,必然能从中获取更大的收益。RT-Thread使用定时器链表数组结构是把对定时器链表的线性遍历增加了“跳跃”功能,实现类似于二叉搜索树的快速查找方法,加快搜索链表元素的速度。

跳表(Skip List)是一种基于并联链表的数据结构,它在链表的基础上增加了 “跳跃” 功能,正是这个功能,使得在查找元素时,跳表能够提供 O(log n)的时间复杂度(普通链表查找元素的时间复杂度为O(n))。

跳表算法采用类似二叉搜索树的方法,把一些节点提取出来作为索引,提取一级索引后的跳表结构图示如下:
跳表一级索引结构
定时器结构rt_timer中row[RT_TIMER_SKIP_LIST_LEVEL]链表数组的成员数量RT_TIMER_SKIP_LIST_LEVEL代表跳表层级(默认为1),对于上面添加了一级索引后,跳表层级就变为2了。跳表是一个以空间换时间的算法,虽然能加快定时器链表的搜索速度,但也要占用更多的内存空间,如果定时器链表比较长,可以继续增加跳表层数,对于实时性要求较高的嵌入式系统,提高定时器搜索速度多占用一些内存空间是值得的。

定时器rt_timer剩余的两个成员是超时函数指针及其参数指针,由用户定义该定时器超时后要执行的操作,在初始化/创建定时器时配置。

1.3 定时器接口函数

如果要使用定时器管理系统,在系统启动时需要对定时器管理系统进行初始化。可以通过下面的函数接口完成:

// rt-thread-4.0.1\src\timer.c
/**
 * This function will initialize system timer
 */
void rt_system_timer_init(void)
{
    int i;

    for (i = 0; i < sizeof(rt_timer_list) / sizeof(rt_timer_list[0]); i++)
    {
        rt_list_init(rt_timer_list + i);
    }
}

如果需要使用 SOFT_TIMER,则系统初始化时,应该调用下面这个函数接口:

// rt-thread-4.0.1\src\timer.c
/**
 * This function will initialize system timer thread
 */
void rt_system_timer_thread_init(void)
{
#ifdef RT_USING_TIMER_SOFT
    int i;

    for (i = 0;
         i < sizeof(rt_soft_timer_list) / sizeof(rt_soft_timer_list[0]);
         i++)
    {
        rt_list_init(rt_soft_timer_list + i);
    }

    /* start software timer thread */
    rt_thread_init(&timer_thread,
                   "timer",
                   rt_thread_timer_entry,
                   RT_NULL,
                   &timer_thread_stack[0],
                   sizeof(timer_thread_stack),
                   RT_TIMER_THREAD_PRIO,
                   10);

    /* startup */
    rt_thread_startup(&timer_thread);
#endif
}

这两个函数已经在rtthread_startup中被调用了,在博客系统启动与初始化中介绍过,从上面的代码对比也能看出定时器HARD_TIMER与SOFT_TIMER模式的区别。

定时器控制块中含有定时器相关的重要参数,在定时器各种状态间起到纽带的作用。定时器的相关操作如下图所示,对定时器的操作包含:创建 / 初始化定时器、启动定时器、运行定时器、删除 / 脱离定时器,所有定时器在定时超时后都会从定时器链表中被移除,而周期性定时器会在它再次启动时被加入定时器链表,这与定时器参数设置相关。在每次的操作系统时钟中断发生时,都会对已经超时的定时器状态参数做改变。
定时器操作函数
内核对象管理中介绍的内核对象接口函数最主要的是构造、析构、查找函数,下面以定时器构造函数代码展示在接口函数实现代码中继承的抽象对象rt_object为rt_timer接口函数代码的实现提供了哪些方便?定时器静态对象与动态对象的构造函数代码:

// rt-thread-4.0.1\src\timer.c
/**
 * This function will initialize a timer, normally this function is used to
 * initialize a static timer object.
 * @param timer the static timer object
 * @param name the name of timer
 * @param timeout the timeout function
 * @param parameter the parameter of timeout function
 * @param time the tick of timer
 * @param flag the flag of timer
 */
void rt_timer_init(rt_timer_t  timer,
                   const char *name,
                   void (*timeout)(void *parameter),
                   void       *parameter,
                   rt_tick_t   time,
                   rt_uint8_t  flag)
{
    /* timer check */
    RT_ASSERT(timer != RT_NULL);

    /* timer object initialization */
    rt_object_init((rt_object_t)timer, RT_Object_Class_Timer, name);

    _rt_timer_init(timer, timeout, parameter, time, flag);
}
RTM_EXPORT(rt_timer_init);

/**
 * This function will create a timer
 * @param name the name of timer
 * @param timeout the timeout function
 * @param parameter the parameter of timeout function
 * @param time the tick of timer
 * @param flag the flag of timer
 * @return the created timer object
 */
rt_timer_t rt_timer_create(const char *name,
                           void (*timeout)(void *parameter),
                           void       *parameter,
                           rt_tick_t   time,
                           rt_uint8_t  flag)
{
    struct rt_timer *timer;

    /* allocate a object */
    timer = (struct rt_timer *)rt_object_allocate(RT_Object_Class_Timer, name);
    if (timer == RT_NULL)
    {
        return RT_NULL;
    }

    _rt_timer_init(timer, timeout, parameter, time, flag);

    return timer;
}
RTM_EXPORT(rt_timer_create);

static void _rt_timer_init(rt_timer_t timer,
                           void (*timeout)(void *parameter),
                           void      *parameter,
                           rt_tick_t  time,
                           rt_uint8_t flag)
{
    int i;

    /* set flag */
    timer->parent.flag  = flag;

    /* set deactivated */
    timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;

    timer->timeout_func = timeout;
    timer->parameter    = parameter;

    timer->timeout_tick = 0;
    timer->init_tick    = time;

    /* initialize timer list */
    for (i = 0; i < RT_TIMER_SKIP_LIST_LEVEL; i++)
    {
        rt_list_init(&(timer->row[i]));
    }
}

定时器对象的构造可分为两部分:一部分是内核共有对象rt_object的构造;另一部分是内核私有成员rt_timer的构造,构造函数_rt_timer_init以static修饰,表明其为私有方法,不开放给用户。定时器对象rt_timer的空间分配也由其父对象rt_object负责,rt_timer只需要为其私有成员变量赋初值即可。定时器对象的析构也可分为两部分,跟构造过程类似,这里就不赘述代码了。

定时器启动函数主要是设置超时时间并将该定时器插入到rt_timer_list 链表中(对于SOFT_TIMER模式则是rt_soft_timer_list链表),定时器的超时检查和超时函数的执行由前面介绍过的rt_timer_check完成,rt_timer_start函数代码因为要维护跳表row[RT_TIMER_SKIP_LIST_LEVEL]代码较为复杂,且跳表算法不是本文重点,就不再赘述代码了。

RT-Thread除了提供定时器的构造、析构、启动、停止四个接口函数外,还额外提供了定时器控制函数接口,以获取或设置更多定时器的信息,控制函数接口也是RT-Thread内核派生对象中常用的函数接口,下面看看定时器的控制函数接口代码:

// rt-thread-4.0.1\src\timer.c
/**
 * This function will get or set some options of the timer
 *
 * @param timer the timer to be get or set
 * @param cmd the control command
 * @param arg the argument
 *
 * @return RT_EOK
 */
rt_err_t rt_timer_control(rt_timer_t timer, int cmd, void *arg)
{
    /* timer check */
    RT_ASSERT(timer != RT_NULL);
    RT_ASSERT(rt_object_get_type(&timer->parent) == RT_Object_Class_Timer);

    switch (cmd)
    {
    case RT_TIMER_CTRL_GET_TIME:
        *(rt_tick_t *)arg = timer->init_tick;
        break;

    case RT_TIMER_CTRL_SET_TIME:
        timer->init_tick = *(rt_tick_t *)arg;
        break;

    case RT_TIMER_CTRL_SET_ONESHOT:
        timer->parent.flag &= ~RT_TIMER_FLAG_PERIODIC;
        break;

    case RT_TIMER_CTRL_SET_PERIODIC:
        timer->parent.flag |= RT_TIMER_FLAG_PERIODIC;
        break;
    }

    return RT_EOK;
}
RTM_EXPORT(rt_timer_control);

// rt-thread-4.0.1\include\rtdef.h
#define RT_TIMER_CTRL_SET_TIME          0x0             /**< set timer control command */
#define RT_TIMER_CTRL_GET_TIME          0x1             /**< get timer control command */
#define RT_TIMER_CTRL_SET_ONESHOT       0x2             /**< change timer to one shot */
#define RT_TIMER_CTRL_SET_PERIODIC      0x3             /**< change timer to periodic */

rt_timer_control接口函数用到了状态机模型,以命令宏替代状态宏作为函数参数,倒是方便了控制接口的编写和未来的扩展能力,以后想要添加新的命令,只需要增加新的状态case即可。

定时器rt_timer常用接口函数总结如下:

/**
 * This function will create a timer
 * @param name the name of timer
 * @param timeout the timeout function
 * @param parameter the parameter of timeout function
 * @param time the tick of timer
 * @param flag the flag of timer
 * @return the created timer object
 */
rt_timer_t rt_timer_create(const char *name,
                           void (*timeout)(void *parameter),
                           void       *parameter,
                           rt_tick_t   time,
                           rt_uint8_t  flag);

/**
 * This function will delete a timer and release timer memory
 * @param timer the timer to be deleted
 * @return the operation status, RT_EOK on OK; RT_ERROR on error
 */
rt_err_t rt_timer_delete(rt_timer_t timer);

/**
 * This function will initialize a timer, normally this function is used to
 * initialize a static timer object.
 * @param timer the static timer object
 * @param name the name of timer
 * @param timeout the timeout function
 * @param parameter the parameter of timeout function
 * @param time the tick of timer
 * @param flag the flag of timer
 */
void rt_timer_init(rt_timer_t  timer,
                   const char *name,
                   void (*timeout)(void *parameter),
                   void       *parameter,
                   rt_tick_t   time,
                   rt_uint8_t  flag);

/**
 * This function will detach a timer from timer management.
 * @param timer the static timer object
 * @return the operation status, RT_EOK on OK; RT_ERROR on error
 */
rt_err_t rt_timer_detach(rt_timer_t timer);

/**
 * This function will start the timer
 * @param timer the timer to be started
 * @return the operation status, RT_EOK on OK, -RT_ERROR on error
 */
rt_err_t rt_timer_start(rt_timer_t timer);

/**
 * This function will stop the timer
 * @param timer the timer to be stopped
 * @return the operation status, RT_EOK on OK, -RT_ERROR on error
 */
rt_err_t rt_timer_stop(rt_timer_t timer);

/**
 * This function will get or set some options of the timer
 * @param timer the timer to be get or set
 * @param cmd the control command
 * @param arg the argument
 * @return RT_EOK
 */
rt_err_t rt_timer_control(rt_timer_t timer, int cmd, void *arg);

1.4 定时器对象管理示例

下面创建两个定时器:一个是单次静态定时器;一个是周期动态定时器,并让周期定时器运行一段时间后停止运行。两个超时函数用static修饰以隐藏方法,并导出自定义命令到MSH命令列表中,在工程目录\projects\stm32l475_kernel_sample\applications下新增了timer_sample.c源文件,在timer_sample.c中编辑该工程代码如下(在博客CPU架构与BSP移植过程基础上编写):

// .\RT-Thread_Projects\projects\stm32l475_kernel_sample\applications\timer_sample.c

#include <rtthread.h>
/* 定时器控制块的句柄指针 */
static rt_timer_t timer1;
/* 定时器的控制块 */
static struct rt_timer timer2;

static int count = 0;

/* 定时器 1 超时函数 */
static void timeout1(void *parameter)
{
    rt_kprintf("periodic timer is timeout %d\n", count);

    /* 运行第 8 次,停止周期定时器 */
    if (count++ > 8)
    {
        rt_timer_stop(timer1);      /* 停止定时器 1 */
        rt_kprintf("periodic timer was stopped! \n");
    }
}

/* 定时器 2 超时函数 */
static void timeout2(void *parameter)
{
    rt_tick_t tick2;
    /* 控制定时器2 获取超时tick值 */
    rt_timer_control(&timer2,               /* 定时器句柄 */
                    RT_TIMER_CTRL_GET_TIME, /* 定时器控制命令 */
                    (void *)&tick2);        /* 定时器控制参数 */

    rt_kprintf("one shot timer is timeout, get tick is %d\n",tick2);

	rt_timer_detach(&timer2);       /* 脱离定时器 2 */

    rt_kprintf("one shot timer was detached! \n");
}

static int timer_sample(void)
{
    /* 创建定时器 1  周期定时器 */
    timer1 = rt_timer_create("timer1",      /* 定时器名字是 timer1 */
                            timeout1,       /* 超时回调的处理函数 */
                            RT_NULL,        /* 超时函数的入口参数 */
                            10,             /* 定时长度为 10 个 OS Tick */
                            RT_TIMER_FLAG_PERIODIC);    /* 周期定时器 */

    /* 启动定时器 1 */
    if (timer1 != RT_NULL)
        rt_timer_start(timer1);

    /* 创建定时器 2 单次定时器 */
    rt_timer_init(&timer2,  /* 定时器句柄是 &timer2 */
                "timer2",   /* 定时器名字是 timer2 */
                timeout2, 	/* 超时回调的处理函数 */
                RT_NULL, 	/* 超时函数的入口参数 */
                30,     	/* 定时长度为 30 个 OS Tick */
                RT_TIMER_FLAG_ONE_SHOT); /* 单次定时器 */

    /* 启动定时器 2 */
    rt_timer_start(&timer2);

    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(timer_sample, timer sample);

在env工具中运行scons --target=mdk5命令重新生成工程文件,打开projects \stm32l475_kernel_sample\project.uvprojx工程,编译无错误,烧录到STM32L475潘多拉开发板中,串口finsh组件导出的MSH命令运行结果如下图所示:
timer_sample运行结果
运行list_timer只看到我们创建的timer1却不见timer2,是因为timer2是单次触发定时器,触发后已停止,故在list_timer中不可见。

新增timer_sample.c源文件后重新编译构建工程为何会自动将新增的源文件添加进工程的呢?下面的工程构建脚本代码能解答该疑惑(会自动包含applications目录下的所有.c格式源文件和该目录下所有头文件,SConscript脚本使用python语法编写):

// .\RT-Thread_Projects\projects\stm32l4xx\applications\SConscript

from building import *

cwd     = GetCurrentDir()
src     = Glob('*.c')
CPPPATH = [str(Dir('#')), cwd]

group = DefineGroup('Applications', src, depend = [''], CPPPATH = CPPPATH)

Return('group')

定时器示例工程源码下载地址:https://github.com/StreamAI/RT-Thread_Projects/tree/master/projects/stm32l475_kernel_sample

在设计软件定时器时,超时回调函数的要求严格:执行时间应该尽量短,执行时不应导致当前上下文挂起、等待。例如在中断上下文中执行的超时函数它不应该试图去申请动态内存、释放动态内存等,也不允许调用rt_thread_delay()等导致上下文挂起的 API 接口。

二、内存对象管理

STM32的存储管理在博客存储管理与虚拟内存中有过详细的介绍,在介绍静态对象与动态对象时也了解了RAM与ROM的分布关系。在系统运行中,变量、中间数据一般存放在 RAM 中,只有在实际使用时才将它们从 RAM 调入到 CPU 中进行运算。一些数据需要的内存大小需要在程序运行过程中根据实际情况确定,这就要求系统具有对内存空间进行动态管理的能力,在用户需要一段内存空间时,向系统申请,系统选择一段合适的内存空间分配给用户,用户使用完毕后,再释放回系统,以便系统将该段内存空间回收再利用,这就是通常所说的内存管理(ROM部分的管理留待后面介绍文件系统时再详述)。

内存管理通常可分为内存堆管理和内存池管理,在博客网络数据包管理中也介绍了LwIP协议栈是如何进行内存堆管理和内存池管理的。下面简单介绍下RT-Thread中是如何进行内存堆管理和内存池管理的,原理类似,就不详述了。

由于实时系统中对时间的要求非常严格,内存管理往往要比通用操作系统要求苛刻得多,:

  • 分配内存的时间必须是确定的:一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面。而寻找这样一个空闲内存块所耗费的时间是不确定的,因此对于实时系统来说,这就是不可接受的,实时系统必须要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定;
  • 应能解决内存碎片问题:随着内存不断被分配和释放,整个内存区域会产生越来越多的碎片(因为在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块,它们地址不连续,不能够作为一整块的大内存分配出去),系统中还有足够的空闲内存,但因为它们地址并非连续,不能组成一块连续的完整内存块,会使得程序不能申请到大的内存。对于通用系统而言,这种不恰当的内存分配算法可以通过重新启动系统来解决 (每个月或者数个月进行一次),但是对于那些需要常年不间断地工作于野外的嵌入式系统来说,就变得让人无法接受了;
  • 为不同大小的内存空间提供不同的分配算法:嵌入式系统的资源环境也是不尽相同,有些系统的资源比较紧张,只有数十 KB 的内存可供分配,而有些系统则存在数 MB 的内存,如何为这些不同的系统,选择适合它们的高效率的内存分配算法,就将变得复杂化。

RT-Thread 操作系统在内存管理上,根据上层应用及系统资源的不同,有针对性地提供了不同的内存分配管理算法。总体上可分为两类:内存堆管理与内存池管理,而内存堆管理又根据具体内存设备划分为三种情况:

  1. 针对小内存块的分配管理(小内存管理算法);
  2. 针对大内存块的分配管理(slab 管理算法);
  3. 针对多内存堆的分配情况(memheap 管理算法)。

2.1 内存堆管理

内存堆可以在当前资源满足的情况下,根据用户的需求分配任意大小的内存块。而当用户不需要再使用这些内存块时,又可以释放回堆中供其他应用分配使用。RT-Thread 系统为了满足不同的需求,提供了不同的内存管理算法,分别是小内存管理算法、slab 管理算法和 memheap 管理算法。

小内存管理算法主要针对系统资源比较少,一般用于小于 2MB 内存空间的系统;而 slab 内存管理算法则主要是在系统资源比较丰富时,提供了一种近似多内存池管理算法的快速算法。除上述之外,RT-Thread 还有一种针对多内存堆的管理算法,即 memheap 管理算法。memheap 方法适用于系统存在多个内存堆的情况,它可以将多个内存 “粘贴” 在一起,形成一个大的内存堆,用户使用起来会非常方便。

这几类内存堆管理算法在系统运行时只能选择其中之一或者完全不使用内存堆管理器,他们提供给应用程序的 API 接口完全相同。因为内存堆管理器要满足多线程情况下的安全分配,会考虑多线程间的互斥问题,所以请不要在中断服务例程中分配或释放动态内存块。因为它可能会引起当前上下文被挂起等待。

2.1.1 小内存管理算法

小内存管理算法是一个简单的内存分配算法。初始时,它是一块大的内存。当需要分配内存块时,将从这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来,如下图所示:
小内存管理结构
每个内存块(不管是已分配的内存块还是空闲的内存块)都包含一个数据头,其中包括:

  • magic:变数(或称为幻数),它会被初始化成 0x1ea0(即英文单词 heap),用于标记这个内存块是一个内存管理用的内存数据块;变数不仅仅用于标识这个数据块是一个内存管理用的内存数据块,实质也是一个内存保护字:如果这个区域被改写,那么也就意味着这块内存块被非法改写(正常情况下只有内存管理器才会去碰这块内存);
  • used:指示出当前内存块是否已经分配。

内存管理的表现主要体现在内存的分配与释放上,小型内存管理算法可以用以下例子体现出来。空闲链表指针 lfree 初始指向 32 字节的内存块。当用户线程要再分配一个 64 字节的内存块时,但此 lfree 指针指向的内存块只有 32 字节并不能满足要求,内存管理器会继续寻找下一内存块,当找到再下一块内存块,128 字节时,它满足分配的要求。因为这个内存块比较大,分配器将把此内存块进行拆分,余下的内存块(52字节)继续留在 lfree链表中,在每次分配内存块前,都会留出 12 字节数据头用于 magic,used 信息及链表节点使用。返回给应用的地址实际上是这块内存块 12 字节以后的地址,而数据头部分是用户永远不应该改变的部分。
小内存管理算法链表结构示意图
分配 64 字节后的链表结构
释放时则是相反的过程,但分配器会查看前后相邻的内存块是否空闲,如果空闲则合并成一个大的空闲内存块。

2.1.2 slab管理算法

RT-Thread 的 slab 分配器是在 DragonFly BSD 创始人 Matthew Dillon 实现的 slab 分配器基础上,针对嵌入式系统优化的内存分配算法。最原始的 slab 算法是 Jeff Bonwick 为 Solaris 操作系统而引入的一种高效内核内存分配算法。

RT-Thread 的 slab 分配器实现主要是去掉了其中的对象构造及析构过程,只保留了纯粹的缓冲型的内存池算法。slab 分配器会根据对象的大小分成多个区(zone),也可以看成每类对象有一个内存池,如下图所示:
RT-Thread slab分配器
一个 zone 的大小在 32K 到 128K 字节之间,分配器会在堆初始化时根据堆的大小自动调整。系统中的 zone 最多包括 72 种对象,一次最大能够分配 16K 的内存空间,如果超出了 16K 那么直接从页分配器中分配。每个 zone 上分配的内存块大小是固定的,能够分配相同大小内存块的 zone 会链接在一个链表中,而 72 种对象的 zone 链表则放在一个数组(zone_array[])中统一管理。

下面是内存分配器主要的两种操作:

  • 内存分配:假设分配一个 32 字节的内存,slab 内存分配器会先按照 32 字节的值,从 zone array 链表表头数组中找到相应的zone 链表。如果这个链表是空的,则向页分配器分配一个新的 zone,然后从 zone 中返回第一个空闲内存块。如果链表非空,则这个zone 链表中的第一个 zone 节点必然有空闲块存在(否则它就不应该放在这个链表中),那么就取相应的空闲块。如果分配完成后,zone中所有空闲内存块都使用完毕,那么分配器需要把这个 zone 节点从链表中删除;
  • 内存释放:分配器需要找到内存块所在的 zone 节点,然后把内存块链接到 zone 的空闲内存块链表中。如果此时 zone 的空闲链表指示出 zone 的所有内存块都已经释放,即 zone 是完全空闲的,那么当 zone 链表中全空闲 zone 达到一定数目后,系统就会把这个全空闲的 zone 释放到页面分配器中去。

2.1.3 memheap管理算法

memheap 管理算法适用于系统含有多个地址可不连续的内存堆。使用 memheap 内存管理可以简化系统存在多个内存堆时的使用:当系统中存在多个内存堆的时候,用户只需要在系统初始化时将多个所需的 memheap 初始化,并开启 memheap 功能就可以很方便地把多个 memheap(地址可不连续)粘合起来用于系统的 heap 分配。

在开启 memheap 之后原来的 heap 功能将被关闭,两者只可以通过打开或关闭 RT_USING_MEMHEAP_AS_HEAP 来选择其一

memheap 工作机制如下图所示,首先将多块内存加入 memheap_item 链表进行粘合。当分配内存块时,会先从默认内存堆去分配内存,当分配不到时会查找 memheap_item 链表,尝试从其他的内存堆上分配内存块。应用程序不用关心当前分配的内存块位于哪个内存堆上,就像是在操作一个内存堆。
RT-Thread memheap算法

2.1.4 内存堆配置和初始化

在使用内存堆时,必须要在系统初始化的时候进行堆的初始化,可以通过函数接口rt_system_heap_init完成,函数原型如下:

/**
 * This function will initialize system heap memory.
 * @param begin_addr the beginning address of system heap memory.
 * @param end_addr the end address of system heap memory.
 */
void rt_system_heap_init(void* begin_addr, void* end_addr)

针对三种内存堆管理算法,内存堆系统初始化函数接口一致,但实现方式各不相同:

  • 小内存管理算法的接口函数实现代码见rt-thread-4.0.1\src\mem.c;
  • slab管理算法的接口函数实现代码见rt-thread-4.0.1\src\slab.c;
  • memheap管理算法的接口函数实现代码见rt-thread-4.0.1\src\memheap.c;

在使用 memheap 堆内存时,必须要在系统初始化的时候进行堆内存的初始化,可以通过函数接口rt_memheap_init完成,函数原型如下:

// rt-thread-4.0.1\src\memheap.c

#ifdef RT_USING_MEMHEAP_AS_HEAP
static struct rt_memheap _heap;

void rt_system_heap_init(void *begin_addr, void *end_addr)
{
    /* initialize a default heap in the system */
    rt_memheap_init(&_heap,
                    "heap",
                    begin_addr,
                    (rt_uint32_t)end_addr - (rt_uint32_t)begin_addr);
}

rt_err_t rt_memheap_init(struct rt_memheap *memheap,		//memheap 控制块
                         const char        *name,			//内存堆的名称
                         void              *start_addr,		//堆内存区域起始地址
                         rt_size_t         size)			//堆内存大小

memheap系统初始化函数实际调用的也是rt_memheap_init,为什么把rt_memheap_init接口单独给出呢?还记得memheap管理算法的原理吧,memheap适合于系统含有多个地址可不连续的内存堆,具体几个内存堆不同的芯片互有差异,用户可根据自己芯片情况通过接口函数rt_memheap_init添加多个内存堆,然后由memheap管理算法将多个内存堆连接起来使用。

以STM32L475芯片为例,通过查询SMT32L475VE Reference manual有如下信息:

The STM32L4x5/STM32L4x6 devices feature up to 320 Kbytes SRAM:
• 96 Kbytes SRAM1 and 32 Kbyte SRAM2 on STM32L475xx/476xx/486xx devices.
• 256 Kbyte SRAM1 and 64 Kbyte SRAM2 on STM32L496xx/4A6xx devices.

手头的STM32L475潘多拉开发板有两个SRAM且不连续,在博客RT-Thread BSP移植中只使用了SRAM1,下面我们使用memheap算法把SRAM2也利用起来。

首先,在board.h中加入如下代码(SRAM2起始地址查手册获得):

// projects\stm32l475_kernel_sample\board\board.h
#include "drv_usart.h"
......
#define STM32_SRAM2_SIZE               (32)
#define STM32_SRAM2_BEGIN              (0x10000000u)
#define STM32_SRAM2_END                (0x10000000 + STM32_SRAM2_SIZE * 1024)
#define STM32_SRAM2_HEAP_SIZE          ((uint32_t)STM32_SRAM2_END - (uint32_t)STM32_SRAM2_BEGIN)

接下来修改调用rt_system_heap_init函数的代码,调用内存堆系统初始化的函数是rt_hw_board_init,该函数在移植时被定义在libraries\HAL_Drivers\drv_common.c文件中,且被RT_WEAK修饰,也即可被重写。如果不想在drv_common.c文件中修改代码,可以将函数rt_hw_board_init代码复制到projects\stm32l475_kernel_sample\board \board.c中并去掉RT_WEAK修饰符,也即在board.c中新增代码如下:

// projects\stm32l475_kernel_sample\board\board.c

#if defined(RT_USING_MEMHEAP) && defined(RT_USING_MEMHEAP_AS_HEAP)
static struct rt_memheap system_heap;
#endif

/**
 * This function will initial STM32 board.
 */
void rt_hw_board_init()
{
	......
    /* Heap initialization */
#if defined(RT_USING_MEMHEAP) && defined(RT_USING_MEMHEAP_AS_HEAP)
    rt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END);
    rt_memheap_init(&system_heap, "sram2", (void *)STM32_SRAM2_BEGIN, STM32_SRAM2_HEAP_SIZE);
#else
    rt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END);
#endif
......
}

如果要使用SRAM2,还要在projects\stm32l475_kernel_sample\rtconfig.h中打开相应的宏定义使能memheap管理算法,我们使用更方便的menuconfig工具使能memheap管理算法,配置界面如下:
menuconfig使能memheap
menuconfig使能all memheap object
用空格键选中相应选项后,保存配置,打开rt_config.h文件,会看到memheap宏定义已经开启了,代码如下:

// projects\stm32l475_kernel_sample\rtconfig.h

/* Memory Management */
#define RT_USING_MEMPOOL
#define RT_USING_MEMHEAP
#define RT_USING_MEMHEAP_AS_HEAP
#define RT_USING_HEAP

RT_USING_MEMHEAP和RT_USING_MEMHEAP_AS_HEAP这两个宏已经打开了,也即使用memheap管理算法,那么系统管理内存堆的时候首先会从SRAM1(96K的那块)分配内存,当SRAM1(96K的那块)用完了再到SRAM2(32K那块)分配。

2.1.5 内存堆管理接口函数

对内存堆的操作如下图所示,包含:初始化、申请内存块、释放内存,所有使用完成后的动态内存都应该被释放,以供其他程序的申请使用。
内存堆接口函数
内存堆的管理主要就是申请内存、释放内存,跟之前系统初始化类似,三种内存堆管理方式提供了统一的接口函数,但有各自不同的实现方式,就连相应的数据结构也互不相同。

首先看看三种内存堆管理算法的数据结构:

// rt-thread-4.0.1\src\mem.c
#define HEAP_MAGIC 0x1ea0
struct heap_mem
{
    /* magic and used flag */
    rt_uint16_t magic;
    rt_uint16_t used;
#ifdef ARCH_CPU_64BIT
    rt_uint32_t resv;
#endif

    rt_size_t next, prev;

#ifdef RT_USING_MEMTRACE
#ifdef ARCH_CPU_64BIT
    rt_uint8_t thread[8];
#else
    rt_uint8_t thread[4];   /* thread name */
#endif
#endif
};

// rt-thread-4.0.1\src\slab.c
/*
 * The IN-BAND zone header is placed at the beginning of each zone.
 */
typedef struct slab_zone
{
    rt_int32_t  z_magic;        /* magic number for sanity check */
    rt_int32_t  z_nfree;        /* total free chunks / ualloc space in zone */
    rt_int32_t  z_nmax;         /* maximum free chunks */

    struct slab_zone *z_next;   /* zoneary[] link if z_nfree non-zero */
    rt_uint8_t  *z_baseptr;     /* pointer to start of chunk array */

    rt_int32_t  z_uindex;       /* current initial allocation index */
    rt_int32_t  z_chunksize;    /* chunk size for validation */

    rt_int32_t  z_zoneindex;    /* zone index */
    slab_chunk  *z_freechunk;   /* free chunk list */
} slab_zone;

#define ZALLOC_SLAB_MAGIC       0x51ab51ab
static slab_zone *zone_array[NZONES];   /* linked list of zones NFree > 0 */
static slab_zone *zone_free;            /* whole zones that have become free */

// rt-thread-4.0.1\src\memheap.c
#define RT_MEMHEAP_MAGIC        0x1ea01ea0

// rt-thread-4.0.1\include\rtdef.h
/**
 * Base structure of memory heap object
 */
struct rt_memheap
{
    struct rt_object        parent;                     /**< inherit from rt_object */

    void                   *start_addr;                 /**< pool start address and size */

    rt_uint32_t             pool_size;                  /**< pool size */
    rt_uint32_t             available_size;             /**< available size */
    rt_uint32_t             max_used_size;              /**< maximum allocated size */

    struct rt_memheap_item *block_list;                 /**< used block list */

    struct rt_memheap_item *free_list;                  /**< free block list */
    struct rt_memheap_item  free_header;                /**< free block list header */

    struct rt_semaphore     lock;                       /**< semaphore lock */
};

/**
 * memory item on the heap
 */
struct rt_memheap_item
{
    rt_uint32_t             magic;                      /**< magic number for memheap */
    struct rt_memheap      *pool_ptr;                   /**< point of pool */

    struct rt_memheap_item *next;                       /**< next memheap item */
    struct rt_memheap_item *prev;                       /**< prev memheap item */

    struct rt_memheap_item *next_free;                  /**< next free memheap item */
    struct rt_memheap_item *prev_free;                  /**< prev free memheap item */
};

三种内存堆管理算法的数据结构对比发现,小内存mem管理与slab管理都没有继承抽象对象rt_object,内存堆管理属于靠近硬件底层的部分,rt_object动态对象的分配便依赖内存堆管理器,相当于内存堆管理是rt_object对象管理的基础之一。

memheap管理数据结构rt_memheap却继承了抽象对象rt_object,也是考虑到不同的内存堆更方便抽象为一个个对象,在单一内存堆内使用数据结构rt_memheap_item进行管理,rt_memheap_item依然没有继承rt_object的属性和方法。

下面总结内存堆管理接口函数的原型如下:

/**
 * This function will allocate a block from system heap memory.
 * @param nbytes is size of the requested block in bytes.
 * @return pointer to allocated memory or NULL if no free memory was found.
 */
void *rt_malloc(rt_size_t nbytes);

/**
 * This function will release the previous allocated memory block by rt_malloc.
 * The released memory block is taken back to system heap.
 * @param ptr the address of memory which will be released
 */
void rt_free(void *ptr);

/**
 * This function will change the size of previously allocated memory block.
 * @param ptr the previously allocated memory block
 * @param size the new size of memory block
 * @return the allocated memory
 */
void *rt_realloc(void *ptr, rt_size_t size);

/**
 * This function will contiguously allocate enough space for count objects
 * that are size bytes of memory each and returns a pointer to the allocated
 * memory.The allocated memory is filled with bytes of value zero.
 * @param count number of objects to allocate
 * @param size size of the objects to allocate
 * @return pointer to allocated memory / NULL pointer if there is an error
 */
void *rt_calloc(rt_size_t count, rt_size_t size);

/**
 * This function will set a hook function, which will be invoked when a memory
 * block is allocated from heap memory.
 * @param hook the hook function
 */
void rt_malloc_sethook(void (*hook)(void *ptr, rt_size_t size));

/**
 * This function will set a hook function, which will be invoked when a memory
 * block is released to heap memory.
 * @param hook the hook function
 */
void rt_free_sethook(void (*hook)(void *ptr));

2.1.6 内存堆管理示例

前面已经启用了SRAM2内存段,下面使用内存堆申请接口函数,尽可能申请更多内存,直到能从SRAM2分配到内存,并打印申请到的内存地址,最后再释放所有已申请内存。在projects\stm32l475_kernel_sample\applications目录新建源文件memory_sample.c,该源文件内编写内存堆示例代码如下:

// projects\stm32l475_kernel_sample\applications\memory_sample.c
#include <rtthread.h>

static int memheap_sample(void)
{
    int i = 0;
    rt_uint8_t *ptr[10];

    for (i = 0; i < 10; i ++)
        ptr[i] = RT_NULL;
    /* 每次分配 (4096 * i) 大小字节数的内存空间 */
    for(i = 1; i < 10; i++){
        if(ptr[i-1] == RT_NULL){
            ptr[i-1] = rt_malloc(4096 * i);
            if(ptr[i-1] != RT_NULL){
                rt_kprintf("malloc memory heap address: %X, size: %d bytes.\n",ptr[i-1], (4096 * i));
            }else{
                rt_kprintf("try to malloc %d bytes memory failed.\n",(4096 * i));
                break;
            }
        }
    }
    /* 释放内存块 */
    for(i = 0; i < 10; i++){
        if(ptr[i] != RT_NULL){
            rt_kprintf("free memory heap address: %X, size: %d bytes.\n", ptr[i], (4096 * i));
            rt_free(ptr[i]);
            ptr[i] = RT_NULL;
        }else{
            rt_kprintf("free memory heap finished.\n");
            break;
        }       
    }
    return RT_EOK;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(memheap_sample, memheap sample);

使用scons --target=mdk5编译构建Keil MDK V5工程,打开.\stm32l475_kernel_sample \project.uvprojx工程编译后出现两个警告:
内存堆示例代码编译警告
提示rt_hw_systick_init与rt_hw_usart_init函数声明无效,查看函数声明知,rt_hw_systick_init在libraries\HAL_Drivers\drv_common.c中定义但未在libraries\HAL_Drivers\drv_common.h中声明,rt_hw_usart_init未包含头文件drv_usart.h,因此添加以下代码以消除警告:

// libraries\HAL_Drivers\drv_common.h
/* SysTick configuration */
void rt_hw_systick_init(void);

// projects\stm32l475_kernel_sample\board\board.h
#include "drv_usart.h"

重新编译后无错误,烧录到我们的STM32L475开发板,执行MSH导出的命令,结果如下:
memheap_sample运行结果
从上图可以看出,memheap正常工作,SRAM2确实可以正常使用了。内存堆管理示例工程源码下载地址:https://github.com/StreamAI/RT-Thread_Projects/tree/master/projects/stm32l475_kernel_sample

2.2 内存池管理

内存堆管理器可以分配任意大小的内存块,非常灵活和方便。但其也存在明显的缺点:一是分配效率不高,在每次分配时,都要空闲内存块查找;二是容易产生内存碎片。为了提高内存分配的效率,并且避免内存碎片,RT-Thread 提供了另外一种内存管理方法:内存池(Memory Pool)。

内存池是一种内存分配方式,用于分配大量大小相同的小内存块,它可以极大地加快内存分配与释放的速度,且能尽量避免内存碎片化。此外,RT-Thread 的内存池支持线程挂起功能,当内存池中无空闲内存块时,申请线程会被挂起,直到内存池中有新的可用内存块,再将挂起的申请线程唤醒。

内存池在创建时先向系统申请一大块内存,然后分成同样大小的多个小内存块,小内存块直接通过链表连接起来(此链表也称为空闲链表)。每次分配的时候,从空闲链表中取出链头上第一个内存块,提供给申请者。从下图中可以看到,物理内存中允许存在多个大小不同的内存池,每一个内存池又由多个空闲内存块组成,内核用它们来进行内存管理。当一个内存池对象被创建时,内存池对象就被分配给了一个内存池控制块,内存控制块的参数包括内存池名,内存缓冲区,内存块大小,块数以及一个等待线程队列。
内存池分配机制
内核负责给内存池分配内存池控制块,它同时也接收用户线程的分配内存块申请,当获得这些信息后,内核就可以从内存池中为内存池分配内存。内存池一旦初始化完成,内部的内存块大小将不能再做调整。

2.2.1 内存池控制块

内存池控制块是操作系统用于管理内存池的一个数据结构,它会存放内存池的一些信息,例如内存池数据区域开始地址,内存块大小和内存块列表等,也包含内存块与内存块之间连接用的链表结构,因内存块不可用而挂起的线程等待事件集合等。

在 RT-Thread 实时操作系统中,内存池控制块由结构体 struct rt_mempool 表示。另外一种 C 表达方式 rt_mp_t,表示的是内存块句柄,在 C 语言中的实现是指向内存池控制块的指针,详细定义情况见以下代码:

// rt-thread-4.0.1\include\rtdef.h
/**
 * Base structure of Memory pool object
 */
struct rt_mempool
{
    struct rt_object parent;                            /**< inherit from rt_object */

    void            *start_address;                     /**< memory pool start */
    rt_size_t        size;                              /**< size of memory pool */

    rt_size_t        block_size;                        /**< size of memory blocks */
    rt_uint8_t      *block_list;                        /**< memory blocks list */

    rt_size_t        block_total_count;                 /**< numbers of memory block */
    rt_size_t        block_free_count;                  /**< numbers of free memory block */

    rt_list_t        suspend_thread;                    /**< threads pended on this resource */
    rt_size_t        suspend_thread_count;              /**< numbers of thread pended on this resource */
};
typedef struct rt_mempool *rt_mp_t;

内存池控制块rt_mempool继承了抽象对象rt_object的属性与方法,每个内存池为一个对象,内存池类型rt_mempool.parent.type为RT_Object_Class_MemPool,rt_mempool.parent.flag未使用,rt_mempool.parent.list则保存所有初始化/创建的内存池对象。

内存池的起始地址、大小、内存块大小、内存块总数、剩余空闲内存块数等就不解释了,一个内存池内的多个空闲内存块组织成一个空闲链表,指针block_list指向空闲链表头节点,由于一个内存池中的内存块大小相同,分配和释放内存块的过程相当于从空闲链表首部取出和插入节点的过程。

内存池的rt_mempool.suspend_thread形成了一个申请内存块的线程等待列表,即当内存池中无可用内存块,并且申请线程允许等待时,申请线程将挂起在 suspend_thread 链表上,rt_mempool.suspend_thread_count为申请内存块的等待线程个数。

2.2.2 内存池管理接口函数

内存池控制块是一个结构体,其中含有内存池相关的重要参数,在内存池各种状态间起到纽带的作用。内存池的相关接口如下图所示,对内存池的操作包含:创建 / 初始化内存池、申请内存块、释放内存块、删除 / 脱离内存池,但不是所有的内存池都会被删除,这与设计者的需求相关,但是使用完的内存块都应该被释放。
内存池管理接口函数
内存池管理接口函数的原型汇总如下:

// rt-thread-4.0.1\src\mempool.c
/**
 * This function will initialize a memory pool object, normally which is used
 * for static object.
 * @param mp the memory pool object
 * @param name the name of memory pool
 * @param start the star address of memory pool
 * @param size the total size of memory pool
 * @param block_size the size for each block
 * @return RT_EOK
 */
rt_err_t rt_mp_init(struct rt_mempool *mp,
                    const char        *name,
                    void              *start,
                    rt_size_t          size,
                    rt_size_t          block_size);

/**
 * This function will detach a memory pool from system object management.
 * @param mp the memory pool object
 * @return RT_EOK
 */
rt_err_t rt_mp_detach(struct rt_mempool *mp);

/**
 * This function will create a mempool object and allocate the memory pool from
 * heap.
 * @param name the name of memory pool
 * @param block_count the count of blocks in memory pool
 * @param block_size the size for each block
 * @return the created mempool object
 */
rt_mp_t rt_mp_create(const char *name,
                     rt_size_t   block_count,
                     rt_size_t   block_size);

/**
 * This function will delete a memory pool and release the object memory.
 * @param mp the memory pool object
 * @return RT_EOK
 */
rt_err_t rt_mp_delete(rt_mp_t mp);

/**
 * This function will allocate a block from memory pool
 * @param mp the memory pool object
 * @param time the waiting time
 * @return the allocated memory block or RT_NULL on allocated failed
 */
void *rt_mp_alloc(rt_mp_t mp, rt_int32_t time);

/**
 * This function will release a memory block
 * @param block the address of memory block to be released
 */
void rt_mp_free(void *block);

内存池中每个内存块占用的空间除了rt_mempool.block_size还包括为了将这些内存块组织成一个单向链表需要的内存块链表节点指针占用的空间sizeof(rt_uint8_t *)。所以创建内存池静态对象时初始化的内存块总数mp->block_total_count = mp->size / (mp->block_size + sizeof(rt_uint8_t *)),创建内存池动态对象时需要分配的内存空间mp->start_address = rt_malloc((block_size + sizeof(rt_uint8_t *)) * block_count)。

2.2.3 内存池管理示例

内存池控制块中包含suspend_thread,且内存池分配函数有一个重要参数:等待时间time,说明内存池在内存不足时挂起等待线程的应用较常见。我们创建一个动态内存池,创建两个线程,一个线程会试图从内存池中获得内存块,另一个线程释放内存块内存块,在projects\stm32l475_kernel_sample\applications\memory_sample.c文件中新增该工程代码如下:

// projects\stm32l475_kernel_sample\applications\memory_sample.c

#define THREAD_PRIORITY      25
#define THREAD_STACK_SIZE    512
#define THREAD_TIMESLICE     5

#define MP_BLOCK_COUNT      4
#define MP_BLOCK_SIZE       1024

static rt_uint8_t *ptr[5];
static rt_mp_t mp;

/* 指向线程控制块的指针 */
static rt_thread_t tid1 = RT_NULL;
static rt_thread_t tid2 = RT_NULL;

/* 线程 1 入口 */
static void thread1_mp_alloc(void *parameter)
{
    int i;

    for (i = 0 ; i < 5 ; i++)
    {
        if (ptr[i] == RT_NULL)
        {
            /* 试图申请内存块 50 次,当申请不到内存块时,线程 1 挂起,转至线程 2 运行 */
            ptr[i] = rt_mp_alloc(mp, RT_WAITING_FOREVER);
            if (ptr[i] != RT_NULL)
                rt_kprintf("allocate memory pool address: %X, block No.%d\n", ptr[i], i);
        }
    }
}

/* 线程 2 入口,线程 2 的优先级比线程 1 低,应该线程 1 先获得执行。*/
static void thread2_mp_release(void *parameter)
{
    int i;

    for (i = 0; i < 5 ; i++)
    {
        /* 释放所有分配成功的内存块 */
        if (ptr[i] != RT_NULL)
        {
            rt_kprintf("release memory pool address: %X, block NO.%d\n", ptr[i], i);
            rt_mp_free(ptr[i]);
            ptr[i] = RT_NULL;
        }
    }
    rt_thread_mdelay(5000);
    rt_mp_delete(mp);
}

static int mempool_sample(void)
{
    int i;

    for (i = 0; i < 5; i ++)
        ptr[i] = RT_NULL;

    /* 创建内存池对象 */
    mp = rt_mp_create("mp1", MP_BLOCK_COUNT, MP_BLOCK_SIZE);
    if(mp == RT_NULL){
        rt_kprintf("memory pool create failed.\n");
        return RT_ERROR;
    }

    /* 创建线程 1:申请内存池 */
    tid1 = rt_thread_create("thread1", thread1_mp_alloc, RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);
    if (tid1 != RT_NULL)
        rt_thread_startup(tid1);


    /* 创建线程 2:释放内存池 */
    tid2 = rt_thread_create("thread2", thread2_mp_release, RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY + 1, THREAD_TIMESLICE);
    if (tid2 != RT_NULL)
        rt_thread_startup(tid2);

    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(mempool_sample, mempool sample);

在Keil MDK V5中编译无错误,烧录到STM32L475开发板后,运行MSH导出的命令,结果如下:
mempool_sample运行结果
从上图可以看出,动态内存池mp1创建成功,内存池空间不足时申请线程确实可以挂起等待,待其他线程释放内存池资源后再唤醒并申请到相应内存空间。内存池管理示例工程源码下载地址:https://github.com/StreamAI/RT-Thread_Projects/tree/master/projects/stm32l475_kernel_sample

更多文章:

发布了65 篇原创文章 · 获赞 35 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/m0_37621078/article/details/100859611
今日推荐