批处理系统
为了让管理员事先准备好一组程序, 让计算机执行完一个程序之后, 就自动执行下一个程序,提出了批处理系统的思想。处理系统的关键, 就是要有一个后台程序, 当一个前台程序执行结束的时候, 后台程序就会自动加载一个新的前台程序来执行,这样的一个后台程序, 其实就是操作系统。
我们希望操作系统和用户进程之间的切换是一种可以限制入口的执行流切换方式,这种方式是无法通过程序代码来实现的.
异常响应机制
为了实现最简单的操作系统, 硬件还需要提供一种可以限制入口的执行流切换方式. 这种方式就是自陷指令, 程序执行自陷指令之后, 就会陷入到操作系统预先设置好的跳转目标. 这个跳转目标也称为异常入口地址.
riscv32提供ecall指令作为自陷指令, 并提供一个mtvec寄存器来存放异常入口地址. 为了保存程序当前的状态, riscv32提供了一些特殊的系统寄存器, 叫控制状态寄存器(CSR寄存器).
PA这里需要用到三个状态寄存器,分别为- mepc寄存器 - 存放触发异常的PC
- mstatus寄存器 - 存放处理器的状态
- mcause寄存器 - 存放触发异常的原因
复制代码 riscv32触发异常后硬件的响应过程如下:- 将当前PC值保存到mepc寄存器
- 在mcause寄存器中设置异常号
- 从mtvec寄存器中取出异常入口地址
- 跳转到异常入口地址
复制代码 若决定无需杀死当前程序, 等到异常处理结束之后, 就根据之前保存的信息恢复程序的状态, 并从异常处理过程中返回到程序触发异常之前的状态. 具体地:
riscv32通过mret指令从异常处理过程中返回, 它将根据mepc寄存器恢复PC.
我们是以状态机的视角看待处理器的,前面在TRM和IOE中,我们说程序是个S = 的状态机,现在为了给机器添加状态响应机制,我们需要给这个机器的R添加功能,将R增加了R = {GPR, PC, SR}。其中GPR为通用寄存器堆,PC为程序计数器,SR为状态控制寄存器。也就是前面说的三个mepc mcause mtvec。
添加异常响应机制之后, 我们允许一条指令的执行会"失败". 为了描述指令执行失败的行为, 我们可以假设CPU有一条虚构的指令raise_intr, 执行这条虚构指令的行为就是上文提到的异常响应过程。- SR[mepc] <- PC
- SR[mcause] <- 一个描述失败原因的号码
- PC <- SR[mtvec]
复制代码- 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;
复制代码 然后调用了ecall指令,那此时我们就需要用到讲义提到的isa_raise_intr()函数。- bool cte_init(Context* (*handler)(Event ev, Context *ctx))用于进行CTE相关的初始化操作. 其中它还接受一个来自操作系统的事件处理回调函数的指针, 当发生事件时, CTE将会把事件和相关的上下文作为参数, 来调用这个回调函数, 交由操作系统进行后续处理.
- void yield()用于进行自陷操作, 会触发一个编号为EVENT_YIELD事件. 不同的ISA会使用不同的自陷指令来触发自陷操作, 具体实现请RTFSC.
复制代码 由于是ecall,那我们这里写他的异常号是11,于是现在跳到了isa_raise_intr()函数中,看下它的源码。- #include
- 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;
- }
复制代码 mstatus是0x1800是因为要通过difftest,mtvec存放触发异常的PC,也就是前面提到的__am_asm_trap,mcause存放出发状态异常的异常号,也就是调用这个函数用的11,mepc保存当前pc值,以便待会跳回来找到下一条指令。
执行完这个isa_raise_intr函数之后,我们就会跳到mtvec所指的地址,也就是__am_asm_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;
- }
复制代码 mcause=11于是ev.event被赋值为EVENT_YIELD,且mepc += 4,用于跑到错误的下一条指令去了。然后调用之前注册的回调函数,也就是- 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;
- }
复制代码 由于event是EVENT_YIELD,于是打印出了y。
然后保存现场,把刚改变的东西都还原回去,随后执行mret指令。
看下mret指令要我们干什么- 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();
- }
- }
复制代码 跳回到刚发生异常的下一个函数去了,也就是进入下一个while(1)中了,于是这样反复循环。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |