前言
本篇文章将带大家深入学习任务的创建和分析任务调度的机制。
一、深入理解任务的创建
创建任务函数原型:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */ const configSTACK_DEPTH_TYPE usStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask )
这里只讲解几个比较重要的参数,其他参数不态清楚的同学可以去看之前的文章:
usStackDepth:任务栈的大小
每一个任务都需要有自己的栈,用来保存寄存器的值和局部变量等。
在FreeRTOS中会使用pvPortMalloc来申请栈,大小为传入的usStackDepth * 4字节。
StackType_t * pxStack; /* Allocate space for the stack used by the task being created. */ pxStack = pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) );
栈分配的大小由局部变量和调用深度确定。
TaskHandle_t:TCB控制块
精简后的TCB任务控制块:
typedef struct tskTaskControlBlock /* The old naming convention is used to prevent breaking kernel aware debuggers. */ { volatile StackType_t * pxTopOfStack; /*< Points to the location of 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 char pcTaskName[ configMAX_TASK_NAME_LEN ]; } tskTCB;
TCB控制块中保存着任务的重要信息:
pxTopOfStack:这个参数指向任务堆栈的最顶部,即最近放入任务堆栈的项目的位置。这必须是 TCB 结构的第一个成员。
ListItem_t xStateListItem:这是一个用于任务状态管理的链表项。它用于将任务插入到就绪、阻塞或挂起状态链表中,以便操作系统可以有效地管理任务状态。
ListItem_t xEventListItem:这是用于将任务插入到事件列表中的链表项。当任务等待某个事件发生时,它会被插入到事件列表中。这允许任务在事件发生时被及时唤醒。
UBaseType_t uxPriority:这是任务的优先级。任务的优先级用于决定它在多任务系统中的调度顺序。较低的数值表示更高的优先级,0通常是最低优先级。
StackType_t * pxStack:这个参数指向任务堆栈的起始位置。任务堆栈是用于保存任务上下文信息的内存区域,包括寄存器值、局部变量等。
char pcTaskName[configMAX_TASK_NAME_LEN]:这个数组用于保存任务的名称,以便在调试和诊断中使用。configMAX_TASK_NAME_LEN 是一个配置参数,定义了任务名称的最大长度。
那么这里就有一个疑问了:创建任务中的函数,和任务中的参数保存到哪里去了。
创建任务中的函数其实就是一个函数指针也就是一个地址,当创建任务时PC会保存函数的地址,当任务被调用时,立刻从PC中取出地址跳转到函数中执行。
参数会保存在R0寄存器中。
二、任务的调度机制
1.FreeRTOS中任务调度的策略
在FreeRTOS中任务的调度支持 可抢占
和时间片轮转
。
可抢占:
在可抢占式调度中,任务可以被更高优先级的任务抢占。当一个高优先级任务变得可用时,它可以打断当前正在执行的低优先级任务,从而使系统立即切换到高优先级任务执行。
在FreeRTOS中,任务调度是基于任务优先级的。当一个任务抢占另一个任务时,它会立即执行,无论被抢占的任务是否已经执行完其时间片。这种方式确保了高优先级任务能够及时响应,并在需要时立即执行,不受低优先级任务的阻碍。
在FreeRTOS中通过配置configUSE_PREEMPTION
来决定是否启动抢占。
时间片轮转:
时间片轮转是指操作系统为每个任务分配一个时间片,即预定义的时间量。在时间片轮转调度方式下,每个任务可以执行一个时间片,然后系统将控制权移交给下一个就绪的任务。如果一个任务在其时间片结束前没有完成,系统会暂停该任务,将控制权交给下一个就绪的任务。
FreeRTOS允许你在配置系统时启用或禁用时间片轮转。时间片的大小可以根据应用程序的需要进行调整。这种调度方式有助于确保任务之间的公平性,避免某些任务长时间占用处理器,同时允许多个任务分享处理时间。
在FreeRTOS中通过配置configUSE_TIME_SLICING
来决定是否启动时间片轮转。
组合应用
在FreeRTOS中,可抢占和时间片轮转调度方式可以结合使用。这样可以实现灵活的任务管理,确保高优先级任务能够抢占低优先级任务,并且为任务提供公平的处理器时间,从而有效地管理系统资源。
2.FreeRTOS任务调度策略实现的核心
在 FreeRTOS 中,任务管理使用就绪链表、阻塞链表和挂起链表来管理任务的状态和调度。这些链表用于维护不同状态的任务列表。让我们逐一了解它们:
1.就绪链表(Ready List)
就绪链表包含所有处于就绪状态的任务。就绪状态的任务是指已经准备好运行,但由于当前执行的任务正在占用 CPU 资源,它们暂时无法立即执行。这些任务按照优先级被组织在就绪链表中。当当前正在执行的任务释放 CPU(例如,由于时间片用完、任务阻塞或挂起等原因)时,调度器从就绪链表中选择优先级最高的任务来执行。
2.阻塞链表(Blocked List)
阻塞链表包含那些由于某种原因而无法立即执行的任务。这些原因可能包括等待某个事件、资源不可用、延时等情况。当任务处于阻塞状态时,它们不会被调度器所执行。这些任务会在特定条件满足之后重新放入就绪链表,等待调度器选择其执行。
3.挂起链表(Suspended List)
挂起链表包含已被显式挂起的任务。当任务被挂起时,它们暂时停止运行,不再参与调度。这些任务不会出现在就绪链表或阻塞链表中,因为它们被明确地挂起,不参与任务调度。
在 FreeRTOS 中,任务的状态转换是动态的。任务可以从就绪状态变为阻塞状态或挂起状态,然后再返回到就绪状态。这些状态的变化取决于任务的执行和系统中的事件。管理任务状态的链表是 FreeRTOS 在调度和管理任务时使用的数据结构。这些链表确保了任务的有效调度和管理,以满足实时系统的要求。
这些链表是 FreeRTOS 内部任务管理的一部分,并且开发者可以通过 FreeRTOS 提供的 API 函数来管理和操作任务的状态以及链表中的任务。
3.FreeRTOS内部链表源码解析
FreeRTOS中使用下面的链表来管理任务的调度:
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; /*< Prioritised ready tasks. */ PRIVILEGED_DATA static List_t xDelayedTaskList1; /*< Delayed tasks. */ PRIVILEGED_DATA static List_t xDelayedTaskList2; /*< Delayed tasks (two lists are used - one for delays that have overflowed the current tick count. */ PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList; /*< Points to the delayed task list currently being used. */ PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList; /*< Points to the delayed task list currently being used to hold tasks that have overflowed the current tick count. */ PRIVILEGED_DATA static List_t xPendingReadyList;
pxReadyTasksLists:
这是一个数组,包含了多个链表,其数量等于configMAX_PRIORITIES,它用于存储处于就绪状态的任务。每个链表对应一个优先级,因此,数组中的每个元素存储了同一优先级的就绪任务。当任务准备好运行时,它将被添加到适当优先级的链表中,以等待被调度器选中执行。
xDelayedTaskList1 和 xDelayedTaskList2:
这两个链表用于存储被延时挂起的任务。通常,xDelayedTaskList1 包含所有未溢出的延时任务,而 xDelayedTaskList2 用于存储延时已经溢出的任务。这种设计允许 FreeRTOS 处理不同时间范围内的延时任务。延时任务在指定的时间段内不会被执行,而是在延时到期后再被移到就绪链表。
pxDelayedTaskList 和 pxOverflowDelayedTaskList:
这两个指针变量用于指向当前使用的延时任务链表。通常,pxDelayedTaskList 指向 xDelayedTaskList1 或 xDelayedTaskList2 中的一个,具体取决于当前的延时情况。这些链表用于存储不同时间范围内的延时任务。
xPendingReadyList:
这个链表用于存储在调度器被挂起时已经准备好运行的任务。当调度器处于挂起状态时,如果有任务变为就绪状态,它们将被添加到这个链表中。当调度器被恢复时,这些任务将被移动到适当的 pxReadyTasksLists 中,以等待被调度执行。
4.如何通过就绪链表管理任务的执行顺序
在创建任务时会通过prvAddNewTaskToReadyList函数将任务添加进入就绪链表。
在创建任务时当新创建的任务优先级大于或者等于当前任务优先级时,pxCurrentTCB当前任务指针指向pxNewTCB新添加任务的指针。
在prvAddNewTaskToReadyList函数中通过prvAddTaskToReadyList函数将不同优先级的任务添加进入不同的就绪链表当中:
vListInsertEnd函数会将新创建的任务添加到当前就绪链表的最后一项。
下面我们举一个例子验证上述代码:
void vTask1( void *pvParameters ) { /* 任务函数的主体一般都是无限循环 */ for( ;; ) { flagIdleTaskrun = 0; flagTask1run = 1; flagTask2run = 0; flagTask3run = 0; /* 打印任务的信息 */ printf("T1\r\n"); } } void vTask2( void *pvParameters ) { /* 任务函数的主体一般都是无限循环 */ for( ;; ) { flagIdleTaskrun = 0; flagTask1run = 0; flagTask2run = 1; flagTask3run = 0; /* 打印任务的信息 */ printf("T2\r\n"); } } void vTask3( void *pvParameters ) { const TickType_t xDelay5ms = pdMS_TO_TICKS( 5UL ); /* 任务函数的主体一般都是无限循环 */ for( ;; ) { flagIdleTaskrun = 0; flagTask1run = 0; flagTask2run = 0; flagTask3run = 1; /* 打印任务的信息 */ printf("T3\r\n"); // 如果不休眠的话, 其他任务无法得到执行 //vTaskDelay( xDelay5ms ); } } xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL); xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL); xTaskCreate(vTask3, "Task 3", 1000, NULL, 1, NULL);
运行结果:
运行的结果是任务3先运行。
根据上述代码分析可以画出一个图来表示:
首先运行Task3:
第二运行Task1:
第三运行Task2:
三、一个任务能够运行多久
1.高优先级任务可抢占低优先级任务一直运行
void vTask1( void *pvParameters ) { /* 任务函数的主体一般都是无限循环 */ for( ;; ) { flagIdleTaskrun = 0; flagTask1run = 1; flagTask2run = 0; flagTask3run = 0; /* 打印任务的信息 */ printf("T1\r\n"); } } void vTask2( void *pvParameters ) { /* 任务函数的主体一般都是无限循环 */ for( ;; ) { flagIdleTaskrun = 0; flagTask1run = 0; flagTask2run = 1; flagTask3run = 0; /* 打印任务的信息 */ printf("T2\r\n"); } } void vTask3( void *pvParameters ) { const TickType_t xDelay5ms = pdMS_TO_TICKS( 5UL ); /* 任务函数的主体一般都是无限循环 */ for( ;; ) { flagIdleTaskrun = 0; flagTask1run = 0; flagTask2run = 0; flagTask3run = 1; /* 打印任务的信息 */ printf("T3\r\n"); // 如果不休眠的话, 其他任务无法得到执行 //vTaskDelay( xDelay5ms ); } } xTaskCreate(vTask1, "Task 1", 1000, NULL, 2, NULL); xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL); xTaskCreate(vTask3, "Task 3", 1000, NULL, 1, NULL);
运行结果:
当把configUSE_PREEMPTION配置为了1时,如果高优先级任务不主动释放CPU,那么其他低优先级的任务将无法执行。
2.相同优先级的任务遵循时间片轮转
当配置了configUSE_TIME_SLICING为1时,相同优先级的任务将轮流执行一个Tick的时间。
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL); xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL); xTaskCreate(vTask3, "Task 3", 1000, NULL, 1, NULL);
运行结果:
四、FreeRTOS中任务如何释放CPU
1.任务主动让出CPU:
任务可以调用vTaskDelay()函数或者vTaskDelayUntil()函数,将自己挂起一段时间,以便其他任务能够运行。这种方式是任务主动放弃CPU的一种方式。
2.阻塞等待事件:
任务可以调用FreeRTOS提供的阻塞函数,如xQueueReceive()、xSemaphoreTake()等,来等待特定事件的发生。当任务在等待某个事件时,它会被置于阻塞状态,从而释放CPU,直到事件发生后才会被唤醒。
3.时间片轮转:
如果使用了时间片轮转调度策略,任务会在其时间片用尽时自动释放CPU,允许其他任务运行。时间片轮转是一种公平分配CPU时间的策略,每个任务都有一个小的时间片来执行,然后被放回就绪队列,等待下一次执行。
4.任务进入阻塞状态:
任务在执行过程中,如果发生某些阻塞事件,如等待一个队列满足条件、等待互斥信号量等,会自动进入阻塞状态,这时会释放CPU。一旦阻塞条件得到满足,任务将被重新置于就绪状态。
总结
本篇文章深入的讲解了任务创建的内部实现和任务调度的源代码分析和实现,学习这篇文章有助于更深入的学习FreeRTOS的源码。