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

news/2025/1/13 2:55:26/

从零手写操作系统之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//执行系统调用ecallret
  • 操作系统内核中驻留的系统调用实现相关库文件

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://错误码使用负数表示,这里简单起见,系统调用出错都返回-1printf("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_SYSCALLint 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);}
#endifwhile (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_SYSCALLint 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);}
#endifwhile (1){uart_puts("Task 0: Running... \n");task_delay(DELAY);}
}

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

在这里插入图片描述


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

相关文章

Jetpack Compose:使用PagerIndicator和Infinity实现滚动的HorizontalPager

Jetpack Compose:使用PagerIndicator和Infinity实现滚动的HorizontalPager 可能你已经知道,Jetpack Compose 默认不包含内置的ViewPager组件。然而,我们可以通过在 build.gradle 文件中添加 accompanist 库依赖,将 ViewPager 功能…

【JavaEE】网络编程之TCP套接字

目录 1、TCP套接字 1.1、ServerSocket 常用API 1.2、Socket 常用API 2、基于TCP套接字实现一个TCP回显服务器 2.1、服务器端代码 2.2、客户端代码 2.3、解决服务器不能同时和多个客户端建立链接的问题 3、基于TCP socket 写一个简单的单词翻译服务器 1、TCP套接字 T…

浅谈 UUID 生成原理及优缺点

UUID 是一套用于生成全局唯一标识符的标准,也被称为 GUID (Globally Unique Identifier),通过使用 UUID 可以在分布式系统中生成唯一的 ID。UUID 的生成方式有多种,本文将详细讲解 UUID 的生成原理、特性、实用场景以及…

Android壳程序实现方式对比

vs. 原生开发、纯H5(依赖浏览器)、浏览器壳程序(混合应用的方式) 本篇指的是最后一种 Android手机壳程序,用系统WebView 和 内嵌Chromium 实现方式的对比使用系统WebView内嵌ChromiumFirefox的引擎 - GeckoView 组件说…

联发科MT76x8使用1-芯片对比

上面是我创建的群聊,欢迎新朋友的加入。 最近新到手一个MT-76X8的板子,盖了个铁壳壳,丝印上写的是MT-7628. 学习一下。 特意对比了MT-7628和MT-7688 对比了一下,没什么太大区别 官方资源路径 https://docs.labs.mediatek.com…

MT7688详细芯片资料下载  MT7688规格说明简介

MT7688详细芯片资料下载 MT7688规格说明简介 今天给大家分享MT7688芯片资料的相关问题,具体资料到闯客网技术论坛下载,也可以加群解决:813238832 或者资料链接:https://bbs.usoftchina.com/ Q: 联发科 MT7688AN是什么? ? 联发…

MT7687芯片资料MT7687原理图资料

MT7687是一种高度集成的单片芯片,提供应用程序处理器、低功率1t1r 802.11b/g/n wi-fi、子系统和电源管理单元。应用程序处理器子系统包含一个带有浮点单元的臂Cortex-m4。它还支持一系列接口,包括uart,i2c,spi,i2s&…

摩托罗拉MT788刷机

好久不用的MT788今天拿出来一看,竟然忘记手势密码,oh my god 然后尝试进入recovery 模式刷机 分享一下方法:1、将手机处于关机状态下。2、按住音量下键和音量上键不松,再同时按电源键开机等待几秒钟后屏幕黑了就松开所有的按键后会…