RVOS-4.实现上下文切换和协作式多任务
4. 上下文切换和协作式多任务
4.1 多任务与上下文
任务切换时需要保存当前任务的上下文**(即x1~x31个寄存器的内容)**
4.2 协作式多任务的设计与实现
- 协作式多任务 (Cooperative Multitasking):协作式环境下,下一个任务被调度的前提是当前任务主动放弃处理器。
- 抢占式多任务 (Preemptive Multitasking):抢占式环境下,操作系统完全决定任务调度方案,操作系统可以剥夺当前任务对处理器的使用,将处理器提供给其它任务。
(1). 关键函数switch to
:
#define LOAD lw
#define STORE sw
#define SIZE_REG 4# Save all General-Purpose(GP) registers to context.
# struct context *base = &ctx_task;
# base->ra = ra;
# ......
# These GP registers to be saved don't include gp
# and tp, because they are not caller-saved or
# callee-saved. These two registers are often used
# for special purpose. For example, in RVOS, 'tp'
# (aka "thread pointer") is used to store hartid,
# which is a global value and would not be changed
# during context-switch.
.macro reg_save baseSTORE ra, 0*SIZE_REG(\base)STORE sp, 1*SIZE_REG(\base)STORE t0, 4*SIZE_REG(\base)STORE t1, 5*SIZE_REG(\base)STORE t2, 6*SIZE_REG(\base)STORE s0, 7*SIZE_REG(\base)STORE s1, 8*SIZE_REG(\base)STORE a0, 9*SIZE_REG(\base)STORE a1, 10*SIZE_REG(\base)STORE a2, 11*SIZE_REG(\base)STORE a3, 12*SIZE_REG(\base)STORE a4, 13*SIZE_REG(\base)STORE a5, 14*SIZE_REG(\base)STORE a6, 15*SIZE_REG(\base)STORE a7, 16*SIZE_REG(\base)STORE s2, 17*SIZE_REG(\base)STORE s3, 18*SIZE_REG(\base)STORE s4, 19*SIZE_REG(\base)STORE s5, 20*SIZE_REG(\base)STORE s6, 21*SIZE_REG(\base)STORE s7, 22*SIZE_REG(\base)STORE s8, 23*SIZE_REG(\base)STORE s9, 24*SIZE_REG(\base)STORE s10, 25*SIZE_REG(\base)STORE s11, 26*SIZE_REG(\base)STORE t3, 27*SIZE_REG(\base)STORE t4, 28*SIZE_REG(\base)STORE t5, 29*SIZE_REG(\base)# we don't save t6 here, due to we have used# it as base, we have to save t6 in an extra step# outside of reg_save
.endm# restore all General-Purpose(GP) registers from the context
# except gp & tp.
# struct context *base = &ctx_task;
# ra = base->ra;
# ......
.macro reg_restore baseLOAD ra, 0*SIZE_REG(\base)LOAD sp, 1*SIZE_REG(\base)LOAD t0, 4*SIZE_REG(\base)LOAD t1, 5*SIZE_REG(\base)LOAD t2, 6*SIZE_REG(\base)LOAD s0, 7*SIZE_REG(\base)LOAD s1, 8*SIZE_REG(\base)LOAD a0, 9*SIZE_REG(\base)LOAD a1, 10*SIZE_REG(\base)LOAD a2, 11*SIZE_REG(\base)LOAD a3, 12*SIZE_REG(\base)LOAD a4, 13*SIZE_REG(\base)LOAD a5, 14*SIZE_REG(\base)LOAD a6, 15*SIZE_REG(\base)LOAD a7, 16*SIZE_REG(\base)LOAD s2, 17*SIZE_REG(\base)LOAD s3, 18*SIZE_REG(\base)LOAD s4, 19*SIZE_REG(\base)LOAD s5, 20*SIZE_REG(\base)LOAD s6, 21*SIZE_REG(\base)LOAD s7, 22*SIZE_REG(\base)LOAD s8, 23*SIZE_REG(\base)LOAD s9, 24*SIZE_REG(\base)LOAD s10, 25*SIZE_REG(\base)LOAD s11, 26*SIZE_REG(\base)LOAD t3, 27*SIZE_REG(\base)LOAD t4, 28*SIZE_REG(\base)LOAD t5, 29*SIZE_REG(\base)LOAD t6, 30*SIZE_REG(\base)
.endm# Something to note about save/restore:
# - mscratch: hold a pointer to context of current task
# - t6: as the 'base' for reg_save/reg_restore, because it is the
# very bottom register (x31) and would not be overwritten during loading.
# Note: CSRs(mscratch) can not be used as 'base' due to load/restore
# instruction only accept general purpose registers..text
# void switch_to(struct context *next);
# a0: pointer to the context of the next task
.globl switch_to
.balign 4
switch_to:csrrw t6, mscratch, t6 # swap t6 and mscratchbeqz t6, 1f # Note: the first time switch_to() is# called, mscratch is initialized as zero# (in sched_init()), which makes t6 zero,# and that's the special case we have to# handle with t6reg_save t6 # save context of prev task# Save the actual t6 register, which we swapped into mscratchmv t5, t6 # t5 points to the context of current taskcsrr t6, mscratch # read t6 back from mscratchSTORE t6, 30*SIZE_REG(t5) # save t6 with t5 as base1:# switch mscratch to point to the context of the next taskcsrw mscratch, a0# Restore all GP registers# Use t6 to point to the context of the new taskmv t6, a0reg_restore t6# Do actual context switching.ret.end
(2). 创建和初始化第 1 号任务
(3). 踏出 context switch 的第一步,切换到第一个用户任务
(4). 协作式多任务 - 调度
- 定义了最大任务数
MAX_TASKS
和每个任务的栈大小STACK_SIZE
(---->这里都是静态分配内存,放在全局数据段.data
段(已初始化数据段).bss
段(未初始化数据段))。 - 为每个任务分配了栈空间
task_stack
和上下文ctx_tasks
。 - 使用
_top
和_current
变量来管理任务的创建和调度。 schedule
函数实现了基本的任务调度逻辑,通过循环遍历任务列表来选择下一个要运行的任务。task_yield
函数允许当前任务主动放弃CPU,触发调度器选择下一个任务运行。
(5). 协作式多任务 - 初始化和任务创建
(6). 协作式多任务 - 任务运行
shed.c:
#include "os.h"/* defined in entry.S */
extern void switch_to(struct context *next);/** In the standard RISC-V calling convention, the stack pointer sp* is always 16-byte aligned.* 使用 __attribute__((aligned(16))) 属性确保栈的起始地址是 16 字节对齐的。* 这是 RISC-V 调用约定的要求,以确保栈指针 sp 总是 16 字节对齐。*/
#define STACK_SIZE 1024
#define MAX_TASKS 10uint8_t __attribute__((aligned(16))) task_stack[MAX_TASKS][STACK_SIZE];
struct context ctx_tasks[MAX_TASKS];/** _top is used to mark the max available position of ctx_tasks* _current is used to point to the context of current task*/
static int _top = 0;
static int _current = -1;static void w_mscratch(reg_t x)
{asm volatile("csrw mscratch, %0" : : "r" (x));
};
void sched_init()
{w_mscratch(0);
}/** implment a simple cycle FIFO schedular* 简单的FIFO调度器*/
void schedule()
{if (_top <= 0) {panic("Num of task should be greater than zero!");return;}_current = (_current + 1) % _top;struct context *next = &(ctx_tasks[_current]);switch_to(next);
}/** DESCRIPTION* Create a task.* - start_routin: task routine entry* RETURN VALUE* 0: success* -1: if error occured*/
int task_create(void (*start_routin)(void))
{if (_top < MAX_TASKS) {ctx_tasks[_top].sp = (reg_t) &task_stack[_top][STACK_SIZE];ctx_tasks[_top].ra = (reg_t) start_routin;_top++;return 0;} else {return -1;}
}/** DESCRIPTION* task_yield() causes the calling task to relinquish the CPU and a new * task gets to run.*/
void task_yield()
{schedule();
}/** a very rough implementaion, just to consume the cpu*/
void task_delay(volatile int count)
{count *= 50000;while (count--);
}
user.c
:
#include "os.h"#define DELAY 1000void user_task0(void)
{uart_puts("Task 0: Created!\n");while (1) {uart_puts("Task 0: Running...\n");task_delay(DELAY);task_yield();}
}void user_task1(void)
{uart_puts("Task 1: Created!\n");while (1) {uart_puts("Task 1: Running...\n");task_delay(DELAY);task_yield();}
}/* NOTICE: DON'T LOOP INFINITELY IN main() */
void os_main(void)
{task_create(user_task0);task_create(user_task1);
}
练习 9-1
要求:参考 code/os/04-multitask,在此基础上进⼀步改进任务管理功能。具体要求:改进 task_create()
,提供更多的参数,具体改进后的函数如下所⽰:
int task_create(void (*task)(void* param),void *param, uint8_t priority);
其中:param 用于在创建任务执行函数时可带入参数,如果没有参数则传入 NULL。priority 用于指定任务的优先级,目前要求最多⽀持 256 级,0 最高,依次类推。同时修改任务调度算法,在原先简单轮转的基础上⽀持按照优先级排序,优先选择优先级高的任务运行,同⼀级多个任务再轮转。
增加任务退出接口 task_exit()
,当前任务可以通过调用该接口退出执行,内核负责将该任务回收,并调度下⼀个可运行任务。建议的接⼝函数如下:
void task_exit(void);
练习 9-2
⽬前 code/os/04-multitask 实现的任务调度中,前⼀个用户任务直接调用 task_yield()
函数并最终调用switch_to()
切换到下⼀个⽤户任务。task_yield()
作为内核路径借用了用户任务的栈,当用户任务的函数调用层次过多或者 task_yield()
本⾝函数内部继续调⽤函数,可能会导致用户任务的栈空间溢出。
参考"mini-riscv-os" 的 03-MultiTasking 的实现,为内核调度单独实现⼀个任务,在任务切换中,前⼀个用户任务首先切换到内核调度任务,然后再由内核调度任务切换到下⼀个⽤户任务,这样就可以避免前⾯提到的问题了.
这次练习有点难,先放一下吧,后续尝试实现一下,最近玩的实在太多了,要收心学习啦!!!被聪哥追上好多了,学如逆水行舟,不进则退!!!
!