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 base
STORE 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 base
LOAD 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 mscratch
beqz 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 t6
reg_save t6 # save context of prev task
# Save the actual t6 register, which we swapped into mscratch
mv t5, t6 # t5 points to the context of current task
csrr t6, mscratch # read t6 back from mscratch
STORE t6, 30*SIZE_REG(t5) # save t6 with t5 as base
1:
# switch mscratch to point to the context of the next task
csrw mscratch, a0
# Restore all GP registers
# Use t6 to point to the context of the new task
mv t6, a0
reg_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 10
uint8_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 1000
void 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 的实现,为内核调度单独实现⼀个任务,在任务切换中,前⼀个用户任务首先切换到内核调度任务,然后再由内核调度任务切换到下⼀个⽤户任务,这样就可以避免前⾯提到的问题了.
这次练习有点难,先放一下吧,后续尝试实现一下,最近玩的实在太多了,要收心学习啦!!!被聪哥追上好多了,学如逆水行舟,不进则退!!!
!