我认为在任务管理功能是任何一个操作系统最精华的地方,能让所有任务有条不紊地使用资源,对于人而言,系统能及时响应我们的操作,多任务“并行”都离不开操作系统对任务的管理。操作系统是如何管理任务的呢?任务调度的策略又是什么?一个任务切换的背后需要做哪些事情?
在裸机程序中,是没有任务概念的。通常是写好程序,在一个死循环,没有优先级之分。在多任务系统(FreeRTOS)中,我们把任务定义成一个个功能独立,且实现在无法独立返回的死循环中,我们可以类比linux中的线程概念,在RTOS中任务也是最小的调度单元,因此每个任务都有自己独立的堆栈和上下文信息。我们看一下FreeRTOS中是如何实现任务这一概念的。
首先我们需要知道帧栈的概念:帧栈其是就是内存的一段空间,专门用于存储任务的局部变量、函数调用的返回地址、寄存器内容等,帧栈可以反映我们任务当前运行的详细信息。在后面创建任务的时候,我们需要指定这个内存区域的大小。
了解任务这一概念,我觉得先从创建任务的函数入手比较容易理解:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )这个函数是动态创建任务的API,至于静态创建是什么,了解即可,目前开发中大部分都是用动态创建。
我们先来看看参数:
void Task1()
{
while(1)
{
A();
B();
C();
}
}也就是说我们将任务逻辑实现在框架中,此时这个函数还不是一个真正的任务,因为它无法参与调度。我们必须调用xTaskCreate()函数将这个函数实现为任务。大概是下面这样子。
xTaskCreate( Task1,
"Test",
configMINIMAL_STACK_SIZE,
NULL,
mainCHECK_TASK_PRIORITY,
NULL );这样这个函数就成为了一个任务,一个有着堆栈空间、优先级、名称的任务。
到这里还是有个疑问,就是这个create函数究竟干了什么?从参数来看,内部肯定形成了个链接,再运行任务时,能够跳转到Task1,并且由于给定了帧栈大小,所以内部应该开辟了我们指定大小的内存空间,那么我们设定的优先级传递给谁了呢?带着这样的疑问,我们看下这个函数的内部实现:
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 )
{
TCB_t * pxNewTCB;
BaseType_t xReturn;
StackType_t * pxStack;
/* Allocate space for the stack used by the task being created. */
pxStack = pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e9079 All values returned by pvPortMalloc() have at least the alignment required by the MCU's stack and this allocation is the stack. */
if( pxStack != NULL )
{
/* Allocate space for the TCB. */
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); /*lint !e9087 !e9079 All values returned by pvPortMalloc() have at least the alignment required by the MCU's stack, and the first member of TCB_t is always a pointer to the task's stack. */
if( pxNewTCB != NULL )
{
memset( ( void * ) pxNewTCB, 0x00, sizeof( TCB_t ) );
/* Store the stack location in the TCB. */
pxNewTCB->pxStack = pxStack;
}
else
{
/* The stack cannot be used as the TCB was not created. Free
* it again. */
vPortFreeStack( pxStack );
}
}
else
{
pxNewTCB = NULL;
}
if( pxNewTCB != NULL )
{
#if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /*lint !e9029 !e731 Macro has been consolidated for readability reasons. */
{
/* Tasks can be created statically or dynamically, so note this
* task was created dynamically in case it is later deleted. */
pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
}
#endif /* tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE */
prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
prvAddNewTaskToReadyList( pxNewTCB );
xReturn = pdPASS;
}
else
{
xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}
return xReturn;
}emm..可以看到这个函数进来先申请堆栈空间,并将申请的空间保存在pxStack指针处:
pxStack = pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) );如果堆栈申请成功了,接下来就会去开辟一个TCB_t的结构,并将这个结构体的pxStack成员指向我们刚申请的堆栈。
这个TCB_t是什么呢?我粘出TCB_t结构体的一部分:
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. */
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting; /*< Holds the critical section nesting depth for ports that do not maintain their own count in the port layer. */
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber; /*< Stores a number that increments each time a TCB is created. It allows debuggers to determine when a task has been deleted and then recreated. */
UBaseType_t uxTaskNumber; /*< Stores a number specifically for use by third party trace code. */
#endif
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; /*< The priority last assigned to the task - used by the priority inheritance mechanism. */
UBaseType_t uxMutexesHeld;
#endif
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif
#if ( configGENERATE_RUN_TIME_STATS == 1 )
configRUN_TIME_COUNTER_TYPE ulRunTimeCounter; /*< Stores the amount of time the task has spent in the Running state. */
#endif
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
#endif
} tskTCB;
typedef tskTCB TCB_t;看到这,就明白了,TCB_t任务控制块,就相当于任务的句柄了,每个任务都有一个自己的任务控制块,我们有了这些任务控制块就可以调动这些任务的执行顺序了。任务控制块详尽的描述了任务的信息,比如任务此时的栈顶指针(用于保存上下文和恢复)、任务的优先级、任务状态等等。比身份证还详尽。按照这个顺序继续往下看create代码,果然:
prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );这个函数会根据我们传进的参数、自己创建的内存,将TCB_t结构填充好,后面我们靠这个TCB_t 就可以控制任务了。
再往下看:
prvAddNewTaskToReadyList( pxNewTCB );这个函数从函数名来看好像是将我们的近程控制块结构添加到Ready链表,没错,任务在任何时刻都有一个状态且只能有一个,分别是:就绪(ready)、运行(running)、阻塞(blocked)、挂起(suspend),所以FreeRTOS在内部维护了四个大链表,根据TCB中的xStateListItem,将TCB挂载到不同的链表上,表示任务此时的状态。这四种链表分别对应着pxCurrentTCB,pxReadyTasksLists,pxDelayedTaskList,xSuspendedTaskList这四个变量。看到这我就明白了,一个任务的创建大体需要经过什么样的流程了。
所谓的任务,其实就是在指定的框架内(while(1))实现的功能逻辑,只不过在多任务系统里,资源竞争成了核心问题,所以我们必须有让多个任务分时获取资源的能力,这个能力就是保存和恢复上下文,为此我们需要额外开辟内存空间去描述任务的堆栈、优先级、状态等,这个结构就是TCB。有了让任务暂停和恢复的能力,所谓的任务切换的背后不过是TCB在不同状态链表切换罢了。
任务是用来完成特定的目的而编写的函数。我们编写的程序依最终会被编译成机器码,在内存中cpu顺序执行。通过上面对任务的介绍,我们可知任务切换的核心就是保存上下文和恢复上下文。接下来我们来看看,FreeRTOS中究竟是怎么做到的。
首先看一下Cortex M系列的寄存器都有哪些:

FreeRTOS 会将这些上下文信息 保存在任务的栈中,并把 栈顶指针(即当前任务运行到哪)记录在任务的 TCB 中。
还记得我们在移植FreeRTOS到STM32F10X的时候,用宏定义将STM32上的三个函数:
分别重定位到了
这是因为FreeRTOS为我们实现了这三个中断服务函数,并完成非常重要的工作。
首先,来看xPortSysTickHandler吧。
在FreeRTOS中使用的是Systick定时器作为心跳时种,默认配置中是1ms,触发一次Systick中断,在这个中断中,内核会进入处理模式,系统会在ReadyList就绪链表中选取最高优先级的任务,进行调度。任务一旦从就绪链表转移到运行链表,即任务状态发生变化,就会触发PendSV异常,进入xPortPendSVHandler函数,完成帧栈保存和任务切换。
我刚开始理解这段话时也非常疑惑,保存帧栈切换任务这个工作值接在Systick中断完成就好了,为什么费劲整到xPortPendSVHandler函数中呢?原来如果在systick中实现切换任务的话,出现下面情况:一个中断请求在Systick异常前到达,Systick产生的中断会抢占这个中断,这在实时系统中难以忍受的,所以将切换任务的职责交给PendSV,它会等待所有ISR完成后才切换任务。代码就不放了,只要明白在一定条件下(时间片耗尽)这个xPortSysTickHandler函数会在内部调用xPortPendSVHandler来完成任务的切换。
下面是xPortPendSVHandler的源码实现:
void xPortPendSVHandler( void )
{
/* This is a naked function.
1.产生PendSV中断,硬件自动保存栈帧到任务A的栈中
2.读取当前任务A的栈指针PSP,手动把一些寄存器压栈到当前任务栈。
3.把当前任务A栈顶指针保存到任务A的任务控制块中。
4.找到下一个任务B的任务控制块。(查找下一个优先级最高的就绪任务)
5.把任务B控制块的栈顶指针指向的数据弹出到寄存器中
6.更新PSP为任务B的栈顶指针。
7.跳出PendSV中断。
8.硬件自动弹出任务B栈中的栈帧。
*/
__asm volatile
(
" .syntax unified \n"
" mrs r0, psp \n"/*将psp值放到r0,此时sp得值为msp*/
" \n"
" ldr r3, pxCurrentTCBConst \n" /* Get the location of the current TCB. 获取当前任务控制块,其实就获取任务栈顶 */
" ldr r2, [r3] \n"/*将r3寄存器值作为指针取内容存到r2,此时r2保存的为任务控制块首地址*/
" \n"
" subs r0, r0, #32 \n" /* Make space for the remaining low registers. */
" str r0, [r2] \n" /* Save the new top of stack. */
" stmia r0!, {r4-r7} \n" /* Store the low registers that are not saved automatically. */
" mov r4, r8 \n" /* Store the high registers. */
" mov r5, r9 \n"
" mov r6, r10 \n"
" mov r7, r11 \n"
" stmia r0!, {r4-r7} \n"
" \n"
" push {r3, r14} \n"
" cpsid i \n"
" bl vTaskSwitchContext \n"/*执行上线文切换*/
" cpsie i \n"
" pop {r2, r3} \n" /* lr goes in r3. r2 now holds tcb pointer. */
" \n"
" ldr r1, [r2] \n"
" ldr r0, [r1] \n" /* The first item in pxCurrentTCB is the task top of stack. */
" adds r0, r0, #16 \n" /* Move to the high registers. */
" ldmia r0!, {r4-r7} \n" /* Pop the high registers. */
" mov r8, r4 \n"
" mov r9, r5 \n"
" mov r10, r6 \n"
" mov r11, r7 \n"
" \n"
" msr psp, r0 \n" /* Remember the new top of stack for the task.记住新的栈顶指针 */
" \n"
" subs r0, r0, #32 \n" /* Go back for the low registers that are not automatically restored. */
" ldmia r0!, {r4-r7} \n" /* Pop low registers. */
" \n"
" bx r3 \n"
" \n"
" .align 4 \n"
"pxCurrentTCBConst: .word pxCurrentTCB "
);
}
汇编实现了上下文的保存和任务的切换
最后一个vPortSVCHandler这个函数是干什么的?我觉得不必深究这个,SVC是一个可编程中断,这个中断产生必须立即得到响应,FreeRTOS中,将这个中断用于启动第一个任务了,感兴趣的小伙伴可以自己查看响应的汇编代码。
通过上述分析,可知FreeRTOS内部实现任务切换的根本,就是将寄存器的信息保存到TCB,然后将PSP/MSP指针指向下一个就绪的TCB的寄存器,将其恢复,继续运行。
有了这么多理论知识,只说不练假把式,只有真正动手才能发现问题学到知识,笔者打算后面实际上手用一个Stm32f103c8t6最小系统板做一个多任务的demo。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。