写点什么

带你剖析鸿蒙轻内核任务栈的源代码

发布于: 2021 年 06 月 09 日

​​​​​​​​摘要:本文带领大家一起学习了鸿蒙轻内核的任务栈、任务上下文的基础概念,剖析了任务栈初始化的代码。


本文分享自华为云社区《鸿蒙轻内核M核源码分析系列七 任务及任务调度(1)任务栈》,原文作者:zhushy 。


我们本文开始要分析下任务及任务调度模块。首先,我们介绍下任务栈的基础概念。任务栈是高地址向低地址生长的递减栈,栈指针指向即将入栈的元素位置。初始化后未使用过的栈空间初始化的内容为宏 OS_TASK_STACK_INIT 代表的数值 0xCACACACA,栈顶初始化为宏 OS_TASK_MAGIC_WORD 代表的数值 0xCCCCCCCC。一个任务栈的示意图如下,其中,栈底指针是栈的最大的内存地址,栈顶指针,是栈的最小的内存地址,栈指针从栈底向栈顶方向生长。



​任务上下文(Task Context)是任务及任务调度模块的另外一个重要的概念,它指的是任务运行的环境,例如包括程序计数器、堆栈指针、通用寄存器等内容。在多任务调度中,任务上下文切换(Task Context Switching)属于核心内容,是多个任务运行在同一 CPU 核上的基础。在任务调度时,保存退出运行状态的任务使用的寄存器信息到任务栈,还会从进入运行状态的任务的栈中读取上下文信息,恢复寄存器信息。


下面,我们剖析下任务栈、任务栈初始化的源代码,若涉及开发板部分,以开发板工程 targets\cortex-m7_nucleo_f767zi_gcc\为例进行源码分析。首先,看下任务上下文结构体。

1、 TaskContext 上下文结构体定义


在文件 kernel\arch\arm\cortex-m7\gcc\los_arch_context.h 中,定义的上下文的结构体如下,主要是浮点寄存器,通用寄存器。


typedef struct TagTskContext {#if ((defined(__FPU_PRESENT) && (__FPU_PRESENT == 1U)) && \     (defined(__FPU_USED) && (__FPU_USED == 1U)))    UINT32 S16;    UINT32 S17;    UINT32 S18;    UINT32 S19;    UINT32 S20;    UINT32 S21;    UINT32 S22;    UINT32 S23;    UINT32 S24;    UINT32 S25;    UINT32 S26;    UINT32 S27;    UINT32 S28;    UINT32 S29;    UINT32 S30;    UINT32 S31;#endif    UINT32 uwR4;    UINT32 uwR5;    UINT32 uwR6;    UINT32 uwR7;    UINT32 uwR8;    UINT32 uwR9;    UINT32 uwR10;    UINT32 uwR11;    UINT32 uwPriMask;    UINT32 uwR0;    UINT32 uwR1;    UINT32 uwR2;    UINT32 uwR3;    UINT32 uwR12;    UINT32 uwLR;    UINT32 uwPC;    UINT32 uwxPSR;#if ((defined(__FPU_PRESENT) && (__FPU_PRESENT == 1U)) && \     (defined(__FPU_USED) && (__FPU_USED == 1U)))    UINT32 S0;    UINT32 S1;    UINT32 S2;    UINT32 S3;    UINT32 S4;    UINT32 S5;    UINT32 S6;    UINT32 S7;    UINT32 S8;    UINT32 S9;    UINT32 S10;    UINT32 S11;    UINT32 S12;    UINT32 S13;    UINT32 S14;    UINT32 S15;    UINT32 FPSCR;    UINT32 NO_NAME;#endif} TaskContext;
复制代码

2、 任务栈相关函数

2.1 任务栈初始化函数


在文件 kernel\arch\arm\cortex-m7\gcc\los_context.c 中定义了任务栈初始化函数 VOID*HalTskStackInit(t()。该函数被文件 kernel\src\los_task.c 中的函数 UINT32OsNewTaskInit()调用完成任务初始化,并进一步在创建任务函数 UINT32 LOS_TaskCreateOnly()中调用,完成新创建任务的任务栈初始化。


该函数使用 3 个参数,一个是任务编号 UINT32 taskID,一个是初始化的栈的大小 UINT32stackSize,第 3 个参数是栈顶指针 VOID *topStack。⑴处代码把栈内容初始化为 OS_TASK_STACK_INIT,⑵处把栈顶初始化为 OS_TASK_MAGIC_WORD。


⑶处代码获取任务上下文的指针地址 TaskContext *context。对于新创建任务,从栈的底部开始,大小为 sizeof(TaskContext)的栈空间存放上下文的数据。⑷处如果支持浮点数计算,需要初始化浮点数相关的寄存器。⑸初始化通用寄存器,其中.uwLR 初始化为(UINT32)(UINTPTR)HalSysExit。.uwPC 初始化为(UINT32)(UINTPTR)OsTaskEntry,这是 CPU 首次执行该任务时运行的第一条指令的位置。这 2 个函数下文会分析。


⑹处返回值是指针(VOID*)taskContext,这个就是任务初始化后的栈指针,注意不是从栈底开始了,栈底保存的是上下文,栈指针要减去上下文占用的栈大小。在栈中,从 TaskContext *context

指针增加的方向,依次保存上下文结构体的第一个成员,第二个成员…另外,初始化栈的时候,除了特殊的几个寄存器,不同寄存器的初始值虽然没有什么意义,也有些初始化的规律。比如 R2 寄存器初始化为 0x02020202L,R12 寄存器初始化为 0x12121212L 初始化的内容和寄存器编号有关联,其余类似。


LITE_OS_SEC_TEXT_INIT VOID *HalTskStackInit(UINT32 taskID, UINT32 stackSize, VOID *topStack){    TaskContext *context = NULL;    errno_t result;
/* initialize the task stack, write magic num to stack top */⑴ result = memset_s(topStack, stackSize, (INT32)(OS_TASK_STACK_INIT & 0xFF), stackSize); if (result != EOK) { printf("memset_s is failed:%s[%d]\r\n", __FUNCTION__, __LINE__); }⑵ *((UINT32 *)(topStack)) = OS_TASK_MAGIC_WORD;
⑶ context = (TaskContext *)(((UINTPTR)topStack + stackSize) - sizeof(TaskContext));
#if ((defined(__FPU_PRESENT) && (__FPU_PRESENT == 1U)) && \ (defined(__FPU_USED) && (__FPU_USED == 1U)))⑷ context->S16 = 0xAA000010; context->S17 = 0xAA000011; context->S18 = 0xAA000012; context->S19 = 0xAA000013; context->S20 = 0xAA000014; context->S21 = 0xAA000015; context->S22 = 0xAA000016; context->S23 = 0xAA000017; context->S24 = 0xAA000018; context->S25 = 0xAA000019; context->S26 = 0xAA00001A; context->S27 = 0xAA00001B; context->S28 = 0xAA00001C; context->S29 = 0xAA00001D; context->S30 = 0xAA00001E; context->S31 = 0xAA00001F; context->S0 = 0xAA000000; context->S1 = 0xAA000001; context->S2 = 0xAA000002; context->S3 = 0xAA000003; context->S4 = 0xAA000004; context->S5 = 0xAA000005; context->S6 = 0xAA000006; context->S7 = 0xAA000007; context->S8 = 0xAA000008; context->S9 = 0xAA000009; context->S10 = 0xAA00000A; context->S11 = 0xAA00000B; context->S12 = 0xAA00000C; context->S13 = 0xAA00000D; context->S14 = 0xAA00000E; context->S15 = 0xAA00000F; context->FPSCR = 0x00000000; context->NO_NAME = 0xAA000011;#endif
⑸ context->uwR4 = 0x04040404L; context->uwR5 = 0x05050505L; context->uwR6 = 0x06060606L; context->uwR7 = 0x07070707L; context->uwR8 = 0x08080808L; context->uwR9 = 0x09090909L; context->uwR10 = 0x10101010L; context->uwR11 = 0x11111111L; context->uwPriMask = 0; context->uwR0 = taskID; context->uwR1 = 0x01010101L; context->uwR2 = 0x02020202L; context->uwR3 = 0x03030303L; context->uwR12 = 0x12121212L; context->uwLR = (UINT32)(UINTPTR)HalSysExit; context->uwPC = (UINT32)(UINTPTR)OsTaskEntry; context->uwxPSR = 0x01000000L;
⑹ return (VOID *)context;}
复制代码

​2.2 获取任务栈水线函数


随着任务栈入栈、出栈,当前栈使用的大小不一定是最大值,UINT32 OsGetTaskWaterLine(UINT32 taskID)可以获取的栈使用的最大值即水线 WaterLine。该函数定义在文件 kernel\src\los_task.c,它需要 1 个参数,即 UINT32 taskID 任务编号,返回值 UINT32peakUsed 表示获取的水线值,即任务栈使用的最大值。


我们详细看下代码,⑴处代码表示如果栈顶等于设置的魔术字,说明栈没有被溢出破坏,从栈顶开始栈内容被写满宏 OS_TASK_STACK_INIT 的部分是没有使用过的栈空间。使用临时栈指针 stackPtr 指针变量依次向栈底方向增加,判断栈是否被使用过,while 循环结束,栈指针 stackPtr 指向最大的未使用过的栈地址。⑵处代码获取最大的使用过的栈空间大小,即需要的水线。⑶处如果栈顶溢出,则返回无效值 OS_NULL_INT。


该函数被 kernel\base\los_task.c 中的函数 LOS_TaskInfoGet(UINT32taskId, TSK_INFO_S *taskInfo)调用,获取任务的信息。在 shell 模块也会使用来或者栈信息。


UINT32 OsStackWaterLineGet(const UINTPTR *stackBottom, const UINTPTR *stackTop, UINT32 *peakUsed){    UINT32 size;    const UINTPTR *tmp = NULL;⑴  if (*stackTop == OS_STACK_MAGIC_WORD) {        tmp = stackTop + 1;        while ((tmp < stackBottom) && (*tmp == OS_STACK_INIT)) {            tmp++;        }⑵      size = (UINT32)((UINTPTR)stackBottom - (UINTPTR)tmp);        *peakUsed = (size == 0) ? size : (size + sizeof(CHAR *));        return LOS_OK;    } else {        *peakUsed = OS_INVALID_WATERLINE;        return LOS_NOK;    }}
复制代码

UINT32 OsGetTaskWaterLine(UINT32 taskID){    UINT32 *stackPtr = NULL;    UINT32 peakUsed;
⑴ if (*(UINT32 *)(UINTPTR)OS_TCB_FROM_TID(taskID)->topOfStack == OS_TASK_MAGIC_WORD) { stackPtr = (UINT32 *)(UINTPTR)(OS_TCB_FROM_TID(taskID)->topOfStack + OS_TASK_STACK_TOP_OFFSET); while ((stackPtr < (UINT32 *)(OS_TCB_FROM_TID(taskID)->stackPointer)) && (*stackPtr == OS_TASK_STACK_INIT)) { stackPtr += 1; }⑵ peakUsed = OS_TCB_FROM_TID(taskID)->stackSize - ((UINT32)(UINTPTR)stackPtr - OS_TCB_FROM_TID(taskID)->topOfStack); } else {⑶ PRINT_ERR("CURRENT task %s stack overflow!\n", OS_TCB_FROM_TID(taskID)->taskName); peakUsed = OS_NULL_INT; } return peakUsed;}
复制代码


3、 任务进入退出函数

3.1、任务退出函数


在初始化上下文的时候,链接寄存器设置的是函数(UINT32)(UINTPTR)HalSysExit,该函数定义在文件 kernel\src\los_task.c。函数代码里调用 LOS_IntLock()关中断,然后进入死循环。在任务正常调度期间,该函数理论上不会被执行。在系统异常时,主动调用 LOS_Panic()c 触发异常时,也会调用该函数。


LITE_OS_SEC_TEXT_MINOR VOID HalSysExit(VOID){    LOS_IntLock();    while (1) {    }}
复制代码


3.2、任务进入函数


在初始化上下文的时候,PC 寄存器设置的是函数 VOIDOsTaskEntry(UINT32 taskId),该函数定义在文件 kernel\base\los_task.c,我们来分析下源代码,⑴处代码获取 taskCB,然后执行⑵调用任务的入口函数。等任务执行完毕后,执行⑶删除任务。通常任务入口执行函数都是 while 循环,任务不执行时,会调度到其他任务或者空闲任务,不会执行到删除任务阶段。


LITE_OS_SEC_TEXT_INIT VOID OsTaskEntry(UINT32 taskID){    UINT32 retVal;⑴  LosTaskCB *taskCB = OS_TCB_FROM_TID(taskID);
⑵ (VOID)taskCB->taskEntry(taskCB->arg);
⑶ retVal = LOS_TaskDelete(taskCB->taskID); if (retVal != LOS_OK) { PRINT_ERR("Delete Task[TID: %d] Failed!\n", taskCB->taskID); }}
复制代码


小结


本文带领大家一起学习了鸿蒙轻内核的任务栈、任务上下文的基础概念,剖析了任务栈初始化的代码。后续也会陆续推出更多的分享文章,敬请期待,也欢迎大家分享学习、使用鸿蒙轻内核的心得,有任何问题、建议,都可以留言给我们: https://gitee.com/openharmony/kernel_liteos_m/issues 。为了更容易找到鸿蒙轻内核代码仓,建议访问 https://gitee.com/openharmony/kernel_liteos_m ,关注 Watch、点赞 Star、并 Fork 到自己账户下,谢谢。


点击关注,第一时间了解华为云新鲜技术~

发布于: 2021 年 06 月 09 日阅读数: 557
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
带你剖析鸿蒙轻内核任务栈的源代码