【嵌入式八股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 | 可对多个内存设备进行合并管理 | 好比多个瓜,吃完一个接着拿下一个,实现不同内存区域的统一调配 |
- mem 小内存管理算法(类似 heap_4):
- 数据结构:采用链表组织内存块,每个链表表项包含 {magic(用于判断是否被非法改写), used(标识是否被使用), next(指针域,指向下一个表项), prev(指针域,指向前一个表项)}。
- 内存分配操作:以分配 64 Byte 内存为例,从链表表头开始遍历,寻找可用的内存空间进行分配(需要注意表头本身占用 3*4 Byte 的空间)。
- 内存释放操作:释放内存时,更改 used 表项的状态,并检查前后相邻的内存块是否为空闲状态。若有相邻空闲块,则将它们合并为一个大的内存块,以提高内存利用率。
- slab 大内存管理算法(内存池):为避免频繁的内存分配和释放操作带来的性能开销,该算法提前将内存划分成固定大小的块,形成内存池。当需要使用内存时,直接从内存池中获取相应的内存块;使用完毕后,将内存块归还到内存池中,从而实现高效的内存管理。
- memheap 内存管理算法(类似 heap_5):此算法能够将多个不连续的内存地址进行合并拼接,实现对不同内存区域的统一管理和使用。这对于一些具有分散内存资源的嵌入式系统来说,能够有效地提高内存的使用效率和灵活性。
二、RT-Thread 链表
- 链表类型对比:
- 普通双向循环链表:通常针对每一个数据结构固定的节点进行操作,节点的数据类型和结构在链表创建时就已经确定,灵活性相对较低。
- RTT 中双向循环链表:数据结构不固定,其指针域指向下一个指针域,这使得插入的元素可以为不同类型,大大提高了链表的通用性和灵活性。
- 链表操作函数:
- 指定节点前插入:
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;
}
- 节点元素的访问:由于在 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
任务则进行抢占式打印操作,随后进入阻塞状态。调度器的具体执行顺序如下:
- 高优先级任务
t1
首先执行,当执行到rt_thread_mdelay()
函数时,该函数会调用rt_thread_sleep()
中的rt_schedule()
函数,将t1
任务挂起。 - 调度器介入,通过特定的算法寻找到当前最高优先级的任务(此时为
t2
任务),并让其运行。 - 在低优先级任务
t2
的时间片未到期的情况下,由于高优先级任务t1
的rt_thread_mdelay()
超时,其定时计数器会发生相应的变化。 - 当下一个节拍周期到达时,会定时执行
rt_tick_increase()
函数,该函数会调用rt_timer_check()
中的timeout_func()
函数。 - 通过函数指针跳转到
rt_thread_timeout()
函数,在该函数中执行rt_schedule()
函数,触发任务调度。 - 进入 PendSV 中断处理函数,在该函数中进行线程的上下文切换,将 CPU 的控制权交给下一个任务。
四、FreeRTOS 内存管理
- 内存位置:FreeRTOS 的内存位于
.bss
段,而不是传统意义上的 heap(即启动文件中所定义的堆空间大小)。当使用pvPortMalloc
函数申请内存时,实际上是从这个系统堆(位于.bss
段)中进行申请的。
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 100 * 1024 ) // 申请 100KB 内存用于 RTOS 系统堆内存
- 内存管理策略示例:以
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 任务调度
- 调度依据:系统通过时钟来判断当前最高优先级的任务,并进行相应的调度,确保高优先级的任务能够优先获得 CPU 资源,从而保证系统的实时性和响应速度。
- 任务让出 CPU 使用权:当前任务可以主动执行
taskYIELD()
或portYIELD_FROM_ISR()
函数,让出 CPU 的使用权,以便调度器能够切换到其他任务进行执行。
六、FreeRTOS 创建任务
在 FreeRTOS 中创建任务时,会在堆中(实际上是位于 .bss
段的系统堆)通过 pvPortMalloc
函数分配内存给任务控制块(TCB),用于存储任务的相关信息,如任务的状态、优先级、上下文等。
七、任务堆栈
在创建任务时,用户可以选择动态创建或静态创建任务堆栈。静态的任务栈在任务结束后无法被回收,会一直占用内存空间;而动态的任务栈则可以在任务结束后,将分配的内存空间归还给系统,提高内存的使用效率。
八、RTOS 堆栈溢出的检测
- 方案 1:检查栈指针是否越界:
- 检测方法:在调度时,利用任务保存的栈顶和栈大小信息,检查栈指针是否越界。即每次任务切换时,对比栈指针的位置与栈顶和栈底的位置关系,判断是否发生了堆栈溢出。
- 优点:能够快速检测到堆栈溢出的情况,只要栈指针超出了预设的范围,就能立即发现问题。
- 缺点:对于任务运行时发生堆栈溢出,但在切换任务前又恢复正常的情况,无法进行有效的检测。因为在任务切换时,栈指针可能已经回到了正常范围内,导致检测失败。
- 方案 2:检查栈末尾字节是否改变:
- 检测方法:在创建任务时,将栈末尾的 16 个字节初始化为特定的字符。每次任务切换时,判断这些特定字符是否被改写。如果被改写,则说明发生了堆栈溢出。
- 优点:可检出几乎所有的堆栈溢出情况,因为只要堆栈发生溢出,就很可能会覆盖栈末尾的这些特定字符。
- 缺点:检测速度相对较慢,因为每次任务切换时都需要对栈末尾的 16 个字节进行比较操作,增加了系统的开销。
九、RT-Thread PendSV 系统调用--上下文切换
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
嵌入式八股/模拟面试拷打 文章被收录于专栏
一些八股模拟拷打Point,万一有点用呢