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

news2024/12/26 9:32:47

从零手写操作系统之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 cycle
	reg_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];
		//初始化当前创建任务的mepc
		ctx_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 1000

void 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.coloradmin.cn/o/614093.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

QT QVerticalSpacer 弹簧控件

本文详细的介绍了QVerticalSpacer控件的各种操作&#xff0c;例如&#xff1a;新建界面、控件布局、隐藏控件、设置宽高、添加布局、其它参数、.h源文件、cpp源文件、其它文章等等操作。 实际开发中&#xff0c;一个界面上可能包含十几个控件&#xff0c;手动调整它们的位置既费…

chatgpt赋能python:Python如何优化SEO

Python如何优化SEO SEO是指搜索引擎优化&#xff0c;是提高网站在搜索引擎中排名的技术实践。Python是一种高效、易用、灵活的编程语言&#xff0c;可以被应用于SEO的优化过程中。 关键词优化 关键词是SEO过程中的重要元素&#xff0c;Python可以帮助我们快速优化关键词。以…

chatgpt赋能python:Python拆分:如何有效地拆分Python代码

Python拆分&#xff1a;如何有效地拆分Python代码 Python是一种简洁而优雅的编程语言&#xff0c;它拥有庞大的库&#xff0c;使得它可以轻松处理各种任务。然而&#xff0c;在开发大型项目时&#xff0c;代码可能会变得冗长而难以维护。 通过拆分代码&#xff0c;可以使得代码…

WhatWeb使用教程,图文教程(超详细)

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 WhatWeb 一、扫描网站指纹二、扫描强度三、扫描内网…

Golang每日一练(leetDay0088) 数组的乘积、搜索二维矩阵II

目录 238. 除自身以外数组的乘积 Product of Array Except Self &#x1f31f;&#x1f31f; 240. 搜索二维矩阵 II Search A 2d Matrix ii &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rust每日一练 专栏 Golang每日一练 专栏 Python每…

SpringBoot中的增删改查案例

目录 一、案例说明 二、数据库 三、案例源码 3.1、pom文件 3.2、Application.yml 3.3、项目结构 a&#xff09;整体结构 b&#xff09;Java代码部分 c&#xff09;资源文件部分 3.4、业务功能的实现 1&#xff09;配置类 2&#xff09;实体类 3&#xff09;Mapper文…

chatgpt赋能python:Python中如何对数据进行归一化

Python中如何对数据进行归一化 什么是数据归一化&#xff1f; 在数据分析和机器学习中&#xff0c;数据归一化&#xff08;Normalization&#xff09;指的是对数据进行缩放以使其值域范围映射到特定范围内&#xff0c;以便更好地进行处理和分析。通常情况下&#xff0c;数据归…

《Java并发编程实战》课程笔记(十四)

原子类&#xff1a;无锁工具类的典范 对于简单的原子性问题&#xff0c;还有一种无锁方案。Java SDK 并发包将这种无锁方案封装提炼之后&#xff0c;实现了一系列的原子类。无锁方案相对互斥锁方案&#xff0c;最大的好处就是性能。 互斥锁方案为了保证互斥性&#xff0c;需要…

chatgpt赋能python:Python如何处理SEO

Python如何处理SEO Python语言是一种非常流行的编程语言&#xff0c;它可以用于各种类型的应用程序开发&#xff0c;包括网页开发。在网页开发中&#xff0c;搜索引擎优化&#xff08;SEO&#xff09;是至关重要的&#xff0c;因为它决定了搜索引擎是否能够有效地找到和展示您…

chatgpt赋能python:Python如何快速找到函数

Python如何快速找到函数 作为一名有10年Python编程经验的工程师&#xff0c;我深知在Python编程过程中如何快速找到函数是非常重要的。在学习和掌握Python函数之前&#xff0c;首先需要学会如何快速地找到所需的Python函数。这篇文章将介绍一些我在编程中经常使用的方法&#…

RK3588平台开发系列讲解(驱动基础篇)I2C 总线实现 client 设备

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、非设备树实现 i2c二、设备树实现 i2c沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 Linux 中的 I2C 也是按照平台总线模型设计的,既然也是按照平台总线模型设计的,是不是也分为一个device 和一个 driv…

编程比赛 入门 学习路线

内容若有不足与纰漏&#xff0c;请多指教&#xff01; 文章目录 写在前面入门建议掌握的知识点数学思想算法 | 数据结构c STL容器类容器适配器组件迭代器常用算法函数 其他 编程 | 学习学习 | 练题 平台编程笔记 | 题解 比赛相关要点注意赛前赛中赛后 资料分享笔记题解资料PDF&…

JVM栈帧结构及动态链接

1. 栈帧结构 附加信息&#xff08;此处官网未具体说明&#xff0c;可忽略&#xff0c;参考图中结构理解即可&#xff09;&#xff1a;栈帧的高度&#xff0c;虚拟机版本信息 栈帧信息&#xff1a;附加信息动态链接方法的返回地址 局部变量表&#xff1a;方法中定义的局部变量…

基于flask的web应用开发——登录界面

目录 0. 前言1. request2. redirect3. 动态路由4. Jinja2代码实现 0. 前言 打算在云服务器上部署一个 TODO LIST 来练手&#xff0c;也就是一个代办事项提醒表。 本节学习使用 flask 库制作出一个登录界面&#xff0c;并且使用Redis数据库实现账号密码加载功能&#xff0c;关…

微信小程序登录的最佳实践

微信小程序登录的最佳实践 官方文档的介绍 小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识&#xff0c;快速建立小程序内的用户体系。 登录流程时序 说明 调用 wx.login() 获取 临时登录凭证code &#xff0c;并回传到开发者服务器。 调用 auth.co…

图解C++对象模型

C对象模型是什么 《深度探索C对象模型》这本书中对对象模型的描述如下&#xff1a; 有两个概念可以解释C对象模型&#xff1a; 语言中直接支持面向对象程序设计的部分。 对于各种支持的底层实现机制。 语言中直接支持面向对象程序设计的部分&#xff0c;包括了构造函数、析构函…

chatgpt赋能python:Python如何快速复制上一行?

Python 如何快速复制上一行&#xff1f; 在编写Python代码时&#xff0c;经常需要快速复制上一行代码进行修改。如果只是简单的手动复制粘贴&#xff0c;会造成不必要的时间浪费并且容易出错。本文将介绍三种快速复制上一行代码的方法。 方法一&#xff1a;使用快捷键 在Pyt…

chatgpt赋能python:Python如何拟合曲线

Python如何拟合曲线 拟合曲线是数据分析中常见的一种方法。Python作为一种强大的编程语言&#xff0c;具有丰富的数据分析库和拟合曲线的功能。本文将介绍如何在Python中使用numpy、matplotlib和scipy库进行曲线拟合。 numpy库 numpy是Python中常用的数值计算库。它提供了许…

Error系列-CVE CIS-2023系统漏洞处理方案集合

问题1&#xff1a; CVE-2023-29491 Type: OS涉及到的包&#xff1a;ncurses-dev,ncurses-libs,ncurses-terminfo-base描述&#xff1a;当前系统安装的ncurses&#xff0c;存在漏洞&#xff0c;当被setuid应用程序使用时&#xff0c;允许本地用户通过在$HOME/中找到的终端数据库…