从零手写操作系统之RVOS系统调用实现-09

news2024/11/27 9:56:05

从零手写操作系统之RVOS系统调用实现-09

  • 系统模式:用户态和内核态
    • 如何让任务运行在用户态下
  • 系统模式的切换
    • 用户模式下访问特权指令测试
    • 系统调用
      • 系统调用执行流程
      • 系统调用传参规范
      • 系统调用封装
    • 系统调用完整流程解析
    • 执行测试


本系列参考: 学习开发一个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
  • 从零手写操作系统之RVOS抢占式多任务实现-06
  • 从零手写操作系统之RVOS任务同步和锁实现-07
  • 从零手写操作系统之RVOS软件定时器实现-08

系统模式:用户态和内核态

在之前章节中,我们的程序其实一直都运行在Machine态下,但是RISC-V是支持3种不同的运行模式的,如下图所示:
在这里插入图片描述
本节中,想要实现的目标就是改造我们的RVOS系统,使其能够支持M和U模式,也就是U模式作为用户态,M模式作为内核态。

在支持虚拟内存的类Linux操作系统中,内核态可能指的是的S模式


在这里插入图片描述
在抢占式任务实现篇中,我们详细分析了上图start.s启动汇编中那几行代码,其作用简单来说就是:

  • 设置mstatus的MPP和MPIE位为1

在start_kernel函数中,通过schedule函数手动切换到初始任务执行,该过程会调用switch_to函数完成指令流的切换执行。

switch_to函数最后会调用mret指令,该指令会将MPP保存的特权级恢复为当前特权级别,MPIE保存的中断使能位,恢复为当前中断使能位,效果就是设置当前任务也运行在M态下,并且打开全局中断使能。


如何让任务运行在用户态下

那么如何设置让任务运行在U态下呢?

  • 由于mstatus的MPP位默认为0,所以我们只需要在start.s汇编文件中,去掉对MPP位的设置即可:
    在这里插入图片描述

当switch_to第一次被手动调用时,执行mret指令,该指令将MPP保存的特权级恢复为当前特权级别,此时当前特权级别为用户态。

随后,跳转到任务入口地址处执行,这样就可以确保任务运行在用户态下。


系统模式的切换

用户模式下访问特权指令测试

当我们的用户程序跑在用户态下的时候,其访问M态下才能访问的资源时,就会受到限制,那么如何解决呢?

我们首先来测试看看在用户态下,执行特权指令是否会触发异常:

  • 首先看一下start.s中的更改
    在这里插入图片描述
  • 在来看一下user.c中的更改
void user_task0(void)
{
	uart_puts("Task 0: Created!\n");

	unsigned int hid = -1;

	/*
	 * if syscall is supported, this will trigger exception, 
	 * code = 2 (Illegal instruction)
	 * 在用户模式下,尝试读取mhartid寄存器内容,会抛出非法指令异常,错误码为2
	 */
	hid = r_mhartid();
	printf("hart id is %d\n", hid);
	
	while (1){
		uart_puts("Task 0: Running... \n");
		task_delay(DELAY);
	}
}

/* which hart (core) is this? */
static inline reg_t r_mhartid()
{
	reg_t x;
	asm volatile("csrr %0, mhartid" : "=r" (x) );
	return x;
}
  • 测试希望效果
    在这里插入图片描述
  • 测试结果符合预期
    在这里插入图片描述
    注意: makeFile文件不要忘记携带SYSCALL宏定义
    在这里插入图片描述

系统调用

RISC-V处于安全考虑,不允许用户态程序直接执行部分特权指令,因此只能采用间接的方式进行访问,也就是通过系统调用的方式进行特权资源访问。

所谓系统调用,就是通过一条特殊的ecall指令,帮助我们从用户态切换到内核态执行,然后通过一条eret指令,从内核态再切换回用户态执行:
在这里插入图片描述
ecall指令执行本质就是触发一次异常:
在这里插入图片描述

  • 用户态下调用ecall指令,触发得到的错误码为8
  • S态下,为9
  • M态下,为11

异常产生时,epc寄存器的值存放的是ECALL指令本身的地址,因此,我们需要注意将epc值更改为ECALL下一条指令的地址,否则就会触发死循环,不断执行ECALL指令。


系统调用执行流程

在这里插入图片描述
因为ECALL指令本质是主动触发一次异常,所以ECALL指令的执行流程和前面讲过的统一异常处理流程是一致的,这里不再过多展开。

为了解决用户态下无法直接读取mhartid寄存器来获取当hart Id的问题,我们需要编写一个系统调用函数gethid,让用户程序通过调用该函数,完成上面的需求。

整个系统调用流程如下图所示:
在这里插入图片描述

  1. gethid函数中通过ecall指令进行系统调用,主动触发一次异常
  2. hart跳到mvetc指向的中断程序入口地址处执行,同时MPP保存进入trap前的特权级别,MPIE保存进入trap前的全局中断使能位
  3. trap_vector进行上下文保存,然后调用trap_handler中断处理程序
  4. trap_handler中断处理程序中,发现此次发生的trap是异常,又根据错误码发现此次发生的异常实际是一次系统调用
  5. 执行系统调用函数
  6. 将返回地址加上4个字节,也就是跳到发生异常的下一条指令去执行,而非重试异常指令,避免陷入死循环
  7. mret进行中断返回,将当前特权级别恢复为MPP,当前全局中断使能恢复为MPIE

为了能在中断处理程序中访问到当前任务上下文,我们新增了将任务上下文地址作为参数传入中断处理程序的逻辑:

在这里插入图片描述
中断处理程序函数中新增一个context参数,用于接收当前任务上下文地址:

在这里插入图片描述


系统调用传参规范

在这里插入图片描述
ecall指令用来触发一次系统调用,但是ecall这条指令本身并没有提供额外的位用于存放标记,来区分不同的系统调用,如: write系统调用,read系统调用 ,open系统调用…

为了区分这些系统调用,我们需要给每个系统调用分配一个号码,称为系统调用号,系统调用号在本系统中存放于a7寄存器中。

虽然系统调用传参规则由不同的系统自己决定,但是也要遵循RISC-V函数传参规范

系统调用本质也是一个函数,也需要有参数,但是不同的系统调用需要的参数个数未必一样,所以我们这里规定系统调用参数使用寄存器范围在a0-a5之间。

系统调用返回值放在a0中,用于表示成功还是失败,成功一般为0,如果失败了,则使用负数来表示不同的错误码。


系统调用封装

在这里插入图片描述

为了让用户程序能够访问特权资源,我们可以借助ecall系统调用指令,并借助于系统调用号区分不同的系统调用。

我们的系统所要做的就是提供不同的系统调用,每个系统调用由系统调用号和系统调用处理函数组成,系统调用号存放于一个单独的syscall.h头文件中,而具体的系统调用函数实现则存放于syscall.c文件中。

同时,为了让用户程序调用我们的系统调用,我们需要编写一份相同的syscall.h头文件,该头文件列举了当前系统支持的所有系统调用号,同时编写对应的usys.S文件,为每个系统调用封装一层函数,用于向用户屏蔽通过ecall指令加系统调用号来调用底层系统调用函数的处理过程。

我们将上图中左部分存放于C库中,暴露给用户程序访问,而右部分存放于内核中,作为系统调用具体实现,这种分离的做法,也是Linux操作系统采用的策略。

  • 暴露给用户的库文件

syscall.h

// System call numbers
#define SYS_gethid	1

usys.S

#include "syscall.h"

.global gethid
gethid:
    //将系统调用号,加载到a7寄存器中
	li a7, SYS_gethid
	//执行系统调用
	ecall
	ret
  • 操作系统内核中驻留的系统调用实现相关库文件

syscall.h

// System call numbers
#define SYS_gethid	1

syscall.c

#include "os.h"
#include "syscall.h"

//获取当前hart id的系统调用
int sys_gethid(unsigned int *ptr_hid)
{
	printf("--> sys_gethid, arg0 = 0x%x\n", ptr_hid);
	if (ptr_hid == NULL) {
		return -1;
	} else {
	    //hart id存放于传入内存地址处
		*ptr_hid = r_mhartid();
		return 0;
	}
}

//根据系统调用号,完成系统调用分发处理
void do_syscall(struct context *cxt)
{
    //从当前任务的上下文中获取系统调用号
	uint32_t syscall_num = cxt->a7;
	//根据系统调用号完成系统调用任务执行的派发
	switch (syscall_num) {
	case SYS_gethid:
	    //进行获取hart id的系统调用,结果存放于a0寄存器中
	    //hart id存放于a0寄存器保存的内存地址处
	    //a0寄存器这里即作为函数调用参数,又作为函数返回值进行传递
		cxt->a0 = sys_gethid((unsigned int *)(cxt->a0));
		break;
	default:
	    //错误码使用负数表示,这里简单起见,系统调用出错都返回-1
		printf("Unknown syscall no: %d\n", syscall_num);
		cxt->a0 = -1;
	}
	return;
}

trap返回时,会将当前任务的上下文进行恢复,这样用户程序就可以从a0寄存器中取出系统调用的结果了。


系统调用完整流程解析

在这里插入图片描述

  1. 编写任务0,在该任务中执行我们编写的系统调用
void user_task0(void)
{
	uart_puts("Task 0: Created!\n");

	unsigned int hid = -1;

	/*
	 * if syscall is supported, this will trigger exception, 
	 * code = 2 (Illegal instruction)
	 */
	//hid = r_mhartid();
	//printf("hart id is %d\n", hid);

//携带该宏定义,进行系统调用测试
#ifdef CONFIG_SYSCALL
	int ret = -1;
	//执行系统调用
	//结果存放于hid变量中
	ret = gethid(&hid);
	//ret = gethid(NULL);
	if (!ret) {
		printf("system call returned!, hart id is %d\n", hid);
	} else {
		printf("gethid() failed, return: %d\n", ret);
	}
#endif

	while (1){
		uart_puts("Task 0: Running... \n");
		task_delay(DELAY);
	}
}
  1. 执行系统调用包装函数


3. ecall指令触发异常,错误码为8 (当前处于U态下)

trap_vector中断处理程序入口代码基本没有变动,只是额外新增了当前任务上下文地址作为参数进行传递。

在这里插入图片描述
4. trap_handler函数根据错误码完成异常转发

在这里插入图片描述
5. do_syscall函数根据系统调用号再次进行转发

在这里插入图片描述
6. do_syscall函数返回 , a0存放返回值,即中断调用结果,但是注意此时a0的值是存放于当前任务的上下文中
7. trap_handler函数返回,返回值为mepc+4,返回值存放于a0寄存器中
8. trap_vector函数返回, 将a0赋值给mepc,恢复当前任务的上下文,此时a0中存放的是系统调用的返回结果,然后利用mret指令跳到mepc地址处执行 —> gethid函数的ret指令,即ecall指令的下一条指令
9. gethid函数返回,此时a0寄存器存放的是系统调用结果
10. user_task0任务中拿到系统调用执行结果


执行测试

在前面代码基础上,只对user_task0号任务进行修改:

void user_task0(void)
{
	uart_puts("Task 0: Created!\n");

	unsigned int hid = -1;

	/*
	 * if syscall is supported, this will trigger exception, 
	 * code = 2 (Illegal instruction)
	 */
	//hid = r_mhartid();
	//printf("hart id is %d\n", hid);

#ifdef CONFIG_SYSCALL
	int ret = -1;
	ret = gethid(&hid);
	//ret = gethid(NULL);
	if (!ret) {
		printf("system call returned!, hart id is %d\n", hid);
	} else {
		printf("gethid() failed, return: %d\n", ret);
	}
#endif

	while (1){
		uart_puts("Task 0: Running... \n");
		task_delay(DELAY);
	}
}

在这里插入图片描述
测试结果如下:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/627104.html

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

相关文章

基于html+mysql+Spring+mybatis+Springboot的Springboot宠物医院管理系统

运行环境: 最好是java jdk 1.8,我在这个平台上运行的。其他版本理论上也可以。 IDE环境: Eclipse,Myeclipse,IDEA或者Spring Tool Suite都可以,如果编译器的版本太低,需要升级下编译器,不要弄太低的版本 tomcat服务器环…

一般人不要轻易去自学网络安全(黑客)

笔者本人 17 年就读于一所普通的本科学校,20 年 6 月在三年经验的时候顺利通过校招实习面试进入大厂,现就职于某大厂安全联合实验室。 我为啥说自学黑客,一般人我还是劝你算了吧!因为我就是那个不一般的人。 首先我谈下对黑客&a…

[游戏开发][Unity]Assetbundle下载篇(1)热更前准备与下载AB包清单

热更流程都不是固定的,每个人写的状态机也有所差别,但是一些必要步骤肯定不可能少,例如下载清单,对比版本,下载AB包,标记下载完成。我接下来的每一篇文章都是下载AB包的重要步骤,大概率是不能省…

Kibana:使用 Kibana 自带数据进行可视化(三)

在今天的练习中,我们将使用 Kibana 自带的数据来进行一些可视化的展示。希望对刚开始使用 Kibana 的用户有所帮助。这是这个系列的第三篇文章。这个是继上一篇文章 “Kibana:使用 Kibana 自带数据进行可视化(二)” 的续篇。 前提条…

学成在线----day5

1、媒资管理需求分析 2、为什么要用网关 当前要开发的是媒资管理服务,目前为止共三个微服务:内容管理、系统管理、媒资管理,如下图: 后期还会添加更多的微服务,当前这种由前端直接请求微服务的方式存在弊端&#xff…

微服务架构中的数据一致性:解决方案与实践| 得物技术

1 为什么要做服务之间的数据一致性 作为互联网公司的研发工程师,微服务的架构思想对于各位读者朋友来说,已经不是陌生东西。我们当中的大多数人,或多或少经历过从单体应用到微服务化的系统拆分和演进过程。我们按照庞大系统的业务功能和特征…

都说网络安全渗透工程师前景好,好在哪?

渗透工程师前景非常好,网络安全发展规模不断扩大,未来行业类的人才需求也会越来越多。就目前看来在网络安全方向上就业的薪资待遇也十分可观。 其就业方向有很广泛,如网络安全工程师,渗透测试工程师等。 渗透测试人员通常对网络…

keras搭建轻量级卷积神经网络CNN开发构建国家一级保护动物识别分析系统,集成开发GradCAM实现热力图分析可视化

动物识别相关的项目本质上属于图像识别,在我之前的博文中已经有过不少实践了,感兴趣的话可以自行移步阅读即可,这里不是说想要单纯地去做一个动物识别的项目,昨晚在玩手机的时候突然被小孩问到一个动物是不是国家保护动物&#xf…

SpringBoot 事件监听处理(五十一)

当死亡笼罩在脑海,请用生的信念打败它 上一章简单介绍了Retry重试机制(五十), 如果没有看过,请观看上一章 参考文章: https://blog.csdn.net/qq_37758497/article/details/118863308 一. Spring 事件监听 Spring的事件监听(也叫事件驱动)是观察者模式的一种实现&…

Windows 10磁盘碎片整理:含义和操作方法

什么是Windows磁盘碎片? 随着电脑硬盘使用时间的增长,磁盘上会产生大量的垃圾碎片。这些碎片会分布在磁盘的各个角落,严重影响磁盘的响应速度。为了在一定程度上提高系统性能,定期使用Windows10的磁盘碎片整理工具来进行碎片整…

Vue.js中的Mixin和组件插槽

Vue.js中的Mixin和组件插槽 介绍 在Vue.js中,Mixin和组件插槽是两个非常有用的概念。Mixin是一种可重用Vue组件的方式,而组件插槽则提供了一种在组件之间共享内容的方式。虽然这两个概念在功能上有所不同,但它们对于Vue.js应用程序的开发都非…

Vue 中的数据请求如何进行拦截与错误处理

Vue 中的数据请求拦截与错误处理 在 Vue.js 中,我们经常需要向后端服务器发送数据请求,以获取或提交数据。在这个过程中,我们可能会遇到一些问题,例如无效的请求参数、网络连接错误、服务器错误等。为了更好地处理这些问题&#…

优化和扩展:处理不同操作和参数的数字列表

引言 在编程中,我们有时需要根据输入执行不同的操作,而这些操作涉及到数字列表,并且每个操作可能具有不同数量的参数。本文将介绍如何优化和扩展代码,以便更好地处理这种情况。 问题描述 当前遇到的问题是需要根据输入执行不同…

Mysql中的Buffer pool

Buffer Pool在数据库里的地位 1、回顾一下Buffer Pool是个什么东西? 数据库中的Buffer Pool是个什么东西?其实他是一个非常关键的组件,数据库中的数据实际上最终都是要存放在磁盘文件上的,如下图所示。 但是我们在对数据库执行增…

2023最新Java面试八股文,阿里/腾讯 / 美团 / 字节 1 000道 Java 中高级面试题

企业调薪、裁员、组织架构调整等等,坏消息只多不少,最近也有很多来咨询跳槽的朋友,都是因为之前的公司出现了比较大的薪资和组织变动 近期有许多粉丝非常关注最新的面试题!于是小编去各大平台搜罗了一份近期大厂面试的一些内容&a…

基础工程(cubeide串口调试,printf实现,延时函数)

0.基础工程(cubeide串口调试,printf实现,延时函数) 文章目录 0.基础工程(cubeide串口调试,printf实现,延时函数)外部时钟源CLOCK(RCC)系统时钟SYS与DEBUG设置UART串口设置cubeide设置…

世界研发管理组织在美国成立,中国籍研发管理专家江新安当选总干事

World R&D Management Organization世界研发管理组织(WRDMO)由来自世界各地的研发管理研究组织,创新技术研究机构,院校以及研发管理咨询机构联合发起。是一个具有开放性,无党派性,非营利性的国际先进研…

如从亿点点失误,到一点点失误,我是如何做的【工作失误怎么办】

前言 只要我们还在做事,或者说还活着,就没有不犯错的时候。作为一名前端搬砖工,哪怕工作中再仔细小心,也免不了一些失误。 那这是不是说,失误很正常,改了就是嘛? 这么说好像没错。作为失误本…

【计算机组成与体系结构Ⅰ】知识点整理

第一章 计算机系统概论 1.1 从源文件到可执行文件 .c源程序、.i源程序、.s汇编语言程序、.o可重定位目标程序、可执行目标程序;后两个为二进制,前面为文本 1.2 可执行文件的启动和执行 冯诺依曼结构计算机模型的5大基本部件:运算器、控制…

【ChatGLM】使用ChatGLM-6B-INT4模型进行P-Tunning训练记录及参数讲解

文章目录 模型训练步骤参数含义名词解释欠拟合泛化能力收敛性梯度爆炸 初步结论 小结 模型训练 首先说明一下训练的目的:提供本地问答知识文件,训练之后可以按接近原文语义进行回答,类似一个问答机器人。 步骤 安装微调需要的依赖&#xf…