时间片轮询的任务调度方法(二)
调度器设计思路
在上篇文章末,对时间调度的基本结构进行抽象。
一个时间片轮询任务调度器(定时器调度),包括任务函数(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博客:非典型技术宅
个人公众号: