手把手教学自制任务调度系统

原创 大橙子疯嵌入式 2024-10-21 08:15

点击上方 蓝色字体 了解更多的嵌入式编程实用技能。
如果你觉得该文章对你有帮助,欢迎点赞+关注

1. 前言

setjmplongjmp是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);

功能实现

利用setjmplongjmp实现一个任务调度系统(协程),setjmp用于保存当前程序的执行环境,而longjmp用于跳转到之前保存的执行环境。

具体需要实现三个核心功能。

流程定义

创建任务

  1. 初始化任务相关变量:申请相关内存,后续储存任务栈信息等

  2. 保存新任务的入口环境:设置新任务栈顶指针后,保存该环境,方便后续任务启动时从这里开始执行

  3. 将新的任务添加到任务列表:任务调度使用

启动任务

  1. 保存当前启动函数的执行环境:当所有任务都结束后还可以跳转到这退出该函数

  2. 跳转到第一个任务函数的入口执行环境,开始执行任务

休眠任务

  1. 更新保存当前任务函数此时的执行环境:下次任务切换运行时可以恢复到当前位置继续往下执行

  2. 查询就绪任务并跳转到就绪任务函数的入口执行环境或者更新后的执行环境

流程图

任务函数的流程走向图:

代码实现

TCB等信息定义

typedef struct stTCB
{

    uint8_t state;
    uint32_t nextRunTime;
    jmp_buf env;
    cotOsTask_f pfnOsTaskEnter;
    struct stTCB *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修饰的变量,不会占用栈空间),同时只能在入口函数这一层中去休眠任务(在嵌套函数休眠,所使用的栈空间更多,那么需要备份的栈数据就更多)

代码实现

创建任务

  1. 初始化任务相关变量:申请相关内存,后续储存任务栈信息等

  2. 保存新任务的入口环境:设置新任务栈顶指针后,保存该环境,方便后续任务启 动时从这里开始执行

  3. 将新的任务添加到任务列表:任务调度使用

新增:

  1. 区分共享栈和独立栈的处理

cotOsTask_t cotOs_CreatTask(cotOsTask_f pfnOsTaskEnter, CotOSStackType_e eStackType, void *pStack, size_t stackSize)
{
    // 防止设置新的堆栈后该变量生命周期失效
    static TCB_t *s_pNewTCB = NULL;
    static jmp_buf s_creatTaskEnv;

    if (eStackType == COT_OS_UNIQUE_STACK && (pStack == NULL || stackSize == 0))
    {
        return NULL;
    }

    if (eStackType == COT_OS_SHARED_STACK && sg_OsInfo.sharedStackTop == 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;
    s_pNewTCB->pBakStack = eStackType == COT_OS_SHARED_STACK ? CreatTCBStack(&sg_OsInfo) : NULL;

    // 这里先判断类型再保存环境,防止先保存环境后再去判断类型分别设置栈顶,导致入栈数据错乱引发异常问题
    if (eStackType == COT_OS_UNIQUE_STACK)
    {
        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
            {
                RunTask(&sg_OsInfo);
            }
        }
    }
    else
    {
        if (s_pNewTCB->pBakStack == NULL)
        {
            DestroyTCB(&sg_OsInfo, sg_OsInfo.pCurTCB);
            return NULL;
        }

        if (0 == setjmp(s_creatTaskEnv))
        {
            COT_OS_SET_STACK(sg_OsInfo.sharedStackTop);

            if (COMMON_TASK_INTI == setjmp(s_pNewTCB->env))
            {
                longjmp(s_creatTaskEnv, 1);
            }
            else
            {
                RunTask(&sg_OsInfo);
            }
        }
    }

    AddToTCBTaskList(&sg_OsInfo, s_pNewTCB);

    return s_pNewTCB;
}

启动任务

  1. 保存当前启动函数的执行环境:当所有任务都结束后还可以跳转到这退出该函数

  2. 跳转到第一个任务函数的入口执行环境,开始执行任务

新增:

  1. 共享栈任务需要运行时,先跳转到该位置,利用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;
}

休眠任务

  1. 更新保存当前任务函数此时的执行环境:下次任务切换运行时可以恢复到当前位置继续往下执行

  2. 查询就绪任务并跳转到就绪任务函数的入口执行环境或者更新后的执行环境

新增:

  1. 查询前如果当前任务是共享栈任务,则先将栈空间保存到该任务的备份栈空间。

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);
    }
}

总结

至此,已完成一个任务调度系统的实现。

想看完整代码实现的,程序源码:查询协作式多任务系统


评论
  • 彼得·德鲁克被誉为“现代管理学之父”,他的管理思想影响了无数企业和管理者。然而,关于他的书籍分类,一种流行的说法令人感到困惑:德鲁克一生写了39本书,其中15本是关于管理的,而其中“专门写工商企业或为企业管理者写的”只有两本——《为成果而管理》和《创新与企业家精神》。这样的表述广为流传,但深入探讨后却发现并不完全准确。让我们一起重新审视这一说法,解析其中的矛盾与根源,进而重新认识德鲁克的管理思想及其著作的真正价值。从《创新与企业家精神》看德鲁克的视角《创新与企业家精神》通常被认为是一本专为企业管
    优思学院 2025-01-06 12:03 124浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 76浏览
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 48浏览
  • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
    华普微HOPERF 2025-01-06 15:29 131浏览
  • 这篇内容主要讨论三个基本问题,硅电容是什么,为什么要使用硅电容,如何正确使用硅电容?1.  硅电容是什么首先我们需要了解电容是什么?物理学上电容的概念指的是给定电位差下自由电荷的储藏量,记为C,单位是F,指的是容纳电荷的能力,C=εS/d=ε0εrS/4πkd(真空)=Q/U。百度百科上电容器的概念指的是两个相互靠近的导体,中间夹一层不导电的绝缘介质。通过观察电容本身的定义公式中可以看到,在各个变量中比较能够改变的就是εr,S和d,也就是介质的介电常数,金属板有效相对面积以及距离。当前
    知白 2025-01-06 12:04 173浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球无人机锂电池产值达到2457百万美元,2024-2030年期间年复合增长率CAGR为9.6%。 无人机锂电池是无人机动力系统中存储并释放能量的部分。无人机使用的动力电池,大多数是锂聚合物电池,相较其他电池,锂聚合物电池具有较高的能量密度,较长寿命,同时也具有良好的放电特性和安全性。 全球无人机锂电池核心厂商有宁德新能源科技、欣旺达、鹏辉能源、深圳格瑞普和EaglePicher等,前五大厂商占有全球
    GIRtina 2025-01-07 11:02 74浏览
  • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
    华普微HOPERF 2025-01-06 17:23 146浏览
  • 大模型的赋能是指利用大型机器学习模型(如深度学习模型)来增强或改进各种应用和服务。这种技术在许多领域都显示出了巨大的潜力,包括但不限于以下几个方面: 1. 企业服务:大模型可以用于构建智能客服系统、知识库问答系统等,提升企业的服务质量和运营效率。 2. 教育服务:在教育领域,大模型被应用于个性化学习、智能辅导、作业批改等,帮助教师减轻工作负担,提高教学质量。 3. 工业智能化:大模型有助于解决工业领域的复杂性和不确定性问题,尽管在认知能力方面尚未完全具备专家级的复杂决策能力。 4. 消费
    丙丁先生 2025-01-07 09:25 83浏览
  • 根据Global Info Research项目团队最新调研,预计2030年全球封闭式电机产值达到1425百万美元,2024-2030年期间年复合增长率CAGR为3.4%。 封闭式电机是一种电动机,其外壳设计为密闭结构,通常用于要求较高的防护等级的应用场合。封闭式电机可以有效防止外部灰尘、水分和其他污染物进入内部,从而保护电机的内部组件,延长其使用寿命。 环洋市场咨询机构出版的调研分析报告【全球封闭式电机行业总体规模、主要厂商及IPO上市调研报告,2025-2031】研究全球封闭式电机总体规
    GIRtina 2025-01-06 11:10 104浏览
  • 本文介绍Linux系统更换开机logo方法教程,通用RK3566、RK3568、RK3588、RK3576等开发板,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。制作图片开机logo图片制作注意事项(1)图片必须为bmp格式;(2)图片大小不能大于4MB;(3)BMP位深最大是32,建议设置为8;(4)图片名称为logo.bmp和logo_kernel.bmp;开机
    Industio_触觉智能 2025-01-06 10:43 87浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦