批处理系统

为了让管理员事先准备好一组程序, 让计算机执行完一个程序之后, 就自动执行下一个程序,提出了批处理系统的思想。处理系统的关键, 就是要有一个后台程序, 当一个前台程序执行结束的时候, 后台程序就会自动加载一个新的前台程序来执行,这样的一个后台程序, 其实就是操作系统。

我们希望操作系统和用户进程之间的切换是一种可以限制入口的执行流切换方式,这种方式是无法通过程序代码来实现的.

异常响应机制

为了实现最简单的操作系统, 硬件还需要提供一种可以限制入口的执行流切换方式. 这种方式就是自陷指令, 程序执行自陷指令之后, 就会陷入到操作系统预先设置好的跳转目标. 这个跳转目标也称为异常入口地址.

riscv32提供ecall指令作为自陷指令, 并提供一个mtvec寄存器来存放异常入口地址. 为了保存程序当前的状态, riscv32提供了一些特殊的系统寄存器, 叫控制状态寄存器(CSR寄存器).

PA这里需要用到三个状态寄存器,分别为

mepc寄存器 - 存放触发异常的PC
mstatus寄存器 - 存放处理器的状态
mcause寄存器 - 存放触发异常的原因

riscv32触发异常后硬件的响应过程如下:

将当前PC值保存到mepc寄存器
在mcause寄存器中设置异常号
从mtvec寄存器中取出异常入口地址
跳转到异常入口地址

若决定无需杀死当前程序, 等到异常处理结束之后, 就根据之前保存的信息恢复程序的状态, 并从异常处理过程中返回到程序触发异常之前的状态. 具体地:

riscv32通过mret指令从异常处理过程中返回, 它将根据mepc寄存器恢复PC.

我们是以状态机的视角看待处理器的,前面在TRMIOE中,我们说程序是个S = <R, M>的状态机,现在为了给机器添加状态响应机制,我们需要给这个机器的R添加功能,将R增加了R = {GPR, PC, SR}。其中GPR为通用寄存器堆,PC为程序计数器,SR为状态控制寄存器。也就是前面说的三个mepc mcause mtvec

添加异常响应机制之后, 我们允许一条指令的执行会"失败". 为了描述指令执行失败的行为, 我们可以假设CPU有一条虚构的指令raise_intr, 执行这条虚构指令的行为就是上文提到的异常响应过程。

SR[mepc] <- PC
SR[mcause] <- 一个描述失败原因的号码
PC <- SR[mtvec]

至于这个号码需要你自行RTFM了。

like this

那么, "一条指令的执行是否会失败"这件事是不是确定性的呢? 显然这取决于"失败"的定义, 例如除0就是"除法指令的第二个操作数为0", 非法指令可以定义成"不属于ISA手册描述范围的指令", 而自陷指令可以认为是一种特殊的无条件失败. 不同的ISA手册都有各自对"失败"的定义, 例如RISC-V手册就不认为除0是一种失败, 因此即使除数为0, 在RISC-V处理器中这条指令也会按照指令手册的描述来执行.

事实上, 我们可以把这些失败的条件表示成一个函数fex: S -> {0, 1}, 给定状态机的任意状态S, fex(S)都可以唯一表示当前PC指向的指令是否可以成功执行,若fex(S)=0,那就

异常响应机制的加入还伴随着一些系统指令的添加,如csrrw,csrrs,ecall,mret,具体含义请RTFM。

将上下文管理抽象成CTE

我们刚才提到了程序的状态, 在操作系统中有一个等价的术语, 叫"上下文". 因此, 硬件提供的上述在操作系统和用户程序之间切换执行流的功能, 在操作系统看来, 都可以划入上下文管理的一部分。

上下文(context)在操作系统里指的是“使程序能够从中断点恢复并继续执行的全部状态”。

为什么需要上下文?

上下文保存使得操作系统可以暂停一个进程(或线程),去运行另一个,然后再把之前的执行恢复到完全相同状态(时间片切换、阻塞/唤醒、异常/中断返回等)。

典型的上下文切换步骤(简化):

触发:定时中断 / 系统调用 / 异常 / 阻塞事件。

保存:把当前 CPU 寄存器、PC、状态寄存器保存到当前 PCB(或内核栈)。

切换地址空间:切换页表基址等 MMU 设置(如果需要)。

恢复:从下一个进程的 PCB 恢复寄存器、PC、状态寄存器,返回用户态继续执行。

操作系统处理上下文需要机器状态异常的原因程序的上下文,关于上下文,在处理过程中, 操作系统可能会读出上下文中的一些寄存器, 根据它们的信息来进行进一步的处理. 例如操作系统读出PC所指向的非法指令, 看看其是否能被模拟执行.

typedef struct {
enum {
EVENT_NULL = 0,
EVENT_YIELD, EVENT_SYSCALL, EVENT_PAGEFAULT, EVENT_ERROR,
EVENT_IRQ_TIMER, EVENT_IRQ_IODEV,
} event;
uintptr_t cause, ref;
const char *msg;
} Event;

因此CTE定义了名为"事件"的如上数据结构。

其中event表示事件编号, cause和ref是一些描述事件的补充信息, msg是事件信息字符串, 我们在PA中只会用到event

此外还用另外两个统一的API

bool cte_init(Context* (*handler)(Event ev, Context *ctx))用于进行CTE相关的初始化操作. 其中它还接受一个来自操作系统的事件处理回调函数的指针, 当发生事件时, CTE将会把事件和相关的上下文作为参数, 来调用这个回调函数, 交由操作系统进行后续处理.
void yield()用于进行自陷操作, 会触发一个编号为EVENT_YIELD事件. 不同的ISA会使用不同的自陷指令来触发自陷操作, 具体实现请RTFSC.

触发第一个异常

通过AM中的am-test中的hello_intr测试用例来触发一次自陷。

#include <amtest.h>

void (*entry)() = NULL; // mp entry

static const char *tests[256] = {
['h'] = "hello",
['H'] = "display this help message",
['i'] = "interrupt/yield test",
['d'] = "scan devices",
['m'] = "multiprocessor test",
['t'] = "real-time clock test",
['k'] = "readkey test",
['v'] = "display test",
['a'] = "audio test",
['p'] = "x86 virtual memory test",
}; int main(const char *args) {
switch (args[0]) {
CASE('h', hello);
CASE('i', hello_intr, IOE, CTE(simple_trap));
CASE('d', devscan, IOE);
CASE('m', mp_print, MPE);
CASE('t', rtc_test, IOE);
CASE('k', keyboard_test, IOE);
CASE('v', video_test, IOE);
CASE('a', audio_test, IOE);
CASE('p', vm_test, CTE(vm_handler), VME(simple_pgalloc, simple_pgfree));
case 'H':
default:
printf("Usage: make run mainargs=*\n");
for (int ch = 0; ch < 256; ch++) {
if (tests[ch]) {
printf(" %c: %s\n", ch, tests[ch]);
}
}
}
return 0;
}

make ARCH=riscv32-nemu minargs=i run的时候会执行

CASE('i', hello_intr, IOE, CTE(simple_trap));这一句。

首先申明hello_intr()并设置为入口,并且初始化IOE,这里对这个不做要求,然后就申明了一个Context类型的函数指针名为simple_trap,这个函数指针需要Event和Context结构体的变量。simple_trap代码如下。

Context *simple_trap(Event ev, Context *ctx) {
switch(ev.event) {
case EVENT_IRQ_TIMER:
putch('t'); break;
case EVENT_IRQ_IODEV:
putch('d'); break;
case EVENT_YIELD:
putch('y'); break;
default:
panic("Unhandled event"); break;
}
return ctx;
}

随后执行cte_init的函数,其变量是刚刚的simple_trap

cte_init代码如下:

bool cte_init(Context*(*handler)(Event, Context*)) {
// initialize exception entry
asm volatile("csrw mtvec, %0" : : "r"(__am_asm_trap)); //把amasmtrap的地址传给mtvec // register event handler
user_handler = handler; return true;
}

cte_init()函数会做两件事情, 第一件就是设置异常入口地址:也就是把__am_asm_trap函数的地址直接传给mtvec寄存器,

第二件事是注册一个事件处理回调函数:user_handler = handler;在这里赋值。

那现在函数从cte_init()继续执行,下一步就是到执行hello_intr()

void hello_intr() {
printf("Hello, AM World @ " __ISA__ "\n");
printf(" t = timer, d = device, y = yield\n");
io_read(AM_INPUT_CONFIG);
iset(1);
while (1) {
for (volatile int i = 0; i < 1000000; i++) ;
yield();
}
}

首先打印一点东西出来,然后iset(1)打开中断但是现在没有实现所以可以当做这个函数没有作用所以继续向后看,终于跳到了yield()函数中。

void yield() {
#ifdef __riscv_e
asm volatile("li a5, -1; ecall");
#else
asm volatile("li a7, -1; ecall"); #endif
}

进来就是一个内联汇编,首先

li a7,- 1 然后再 ecallecall我们知道是用来通过引发环境调用异常来请求执行环境。那这个li a7,- 1是用来干嘛呢?

yield的异常号是11,系统调用号是-1。

约定了GPR1是gpr[17]也就是a7。

Context* __am_irq_handle(Context *c) {
if (user_handler) {
Event ev = {0};
switch (c->mcause) {
case 11:
ev.event=EVENT_YIELD;
if(c->GPR1!=-1)
ev.event = EVENT_SYSCALL;
c->mepc += 4;
break;
default: ev.event = EVENT_ERROR; break;
}
//printf("mcause = %s\n",c->mcause);
c = user_handler(ev, c); //调用之前注册的handler
assert(c != NULL);
}
return c;
}
struct Context {
// TODO: fix the order of these members to match trap.S
uintptr_t gpr[NR_REGS], mcause, mstatus, mepc;
void *pdir;
}; #ifdef __riscv_e
#define GPR1 gpr[15] // a5
#else
#define GPR1 gpr[17] // a7
#endif #define GPR2 gpr[10] // a0
#define GPR3 gpr[11] // a1
#define GPR4 gpr[12] // a2
#define GPRx gpr[10] // a0

然后调用了ecall指令,那此时我们就需要用到讲义提到的isa_raise_intr()函数。

  INSTPAT("0000000 00000 00000 000 00000 11100 11", ecall  , I, s->dnpc = isa_raise_intr(11,s->pc);etrace());

由于是ecall,那我们这里写他的异常号是11,于是现在跳到了isa_raise_intr()函数中,看下它的源码。

word_t isa_raise_intr(word_t NO, vaddr_t epc) {
/* TODO: Trigger an interrupt/exception with ``NO''. 待办事项:使用“NO”触发中断/异常。
* Then return the address of the interrupt/exception vector. 然后返回中断/异常向量的地址
*/
cpu.mstatus = 0x00001800;
cpu.mepc = epc;
cpu.mcause = NO;
return cpu.mtvec;
}

mstatus是0x1800是因为要通过difftest,mtvec存放触发异常的PC,也就是前面提到的__am_asm_trap,mcause存放出发状态异常的异常号,也就是调用这个函数用的11,mepc保存当前pc值,以便待会跳回来找到下一条指令。

执行完这个isa_raise_intr函数之后,我们就会跳到mtvec所指的地址,也就是__am_asm_trap,代码贴出如下。

__am_asm_trap:
addi sp, sp, -CONTEXT_SIZE MAP(REGS, PUSH) csrr t0, mcause
csrr t1, mstatus
csrr t2, mepc STORE t0, OFFSET_CAUSE(sp)
STORE t1, OFFSET_STATUS(sp)
STORE t2, OFFSET_EPC(sp) # set mstatus.MPRV to pass difftest
li a0, (1 << 17)
or t1, t1, a0
csrw mstatus, t1 mv a0, sp
call __am_irq_handle
mv sp, a0
LOAD t1, OFFSET_STATUS(sp)
LOAD t2, OFFSET_EPC(sp)
csrw mstatus, t1
csrw mepc, t2 MAP(REGS, POP) addi sp, sp, CONTEXT_SIZE
mret

首先把栈指针下移,腾出((NR_REGS + 3) * XLEN)的空间用于保存上下文。

然后MAP(REGS, PUSH)是把32个寄存器压入栈中,保存通用寄存器。

然后读取并保存CSR寄存器。

把mstatus的第十七位(MPRV)置为1。

然后把栈寄存器存放到a0中,并进入__am_irq_handle()函数中。

Context* __am_irq_handle(Context *c) {
if (user_handler) {
Event ev = {0};
switch (c->mcause) {
case 11:
ev.event=EVENT_YIELD;
if(c->GPR1!=-1)
ev.event = EVENT_SYSCALL;
c->mepc += 4;
break;
default: ev.event = EVENT_ERROR; break;
}
//printf("mcause = %s\n",c->mcause);
c = user_handler(ev, c); //调用之前注册的handler
assert(c != NULL);
}
return c;
}

mcause=11于是ev.event被赋值为EVENT_YIELD,且mepc += 4,用于跑到错误的下一条指令去了。然后调用之前注册的回调函数,也就是

Context *simple_trap(Event ev, Context *ctx) {
switch(ev.event) {
case EVENT_IRQ_TIMER:
putch('t'); break;
case EVENT_IRQ_IODEV:
putch('d'); break;
case EVENT_YIELD:
putch('y'); break;
default:
panic("Unhandled event"); break;
}
return ctx;
}

由于event是EVENT_YIELD,于是打印出了y。

然后保存现场,把刚改变的东西都还原回去,随后执行mret指令。

看下mret指令要我们干什么

INSTPAT("0011000 00010 00000 000 0000 011100 11", mret   , R, s->dnpc = cpu.mepc);

跳回到刚发生异常的下一个函数去了,也就是进入下一个while(1)中了,于是这样反复循环。

ysyx:pa3.1批处理系统的更多相关文章

  1. 5105 pa3 Distributed File System based on Quorum Protocol

    1 Design document 1.1 System overview We implemented a distributed file system using a quorum based ...

  2. Tsinghua 2018 DSA PA3简要题解

    CST2018 3-1-1 Sum (15%) 简单的线段树,单点修改,区间求和. 很简单. CST2018 3-1-2 Max (20%) 高级的线段树. 维护区间最大和,区间和,左边最大和,右边最 ...

  3. 《大数据日知录》读书笔记-ch11大规模批处理系统

    MapReduce: 计算模型: 实例1:单词统计 实例2:链接反转 实例3:页面点击统计 系统架构: 在Map阶段还可以执行可选的Combiner操作,类似于Reduce,但是在Mapper sid ...

  4. kafka学习笔记:知识点整理

    一.为什么需要消息系统 1.解耦: 允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束. 2.冗余: 消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险. ...

  5. 非技术1-学期总结&ending 2016

    好久好久没写博客了,感觉动力都不足了--12月只发了一篇博客,好惭愧-- 今天是2016年最后一天,怎么能不写点东西呢!! 学期总结 大学中最关键一年的第一个学期,共4个月.前20天在学网络方面的,当 ...

  6. 子DIV设置margin-top影响父DIV位置的解决办法

    父div如果没有任何东西,子div设置margin-top,父div会下落 <!DOCTYPE html> <html lang="en"> <hea ...

  7. CPU阿甘之烦恼

    转自“码农翻身”公共号,原文地址CPU阿甘之烦恼 总结:(程序加载到内存运行的演变过程) 内存存放程序.OS负责加载程序到内存.CPU负责运行内存中的程序 1.串行:加载一个完整程序到内存,CPU运行 ...

  8. axis2开发webservice程序

    一.环境 eclipse + jdk 6.0 + win7 64位 +tomcat7.0 二.创建服务端程序 1.新建web项目,webserviceTest 2.下载axis2,将lib目录下的ja ...

  9. Kafka设计解析(四)- Kafka Consumer设计解析

    本文转发自Jason’s Blog,原文链接 http://www.jasongj.com/2015/08/09/KafkaColumn4 摘要 本文主要介绍了Kafka High Level Con ...

  10. Kafka设计解析(一)- Kafka背景及架构介绍

    本文转发自Jason’s Blog,原文链接 http://www.jasongj.com/2015/01/02/Kafka深度解析 背景介绍 Kafka简介 Kafka是一种分布式的,基于发布/订阅 ...

随机推荐

  1. CGI 简单的python显示的页面

    简介 python 进行服务器的页面的显示 cgi common gateway interface 公用网关接口 简单操作 python3 -m http.server --cgi 8001 新建一 ...

  2. java 切换不同的显示风格

    简介 java 切换不同的显示风格 code import java.awt.*; import javax.swing.*; public class ImageTest { public stat ...

  3. Kafka为什么吞吐量大,速度快?

    前言 根据个人的经历,无论在工作中,还是即将要经历的面试,MQ这部分是肯定要了解的,虽然之前工作中一直使用Kafka但是一些详细的细节知识还是了解的不深,所以这次总结一波. Kafka为什么吞吐量这么 ...

  4. 基础篇:6.2)GD&T较线性尺寸公差的优缺点

    本章目的:理解GD&T标注对比线性/传统/坐标尺寸公差的优势,但也不要忘记其使用限制. 1.线性尺寸公差   1.1 定义 线性尺寸公差=传统尺寸公差=坐标尺寸公差. 传统尺寸公差(Tradi ...

  5. VHC - 一款基于 vue3 的移动端日期、时间选择插件 功能丰富 文档细致

    vue3-hash-calendar 基于 vue3 的移动端日历组件 文档网站    更新日志 效果图 仓库地址 github:https://github.com/TangSY/vue3-hash ...

  6. Wordless: 一个周末打造的小爆游戏

    这个项目是什么 Wordless就是个类似 Wordle 的猜单词游戏,用 Next.js 搭建的.玩家有 6 次机会猜出单词,支持 3 到 8 个字母的单词.说实话,开始只是想做点跟 wordle ...

  7. CNVD-2024-15077 AJ-Report 认证绕过与远程代码执行漏洞 (复现)

    CNVD-2024-15077目录终端下执行docker compose up -d启动容器, 访问ip:9095进入漏洞页面 向目标服务器发送以下 POST 请求,利用漏洞执行任意命令 curl - ...

  8. 联想3650 X型号服务器的raid划分

    1进入bios,点击System setting选项: 2进去后,点击Storage选项: 3进去后,只有一个选项,就是AVAGO MegaRAID<ServerRAID M5210> C ...

  9. 2025杭电多校第五场 四角洲行动、“合理”避税、随机反馈、k-MEX 个人题解

    k-MEX 组合数 #数学 题目 思路 \[\begin{align} &\sum_{i=0}^{n-1}{i\times P\{ mex=i \}}\\ \\ &=\sum_{i=0 ...

  10. 文件上传MultipartFile异常java.lang.NoClassDefFoundError: javax/mail/internet/MimeUtility

    接口用MultipartFile接收文件,突然有天报了个org.springframework.web.multipart.MultipartException: Failed to parse mu ...