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

从零手写操作系统之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系统调用实现-09_第1张图片
本节中,想要实现的目标就是改造我们的RVOS系统,使其能够支持M和U模式,也就是U模式作为用户态,M模式作为内核态。

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


从零手写操作系统之RVOS系统调用实现-09_第2张图片
在抢占式任务实现篇中,我们详细分析了上图start.s启动汇编中那几行代码,其作用简单来说就是:

  • 设置mstatus的MPP和MPIE位为1

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

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


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

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

  • 由于mstatus的MPP位默认为0,所以我们只需要在start.s汇编文件中,去掉对MPP位的设置即可:
    从零手写操作系统之RVOS系统调用实现-09_第3张图片

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

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


系统模式的切换

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

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

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

  • 首先看一下start.s中的更改
    从零手写操作系统之RVOS系统调用实现-09_第4张图片
  • 在来看一下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;
}
  • 测试希望效果
    从零手写操作系统之RVOS系统调用实现-09_第5张图片
  • 测试结果符合预期
    从零手写操作系统之RVOS系统调用实现-09_第6张图片
    注意: makeFile文件不要忘记携带SYSCALL宏定义
    从零手写操作系统之RVOS系统调用实现-09_第7张图片

系统调用

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

所谓系统调用,就是通过一条特殊的ecall指令,帮助我们从用户态切换到内核态执行,然后通过一条eret指令,从内核态再切换回用户态执行:
从零手写操作系统之RVOS系统调用实现-09_第8张图片
ecall指令执行本质就是触发一次异常:
从零手写操作系统之RVOS系统调用实现-09_第9张图片

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

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


系统调用执行流程

从零手写操作系统之RVOS系统调用实现-09_第10张图片
因为ECALL指令本质是主动触发一次异常,所以ECALL指令的执行流程和前面讲过的统一异常处理流程是一致的,这里不再过多展开。

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

整个系统调用流程如下图所示:
从零手写操作系统之RVOS系统调用实现-09_第11张图片

  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

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

从零手写操作系统之RVOS系统调用实现-09_第12张图片
中断处理程序函数中新增一个context参数,用于接收当前任务上下文地址:

从零手写操作系统之RVOS系统调用实现-09_第13张图片


系统调用传参规范

从零手写操作系统之RVOS系统调用实现-09_第14张图片
ecall指令用来触发一次系统调用,但是ecall这条指令本身并没有提供额外的位用于存放标记,来区分不同的系统调用,如: write系统调用,read系统调用 ,open系统调用…

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

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

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

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


系统调用封装

从零手写操作系统之RVOS系统调用实现-09_第15张图片

为了让用户程序能够访问特权资源,我们可以借助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寄存器中取出系统调用的结果了。


系统调用完整流程解析

从零手写操作系统之RVOS系统调用实现-09_第16张图片

  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. 执行系统调用包装函数

从零手写操作系统之RVOS系统调用实现-09_第17张图片
3. ecall指令触发异常,错误码为8 (当前处于U态下)

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

从零手写操作系统之RVOS系统调用实现-09_第18张图片
4. trap_handler函数根据错误码完成异常转发

从零手写操作系统之RVOS系统调用实现-09_第19张图片
5. do_syscall函数根据系统调用号再次进行转发

从零手写操作系统之RVOS系统调用实现-09_第20张图片
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);
	}
}

从零手写操作系统之RVOS系统调用实现-09_第21张图片
测试结果如下:

从零手写操作系统之RVOS系统调用实现-09_第22张图片

你可能感兴趣的:(#,java,前端,linux)