FreeRTOS

线程管理

TCB

线程控制块是操作系统用于管理线程的一个数据结构,它会存放线程的一些信息,例如优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构,线程等待事件集合等,详细定义如下

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
typedef struct tskTaskControlBlock
{
// 1. 栈指针 - 最重要的成员之一
// 当任务被切换时,它的当前上下文(寄存器值)就保存在这个栈顶。
volatile StackType_t * pxTopOfStack;

// 2. 任务状态链表项
// 用于将TCB插入到不同的状态列表中(如就绪列表、阻塞列表、挂起列表)。
ListItem_t xStateListItem;

// 3. 事件链表项
// 用于将TCB插入到事件列表(如队列、信号量)中,当任务因等待事件而阻塞时使用。
ListItem_t xEventListItem;

// 4. 任务优先级
UBaseType_t uxPriority;
// 指向任务栈的起始位置(用于检查栈溢出)
StackType_t * pxStack;
// 5. 任务名称(便于调试)
char pcTaskName[ configMAX_TASK_NAME_LEN ];

// 6. 用于调试和栈溢出检测的标记
#if ( configCHECK_FOR_STACK_OVERFLOW > 1 )
uint16_t usStackCheck;
#endif

// 7. 任务通知(Task Notifications)相关变量
// 每个任务自带一个通知值,可以模拟轻量级的信号量、事件标志等。
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue; // 通知值
volatile uint8_t ucNotifyState; // 通知状态(eNoAction, eWaitingNotification, eNotified)
#endif

// 8. 标签值(用于调试或用户自定义)
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif

// 9. 互斥量优先级继承相关
// 当任务持有互斥量时,可能会发生优先级继承,这个变量记录它原本的优先级。
UBaseType_t uxBasePriority;
// 同样用于优先级继承,记录该任务持有的互斥量数量。
UBaseType_t uxMutexesHeld;

// 10. 运行时间统计相关
#if ( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter;
#endif

} tskTCB;

// 将 tskTCB 重命名为更常用的 TCB_t
typedef tskTCB TCB_t;

线程栈

RTOS中各线程具有独立的栈,当进行线程切换时,会将当前线程的上下文、局部变量、函数调用信息(返回地址、帧指针)存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复

线程状态

  • 就绪态:任务已经准备就绪,只是当前CPU正在运行更高优先级或同优先级的另一个任务。任务在就绪列表中排队等待
  • 运行态:任务正在CPU上实际执行。在单核CPU中,任何时刻都只有一个任务处于此状态
  • 阻塞态:任务正在等待某个事件发生。处于此状态的任务不会被调度器考虑,因此不会消耗任何CPU时间
  • 挂起态:任务被强制性地暂停,对调度器“不可见”。它不会被调度,也不会被任何事件唤醒。只有通过其他任务调用 vTaskResume()xTaskResumeFromISR() 才能将其唤醒
  • 删除态:已删除,等待清理

线程优先级

  • FreeRTOS中,数值越大,优先级越高
  • 调度器总是选择处于就绪状态的、优先级最高的任务来运行

系统线程

系统线程通常指的是由内核本身创建和管理的任务,它们运行在后台,为应用程序任务提供关键的服务

空闲任务

空闲任务是系统创建的最低优先级的线程,线程状态永远为就绪态。当系统中无其他就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起

  • 创建时机:在 vTaskStartScheduler() 被调用时自动创建
  • 优先级:固定为 0 (tskIDLE_PRIORITY),这是最低优先级
  • 作用:
    • 后台任务:当没有任何用户任务或更高优先级的系统任务需要运行时,调度器就会运行空闲任务。它本质上是一个“永不返回”的 while(1) 循环,确保CPU永远有事情做
    • 清理任务:负责清理被 vTaskDelete() 删除的任务所占用的内存(TCB和栈空间)。这就是为什么删除任务后必须运行空闲任务才能真正释放内存
    • 钩子函数 (Hook Function):允许用户向空闲任务注入自定义代码
      • 通过设置 configUSE_IDLE_HOOK 为 1,并实现 void vApplicationIdleHook( void ) 函数。
      • 注意事项:钩子函数中不能调用任何会导致任务阻塞的API(如 vTaskDelay(), xQueueReceive()),因为这会改变空闲任务的行为,可能导致系统崩溃。
    • 低功耗处理:是实现 Tickless Idle 模式的关键。当系统进入低功耗模式时,空闲任务会计算下一个要处理的事件的时间,并设置硬件定时器在那时唤醒,从而在等待期间完全停止系统节拍时钟(Tick),极大降低功耗

定时器服务任务

这个任务用于处理 FreeRTOS 的软件定时器

  • 创建时机:并非默认创建。只有在 FreeRTOSConfig.h 中将 configUSE_TIMERS 设置为 1 时,才会在调度器启动时自动创建
  • 优先级:由 FreeRTOSConfig.h 中的 configTIMER_TASK_PRIORITY 定义
  • 作用:
    • 用户调用软件定时器API(如 xTimerStart(), xTimerStop())时,这些调用实际上只是向定时器服务任务的命令队列发送了一条消息
    • 定时器服务任务在后台接收并处理这些命令消息,执行实际的启动、停止、重置定时器等操作,并在定时器超时后执行其回调函数
    • 这种“代理”机制使得定时器API可以在中断服务程序(ISR)中安全调用(使用 FromISR 版本),因为发送到队列的操作是中断安全的

面试题

1.TCB在FreeRTOS中有什么作用

  • 存储任务的上下文、状态、属性(优先级、名称)、实现任务调度(挂到调度器维护的链表中)

2.FreeRTOS中的任务优先级反转问题是什么?如何解决

  • 问题描述:低优先级任务(L)占用某个共享资源(如互斥锁 Mutex),导致高优先级任务(H)被迫等待,而中优先级任务(M)抢占 CPU,导致 H 被长时间阻塞
  • 解决办法:
    • 优先级继承:当低优任务持有锁并阻塞高优任务时,临时继承高任务的优先级,直到释放锁为止
    • 优先级天花板:每个资源预设一个“上限优先级”,任何任务拿锁后立即提升到该上限

3.在FreeRTOS中,如何实现任务的挂起和恢复

  • vTaskSuspend() / vTaskResume()

4.FreeRTOS中的软件定时器是如何实现的

  • FreeRTOS 在启动时自动创建一个定时器服务任务,这个任务负责管理所有软件定时器的到期检查和回调函数触发。用户调用软件定时器的相关API实际上是给该任务发一些命令

5.FreeRTOS中的任务状态转换图是怎样的

6.FreeRTOS中如何实现动态任务创建和删除

  • xTaskCreate:动态创建
  • xTaskCreateStatic:静态创建

线程通信

定义:线程通信是指操作系统中多个任务之间进行数据交换与事件同步的机制

  • 线程同步:多个线程通过特定的机制(如互斥量,事件对象,临界区)来控制线程之间的执行顺序。线程的同步方式有很多种,其核心思想都是:在访问临界区的时候只允许一个线程运行
  • 线程互斥:对于临界区资源访问的排它性(任何时刻最多只允许一个线程去使用),线程互斥可以看成是一种特殊的线程同步

FreeRTOS支持以下几种线程通信机制

通信方式 功能类型 典型用途 特点 适用场景
队列(Queue) 数据传递 + 同步 传递消息、命令、结构体 FIFO;内存开销大 复杂数据交互,多对多通信
二值信号量(Binary Semaphore) 同步 任务唤醒、中断通知任务 值只能为 0 或 1;不传递数据 事件触发(如外设中断唤醒任务)
计数信号量(Counting Semaphore) 同步 + 资源计数 管理多个相同资源 值可大于 1;不传递数据 多任务共享有限数量的资源
互斥量(Mutex) 互斥访问 共享外设、全局变量保护 带优先级继承,防止优先级反转;ISR中无法使用 多任务竞争访问同一资源
事件组(Event Group) 多事件同步 等待单个或多个事件满足 位图方式;能同时等待多个事件 多条件触发任务执行(如等待多个传感器信号)
流缓冲区(Stream Buffer) 数据传递(字节流) 串口/传感器数据流 单生产者单消费者;高效 连续字节流传输(UART、ADC 数据)
消息缓冲区(Message Buffer) 数据传递(消息块) 命令/报文传输 单生产者单消费者;支持消息边界 单任务向另一任务传输完整消息

面试题

1.FreeRTOS中如何实现任务间的消息传递

  • 用消息队列(它是线程安全的,内部已经加了同步机制)
1
2
3
4
5
6
7
QueueHandle_t xMsgQueue = xQueueCreate(10, sizeof(int)); // 存储10个int类型消息

int value = 42;
xQueueSend(xMsgQueue, &value, portMAX_DELAY); // 发送消息,无限等待

int received;
xQueueReceive(xMsgQueue, &received, portMAX_DELAY); // 接收消息

2.FreeRTOS中如何使用信号量?信号量的类型有哪些

  • 二值信号量
  • 计数信号量

3.FreeRTOS的消息队列机制是如何工作的

  • 内部是环形队列,存储数据的副本而不是指针
  • 通过任务调度器保证原子操作,当队列空/满时,任务可阻塞等待

4.如何在FreeRTOS中处理任务间的共享资源

  • 互斥锁
  • 临界区(关闭中断从而避免任务切换)

5.FreeRTOS的任务通知机制是什么?如何使用

  • 任务通知是轻量级的任务间通信方式,比队列/信号量更高效(对应字段直接嵌在TCB里面,无需创建额外对象)
  • 每个任务的TCB有2个核心变量:ulNotifiedValueucNotifyState,支持以下操作:
    • 设置/清除通知位(类似信号量)
    • 更新通知值(类似队列)
    • 阻塞等待通知
1
2
3
4
5
6
// 任务1:发送通知
xTaskNotify(xTask2Handle, 0x01, eSetBits);

// 任务2:等待通知
uint32_t notifyVal;
xTaskNotifyWait(0x00, 0xFF, &notifyVal, portMAX_DELAY);

调度机制

调度算法 配置方式 特点 优点 缺点 应用场景
优先级抢占式 configUSE_PREEMPTION = 1 高优先级随时抢占低优先级 实时性强 可能出现低优先级任务“饿死” 实时系统
协作式 configUSE_PREEMPTION = 0 任务主动让出 CPU 简单、可预测 一个任务卡住会影响全局 任务少、调试验证
时间片轮转 configUSE_TIME_SLICING = 1 同优先级任务轮流执行 公平 实时性较弱 多任务公平性需求
FIFO configUSE_TIME_SLICING = 0 同优先级的任务不会自动轮转,只有当前任务阻塞/挂起时才会切到下一个

调度触发条件

  • 任务创建/删除:需要重新选择最高优先级任务
  • 任务阻塞/解锁:比如 vTaskDelay() 或信号量/队列释放
  • 中断发生:中断服务程序可能唤醒高优先级任务(使用 portYIELD_FROM_ISR()
  • 时间片耗尽:如果配置了 configUSE_TIME_SLICING,同优先级任务轮流执行

相关数据结构

  • 就绪任务列表:按优先级划分,每个优先级有一个链表
  • 任务控制块:保存任务的栈指针、状态等
  • 全局变量 pxCurrentTCB:指向当前正在运行的任务

面试题

1.你知道那些实时操作系统的调度算法

  • 抢占/协作,RR,FIFO

2.FreeRTOS的时间片轮转调度是如何工作的

  • 内核会把同优先级的任务挂在一个双向链表,当Systick发生时,检查当前任务的时间片是否用完,用完的话就把此任务放到队尾,然后切换到另一个任务

3.FreeRTOS的任务调度是如何影响系统性能的

  • 内存开销:每个任务要分配独立的TCB和栈空间
  • 上下文切换开销
  • 中断开销:定时触发中断,消耗CPU资源

4.任务切换的完整流程

(1) SysTick 中断(优先级较高)

  • 触发时机:由硬件定时器周期性触发(如每 1ms)
  • 核心职责:
    1. 更新系统时钟:递增 xTickCount,处理任务延迟列表
    2. 检查任务切换需求:
      • 判断当前任务的时间片是否耗尽(时间片轮转调度)
      • 检查是否有更高优先级任务就绪(抢占式调度)
    3. 触发 PendSV 中断:若需要任务切换,设置 PendSV 挂起位
  • 关键特点:
    • 不执行上下文切换,仅做逻辑判断
    • 执行速度快,避免长时间阻塞其他中断

(2) PendSV 中断(优先级最低)

  • 触发时机:由 SysTick 或其他调度逻辑(如 vTaskYield())手动挂起
  • 核心职责:
    1. 执行完整的上下文切换:
      • 保存当前任务的寄存器状态(压栈)
      • 从待运行任务的栈中恢复寄存器
    2. 切换任务栈指针:将 PSP指向新任务的栈
  • 关键特点:
    • 纯硬件上下文切换,不涉及调度逻辑
    • 优先级最低,确保其他中断能即时响应

内存管理

FreeRTOS 内核本身不依赖外部的 malloc/free,而是通过内存管理实现文件heap_x.c 实现堆管理

所有动态内存分配都通过两个接口完成:

  • 动态分配内存:pvPortMalloc(size_t xSize)
  • 释放内存:vPortFree(void *pv)
方案 特点 适用场景
heap_1.c 仅分配,不释放(静态分配) 无动态释放需求的简单系统
heap_2.c 支持分配和释放,但不合并碎片 小型系统,内存释放后无碎片问题
heap_3.c 封装标准库的 malloc()free() 已有成熟堆管理的系统(如 Linux 移植)
heap_4.c 支持分配、释放及碎片合并(最佳适配算法) 最常用,适用于大多数嵌入式系统
heap_5.c 支持多块非连续内存区域(如 SRAM + SDRAM) 复杂内存布局的系统(如外部 SDRAM)

1.heap_1.c

  • 最简单,只支持分配,不支持释放
  • 内部就是一个全局数组 ucHeap[configTOTAL_HEAP_SIZE],每次分配直接从这个数组里“往后挪指针”
  • 适用场景:系统初始化阶段一次性创建所有任务和对象,运行过程中不再动态分配
  • 优点:极小开销、确定性强
  • 缺点:内存不能释放,会导致浪费

2.heap_2.c

  • 使用首次适配(First Fit)算法进行内存管理,支持分配和释放,但不合并相邻空闲块
  • 使用空闲块链表管理内存,分配时遍历链表寻找可用块,返回第一个找到的满足要求的内存块
  • 适用场景:需要动态创建/删除任务或对象,但内存碎片不严重
  • 优点:比 heap_1 灵活
  • 缺点:可能产生内存碎片

3.heap_3.c

  • 直接封装标准库的 malloc()free()

  • 最简单,但实时性和可移植性较差

  • 适用场景:有可靠的 malloc/free 实现(如带 MMU 的系统),对确定性要求不高

  • 优点:几乎零工作量

  • 缺点:不可预测,移植性差

4.heap_4.c

  • 使用最佳适配(Best Fit)算法进行内存管理,每次从空闲链表中选择能满足需求的最小空闲块,并引入了合并相邻空闲块的功能,减少内存碎片
  • 适用场景:动态分配和释放频繁的系统
  • 优点:灵活,碎片管理好
  • 缺点:遍历链表开销大,分配时间不确定

5.heap_5.c

  • 类似 heap_4,但支持多个非连续内存区域

  • 适合在系统中存在多块 RAM 的情况(比如内部 SRAM + 外部 SDRAM)

  • 用户需要提供各内存区域的地址和大小

  • 优点:适合多段内存环境

  • 缺点:实现稍复杂

面试题

1.FreeRTOS的内存分配策略是什么

2.FreeRTOS的内存保护机制是如何实现的

  • Cortex-M的MCU带有MPU,FreeRTOS提供MPU 移植层,允许为任务分配受保护的内存区域

3.FreeRTOS中的任务栈溢出检测是如何工作的

(1) 方法一:栈填充模式(Stack Fill Pattern)​​

原理​​:在任务创建时,用特定值(如 0xA5A5A5A5)填充栈的未使用部分。运行时定期检查这些值是否被修改

  • 配置​​:define configCHECK_FOR_STACK_OVERFLOW 1

  • 检测时机​​:任务切换时检查栈顶区域

  • 优点​​:开销小,适用于大多数场景

  • 缺点​​:只能检测​​已发生的溢出​​,无法预防

(2) 方法二:栈指针边界检查(Stack Pointer Bound Check)​​

原理​​:在任务创建时记录栈的合法范围(pxStack和 pxEndOfStack),每次任务运行时检查当前栈指针是否越界

  • 配置​​:define configCHECK_FOR_STACK_OVERFLOW 2

  • 检测时机​​:每次任务切换和系统调用时检查SP

  • 优点​​:能​​实时捕获​​溢出(如递归调用过深导致的溢出)

  • 缺点​​:增加上下文切换的开销

4.有哪些物理内存管理算法

算法名称 工作原理 优点 缺点 应用场景
首次适应 (First Fit) 从空闲链表头部开始查找,分配第一个满足大小的空闲块 分配速度快,实现简单 容易产生外部碎片 FreeRTOS heap_2.c
最佳匹配 (Best Fit) 遍历整个空闲链表,分配能满足需求的最小空闲块 内存利用率较高 速度慢,可能产生微小碎片 FreeRTOS heap_4.c
最差匹配 (Worst Fit) 总是分配最大的空闲块 减少微小碎片产生 大块内存容易被拆分 特殊场景(如长期运行的大块分配)
伙伴系统 (Buddy System) 将内存划分为2的幂次方大小块,合并时只能与“伙伴”合并 碎片少,合并效率高 内存浪费(内部碎片) Linux 内核物理内存管理
分离空闲链表 (Segregated Free Lists) 按大小分类维护多个空闲链表(如 16B、32B、64B…) 分配速度快,减少搜索时间 需要预分配固定大小分类 TLSF(实时内存分配器)