很多小伙伴用了很长时间的RTOS实时操作系统,但却对内核调度的机制原理一点不懂。
不了解内核调度机制,可能在写应用代码的时候就会存在一些困惑,比如:CPU是怎么同时执行多个任务、多个任务之间到底有没有关联之类的困惑。
今天手把手教你自制任务调度系统,从底层掌握相关的技术。
1. 前言
setjmp
和longjmp
是C语言标准库头文件中提供的函数。它们的功能是实现非局部跳转,可以在程序的不同位置之间进行跳转,类似于goto
语句的扩展。这种非局部跳转的能力为我们构建查询式协作多任务系统提供了基础。
实现前先简单了解一下相关知识,方便后续开展实现。
跳转函数
setjmp函数:用于保存当前程序状态,创建一个可以供后续longjmp
函数跳转的上下文环境。
longjmp函数:实现了对保存的上下文环境的跳转操作。通过传递之前由setjmp
函数保存的jmp_buf
标识符,longjmp
函数会将程序的状态还原到对应的上下文环境,并且会返回到setjmp
处继续执行。
- 在调用
setjmp
时,程序会记录当前的程序计数器、寄存器和堆栈等状态信息,并将这些信息保存在一个jmp_buf
结构中。同时,setjmp
函数返回0作为普通调用的返回值,并将jmp_buf
作为标识符存储起来。- 不同平台的
jmp_buf
的类型定义不一样,因为不同平台的相关寄存器等不一样,因此占用的大小也不同。
栈
- 栈指针:每个任务在运行时都有一个栈指针,指向其栈的顶部。任务切换时,需要保存这个指针(
jmp_buf
会保存),以便在任务恢复时能够正确访问该任务的栈数据。 - 局部变量和返回地址:栈用于存储任务的局部变量、函数参数和返回地址。在上下文切换时,这些信息也需要被保存,以确保任务能够在恢复时继续执行。
- 独立栈空间:每个任务都有自己的栈,确保任务之间的局部变量和状态不会相互干扰。这种隔离使得并发执行的任务能够独立运行,提高系统的稳定性。
汇编
这里需要用到一点点汇编,即设置栈顶的位置,不同平台使用的汇编不一样,这里可以在网上查到或者提供的demo中也能找到,只需要一条语句即可。
如 :
- x86 平台:
#define COT_OS_SET_STACK(p) __asm__ volatile("mov %0, %%rsp" : : "r" (p) : "memory");
stm32
#define COT_OS_SET_STACK(p) __set_MSP(p);
1.功能实现
利用setjmp
和longjmp
实现一个任务调度系统(协程),setjmp
用于保存当前程序的执行环境,而longjmp
用于跳转到之前保存的执行环境。
具体需要实现三个核心功能。
流程定义
创建任务
- 初始化任务相关变量:申请相关内存,后续储存任务栈信息等
- 保存新任务的入口环境:设置新任务栈顶指针后,保存该环境,方便后续任务启动时从这里开始执行
- 将新的任务添加到任务列表:任务调度使用
启动任务
- 保存当前启动函数的执行环境:当所有任务都结束后还可以跳转到这退出该函数
- 跳转到第一个任务函数的入口执行环境,开始执行任务
休眠任务
- 更新保存当前任务函数此时的执行环境:下次任务切换运行时可以恢复到当前位置继续往下执行
- 查询就绪任务并跳转到就绪任务函数的入口执行环境或者更新后的执行环境
流程图
任务函数的流程走向图:
代码实现
TCB等信息定义
typedefstructstTCB{uint8_t state;uint32_t nextRunTime; jmp_buf env; cotOsTask_f pfnOsTaskEnter;structstTCB *pNext;} TCB_t;
#define COMMON_TASK_INTI 0#define COMMON_TASK_RUN 1
#define MAIN_TASK_INTI 0#define MAIN_TASK_EXIT 1
#define TASK_STATUS_READY 0 // 就绪#define TASK_STATUS_RUNNING 1 // 运行#define TASK_STATUS_SUSPEND 2 // 挂起#define TASK_STATUS_DELETED 3 // 删除
创建任务
在函数中,设置新的栈顶后,由于还需要函数中定义的变量,为了防止设置新的堆栈后相关变量生命周期失效,需要使用static
修饰定义,保证其生命周期。
cotOsTask_t cotOs_CreatTask(cotOsTask_f pfnOsTaskEnter, void *pStack, size_t stackSize){ // 防止设置新的堆栈后该变量生命周期失效 static TCB_t *s_pNewTCB = NULL; static jmp_buf s_creatTaskEnv; if (pStack == NULL || stackSize == 0) { return NULL; } s_pNewTCB = CreatTCB(&sg_OsInfo); if (NULL == s_pNewTCB) { return NULL; } s_pNewTCB->pfnOsTaskEnter = pfnOsTaskEnter; s_pNewTCB->pNext = NULL; s_pNewTCB->state = TASK_STATUS_READY; s_pNewTCB->nextRunTime = 0; if (0 == setjmp(s_creatTaskEnv)) { COT_OS_SET_STACK(((size_t)pStack + stackSize)); if (COMMON_TASK_INTI == setjmp(s_pNewTCB->env)) { // 设置新的栈顶后记录创建任务的入口后返回原来的任务栈继续运行 longjmp(s_creatTaskEnv, 1); } else { // 任务入口位置 sg_OsInfo.pCurTCB->state = TASK_STATUS_RUNNING; sg_OsInfo.pCurTCB->pfnOsTaskEnter(sg_OsInfo.pCurTCB->param); sg_OsInfo.pCurTCB->state = TASK_STATUS_DELETED; DestoryTask(&sg_OsInfo, sg_OsInfo.pCurTCB); if (GetTaskNum(&sg_OsInfo) > 0) { JumpNextTask(&sg_OsInfo); } else { // 没有任务则返回到启动任务的位置,可以退出 longjmp(sg_OsInfo.env, MAIN_TASK_EXIT); } } } AddToTCBTaskList(&sg_OsInfo, s_pNewTCB); return s_pNewTCB;}
启动任务
启动任务,保存该位置的执行环境,方便所有任务退出后这里可以正常退出函数。
int cotOs_Start(void){ if (sg_OsInfo.pTCBList == NULL) { return -1; } int ret = setjmp(sg_OsInfo.env); if (MAIN_TASK_INTI == ret) { sg_OsInfo.pCurTCB = sg_OsInfo.pTCBList; longjmp(sg_OsInfo.pCurTCB->env, COMMON_TASK_RUN); } // 退出 return 0;}
休眠任务
任务休眠,则更新当前执行环境,并查询可运行的函数进行跳转
void cotOs_Wait(uint32_t time){ sg_OsInfo.pCurTCB->nextRunTime = sg_OsInfo.pfnGetTimerMs() + time; sg_OsInfo.pCurTCB->state = TASK_STATUS_SUSPEND; if (COMMON_TASK_RUN != setjmp(sg_OsInfo.pCurTCB->env)) { JumpNextTask(&sg_OsInfo); }}
功能扩展
上述实现了基本功能,要求每个任务都有自己独立的栈空间。
为了适应内存资源少的平台,可以增加共享栈的任务调度,即多个任务使用同一个栈空间运行各自的任务。
共享栈任务的核心有:
- 任务在运行时独享该共享栈:单线程运行的,只有待该任务休眠时则释放该栈空间给到下一个该类型的任务独享运行。
- 每个任务都有自己的备用栈:主要用来储存任务休眠前时储存在共享栈的数据,不过该备用栈的大小较小,只需要几十或者上百字节即可。
- 使用其他独立栈切换共享栈任务:共享栈任务互相切换时,由于需要将备份栈的数据恢复到共享栈空间,为了防止破坏当前任务切换使用的栈数据,需要跳转到独立的栈空间中进行数据恢复并切换。
- 轻量级任务:由于备用栈的空间较小,因此要求该类型任务尽量不在入口函数中定义局部变量(可以定义static修饰的变量,不会占用栈空间),同时只能在入口函数这一层中去休眠任务(在嵌套函数休眠,所使用的栈空间更多,那么需要备份的栈数据就更多)
代码实现
创建任务
- 初始化任务相关变量:申请相关内存,后续储存任务栈信息等
- 保存新任务的入口环境:设置新任务栈顶指针后,保存该环境,方便后续任务启 动时从这里开始执行
- 将新的任务添加到任务列表:任务调度使用
新增:
- 区分共享栈和独立栈的处理
void cotOs_Wait(uint32_t time){ sg_OsInfo.pCurTCB->nextRunTime = sg_OsInfo.pfnGetTimerMs() + time; sg_OsInfo.pCurTCB->state = TASK_STATUS_SUSPEND; if (COMMON_TASK_RUN != setjmp(sg_OsInfo.pCurTCB->env)) { JumpNextTask(&sg_OsInfo); }}
启动任务
- 保存当前启动函数的执行环境:当所有任务都结束后还可以跳转到这退出该函数
- 跳转到第一个任务函数的入口执行环境,开始执行任务
新增:
- 共享栈任务需要运行时,先跳转到该位置,利用
main
主任务的未使用的栈空间进行任务切换(尽量充分利用未使用的栈空间),先将即将执行的共享栈任务备份数据恢复到共享栈上,然后跳转过去
这里主要防止共享栈任务切换到下一个共享栈任务,还没切换时共享栈就被覆盖破坏导致程序运行异常的问题。
int cotOs_Start(void){ if (sg_OsInfo.pTCBList == NULL || sg_OsInfo.pfnGetTimerMs == NULL) { return -1; } int ret = setjmp(sg_OsInfo.env); if (MAIN_TASK_INTI == ret) { sg_OsInfo.pCurTCB = sg_OsInfo.pTCBList; longjmp(sg_OsInfo.pCurTCB->env, COMMON_TASK_RUN); } else if (MAIN_TASK_JUMP_SHARED_TASK == ret) { TcbMemcpy((uint8_t *)(sg_OsInfo.sharedStackTop - COT_OS_MAX_SHARED_BAK_STACK_SIZE), sg_OsInfo.pCurTCB->pBakStack, COT_OS_MAX_SHARED_BAK_STACK_SIZE); longjmp(sg_OsInfo.pCurTCB->env, COMMON_TASK_RUN); } return 0;}
休眠任务
- 更新保存当前任务函数此时的执行环境:下次任务切换运行时可以恢复到当前位置继续往下执行
- 查询就绪任务并跳转到就绪任务函数的入口执行环境或者更新后的执行环境
新增:
- 查询前如果当前任务是共享栈任务,则先将栈空间保存到该任务的备份栈空间。
void cotOs_Wait(uint32_t time){ sg_OsInfo.pCurTCB->nextRunTime = sg_OsInfo.pfnGetTimerMs() + time; sg_OsInfo.pCurTCB->pCondition = NULL; sg_OsInfo.pCurTCB->state = TASK_STATUS_SUSPEND; if (COMMON_TASK_RUN != setjmp(sg_OsInfo.pCurTCB->env)) { if (sg_OsInfo.pCurTCB->pBakStack != NULL) { TcbMemcpy(sg_OsInfo.pCurTCB->pBakStack, (uint8_t *)(sg_OsInfo.sharedStackTop - COT_OS_MAX_SHARED_BAK_STACK_SIZE), COT_OS_MAX_SHARED_BAK_STACK_SIZE); } JumpNextTask(&sg_OsInfo); }}
总结
至此,已完成一个任务调度系统的实现。
END
作者:大橙子疯
来源:strongerHuang
推荐阅读
欢迎大家点赞留言,更多Arm技术文章动态请关注极术社区嵌入式客栈专栏欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。