从零手写操作系统之RVOS抢占式多任务实现-06

news/2024/11/23 5:14:33/

从零手写操作系统之RVOS抢占式多任务实现-06

  • 多任务系统的分类
    • 抢占式多任务的设计
    • 代码
    • 任务切换流程分析
      • 系统启动
      • 任务mepc初始化
      • 首个被调度执行的任务
      • 任务切换
    • 兼容协作式多任务
      • 软件中断
      • 编码实现
    • 测试
  • 注意点


本系列参考: 学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春 整理而来,主要作为xv6操作系统学习的一个前置基础。

RVOS是本课程基于RISC-V搭建的简易操作系统名称。

课程代码和环境搭建教程参考github仓库: https://github.com/plctlab/riscv-operating-system-mooc/blob/main/howto-run-with-ubuntu1804_zh.md

前置知识:

  • RVOS环境搭建-01
  • RVOS操作系统内存管理简单实现-02
  • RVOS操作系统协作式多任务切换实现-03
  • RISC-V 学习篇之特权架构下的中断异常处理
  • 从零手写操作系统之RVOS外设中断实现-04
  • 从零手写操作系统之RVOS硬件定时器-05

多任务系统的分类

在这里插入图片描述

抢占式多任务的设计

和协作式多任务实现思路类似,只不过将任务切换过程放到了trap_handler中完成。
在这里插入图片描述
在这里插入图片描述


在上一节的定时器处理流程基础上,我们在time_handler中新增对上下文切换的支持:
在这里插入图片描述

void timer_handler() 
{_tick++;printf("tick: %d\n", _tick);//重置时钟中断下一次触发时间timer_load(TIMER_INTERVAL);//进行任务调度schedule();
}//这个代码在之前协作式任务章节中给出过
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);
}

在协作式任务切换一节中的switch_to函数实现里面,我们采用的是ret指令进行的函数返回,ret指令执行后,会跳回到ret指令到ra寄存器保存的地址处继续执行。

而在抢占式多任务的实现中,我们的switch_to函数是在中断处理程序中执行的,所以函数返回靠的应该是mret指令,而非ret指令:
在这里插入图片描述
而对于mret指令而言,我们需要知道:
在这里插入图片描述
因此,和ret指令相比,也就是用于保存返回地址的寄存器改变了,一个是ra,一个是mepc。


代码

当我们把switch_to进程调度的逻辑放置到时钟中断处理程序中时,意味着进程A在进入时钟中断处理过程中后,会进行任务切换,切换到进程B执行,那么中断处理程序返回后,应该跳转到进程B的指令流中继续执行,如下图所示:
在这里插入图片描述

很明显,这里mepc的值是不同的,我们需要中断处理函数调用过程中保存进程A赋值后的mepc到当前进程从Context中,然后在switch_to任务切换函数中,从Context中恢复进程B寄存器相关值,包括mepc的值,从而达成进程A执行过程中触发定时器中断,在中断处理程序中进行任务调度,中断返回后,继续执行进程B的指令流。

  • 首先,我们需要在trap_vector中断程序处理入口中,在之前处理逻辑基础上,新增对于mepc寄存器保存到当前进程Context上下文的逻辑

在这里插入图片描述

  • 其次,我们需要在switch_to函数中,新增从要切换到的进程从Context上下文空间取出先前存储的mepc寄存器的值,进行恢复

在这里插入图片描述

  • 最后,还有一点很重要,context结构体中新增pc属性,用于保存mepc的值
/* task management */
struct context {/* ignore x0 */reg_t ra;reg_t sp;reg_t gp;reg_t tp;reg_t t0;reg_t t1;reg_t t2;reg_t s0;reg_t s1;reg_t a0;reg_t a1;reg_t a2;reg_t a3;reg_t a4;reg_t a5;reg_t a6;reg_t a7;reg_t s2;reg_t s3;reg_t s4;reg_t s5;reg_t s6;reg_t s7;reg_t s8;reg_t s9;reg_t s10;reg_t s11;reg_t t3;reg_t t4;reg_t t5;reg_t t6;// upon is trap frame// save the pc to run in next schedule cyclereg_t pc; // offset: 31 *4 = 124
};

任务切换流程分析

系统启动

先前在特权架构下的中断异常处理篇中介绍过,RISC-V系统启动时,默认是处于machine态下的,并且在发生trap时,RISC-V会使用mstatus.MPP位来保持进入trap前的特权级别,并更改当前特别级别为machine态,而在trap返回时,从MPP中取出先前的特权级别进行恢复。

mstatus的MPP位默认为0,也就是说第一次发生trap返回后,指令流将会执行在用户态下,我们可以通过在系统初始化时,设置MPP为3,让第一次及后续trap发生后,系统始终处于m模式下:
在这里插入图片描述
多任务切换实现篇中对start.s进行了详细解释,本节在该篇基础上新增了对mstatus中MPP位和MPIE位初始化设置。

课程给出的源码中,是将MPP位初始化为了3,也就是让后续任务始终执行在m模式下,同时设置MPIE为1,这是为了让trap返回后,将中断打开。


任务mepc初始化

任务创建的时候,需要初始化它的mepc寄存器,指向程序的入口地址:

/** 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 - 1];//初始化当前创建任务的mepcctx_tasks[_top].pc = (reg_t) start_routin;_top++;return 0;} else {return -1;}
}

而当任务第一次被调用的时候,也就是swtich_to函数在进行任务切换的时候,如果被切换的任务是第一次进行调用,我们必须在任务创建的时候设置好他的mepc寄存器,否则switch_to函数将无法通过任务上下文空间中保存的mepc值,借助mret指令跳到任务的程序入口地址处执行。


首个被调度执行的任务

在这里插入图片描述
要注意的是,首个任务的调度,是直接调用的schedule方法,而不是通过中断程序间接调用的:

/** implment a simple cycle FIFO schedular*/
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函数switch_to(next);
}

因为没有采用中断调用,因此为了让switch_to函数能够像被中断调用那样执行,我们也需要提前将任务在上下文中间中的mepc寄存器值设置好才可以。


任务切换

在这里插入图片描述

  • 进程A执行自己的指令流,执行到指令i+1时,发生异步时钟中断
  • pc被设置为mtvec,同时mepc被设置为i+2
  • 跳到trap handler中断函数处理入口地址,执行中断处理函数,此处mtvec.MODE=DIRECT
  • 执行中断处理函数trap_vector
  • 保存通用寄存器到当前进程上下文空间,保存mepc(A)到进程上下文空间
  • 调用异常处理函数trap_handler,取出中断码,发现等于7,跳到时钟中断处理函数–timer_handler处执行
  • timer_handler中重置时钟中断下一次触发时间,然后调用schedule,执行任务调度
  • schedule方法通过轮询策略选择出一个进程,假设该进程为任务B,然后将任务B的Context上下文地址作为参数传入switch_to函数
  • switch_to函数执行上下文切换,首先从任务B的上下文空间中取出mepc(B),赋值给当前的mepc寄存器,然后恢复任务B的执行上下文
  • mret指令进行中断返回,其将mepc赋值给pc寄存器,然后跳转到pc指向的地址处继续执行
    在这里插入图片描述
  • 然后B任务执行一段后,执行到指令j+1时,再次发生时钟中断
  • pc被设置为mtvec,同时mepc被设置为j+2
  • 跳到trap handler中断函数处理入口地址,执行中断处理函数,此处mtvec.MODE=DIRECT
  • 执行中断处理函数trap_vector
  • 保存通用寄存器到当前进程上下文空间,保存mepc(B)到进程上下文空间
  • 调用异常处理函数trap_handler,取出中断码,发现等于7,跳到时钟中断处理函数–timer_handler处执行
  • timer_handler中重置时钟中断下一次触发时间,然后调用schedule,执行任务调度
  • schedule方法通过轮询策略选择出一个进程,假设该进程为任务A,然后将任务A的Context上下文地址作为参数传入switch_to函数
  • switch_to函数执行上下文切换,首先从任务A的上下文空间中取出mepc(A),赋值给当前的mepc寄存器,然后恢复任务A的执行上下文
  • mret指令进行中断返回,其将mepc赋值给pc寄存器,然后跳转到pc指向的地址处继续执行
  • 以此往复执行

兼容协作式多任务

先前章节中实现的兼容协作式多任务,是通过schedule函数内部调用switch_to函数,由ret指令跳转到ra寄存器保存的地址处继续执行,以此来实现任务切换执行。

但是本节抢占式多任务的实现中,我们已经改变了switch_to函数的工作逻辑,改为mret配合mepc实现任务切换执行。

因此,先前实现的task_yield主动让出cpu方法实现也需要做出相应调整:

/** DESCRIPTION* 	task_yield()  causes the calling task to relinquish the CPU and a new * 	task gets to run.*/
void task_yield()

软件中断

为了在抢占式多任务的实现中兼容协作式多任务,这就需要引出软件中断:
在这里插入图片描述
为什么需要使用软件中断来实现对协作式多任务的兼容呢?

  • 抢占式多任务通过在定时器中断处理程序中增加任务调度逻辑实现,相当于周期性的打电话给我们的CPU,让其进行任务调度
  • 而如果想要兼容协作式多任务的实现,也需要通过打电话的方式通知我们CPU,进行任务调度,只不过这个电话是在我们需要的时候拨通,而不是周期性拨通
  • 本质是需要使用中断方式来实现协作式多任务切换,中断方式加上上面我们对trap_vector和switch_to的调整,可以帮助我们在实习协作式多任务切换时复用已有的mepc和mret处理流程

在这里插入图片描述
软件中断是由程序中的特殊指令或操作触发的中断。与硬件中断不同,软件中断是由软件控制的,而不是由外部设备或硬件信号引发的。

在RISCV中,具体实现如下:
在这里插入图片描述
根据RISC-V规范,mip.MSIP是一个中断挂起位,用于表示是否有来自软件的中断请求。当该位为1时,表示有一个软件中断请求待处理;当该位为0时,表示无软件中断请求。

在QEMU-virt模拟器中,将MSIP寄存器的最低位设置为非零值,会将相应的mip.MSIP位设置为1,从而触发软件中断请求。实际的硬件平台和操作系统可能会有不同的实现方式,但总体原理是类似的。

之所以设置CLIENT提供的MSIP寄存器最低位为1,就可以间接设置mip.MSIP位为1,原理是上图中的第二点:

  • RISCV 规范规定,Machine 模式下的 mip.MSIP 对应到一个memory- mapped的控制寄存器。为此QEMU-virt提供MSIP,该MSIP寄存器 为32—bit,高31位不可用,最低位映射到mip.MSIP。

编码实现

在这里插入图片描述

  • task_yield函数实现更改
/** DESCRIPTION* 	task_yield()  causes the calling task to relinquish the CPU and a new * 	task gets to run.*/
void task_yield()
{/* trigger a machine-level software interrupt */int id = r_mhartid();//打开当前hart的软件中断使能位*(uint32_t*)CLINT_MSIP(id) = 1;
}
  • 新增对软件中断的处理
reg_t trap_handler(reg_t epc, reg_t cause)
{reg_t return_pc = epc;reg_t cause_code = cause & 0xfff;if (cause & 0x80000000) {/* Asynchronous trap - interrupt */switch (cause_code) {//处理软件中断case 3:uart_puts("software interruption!\n");/** acknowledge the software interrupt by clearing* the MSIP bit in mip.*///清空MSIP寄存器的值,作为中断应答,否则会重复触发int id = r_mhartid();*(uint32_t*)CLINT_MSIP(id) = 0;//执行任务调度schedule();break;case 7:uart_puts("timer interruption!\n");timer_handler();break;case 11:uart_puts("external interruption!\n");external_interrupt_handler();break;default:uart_puts("unknown async exception!\n");break;}} else {/* Synchronous trap - exception */printf("Sync exceptions!, code = %d\n", cause_code);panic("OOPS! What can I do!");//return_pc += 4;}return return_pc;
}

这里是引用

switch_to函数实现复用抢占式多任务更改后的版本。


测试

#include "os.h"#define DELAY 1000void user_task0(void)
{uart_puts("Task 0: Created!\n");//测试主动让出cpu是否会触发软件中断task_yield();uart_puts("Task 0: I'm back!\n");while (1) {uart_puts("Task 0: Running...\n");task_delay(DELAY);}
}void user_task1(void)
{uart_puts("Task 1: Created!\n");while (1) {uart_puts("Task 1: Running...\n");task_delay(DELAY);}
}/* NOTICE: DON'T LOOP INFINITELY IN main() */
void os_main(void)
{task_create(user_task0);task_create(user_task1);
}
  • 系统启动函数
void start_kernel(void)
{uart_init();uart_puts("Hello, RVOS!\n");page_init();trap_init();plic_init();timer_init();sched_init();os_main();schedule();uart_puts("Would not go here!\n");while (1) {}; // stop here!
}

在这里插入图片描述


注意点

在这里插入图片描述
对于通过中断调用schedule函数间接实现进程调度的程序而言,上面紫色部分下面是不会调回来继续执行的,因为switch_to函数中后半部分做了和trap_vector后半部分一样的事情

注意: 是任务A由于发生了中断,切换到任务B执行,但是即使下次再切换为任务A继续执行,上面紫色部分下半段代码也是不会执行下去的。

对于其他类型中断而言,trap_vector后半部分逻辑还是有必要的,因为需要依靠这段逻辑完成中断返回,继续执行源程序。


http://www.ppmy.cn/news/234592.html

相关文章

京瓷1020怎么打印自检页_[建筑]喷墨打印机如何打印自检页 详细

展开全部 喷墨打印机如何打印自检页 介绍 打印自我测试、也称为内部测试&#xff0c;可用来32313133353236313431303231363533e59b9ee7ad9431333332636332验证打印机功能。打印自我测试时打印机无需连接到计算机。为每台打印机列出打印自我测试的说明。 可提式的惠普 Deskjet 1…

mysql利用子查询修改表中的数据_Mysql数据库中子查询的使用

废话不多说了,直接个大家贴mysql数据库总子查询的使用。 代码如下所述: 1.子查询是指在另一个查询语句中的SELECT子句。 例句: SELECT * FROM t1 WHERE column1 = (SELECT column1 FROM t2); 其中,SELECT * FROM t1 ...称为Outer Query[外查询](或者Outer Statement), SE…

mysql 多表更新

1.子查询是指在另一个查询语句中的SELECT子句。 例句&#xff1a; SELECT * FROM t1 WHERE column1 (SELECT column1 FROM t2); 其中&#xff0c;SELECT * FROM t1 ...称为Outer Query[外查询](或者Outer Statement), SELECT column1 FROM t2 称为Sub Query[子…

mysql 表连接

1.子查询是指在另一个查询语句中的SELECT子句。 例句&#xff1a; SELECT * FROM t1 WHERE column1 (SELECT column1 FROM t2); 其中&#xff0c;SELECT * FROM t1 ...称为Outer Query[外查询](或者Outer Statement), SELECT column1 FROM t2 称为Sub Query[子查询]。 所以&am…

硅谷的见证人:惠普公司(HP)

昔日硅谷之星 2002年三月的一天&#xff0c;一个豪华的车队浩浩荡荡地来到当时世界第二大微机制造商康柏&#xff08;Compaq&#xff09;公司的总部。凯莉.菲奥莉娜—当年惠普&#xff08;Hewlett-Packard&#xff09;公司高调的女CEO&#xff0c;像女皇一样&#xff0c;在一群…

SQL多表查询

USE h CREATE TABLE zj1(protype_id INT PRIMARY KEY,protype_name VARCHAR(10))SELECT * FROM zj1;INSERT INTO zj1 VALUES(1,家用电器);INSERT INTO zj1 VALUES(2,手机数码);INSERT INTO zj1 VALUES(3,电脑办公);INSERT INTO zj1 VALUES(4,图书音像);INSERT INTO zj1 VALUES(…

MySQL笔记(四)拆表、外键

准备数据 创建数据表 -- 创建 "京东" 数据库 create database jing_dong charsetutf8;-- 使用 "京东" 数据库 use jing_dong;-- 创建一个商品goods数据表 create table goods(id int unsigned primary key auto_increment not null,name varchar(150) not…

SQL演练-建立关系表

建立关系表 创建goods表 create table goods( id int unsigned primary key auto_increment not null, name varchar(150) not null, cate_name varchar(50), brand_name varchar(50), price int not null, is_show bit not null default 1, is_delete bit not null default …