前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从零手写操作系统之RVOS系统调用实现-09

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

作者头像
大忽悠爱学习
发布2023-10-11 08:40:23
2910
发布2023-10-11 08:40:23
举报
文章被收录于专栏:c++与qt学习
从零手写操作系统之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

前置知识:


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

在之前章节中,我们的程序其实一直都运行在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中的更改
代码语言:javascript
复制
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

代码语言:javascript
复制
// System call numbers
#define SYS_gethid	1

usys.S

代码语言:javascript
复制
#include "syscall.h"

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

syscall.h

代码语言:javascript
复制
// System call numbers
#define SYS_gethid	1

syscall.c

代码语言:javascript
复制
#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,在该任务中执行我们编写的系统调用
代码语言:javascript
复制
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. 执行系统调用包装函数
  1. ecall指令触发异常,错误码为8 (当前处于U态下)

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

  1. trap_handler函数根据错误码完成异常转发
  1. do_syscall函数根据系统调用号再次进行转发
  1. do_syscall函数返回 , a0存放返回值,即中断调用结果,但是注意此时a0的值是存放于当前任务的上下文中
  2. trap_handler函数返回,返回值为mepc+4,返回值存放于a0寄存器中
  3. trap_vector函数返回, 将a0赋值给mepc,恢复当前任务的上下文,此时a0中存放的是系统调用的返回结果,然后利用mret指令跳到mepc地址处执行 —> gethid函数的ret指令,即ecall指令的下一条指令
  4. gethid函数返回,此时a0寄存器存放的是系统调用结果
  5. user_task0任务中拿到系统调用执行结果

执行测试

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

代码语言:javascript
复制
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);
	}
}

测试结果如下:

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-06-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 从零手写操作系统之RVOS系统调用实现-09
  • 系统模式:用户态和内核态
    • 如何让任务运行在用户态下
    • 系统模式的切换
      • 用户模式下访问特权指令测试
        • 系统调用
          • 系统调用执行流程
          • 系统调用传参规范
          • 系统调用封装
        • 系统调用完整流程解析
          • 执行测试
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档