RVOS的任务调度优化
12.系统优化–任务调度
12.1 改进任务管理功能
在原有基础上进⼀步改进任务管理功能。具体要求:改进 task_create()
,提供更多的参数,具体改进后的函数如下所⽰:
int task_create(void (*task)(void* param),void *param, uint8_t priority);- param 用于在创建任务执行函数时可带入参数,如果没有参数则传入 NULL。
- priority 用于指定任务的优先级,目前要求最多⽀持 256 级,0 最高,依次类推。
- timeslice:任务的时间片大小,单位是操作系统的时钟节拍(tick),此参数指定该任务一次调度可以运行的最大时间长度·和 priotity 相结合,调度器会首先根据 priority 选择优先级最高的任务运行,而 timeslice 则决定了当没有更高优先级的任务时,当前正在运行的任务可以运行的最大时间长度。
这里没啥好说的,难点就是c语言基本功,各种参数的传递要搞清楚,这里用的都是指针,我们存放的也都是地址,相互之间的参数传递也是使用32为地址来进行的。
(最开始还傻乎乎的以为,void *param,我们往里面传参的时候往往是把参数打包成一个struct传入,那么我用寄存咋能放得了,比如
tasks[_top].ctx.a0 = (reg_t) param;
,这里其实都是地址,所以是ok的。)
#define MAX_TASKS 10
#define MAX_PRIORITY 256struct task {struct context ctx; // 任务上下文uint8_t priority; // 任务优先级uint32_t timeslice; // 时间片大小uint32_t remaining_ticks; // 剩余时间片int active; // 是否活跃(1: 活跃, 0: 已退出)
};uint8_t __attribute__((aligned(16))) task_stack[MAX_TASKS][STACK_SIZE];
struct task tasks[MAX_TASKS];static int _top = 0;
static int _current = -1;void sched_init()
{w_mscratch(0);w_mie(r_mie() | MIE_MSIE);
}/** DESCRIPTION* Create a task.* - task: 任务入口函数* - param: 传递给任务的参数* - priority: 任务优先级* - timeslice: 时间片大小* RETURN VALUE* 0: success* -1: if error occurred*/
int task_create(void (*task)(void* param), void *param, uint8_t priority, uint32_t timeslice)
{if (_top < MAX_TASKS) {tasks[_top].ctx.sp = (reg_t) &task_stack[_top][STACK_SIZE];tasks[_top].ctx.pc = (reg_t) task;tasks[_top].ctx.a0 = (reg_t) param; // 将参数传递给任务tasks[_top].priority = priority;tasks[_top].timeslice = timeslice;tasks[_top].remaining_ticks = timeslice; // 初始化剩余时间片tasks[_top].active = 1; // 标记任务为活跃_top++;return 0;} else {return -1;}
}
12.2 优化任务调度算法
修改任务调度算法,在原先简单轮转的基础上⽀持按照优先级排序,优先选择优先级高的任务运行,同⼀级多个任务再轮转。
出现的问题:调度算法现在有明显问题,问题是高优先任务如果不结束,低优先级任务一直不会得到运行。
问题分析:
当前调度算法的问题是严格优先级调度,导致高优先级任务如果不主动退出或让出 CPU,低优先级任务永远无法运行。这种情况被称为优先级饥饿(Priority Starvation)。为了解决这个问题,可以引入时间片轮转调度和动态优先级调整的机制。
改进思路:
- 时间片轮转调度:
- 即使高优先级任务未结束,当其时间片用完后,也需要切换到其他任务(包括低优先级任务)。
- 通过时间片管理,确保所有任务都有机会运行。
- 动态优先级调整:
- 随着时间推移,未运行的低优先级任务可以逐渐提升优先级,避免长期被高优先级任务压制。
void schedule(void)
{if (_top <= 0) {panic("Num of task should be greater than zero!");return;}int highest_priority = MAX_PRIORITY;int next_task = -1;// 动态优先级调整:逐渐提升未运行任务的优先级for (int i = 0; i < _top; i++) {if (tasks[i].active && i != _current) {tasks[i].priority = (tasks[i].priority > 0) ? tasks[i].priority - 1 : 0;}}// 遍历任务列表,选择优先级最高的任务for (int i = 0; i < _top; i++) {if (tasks[i].active) {if (next_task == -1 || tasks[i].priority < highest_priority || (tasks[i].priority == highest_priority && i > _current)) {highest_priority = tasks[i].priority;next_task = i;}}}if (next_task == -1) {panic("No active tasks to schedule!");return;}// 时间片管理if (_current >= 0 && _current < _top) {tasks[_current].remaining_ticks--;if (tasks[_current].remaining_ticks == 0) {tasks[_current].remaining_ticks = tasks[_current].timeslice; // 重置时间片} else if (tasks[_current].priority <= tasks[next_task].priority) {// 当前任务时间片未用完,且优先级不低于下一个任务,继续运行return;}}// 切换到下一个任务_current = next_task;struct context *next = &(tasks[_current].ctx);switch_to(next);
}
12.3 增加任务退出接口
增加任务退出接口 task_exit()
,当前任务可以通过调用该接口退出执行,内核负责将该任务回收,并调度下⼀个可运行任务。建议的接⼝函数如下:
void task_exit(void);
实现思路:通过触发机器级软件中断来调用调度器,并标记当前任务为非活跃。(区分task_yield()是将任务剩余时间片清零)
/** DESCRIPTION* task_yield() causes the calling task to relinquish the CPU and a new * task gets to run.*/
void task_yield()
{if (_current >= 0 && _current < _top) {tasks[_current].remaining_ticks = 0; }/* trigger a machine-level software interrupt */int id = r_mhartid();*(uint32_t*)CLINT_MSIP(id) = 1;
}
出现的问题1:
task_exit
直接调用schedule()
出现Sync exceptions! Code = 1
问题分析:查阅RISCV体系架构的说明,该问题是指令异常。原因是直接调用 schedule()
函数时,当前任务的上下文没有正确保存,导致任务切换时访问了无效的指令地址或内存区域。
- 直接调用
schedule()
的问题:schedule()
函数会切换到下一个任务,但在切换之前,当前任务的上下文(如寄存器状态、程序计数器等)没有保存。- 当任务切换回来时,CPU 的状态可能已经被破坏,导致访问无效的指令地址或内存区域,从而触发
Instruction access fault
异常。
- 触发机器级软件中断的正确性:
- 使用
CLINT_MSIP
触发机器级软件中断时,RISC-V 的中断机制会自动保存当前任务的上下文(通过陷入中断处理程序)。 - 在中断处理程序中,调度器可以安全地切换任务,因为上下文已经被保存。
- 使用
解决方案
如果希望直接调用 schedule()
,需要确保在调用之前保存当前任务的上下文。以下是两种解决方案:
方案 1:使用中断触发调度(推荐)
保持现有的实现,通过触发机器级软件中断来调用调度器。这种方式利用了硬件中断机制,确保上下文正确保存。
void task_exit(void)
{if (_current >= 0 && _current < _top) {tasks[_current].active = 0; // 标记当前任务为非活跃int id = r_mhartid();*(uint32_t*)CLINT_MSIP(id) = 1; // 触发机器级软件中断}
}
方案 2:手动保存上下文后调用 schedule()
在直接调用 schedule()
之前,手动保存当前任务的上下文。
void task_exit(void)
{if (_current >= 0 && _current < _top) {tasks[_current].active = 0; // 标记当前任务为非活跃// 手动保存上下文struct context *current_ctx = &(tasks[_current].ctx);save_context(current_ctx); // 假设有一个保存上下文的函数// 调用调度器schedule();}
}
save_context()
函数需要实现保存当前任务的寄存器状态、程序计数器等上下文信息。
总结:
- 推荐使用中断触发调度:通过
CLINT_MSIP
触发机器级软件中断,确保上下文正确保存。- 避免直接调用
schedule()
:如果必须直接调用,需确保在调用之前手动保存上下文,但实现复杂且容易出错。
运行显示,任务可以正常退出,正常调度。
12.4 优化任务调度数据结构,提升调度效率
当前的调度器使用了简单的数组和 for
循环来遍历任务列表,导致在任务数量较多时效率低下。为了优化调度效率,可以引入更高效的数据结构和算法来管理任务队列和优先级。
这个其实应该在最开始优化的,数据结构出问题,感觉就是地基出问题,需要大改。