【嵌入式八股14】RTOS

一、RT-Thread 内存管理算法

RT-Thread 通过开辟静态数组的方式来管理内存,以下是具体的定义:

#define RT_HEAP_SIZE 6*1024
/* 从内部 SRAM 申请一块静态内存来作为内存堆使用 */
static uint32_t rt_heap[RT_HEAP_SIZE];	// heap default size: 24K(1024 * 4 * 6)

RT-Thread 提供了多种内存管理算法,适用于不同的内存场景,具体如下:

算法 对应文件 适用场景 形象说明
mem 小内存 mem.c 适用于 2MB 以内的小内存设备 如同一个瓜,根据需求吃多少切多少,灵活分配小内存
slab 大内存 slab.c 适用于大内存设备,采用内存池管理方式 类似一个已经切好固定大小块的瓜,需要时直接拿对应的块,提高分配效率
memheap 多内存 memheap.c 可对多个内存设备进行合并管理 好比多个瓜,吃完一个接着拿下一个,实现不同内存区域的统一调配
  1. mem 小内存管理算法(类似 heap_4)
    • 数据结构:采用链表组织内存块,每个链表表项包含 {magic(用于判断是否被非法改写), used(标识是否被使用), next(指针域,指向下一个表项), prev(指针域,指向前一个表项)}。
    • 内存分配操作:以分配 64 Byte 内存为例,从链表表头开始遍历,寻找可用的内存空间进行分配(需要注意表头本身占用 3*4 Byte 的空间)。
    • 内存释放操作:释放内存时,更改 used 表项的状态,并检查前后相邻的内存块是否为空闲状态。若有相邻空闲块,则将它们合并为一个大的内存块,以提高内存利用率。
  2. slab 大内存管理算法(内存池):为避免频繁的内存分配和释放操作带来的性能开销,该算法提前将内存划分成固定大小的块,形成内存池。当需要使用内存时,直接从内存池中获取相应的内存块;使用完毕后,将内存块归还到内存池中,从而实现高效的内存管理。
  3. memheap 内存管理算法(类似 heap_5):此算法能够将多个不连续的内存地址进行合并拼接,实现对不同内存区域的统一管理和使用。这对于一些具有分散内存资源的嵌入式系统来说,能够有效地提高内存的使用效率和灵活性。

二、RT-Thread 链表

  1. 链表类型对比
    • 普通双向循环链表:通常针对每一个数据结构固定的节点进行操作,节点的数据类型和结构在链表创建时就已经确定,灵活性相对较低。
    • RTT 中双向循环链表:数据结构不固定,其指针域指向下一个指针域,这使得插入的元素可以为不同类型,大大提高了链表的通用性和灵活性。
  2. 链表操作函数
    • 指定节点前插入
rt_inline void rt_list_insert_before(rt_list_t *l, rt_list_t *n)
{
    l->prev->next = n;
    n->prev = l->prev;

    l->prev = n;
    n->next = l;
}
- **指定节点后插入**:
rt_inline void rt_list_insert_after(rt_list_t *l, rt_list_t *n)
{
    l->next->prev = n;
    n->next = l->next;

    l->next = n;
    n->prev = l;
}
- **删除节点**:
rt_inline void rt_list_remove(rt_list_t *n)
{
    n->next->prev = n->prev;
    n->prev->next = n->next;

    n->next = n->prev = n;
}
  1. 节点元素的访问:由于在 RTT 链表的节点中,指针域的存放位置不确定,因此需要通过一种宏定义来从指针域寻找对应的结构体元素。具体来说,就是通过 rt_list_t 成员的地址来访问节点中的其他元素。虽然不同类型节点中 rt_list_t 成员的位置各不相同,但在确定类型的节点中,rt_list_t 成员的偏移是固定的。在获取 rt_list_t 成员地址的情况下,可以通过计算 (rt_list_t 成员地址) - (rt_list_t 成员偏移)得到节点的起始地址。RT-Thread 中提供了相应的算法和宏定义 rt_container_of 来实现这一功能:
/**
 * Double List structure
 */
struct rt_list_node
{
    struct rt_list_node *next;                          /**< point to next node. */
    struct rt_list_node *prev;                          /**< point to prev node. */
};
typedef struct rt_list_node rt_list_t;                  /**< Type for lists. */
struct rt_thread
{
    char        name[RT_NAME_MAX];                      /**< the name of thread */
    rt_list_t   list;                                   /**< the object list */
    rt_list_t   tlist;                                  /**< the thread list */
    rt_uint8_t  current_priority;                       /**< current priority */
    rt_uint8_t  init_priority;                          /**< initialized priority */
};
typedef struct rt_thread *rt_thread_t;

#define rt_container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))
//ptr: 成员首地址(指针域地址,例如 rt_thread_priority_table[highest_ready_priority].next)
//type: 结构体类型(例如 struct rt_thread)
//member: 结构体成员名称(例如 tlist)

三、RT-Thread 抢占式调度实现

假设存在两个线程,低优先级的 t2 任务在 while(1) 循环中执行耗时任务,高优先级的 t1 任务则进行抢占式打印操作,随后进入阻塞状态。调度器的具体执行顺序如下:

  1. 高优先级任务 t1 首先执行,当执行到 rt_thread_mdelay() 函数时,该函数会调用 rt_thread_sleep() 中的 rt_schedule() 函数,将 t1 任务挂起。
  2. 调度器介入,通过特定的算法寻找到当前最高优先级的任务(此时为 t2 任务),并让其运行。
  3. 在低优先级任务 t2 的时间片未到期的情况下,由于高优先级任务 t1rt_thread_mdelay() 超时,其定时计数器会发生相应的变化。
  4. 当下一个节拍周期到达时,会定时执行 rt_tick_increase() 函数,该函数会调用 rt_timer_check() 中的 timeout_func() 函数。
  5. 通过函数指针跳转到 rt_thread_timeout() 函数,在该函数中执行 rt_schedule() 函数,触发任务调度。
  6. 进入 PendSV 中断处理函数,在该函数中进行线程的上下文切换,将 CPU 的控制权交给下一个任务。

四、FreeRTOS 内存管理

  1. 内存位置:FreeRTOS 的内存位于 .bss 段,而不是传统意义上的 heap(即启动文件中所定义的堆空间大小)。当使用 pvPortMalloc 函数申请内存时,实际上是从这个系统堆(位于 .bss 段)中进行申请的。
#define configTOTAL_HEAP_SIZE                        ( ( size_t ) ( 100 * 1024 )   // 申请 100KB 内存用于 RTOS 系统堆内存
  1. 内存管理策略示例:以 heap_4.c 内存管理策略为例,在 map 文件中可以看到 FreeRTOS 使用一个静态数组作为 HEAP,该数组定义在 heap_4.c 文件中。由于这个 HEAP 来自于静态数组,所以它存在于数据段(具体为 .bss 段),而不是通常所认为的系统堆。以下是 map 文件中的相关信息示例:
.bss                  zero     0x2021'7d1c  0x1'9000  heap_4.o [35]  // 实际位于.bss 段

Entry                       Address      Size  Type      Object
-----                       -------      ----  ----      ------
ucHeap                  0x2021'7d1c  0x1'9000  Data  Lc  heap_4.o [35]  // 起始地址与大小

五、FreeRTOS 任务调度

  1. 调度依据:系统通过时钟来判断当前最高优先级的任务,并进行相应的调度,确保高优先级的任务能够优先获得 CPU 资源,从而保证系统的实时性和响应速度。
  2. 任务让出 CPU 使用权:当前任务可以主动执行 taskYIELD()portYIELD_FROM_ISR() 函数,让出 CPU 的使用权,以便调度器能够切换到其他任务进行执行。

六、FreeRTOS 创建任务

在 FreeRTOS 中创建任务时,会在堆中(实际上是位于 .bss 段的系统堆)通过 pvPortMalloc 函数分配内存给任务控制块(TCB),用于存储任务的相关信息,如任务的状态、优先级、上下文等。

七、任务堆栈

在创建任务时,用户可以选择动态创建或静态创建任务堆栈。静态的任务栈在任务结束后无法被回收,会一直占用内存空间;而动态的任务栈则可以在任务结束后,将分配的内存空间归还给系统,提高内存的使用效率。

八、RTOS 堆栈溢出的检测

  1. 方案 1:检查栈指针是否越界
    • 检测方法:在调度时,利用任务保存的栈顶和栈大小信息,检查栈指针是否越界。即每次任务切换时,对比栈指针的位置与栈顶和栈底的位置关系,判断是否发生了堆栈溢出。
    • 优点:能够快速检测到堆栈溢出的情况,只要栈指针超出了预设的范围,就能立即发现问题。
    • 缺点:对于任务运行时发生堆栈溢出,但在切换任务前又恢复正常的情况,无法进行有效的检测。因为在任务切换时,栈指针可能已经回到了正常范围内,导致检测失败。
  2. 方案 2:检查栈末尾字节是否改变
    • 检测方法:在创建任务时,将栈末尾的 16 个字节初始化为特定的字符。每次任务切换时,判断这些特定字符是否被改写。如果被改写,则说明发生了堆栈溢出。
    • 优点:可检出几乎所有的堆栈溢出情况,因为只要堆栈发生溢出,就很可能会覆盖栈末尾的这些特定字符。
    • 缺点:检测速度相对较慢,因为每次任务切换时都需要对栈末尾的 16 个字节进行比较操作,增加了系统的开销。

九、RT-Thread PendSV 系统调用--上下文切换

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

嵌入式八股/模拟面试拷打 文章被收录于专栏

一些八股模拟拷打Point,万一有点用呢

全部评论
大佬是还在找春招吗
点赞 回复 分享
发布于 03-07 22:48 广东
学废了
点赞 回复 分享
发布于 03-07 21:38 陕西

相关推荐

不愿透露姓名的神秘牛友
04-18 00:11
某工业 嵌入式软件工程师 9K*13薪 本科其他
点赞 评论 收藏
分享
评论
2
16
分享

创作者周榜

更多
牛客网
牛客企业服务