时间片轮询的任务调度方法(二)

时间片轮询的任务调度方法(二)

调度器设计思路

上篇文章末,对时间调度的基本结构进行抽象。
一个时间片轮询任务调度器(定时器调度),包括任务函数(func)、任务执行间隔(interval)、上次执行时间(last_time)三个核心参数。

然后将所有待执行的任务节点(time_node)放到任务链表中。

由上面的几个参数组成了调度器的数据结构:

struct time_task
{
    uint32_t last_time;     //上次执行时间
    uint32_t interval;      //执行间隔
    void *para;             //任务参数
    void (*func)(void * arg);   //任务函数
    enum TIME_MODE flag;        //执行模式
    struct list_head time_node; //链表节点
};

注册任务

当开始一个任务时,首先注册一个任务节点,将相关信息添加到节点中。
在代码中具体实现为,申请一块内存保存任务信息(struct time_task):

/*注册任务,创建结构体进行赋值*/
time_task_t time_task_register(uint32_t interval, void (*func)(void *arg), void *para, TIME_MODE flag)
{
    assert(func);

    time_task_t task = malloc(sizeof(struct time_task));
    task->flag = flag;
    task->func = func;
    task->interval = interval;
    task->para = para;

    return task;
}

保存完任务信息后,添加任务节点到链表,链表用于保存所有待执行的任务。在任务链表中根据待执行的先后顺序排列。
即将执行的任务节点为于链表头,在执行时永远执行第一个任务节点。

/*计算下一次执行时间,将任务加入到待执行任务列表*/
void time_task_start(time_task_t task, uint32_t start_time)
{
    task->last_time =  start_time + millis();
    
    /*插入任务到链表,按执行时间先后排列,依次递增*/
    time_node_insert(task);
}

任务调度

在系统初始化时注册任务到链表,然后在主循环中运行调度器。由调度器查找待执行的任务,简化代码。
在任务调度函数中,进行判断当前是否有任务待执行,如果有任务待执行,则执行任务,否则返回。

void time_task_schedule(void)
{
    time_task_t task;
    uint32_t current_time = millis();

    task = time_node_find(current_time);    //查找待执行任务,将任务节点取出
    if (task == NULL)                       //无任务待执行,则退出 
    { return; }

    /*更新执行时间,将任务节点重新插入*/
    if(task->flag == TIME_PERIODIC_TRIG)
    { time_task_start(task,0); }

    task->func(task->para);             //运行任务
    
    if(task->flag == TIME_SINGLE_TRIG)  //删除单次任务
    { free(task); }
    
    return;
}

使用实例

使用同样的任务:每10ms刷新屏幕数据,每20ms检测按键状态,每100ms读取传感器数据,电机每1分钟运行10s后关闭。

运行代码如下:

#include "TimeSchedule.h"
#include <stdio.h>
#include <stdlib.h>

void func(void *arg)
{
    printf("Tick :%d, %s\n", millis(),arg);
}

void stop_motor(void *arg)
{
    printf("Tick :%d, 停止电机\n", millis());
}

void run_motor(void * arg)
{
    time_task_t task;

    printf("Tick :%d, 运行电机\n", millis());
    task = time_task_register(10000, stop_motor,NULL, TIME_SINGLE_TRIG);
    time_task_start(task, 0);
}

int main(int argc, char * argv[])
{
    time_task_t task;

    /*注册任务*/
    task = time_task_register(20, func, "检查按键状态", TIME_PERIODIC_TRIG);
    time_task_start(task, 0);
    task = time_task_register(100, func, "读取传感器数据", TIME_PERIODIC_TRIG);
    time_task_start(task, 0);
    task = time_task_register(60000, run_motor, NULL, TIME_PERIODIC_TRIG);
    time_task_start(task, 0);

    while (1)
    {
        /*运行调度器*/
        time_task_schedule();
    }

}

调度器优缺点

优点:将相同的代码处理逻辑,抽象后形成一个统一的执行框架,可以提高代码复用性,简化代码结构。

缺点:当前调度器只能执行小型任务(执行时间短),当一个任务的执行时间大于其他任务的执行间隔时,执行顺序流将发送异常。
例如:任务A每10ms执行一次,执行用时1ms;任务B时,每1分钟执行一次,需要用时23ms。任务A每一分钟将少执行一次。

这个问题的根本在于分时调度的执行以任务为单位,在执行一个任务时,其他任务无法抢占和切换。所以在代码设计的时候就需要考虑到该问题的存在。


我开通微信公众啦!
在我的公众号还有很多文章更新哟!排版也更好看😎

CSDN博客:非典型技术宅
个人公众号:
在这里插入图片描述

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页