目录
前言
在嵌入式系统开发中,理解任务调度机制、临界区概念以及内存管理算法的运作原理至关重要。这些概念不仅是构建高效、可靠系统的基础,也直接影响到系统的实时性、资源利用率和响应能力。本文将深入探讨这些关键技术,并探索它们在实际应用中的运用和优化策略。
目标
- 了解任务调度机制
- 了解临界区概念
- 了解内存管理算法优缺点
内容
任务控制块
在tasks中,有一段结构体的定义如下,其作为xTaskCreate
的最后一个参数,我们可以得到任务相关的信息。
/*
* Task control block. A task control block (TCB) is allocated for each task,
* and stores task state information, including a pointer to the task's context
* (the task's run time environment, including register values)
*/
typedef struct tskTaskControlBlock /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
volatile StackType_t * pxTopOfStack; /*< Points to the location of the last item placed on the tasks stack. THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */
ListItem_t xStateListItem; /*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */
ListItem_t xEventListItem; /*< Used to reference a task from an event list. */
UBaseType_t uxPriority; /*< The priority of the task. 0 is the lowest priority. */
StackType_t * pxStack; /*< Points to the start of the stack. */
char pcTaskName[ configMAX_TASK_NAME_LEN ]; /*< Descriptive name given to the task when created. Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
...
} tskTCB;
结构体 tskTaskControlBlock 是任务控制块,为每个任务分配一个任务控制块,存储任务状态信息,包括指向任务上下文的指针(任务的运行时环境,包括寄存器值)。
该结构体包括以下成员:
volatile StackType_t * pxTopOfStack
:指向任务堆栈上最后一个放置的项目的位置。这必须是 TCB 结构体的第一个成员。ListItem_t xStateListItem
:任务状态列表项所引用的列表表示任务的状态(就绪、阻塞、挂起)。ListItem_t xEventListItem
:用于从事件列表中引用任务。UBaseType_t uxPriority
:任务的优先级。0 是最低优先级。StackType_t * pxStack
:指向堆栈的起始位置。char pcTaskName[configMAX_TASK_NAME_LEN]
:在创建任务时赋予任务的描述性名称。仅用于便于调试
任务调度机制
任务调度机制,是一种任务调度的策略,如果简单的片面理解,其实就是一句代码:
vTaskStartScheduler();
当然,这句代码内部做了什么事情,怎么实现任务调度的,才是任务调度机制的内核。
任务调度机制按照代码流水账式阅读,大概干了以下事情:
- 创建空闲任务
- 如果使能软件定时器,则创建定时器任务
- 关闭中断,防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断
- 初始化全局变量,并将任务调度器的运行标志设置为已运行
- 初始化任务运行时间统计功能的时基定时器
- 调用函数 xPortStartScheduler()
总的核心在xPortStartScheduler
内部,流水账式的看,大概做了:
- 优先级配置
- 初始化timer中断
- 启动第一个任务
- 任务上下文切换
总结起来看,任务调度机制,主要做的事情有:
- 任务优先级:每个任务都有一个唯一的优先级,优先级值越低,表示任务优先级越高。在任务创建时,可以指定任务的优先级。
- 任务就绪列表:FreeRTOS 会维护一个就绪列表,其中包含所有就绪(可执行)状态的任务。就绪列表按照任务的优先级进行排序。
- 调度器:FreeRTOS 调度器负责根据任务的优先级选择下一个要执行的任务。调度器会选择就绪列表中优先级最高的任务进行执行。
- 任务切换:当一个任务不再是最高优先级任务时,调度器会进行任务切换,将当前任务挂起并切换到下一个要执行的任务。任务切换是通过上下文切换来实现的,它保存当前任务的上下文,并恢复下一个任务的上下文。
- 抢占式调度:如果有更高优先级的任务进入就绪状态,FreeRTOS 调度器会立即抢占当前任务,并切换到更高优先级的任务。这种调度方式称为抢占式调度,它确保了高优先级任务能够及时得到执行。
- 时间片轮转调度(可选):FreeRTOS 还提供了时间片轮转调度的功能。通过配置选项,可以为任务启用时间片轮转调度算法,使任务在同一优先级中轮流获取执行时间片。这可以确保在同一优先级任务中,任务能够公平地共享 CPU 时间。
总的来说,FreeRTOS 的任务调度机制是基于优先级的抢占式调度。它允许高优先级任务抢占低优先级任务的执行,并确保任务按照优先级顺序得到执行。这样可以实现实时性要求和任务间的优先级关系,确保系统能够及时响应关键任务。
一些名词:
- TCB:
Task Control Block
任务控制块,也就是任务栈的区块。 - PSP:
Process Stack Pointer
PSP 是任务堆栈指针,用于保存任务的上下文信息。每个任务都有自己的 PSP,用于保存任务的寄存器值、局部变量和函数调用信息等。任务切换时,当前任务的上下文会保存到其对应的 PSP,然后切换到下一个任务的 PSP。任务切换可以由任务调度器或中断处理程序触发。 - MSP:
Main Stack Pointer
MSP 是 Cortex-M 处理器的主堆栈指针,用于保存中断处理和异常处理期间的堆栈帧。当系统初始化时,MSP 被设置为默认的堆栈起始地址,也是全局堆栈的起始地址。当发生中断或异常时,处理器会自动切换到 MSP,并将相关的上下文保存到 MSP 所指向的堆栈空间。
任务切换时,PSP 和 MSP 的使用方式如下:
- 当一个任务被创建时,它的初始堆栈指针(PSP)被设置为任务的堆栈顶部地址。
- 当任务正在执行时,它的堆栈指针(PSP)指向任务的堆栈空间,该空间用于保存任务的上下文信息。
- 当任务切换发生时,当前任务的上下文会保存到其对应的 PSP,然后从下一个任务的 PSP 恢复上下文,并开始执行下一个任务。
需要注意的是,PSP 和 MSP 是 Cortex-M 处理器的特定寄存器,并由处理器硬件自动管理。FreeRTOS 通过使用这些寄存器来实现任务的上下文切换和堆栈管理,以实现多任务调度的功能。在任务编程中,通常不需要直接操作这些寄存器,而是通过 FreeRTOS 提供的 API 和调度器来进行任务的创建、切换和管理。
临界区
语法格式
taskENTER_CRITICAL();
// 任务创建代码
taskEXIT_CRITICAL();
在 FreeRTOS 中,临界区是一种机制,用于保护共享资源的访问,防止多个任务同时访问和修改共享资源而引发竞态条件。
FreeRTOS 提供了两种方式实现临界区:
- 任务间的临界区:通过使用任务间的临界区宏来限制在当前任务中执行的代码片段。这样,当任务执行到临界区时,FreeRTOS 将禁止其他具有相同或更低优先级的任务抢占当前任务,从而保证临界区代码的原子性。常用的任务间临界区宏包括:
taskENTER_CRITICAL()
:进入临界区。禁用调度器,阻止其他任务抢占当前任务。taskEXIT_CRITICAL()
:退出临界区。启用调度器,允许其他任务抢占当前任务。
示例用法:
taskENTER_CRITICAL();
// 临界区代码
taskEXIT_CRITICAL();
- 中断服务例程(ISR)中的临界区:在中断服务例程中,通过使用中断服务例程临界区宏来保护共享资源的访问。中断服务例程的临界区宏与任务间的临界区宏类似,具有类似的功能和用法。常用的中断服务例程临界区宏包括:
portENTER_CRITICAL()
:进入中断服务例程的临界区。禁用任务调度器和其他中断,保证临界区代码的原子性。portEXIT_CRITICAL()
:退出中断服务例程的临界区。恢复任务调度器和其他中断的正常运行。
示例用法:
void ISR_Handler()
{
portENTER_CRITICAL();
// 临界区代码
portEXIT_CRITICAL();
}
使用临界区可以确保在访问共享资源时的原子性和可靠性,避免竞态条件和数据不一致的问题。但需要注意,临界区的使用应当尽量保持简短,避免在临界区中执行复杂或耗时的操作,以减少系统的响应时间和提高并发性能。同时,要合理地选择临界区的粒度和范围,以平衡保护共享资源的需要和系统的实时性要求。
以下是一些情况下可能需要考虑在任务创建时加入临界区逻辑的情况:
- 任务创建期间需要访问和操作共享资源:如果在任务创建过程中需要访问共享资源,可以在任务创建之前进入临界区,以防止其他任务或中断干扰共享资源的正确初始化。
- 多任务环境下的任务创建同步:在多任务系统中,如果多个任务的创建具有依赖关系,需要确保任务按照特定的顺序创建,可以使用临界区来同步任务的创建过程,以保证任务创建的顺序和依赖关系。
- 避免竞争条件:如果在任务创建过程中存在可能引发竞争条件的情况,可以使用临界区来避免竞争条件的发生。例如,多个任务创建函数调用之间共享的全局变量可能会导致竞争条件,此时可以使用临界区来保护变量的访问。
需要注意的是,临界区的使用应该谨慎,并根据具体情况进行评估。过多地使用临界区可能导致系统响应性下降和任务调度效率降低。只在必要的情况下使用临界区,以确保正确的资源访问和任务创建顺序。
总结起来,不是每次创建任务都需要加入临界区逻辑,而是根据具体的应用需求和场景来决定是否需要在任务创建过程中使用临界区。
内存管理
内存管理算法,其实就是几个文件,heap_1.c
heap_2.c
heap_3.c
heap_4.c
heap_5.c
。我们默认选择的是heap_4.c
。下表是他们的对比:
算法 | 特点 | 应用场景 |
heap_1 | 最简单的内存管理方案,使用静态全局数组作为堆空间,内存块以字节为单位进行分配和释放,没有对齐要求。 | 适用于资源受限的嵌入式系统,特别是具有严格内存限制的应用。由于它的实现简单且占用的内存较少,适用于具有严格资源限制的小型设备。 |
heap_2 | 类似于heap_1,但在分配内存块时会对齐到4字节边界 | 适用于资源受限的系统,需要字节对齐的内存分配。适用于大多数嵌入式系统,特别是那些需要对齐访问的硬件设备。 |
heap_3 | 使用静态全局数组作为堆空间,使用位图来跟踪内存块的分配状态,支持字节对齐的内存块分配。 | 适用于资源受限的系统,需要字节对齐的内存分配。与heap_2相比,heap_3提供了更高级的内存分配功能,可以更有效地管理和利用堆空间。 |
heap_4 | 使用动态分配的内存作为堆空间,通过标准的malloc()和free()函数进行内存分配和释放。 | 适用于具有较大内存需求的应用,支持动态分配和释放内存。适用于大型嵌入式系统或应用,具有较高的灵活性和内存管理需求。 |
heap_5 | 使用自定义的内存分配器接口,允许用户根据需求自定义内存分配器的实现。 | 适用于需要高度定制的内存管理方案的应用。通过自定义内存分配器接口,用户可以根据具体的需求实现自己的内存管理策略,例如使用特定的内存分配算法或集成外部内存管理器。 |
选择合适的内存管理算法取决于应用的需求和系统的资源限制。如果系统资源非常有限,可以选择heap_1或heap_2。如果需要字节对齐的内存分配,可以选择heap_2或heap_3。如果需要动态分配和释放内存,可以选择heap_4。如果需要更高度定制的内存管理方案,可以选择heap_5,并根据具体需求自定义内存分配器的实现。
需要注意的是,选择适当的内存管理算法时应考虑以下因素:
- 系统资源限制:根据系统的内存大小和可用性选择合适的算法。如果系统资源非常有限,则应选择较小的算法。
- 内存对齐需求:如果应用需要进行字节对齐的内存分配,选择支持对齐的算法。
- 动态内存需求:如果应用需要动态分配和释放内存,选择支持动态内存管理的算法。
- 灵活性和定制性需求:如果应用需要高度定制的内存管理方案,可以选择提供自定义接口的算法。
根据应用的具体需求和系统的资源限制,选择合适的内存管理算法可以提高内存的利用率和系统的性能。
总结
任务调度机制是实现多任务协作的核心,它通过任务优先级和抢占式调度策略确保系统对高优先级任务的及时响应。通过任务控制块(TCB)管理任务状态和上下文切换,任务可以有效地共享处理器资源,同时保持系统的实时性和稳定性。
临界区的概念和实现允许任务在访问共享资源时保持数据的一致性和可靠性,避免竞态条件的发生。在多任务环境下,正确使用临界区宏可以有效地保护关键资源,提升系统的并发性能。
内存管理算法的选择取决于系统资源限制、对内存分配的对齐需求以及动态内存管理的灵活性。不同的算法适用于不同的应用场景,选择合适的内存管理策略可以优化系统的内存利用率,并支持复杂应用的开发和部署。
综上所述,深入理解和有效应用任务调度、临界区和内存管理算法,是构建高效、稳定嵌入式系统的关键步骤,也是开发者在设计和优化实时应用程序时必须掌握的核心技能。