NEMU PA3 - 穿越时空的旅程: 批处理系统
请注意你的学术诚信!
本博客只提供个人思路的参考和一些想法, 并非能够抄袭的答案
1.本人水平有限,实现的PA可能有可怕的bug
2.本人思路可能有误,需要各位自行判别
最简单的操作系统
最简单的操作系统有:
-
批处理功能
有一个后台程序, 当一个前台程序执行结束的时候, 后台程序就会自动加载一个新的前台程序来执行
这样的一个后台程序, 其实就是操作系统
用户程序执行结束之后, 可以跳转到操作系统的代码继续执行
操作系统可以加载一个新的用户程序来执行 -
硬件中保护机制相关的功能
为了阻止程序将执行流切换到操作系统的任意位置, 硬件中逐渐出现保护机制相关的功能- i386中引入了保护模式(protected mode)和特权级(privilege level)的概念
- mips32处理器可以运行在内核模式和用户模式
- riscv32则有机器模式(M-mode), 监控者模式(S-mode)和用户模式(U-mode)
-
限制入口的执行流切换方式
如果操作系统崩溃了, 整个计算机系统都将无法工作. 所以, 人们还是希望能把操作系统保护起来, 尽量保证它可以正确工作.
用call/jal指令来进行操作系统和用户进程之间的切换就显得太随意了.
我们所希望的, 是一种可以限制入口的执行流切换方式,阻止程序将执行流切换到操作系统的任意位置,
这种方式就是自陷指令, 程序执行自陷指令之后, 就会陷入到操作系统预先设置好的跳转目标. 这个跳转目标也称为异常入口地址.
穿越时空的旅程
将上下文管理抽象成CTE
在
abstract-machine/scripts/linker.ld
中
_stack_top = ALIGN(0x1000);
. = _stack_top + 0x8000;
_stack_pointer = .;
end = .;
_end = .;
_heap_start = ALIGN(0x1000);
为程序提供了0x8000大小的栈空间
触发自陷操作
我们要实现简单的异常,那么对于riscv32要进行如下几个过程:
- 软件支持:操作系统的代码事先设置异常入口地址
- 硬件支持:触发异常后硬件的响应过程如下
- 将当前PC值保存到mepc寄存器
- 在mcause寄存器中设置异常号
- 从mtvec寄存器中取出异常入口地址
- 跳转到异常入口地址
软件支持在`abstract-machine/am/src/riscv/nemu/cte.c`中已经实现了:
bool cte_init(Context*(*handler)(Event, Context*)) {
// initialize exception entry
asm volatile("csrw mtvec, %0" : : "r"(__am_asm_trap));
// register event handler
user_handler = handler;
return true;
}
其已经实现了将异常入口地址放到mtvec寄存器中的任务了,那么我肯定要在
nemu/src/isa/riscv32/inst.c
实现对应的指令
而且其调用方式是一个宏CTE(simple_trap)
,展开后为:
({ Context *simple_trap(Event, Context *);
cte_init(simple_trap); });
硬件支持就是指令识别和nemu/src/isa/riscv32/system/intr.c word_t isa_raise_intr(word_t NO, vaddr_t epc)
。
我们要在isa_raise_intr()
中实现
- 将当前PC值保存到mepc寄存器
- 在mcause寄存器中设置异常号
- 从mtvec寄存器中取出异常入口地址
然后在解析ecall
中调用isa_raise_intr()
关于word_t isa_raise_intr(word_t NO, vaddr_t epc)参数的问题
epc很容易知道是当前pc值即s->snpc,但是NO是哪里的?
目前唯一的信息是abstract-machine/am/src/riscv/nemu/cte.c
中yield(){asm volatile("li a7, -1; ecall");}
NEMU中控制状态寄存器(CSR寄存器)在哪?
框架代码没有实现...
吐槽...我希望讲义能够讲清楚他没有实现...我找了半天,但是又怀疑我是不是RTFSC不够才没找到?
同时这些CSR寄存器通过指令传递过来编号,然后我们要通过编号识别出上哪个CSR寄存器
//nemu/src/isa/riscv32/include/isa-def.h
typedef struct {
word_t mtvec;
vaddr_t mepc;
word_t mstatus;
word_t mcause;
} MUXDEF(CONFIG_RV64, riscv64_CSRS, riscv32_CSRS);
typedef struct {
word_t gpr[MUXDEF(CONFIG_RVE, 16, 32)];
vaddr_t pc;
MUXDEF(CONFIG_RV64, riscv64_CSRS, riscv32_CSRS) csrs;
} MUXDEF(CONFIG_RV64, riscv64_CPU_state, riscv32_CPU_state);
//nemu/src/isa/riscv32/local-include/reg.h
static inline word_t *check_csrs_idx(word_t idx){
switch (idx)
{
case 0x305:
return &cpu.csrs.mtvec;
case 0x341:
return &cpu.csrs.mepc;
case 0x300:
return &cpu.csrs.mstatus;
case 0x342:
return &cpu.csrs.mcause;
default:
assert(0);
break;
}
}
#define gpr(idx) (cpu.gpr[check_reg_idx(idx)])
#define csrs(idx) *(check_csrs_idx(idx))
暂过:让DiffTest支持异常响应机制
吐槽
我运行后发现报错...
后面测试了下我确实是进去__am_asm_trap了
报错的原因是下面要实现的上下文...我找了半天报错的原因...
保存上下文
NEMU中的重要信息:
以支持现代操作系统的RISC-V处理器为例, 它们存在M, S, U三个特权模式, 分别代表机器模式, 监管者模式和用户模式. M模式特权级最高, U模式特权级最低, 低特权级能访问的资源, 高特权级也能访问
根据KISS法则, 我们并不打算在NEMU中加入保护机制. 我们让所有用户进程都运行在最高特权级
手册中的重要信息:
好吧,看来我们NEMU中的异常号与手册上是不一样的(搞得我一度怀疑Context的顺序实现错误了),还记得我们yield中的内容吗?
yield(){asm volatile("li a7, -1; ecall");}
,看来这里的yield异常号就是-1了
如下是我在__am_irq_handle
中打印的内容
01001100010011000100110001001100 0x4c4c4c4c
10000000000000000000010101111000 0x80000578
01001100010011000100110001001100 0x4c4c4c4c
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
10000000000000000010111000010011 0x80002e13
00000000000000000000000000000000 0x0
00000000100110001001011001111111 0x98967f
00000000000000000000000000000000 0x0
00000000000000000000000000000001 0x1
10000000000010011101111110111100 0x8009dfbc
10000000000000000001000010000000 0x80001080
10000000000000010001001111110000 0x800113f0
10000000000010010101011100000100 0x80095704
00000000100110001001011010000000 0x989680
00000000000000000000000000000000 0x0
11111111111111111111111111111111 0xffffffff
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
00000000000000000000000000000000 0x0
c->mcause: 11111111111111111111111111111111 0xffffffff
c->mstatus: 00000000000000000000000000000000 0x0
c->mepc: 10000000000000000001011100010100 0x80001714
c->pdir: 01001100010011000100110001001100 0x4c4c4c4c
x0,x2出现的是一些意义不明的0x4c4c4c4c,是因为在trap.S中没有将寄存器x0,x1push进栈。
导致我们从栈中取出来的是栈内得到的随机内容
我们的Context __am_irq_handle(Context c)中c这个参数是哪里来的?
我们的__am_irq_handle
是在abstract-machine/am/src/riscv/nemu/trap.S
中直接通过汇编jal __am_irq_handle
调用的
想一想:函数的参数传递是不是要用到栈? 在参数传递的时候(CSAPP一书P170上):
- 寄存器不足够用来传递全部参数时,用栈
对于我们这里的Context *c
这个参数,整个内容都是用栈进行参数传递
越在栈底(即在地址更大的地方)说明这个参数越在后面(如: void add(int a, int b), b参数在a参数后面)
那么在结构体Context
中,这个变量的位置就越在后面,所以顺序如下:
//abstract-machine/am/include/arch/riscv.h
struct Context {
// TODO: fix the order of these members to match trap.S
uintptr_t gpr[NR_REGS], mcause, mstatus, mepc;
void *pdir;
};
c指向的上下文结构体其实NEMU中通用寄存器,CSRS,地址空间的内容。在AM中通过Context这个抽象传给
__am_irq_handle()
了
每一个成员都是最后AM编译成指令然后再NEMU上跑的时候,通过指令将相应的值从NEMU中取出来
事件分发 与 恢复上下文
目前应该只有 -1 这个一个yield信号吧
riscv32的ecall, 保存的是自陷指令的PC, 因此软件需要在适当的地方对保存的PC加上4, 使得将来返回到自陷指令的下一条指令.
Context* __am_irq_handle(Context *c) {
if (user_handler) {
Event ev = {0};
// debugContext(c);
switch (c->mcause) {
case (uintptr_t)-1: ev.event = EVENT_YIELD; break;
default: ev.event = EVENT_ERROR; break;
}
c = user_handler(ev, c);
assert(c != NULL);
}
c->mepc = c->mepc + 4;
return c;
}
- cte_init: 首先软件AM准备好异常入口地址,存放到硬件NEMU的CSR寄存器mtvec上
并且软件注册好一个异常处理程序,这个异常处理程序会根据异常发生时的上下文,进行相应处理 - 然后软件程序主动触发yield(), 则硬件NEMU中执行
ecall
指令,将pc值设置为mtvec中的内容,即设置pc = 异常入口地址
同时根据yield指令asm volatile("li a7, -1; ecall");
设置CSR寄存器mcause异常号,同时保存异常发生时的pc值到CSR寄存器mepc
而对于mstatus:
- 程序跳转到异常入口地址进行执行,异常入口地址程序做的事情是保存上下文,将这些状态压入栈中
- 异常入口地址程序调用
__am_irq_handle
,设置事件,触发异常处理程序 - 异常处理程序结束后,返回异常入口地址程序,恢复上下文
异常处理的踪迹 - etrace
暂时跳过~
用户程序和系统调用
最简单的操作系统
//nanos-lite/src/irq.c
static Context* do_event(Event e, Context* c) {
switch (e.event) {
case EVENT_YIELD:
Log("Nanos in yield"); break;
default: panic("Unhandled event ID = %d", e.event);
}
return c;
}
void init_irq(void) {
Log("Initializing interrupt/exception handler...");
cte_init(do_event);
}
加载第一个用户程序
make失败了?
那你和我一样没看信息了:
检测ELF文件的ISA类型 与 检查ELF文件的魔数
根据讲义的提示,可以在/usr/include/elf.h
发现如下内容:
同时通过readelf -a excefile
命令在elf头可以发现有如下内容:
Machine: RISC-V
宏的参考写法在
navy-apps/libs/libos/src/crt0/start.S
看来他是帮我们字符化了,这里的内容其实数字243
具体实现
可执行文件在哪里?
根据nanos-lite/src/resources.S
中的实现:
.section .data
.global ramdisk_start, ramdisk_end
ramdisk_start:
.incbin "build/ramdisk.img"
ramdisk_end:
.section .rodata
.globl logo
logo:
.incbin "resources/logo.txt"
.byte 0
使用 .incbin 指令将一个名为 "build/ramdisk.img" 的二进制文件的内容直接嵌入到定义的
.section .data
数据段中。
我们访问ramdisk_start的内容就相当于访问了文件build/ramdisk.img
的内容了
代码和数据在可执行文件的哪个位置?
首先我要得到其中的程序头表
然后我要逐个解析其中的条目
解析过程中我要找到type为LOAD
的内容
代码和数据有多少?
offset字段为从可执行文件的第offset个字节开始(即偏移量),到offset+FileSiz为止,将其cpy到内存地址vaddr处
如果多出来MemSiz-FileSiz个字节,则填充0
"正确的内存位置"在哪里?
理解NEMU中的内存
其实NEMU中的内存结构早应该理解清楚,全部的答案在abstract-machine/scripts/linker.ld
中,只是当时理解错了
_stack_top = ALIGN(0x1000)
并不表示_stack_top在0x1000对齐后的位置
我们再往上看一点可以发现_stack_top上是.bss节,因此,_stack_top 的值将是 .bss 节的结束地址加上额外的填充以确保栈的地址对齐到 0x1000 的边界。
可能有些地方画的有点离谱,但是大致结构就这样的
关于ELF的一些提示与资源
操作系统的运行时环境
在PA2中(即实现AM中各种IOE设备运行环境的时候), 我们根据具体实现是否与ISA相关, 将运行时环境划分为两部分.
但对于运行在操作系统上的程序, 它们就不需要直接与硬件交互了
操作系统需要为用户程序提供相应的服务. 这些服务需要以一种统一的接口来呈现, 用户程序也只能通过这一接口来请求服务.
这一接口就是系统调用. 系统调用把整个运行时环境分成两部分:
- 一部分是操作系统内核区,那些会访问系统资源的功能会放到内核区中实现
- 另一部分是用户区. 用户区则保留一些无需使用系统资源的功能(比如strcpy()), 以及用于请求系统资源相关服务的系统调用接口.
系统调用
我们首先来理清一下当程序中调用了_syscall_整个过程到底发生了什么:
-
AM将异常入口地址放到CSR寄存器中,Nanos操作系统注册
do_event
作为异常处理函数(在nanos-lite/src/irq.c
中) -
首先程序中调用_syscall_,如
_syscall_(SYS_yield, 0, 0, 0)
,将系统调用号放进去作为参数了 -
_syscall_中将参数放到寄存器中后,直接执行
ecall
,触发异常 -
然后我的NEMU执行到这条指令后,将pc的值设置为异常入口地址
__am_asm_trap
(在abstract-machine/am/src/riscv/nemu/trap.S
中) -
然后执行
__am_asm_trap
,保存上下文,调用__am_irq_handle
(在abstract-machine/am/src/riscv/nemu/cte.c
中) -
__am_irq_handle
识别异常号,将异常号分装成事件号,并将'事件'与'上下文'当成函数参数,并执行操作系统注册的函数do_event
-
do_event
根据事件号,得知是一个系统调用(EVENT_SYSCALL),那么执行Nanos操作系统中的do_syscall
(在nanos-lite/src/syscall.c
中) -
do_syscall
根据保存在寄存器中异常号得知发生的系统调用是什么,进行相应处理
这里有来自CSAPP一书上P504的一个知识点:
我们将异常引起的状态变化称之为'事件'
异常有很多种类:
* 中断
* 陷阱(系统调用)
* 故障
* 终止
系统调用有许多的异常号,事件号可以将是系统调用的异常号归纳为系统调用,通过事件号,操作系统就可以知道这个异常是哪一大类,从而进行对应操作。
识别系统调用
//abstract-machine/am/src/riscv/nemu/cte.c
/*irq Interrupt Request 中断请求*/
Context* __am_irq_handle(Context *c) {
if (user_handler) {
Event ev = {0};
// debugContext(c);
switch (c->mcause) {
case (uintptr_t)-1: ev.event = EVENT_YIELD; break;
case (uintptr_t) 1: ev.event = EVENT_SYSCALL; break;
default: ev.event = EVENT_ERROR; break;
}
c = user_handler(ev, c);
assert(c != NULL);
}
c->mepc = c->mepc + 4;
return c;
}
实现SYS_yield系统调用
在abstract-machine/am/include/arch/目录下的相应头文件中实现正确的GPR?宏, 让它们从上下文c中获得正确的系统调用参数寄存器.
//abstract-machine/am/include/arch/riscv.h
#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
添加SYS_yield系统调用, 设置系统调用的返回值.
//nanos-lite/src/syscall.c
static void SYS_yield(Context *c){
yield();
c->GPRx = 0;
}
这里我们只要设置寄存器a0的值就行了,因为在
navy-apps/libs/libos/src/syscall.c _syscall_
中register intptr_t ret asm (GPRx); return ret;
也就是取到寄存器a0的值,然后返回罢了
从navy-app程序到在nanos-lite运行全过程分析
首先我们的操作系统nanos-lite也是一个C程序,有main函数
//nanos-lite/src/main.c
int main() {
extern const char logo[];
printf("%s", logo);
Log("'Hello World!' from Nanos-lite");
Log("Build time: %s, %s", __TIME__, __DATE__);
init_mm();
init_device();
init_ramdisk();
#ifdef HAS_CTE
init_irq();
#endif
init_fs();
//在这里调用load加载器并执行开始内存中的程序
//也就是说下面的内容可能不会再执行了
init_proc();
Log("Finish initialization");
#ifdef HAS_CTE
yield();
#endif
panic("Should not reach here");
}
而且在操作系统的main函数运行时, 会做点其他的工作,其中一部分是调用_trm_init函数:
//abstract-machine/am/src/platform/nemu/trm.c
void _trm_init() {
int ret = main(mainargs);
halt(ret);
}
然后就是开启运行我们navy-app程序了,navy-app程序是运行在操作系统上的,因为是操作系统的加载器将程序内容放到了内存,然后让pc指向其中的指令然后运行的
一切程序都有结束的时候,如果接受会调用halt
函数
//abstract-machine/am/src/platform/nemu/trm.c
void halt(int code) {
nemu_trap(code);
// should not reach here
while (1);
}
nemu_trap()中会执行
ebreak
指令
这条指令如果运行在NEMU那么会执行宏:
//nemu/include/cpu/cpu.h
#define NEMUTRAP(thispc, code) set_nemu_state(NEMU_END, thispc, code)
可以看到这里有set_nemu_state,并且将NEMU的状态设置为NEMU_END也就是NEMU要停止执行了
当然上述情况是基于NEMU视角的结束,那基于navy-app程序视角的结束是如何的?
//navy-apps/libs/libos/src/crt0/crt0.c
int main(int argc, char *argv[], char *envp[]);
extern char **environ;
void call_main(uintptr_t *args) {
char *empty[] = {NULL };
environ = empty;
exit(main(0, empty, empty));
assert(0);
}
可以看到在程序的main结束后还进行了善后工作,执行exit函数
这个exit函数中调用了_exit函数:
//navy-apps/libs/libos/src/syscall.c
void _exit(int status) {
_syscall_(SYS_exit, status, 0, 0);
while (1);
}
可以看到最终我们是调用了sys_exit进行处理程序的结束
执行结果和讲义有点不一样?
而我的结果是:
同时发现nanos-lite/src/syscall.h
是空的,里面根本没有东西...
感觉讲义这里搞错了吧,不是
nanos-lite/src/syscall.h
,应该是navy-apps/libs/libos/src/syscall.h
才对,这里面定义了SYS_exit
调试了贼久才发现,报错的EVENT_ERROR是哪里来的:
//abstract-machine/am/src/riscv/nemu/cte.c
Context* __am_irq_handle(Context *c) {
if (user_handler) {
Event ev = {0};
// debugContext(c);
switch (c->mcause) {
case (uintptr_t)-1: ev.event = EVENT_YIELD; break;
case (uintptr_t) 1: ev.event = EVENT_SYSCALL; break;
default: ev.event = EVENT_ERROR; break;
}
c = user_handler(ev, c);
assert(c != NULL);
}
c->mepc = c->mepc + 4;
return c;
}
这里我默认如果找不到对应的异常号(c->mcause
),那么事件号就是EVENT_ERROR
这里早知道应该直接用assert了
这里因为会再次执行ecall,是因为navy-app程序结束后会调用exit,exit中会系统调用_syscall_(SYS_exit, status, 0, 0);
SYS_exit的值为0,所以放到a7中的值为0,NEMU中从a7中取出的c->mcause也为0,在 __am_irq_handle
识别不出来为0的异常号
终于成功了~
系统调用的踪迹 - strace
暂过~
操作系统之上的TRM
标准输出
man syscall
long syscall(long number, ...);
In general, a 0 return value indicates success. A -1 return value indicates an error, and an error number is stored in errno.
Arch/ABI Instruction System call # Ret val Ret val2 Error Notes
riscv ecall a7 a0 a1 -
Arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7 Notes
riscv a0 a1 a2 a3 a4 a5 -
man 2 write
ssize_t write(int fd, const void *buf, size_t count);
On success, the number of bytes written is returned. On error, -1 is returned, and errno is set to indicate the cause of the error.
Note that a successful write() may transfer fewer than count bytes
救命,我希望讲义说清楚执行完后的结果,这里没实现下面的堆区管理会只输出一个字符,这是正常的。
我又又以为我实现错误了,调试半天
堆区管理
所谓program break, 就是用户程序的数据段(data segment)结束的位置.
链接的时候ld会默认添加一个名为_end的符号, 来指示程序的数据段结束的位置. 用户程序开始运行的时候, program break会位于_end所指示的位置, 意味着此时堆区的大小为0
后续的sbrk()调用来动态调整用户程序program break的位置了. 当前program break和和其初始值之间的区间就可以作为用户程序的堆区, 由malloc()/free()进行管理.
又出错了...错误点在哪呢?
注意_sbrk()的返回值!
总结
hello程序一开始在哪里? 在磁盘上
它是怎么出现内存中的? 为什么会出现在目前的内存位置? 它的第一条指令在哪里? 他是通过操作系统中的加载器,通过分析elf上的程序节表,得到elf文件要加载到内存的地址,以及elf文件哪些内容需要被加载。
究竟是怎么执行到它的第一条指令的? 在elf头可以查看执行入口地址,那是第一条指令的地址
hello程序在不断地打印字符串, 每一个字符又是经历了什么才会最终出现在终端上? hello程序调用printf时,会调用malloc()申请一片缓冲区,来存放格式化的内容,malloc每次都会调用void* sbrk(intptr_t increment);
扩容堆区。
然后系统调用SYS_write,将缓冲区的内容输出到终端(在NEMU中通过putch实现单字符输出)
个人认为这一部分4个重点文件:
- navy-apps/libs/libos/src/syscall.c 用户区与操作系统打交道的库
- abstract-machine/am/src/riscv/nemu/cte.c 异常形成事件,陷入操作系统的代码
- navy-apps/libs/libos/src/syscall.h 用户区各种系统调用的调用号
- nanos-lite/src/syscall.c 系统调用函数具体实现代码
文件系统
简易文件系统
注意讲义陷阱!!!,这里光实现
fs_open()
,fs_read()
和fs_close()
是不够的!我们还有实现fs_lseek()
才能完成让loader使用文件
这一块感觉挺容易错的,但是调试还是简单的:
- 在ramdisk_read 使用printf打印出offset
- 在loader 使用printf打印出 ramdisk_offset + 原来使用ramdisk_read传入的参数offset
比较两者结果是否一样,就很容易调试出错了~
如果你和我一样莫名nemu退出了,说明你没有在navy-apps/libs/libos/src/syscall.c
中进行系统调用...
吐槽:害,为啥未实现的不直接assert(0),这样我就知道哪里错了嘛,偏偏用_exit()导致我以为我哪里实现错了...
一切皆文件
AM中的IOE向我们展现了程序进行输入输出的需求. 那么在Nanos-lite上, 如果用户程序想访问设备, 要怎么办呢?
我们之前提到, 文件的本质就是字节序列. 事实上, 计算机系统中到处都是字节序列
既然文件就是字节序列, 那很自然地, 上面这些五花八门的字节序列应该都可以看成文件. Unix就是这样做的, 因此有"一切皆文件"(Everything is a file)的说法.
这种做法最直观的好处就是为不同的事物提供了统一的接口: 我们可以使用文件的接口来操作计算机上的一切, 而不必对它们进行详细的区分
为了实现一切皆文件的思想, 我们之前实现的文件操作就需要进行扩展了: 我们不仅需要对普通文件进行读写, 还需要支持各种"特殊文件"的操作.
这组扩展语义之后的API有一个酷炫的名字, 叫VFS(虚拟文件系统).
在Nanos-lite中, 实现VFS的关键就是Finfo结构体中的两个读写函数指针:
typedef struct {
char *name; // 文件名
size_t size; // 文件大小
size_t disk_offset; // 文件在ramdisk中的偏移
ReadFn read; // 读函数指针
WriteFn write; // 写函数指针
} Finfo;
其中ReadFn和WriteFn分别是两种函数指针, 它们用于指向真正进行读写的函数, 并返回成功读写的字节数. 有了这两个函数指针, 我们只需要在文件记录表中对不同的文件设置不同的读写函数, 就可以通过f->read()和f->write()的方式来调用具体的读写函数了.
有了VFS, 要把IOE抽象成文件就非常简单了.
实现gettimeofday
关于输入设备, 我们先来看看时钟. 时钟比较特殊, 大部分操作系统并没有把它抽象成一个文件, 而是直接提供一些和时钟相关的系统调用来给用户程序访问.
在Nanos-lite中,我们也提供一个SYS_gettimeofday系统调用, 用户程序可以通过它读出当前的系统时间.
man 2 gettimeofday
int gettimeofday(struct timeval *tv, struct timezone *tz);
The tv argument is a struct timeval (as specified in <sys/time.h>):
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
and gives the number of seconds and microseconds since the Epoch (see
time(2))
The use of the timezone structure is obsolete; the tz argument should
normally be specified as NULL. (See NOTES below.)
gettimeofday() and settimeofday() return 0 for success, or -1 for fail‐ure (in which case errno is set appropriately).
所以我们的第二个参数一般都是NULL
//nanos-lite/src/syscall.c
static void sys_gettimeofday(Context *c){
struct timeval *tv = (struct timeval *)c->GPR2;
/*这里不支持实现tz,调用时传入参数NULL*/
//assert(c->GPR3 == NULL);
assert(tv != NULL);
uint64_t us = io_read(AM_TIMER_UPTIME).us;
tv->tv_sec = us / 1000000;
tv->tv_usec = us % 1000000;
c->GPRx = 0;
}
timer-test需要我们自己加,注意navy-apps/tests/timer-test/Makefile
也要加上。
在navy-apps/libs/libc/src/syscalls/sysgettod.c
上其实可以找到gettimeofday
,其会调用_gettimeofday_r
,而_gettimeofday_r
会调用我们在navy-apps/libs/libos/src/syscall.c
中实现的_gettimeofday
上述实现在nemu/src/utils/timer.c
中有提示:
//nemu/src/utils/timer.c
static uint64_t get_time_internal() {
#if defined(CONFIG_TARGET_AM)
uint64_t us = io_read(AM_TIMER_UPTIME).us;
#elif defined(CONFIG_TIMER_GETTIMEOFDAY)
struct timeval now;
gettimeofday(&now, NULL);
uint64_t us = now.tv_sec * 1000000 + now.tv_usec;
#else
struct timespec now;
clock_gettime(CLOCK_MONOTONIC_COARSE, &now);
uint64_t us = now.tv_sec * 1000000 + now.tv_nsec / 1000;
#endif
return us;
}
uint64_t get_time() {
if (boot_time == 0) boot_time = get_time_internal();
uint64_t now = get_time_internal();
return now - boot_time;
}
我们nemu中获得时间是通过get_time得到的,这个时间再通过设备接口给应用程序
clock_gettime(CLOCK_MONOTONIC_COARSE, &now);
得到的时间是相对于系统启动时刻的持续时间,而gettimeofday(&now, NULL);
得到的时间是从 Unix Epoch(1970 年 1 月 1 日)开始的秒数和微秒数。
我猜再我们这里gettimeofday
得到的时间是操作系统nanos-lite启动到目前的时间与clock_gettime
得到的时间是一样的(要不然我也不知道咋实现了)
问了下chatgpt才发现NDL这玩意是SDL的翻版,NDL_GetTicks()对标SDL_GetTicks(),SDL_GetTicks()用于获取从SDL库初始化以来经过的毫秒数。
联系下讲义中的'我们约定程序在使用NDL库的功能之前必须先调用NDL_Init()',所以我们先要记录下NDL库是初始化的时间。
//navy-apps/libs/libndl/NDL.c
uint32_t NDL_GetTicks() {
struct timeval now;
gettimeofday(&now, NULL);
return (now.tv_sec * 1000000 + now.tv_usec) - (NDL_startTime.tv_sec * 1000000 + NDL_startTime.tv_usec);
}
int NDL_Init(uint32_t flags) {
gettimeofday(&NDL_startTime, NULL);
if (getenv("NWM_APP")) {
evtdev = 3;
}
return 0;
}
找不到NDL.h库?
八成是Makefile在编译链接的时候没有引入libndl
库
//navy-apps/Makefile
### Add default libraries for ISA != native
ifneq ($(ISA), native)
LIBS += libc libos libndl
CFLAGS += -U_FORTIFY_SOURCE # fix compile error in Newlib on ubuntu
else
WL = -Wl,
endif
把按键输入抽象成文件
说实话,这里把我搞不会了
fopen和open在navy-apps/libs/libos/src/native.cpp
中有实现
int open(const char *path, int flags, ...) {
if (strcmp(path, "/proc/dispinfo") == 0) {
return dispinfo_fd;
} else if (strcmp(path, "/dev/events") == 0) {
return evt_fd;
} else if (strcmp(path, "/dev/fb") == 0) {
return fb_memfd;
} else if (strcmp(path, "/dev/sb") == 0) {
return sb_fifo[1];
} else if (strcmp(path, "/dev/sbctl") == 0) {
return sbctl_fd;
} else {
char newpath[512];
return glibc_open(redirect_path(newpath, path), flags);
}
}
FILE *fopen(const char *path, const char *mode) {
char newpath[512];
if (glibc_fopen == NULL) {
glibc_fopen = (FILE*(*)(const char*, const char*))dlsym(RTLD_NEXT, "fopen");
assert(glibc_fopen != NULL);
}
return glibc_fopen(redirect_path(newpath, path), mode);
}
其中最炸裂的是open中的返回值:
static int dummy_fd = -1;
static int dispinfo_fd = -1;
static int fb_memfd = -1;
static int evt_fd = -1;
static int sb_fifo[2] = {-1, -1};
static int sbctl_fd = -1;
而且在navy-apps/libs/libos/src/native.cpp
也实现了read
和write
,但是read中
if (fd == dispinfo_fd) ...
else if (fd == evt_fd) ...
else if (fd == sbctl_fd) ...
这些值不都是-1吗?咋能够通过if else区别出要做的事情呢?
Init()中的一顿操作,可能改变了这些值,但是我看不懂
好吧既然搞不懂就先放置到这里吧,按照的讲义的想法,我们应该是要实现event_read
从IOE中读取键盘事件,并保存到buf中;
//nanos-lite/src/device.c
size_t events_read(void *buf, size_t offset, size_t len) {
AM_INPUT_KEYBRD_T ev = io_read(AM_INPUT_KEYBRD);
if (ev.keycode == AM_KEY_NONE)
return 0;
size_t ret = snprintf(buf, len, "%s %s\n", ev.keydown?"kd":"ku", keyname[ev.keycode]);
return ret;
}
然后在fs_read中的ReadFn read中注册event_read
,这样当应用程序要读键盘即/dev/event
时,就会通过系统调用SYS_read
调用fs_read;fs_read通过注册的event_read
读出键盘值
//nanos-lite/src/fs.c
static Finfo file_table[] __attribute__((used)) = {
[FD_STDIN] = {"stdin", 0, 0, invalid_read, invalid_write},
[FD_STDOUT] = {"stdout", 0, 0, invalid_read, serial_write},
[FD_STDERR] = {"stderr", 0, 0, invalid_read, serial_write},
{"/dev/events", 0, 0, events_read, invalid_write},
#include "files.h"
};
size_t fs_read(int fd, void *buf, size_t len){
size_t ret;
if (file_table[fd].read != NULL)
ret = file_table[fd].read(buf, 0, len);
else {
assert(file_table[fd].open_offset <= file_table[fd].size);
ret = ramdisk_read(buf, file_table[fd].disk_offset + file_table[fd].open_offset, len);
file_table[fd].open_offset += ret;
}
return ret;
}
NDL_PollEvent()
即是经过进一步封装的函数,说实话当时我考虑他到底是调用啥来实现呢?
直到我看到了这个.c文件其他NDL函数的实现,他们直接使用了write,那我猜这个NDL_PollEvent()
应该是用read
吧。
然后查了查open和fopen的区别:from this blog
所以我这里用open
//navy-apps/libs/libndl/NDL.c
int NDL_PollEvent(char *buf, int len) {
int ret;
int fd = open("/dev/events", 0);
ret = read(fd, buf, len);
close(fd);
return ret;
}
VGA
在NDL中获取屏幕大小
结果为:
我这里对bmp-test做了变化
void *bmp = BMP_Load("/share/pictures/projectn.bmp", &w, &h);
assert(bmp);
printf("w: %d, h: %d\n", w, h);
w = 0;
h = 0;
NDL_OpenCanvas(&w, &h);
printf("w: %d, h: %d\n", w, h);
可知,系统最大屏幕为:400x300,原来的屏幕为128x128
在下navy-apps/README.md
:
2. procfs文件系统: 所有的文件都是key-value pair, 格式为` [key] : [value]`, 冒号左右可以有任意多(0个或多个)的空白字符(whitespace).
* `/proc/dispinfo`: 屏幕信息, 包含的keys: `WIDTH`表示宽度, `HEIGHT`表示高度.
* `/proc/cpuinfo`(可选): CPU信息.
* `/proc/meminfo`(可选): 内存信息.
例如一个合法的 `/proc/dispinfo`文件例子如下:
WIDTH : 640
HEIGHT:480
在讲义中其说明了:'屏幕大小的信息通过/proc/dispinfo文件来获得, 它需要支持读操作. navy-apps/README.md中对这个文件内容的格式进行了约定'
实现dispinfo_read(),通过IOE的相应API来获取.具体的屏幕大小
size_t dispinfo_read(void *buf, size_t offset, size_t len) {
int w = io_read(AM_GPU_CONFIG).width;
int h = io_read(AM_GPU_CONFIG).height;
size_t ret = snprintf(buf, len, "%s:%d\n%s:%d\n", "WIDTH", w, "HEIGHT", h);
return ret;
}
相应地,我要在nanos-lite/src/fs.c
中注册一个Finfo:
//nanos-lite/src/fs.c
static Finfo file_table[] __attribute__((used)) = {
[FD_STDIN] = {"stdin", 0, 0, invalid_read, invalid_write},
[FD_STDOUT] = {"stdout", 0, 0, invalid_read, serial_write},
[FD_STDERR] = {"stderr", 0, 0, invalid_read, serial_write},
[FD_EVENT] = {"/dev/events", 0, 0, events_read, invalid_write},
[FD_FB] = {"/proc/dispinfo", 0, 0, dispinfo_read, invalid_write},
#include "files.h"
};
当我们对/proc/dispinfo
进行读的时候,这直接调用dispinfo_read
而在dispinfo_read
中,会通过AM中实现的IOE读出系统屏幕大小。
然后navy-apps/libs/libndl/NDL.c
中的void NDL_OpenCanvas(int *w, int *h)
就属于应用程序基本了,其调用read函数
以触发系统调用
//navy-apps/libs/libndl/NDL.c
void NDL_OpenCanvas(int *w, int *h) {
int fd = open("/proc/dispinfo", 0);
char dispinfo_buf[64];
read(fd, dispinfo_buf, 64);
sscanf(dispinfo_buf, "WIDTH:%d\nHEIGHT:%d\n", &screen_w, &screen_h);
if (*w == 0 && *h == 0){
*w = screen_w;
*h = screen_h;
}
if (*w > screen_w) *w = screen_w;
if (*h > screen_h) *h = screen_h;
close(fd);
}
把VGA显存抽象成文件
在init_fs()(在nanos-lite/src/fs.c中定义)中对文件记录表中/dev/fb的大小进行初始化
感觉好没用的操作...
//nanos-lite/src/fs.c
extern int fs_screen_w;
extern int fs_screen_h;
void init_fs() {
// TODO: initialize the size of /dev/fb
int fd = fs_open("/proc/dispinfo", 0, 0);
char buf[64];
fs_read(fd, buf, 64);
file_table[fd].size = fs_screen_w * fs_screen_h * sizeof(uint32_t);
file_table[fd].open_offset = file_table[fd].disk_offset;
fs_cloes(fd);
}
//然后在nanos-lite/src/device.c
int fs_screen_w;
int fs_screen_h;
size_t dispinfo_read(void *buf, size_t offset, size_t len) {
screen_w = io_read(AM_GPU_CONFIG).width;
screen_h = io_read(AM_GPU_CONFIG).height;
//这个是为了让fs.c中能够得到系统屏幕大小
fs_screen_w = screen_w;
fs_screen_h = screen_h;
size_t ret = snprintf(buf, len, "%s:%d\n%s:%d\n", "WIDTH", screen_w, "HEIGHT", screen_h);
return ret;
}
感觉这里我实现的不太好...,但是能用且不知道咋改进了
接下来我感觉先考虑在NDL中实现NDL_DrawRect()比较轻松:
在NDL中实现NDL_DrawRect()
在
am-kernels/tests/am-tests/src/tests/video.c
中使用IOE操作的案例
要实现画布居中,则要理清画布和系统屏幕的关系:
- 我们用IOE(
dispinfo_read
)得到的屏幕大小是系统屏幕大小 - 我们用
NDL_DrawRect
传入的参数,其中w*h可以看做是画布的大小,然后画布和系统屏幕的左上角都是在(0,0),如:
我们的任务就是在NDL_DrawRect
将相对于画布的位置变成相对于系统屏幕居中的位置,这是通过lseek实现的,即让(x,y)->(nx,ny)
因为我们的fs_write是每一次将buf中的len个字符写入文件中
因为我们对于文件/dev/fb
在写的时候是调用fb_write
,fb_write
又会调用io_write(AM_GPU_FBDRAW,...)
如果理清了他们之间参数传递的关系,则会发现调用io_write必须是如下的io_write(AM_GPU_FBDRAW, x, y, (void *)buf, len, 1, true);
即每次画一行
//nanos-lite/src/device.c
size_t fb_write(const void *buf, size_t offset, size_t len) {
int x = offset % screen_w;
int y = offset / screen_w;
//printf("fb_write: x:%d, y:%d\n", x, y);
io_write(AM_GPU_FBDRAW, x, y, (void *)buf, len, 1, true);
return 0;
}
//navy-apps/libs/libndl/NDL.c
void NDL_DrawRect(uint32_t *pixels, int x, int y, int w, int h) {
int fd = open("/dev/fb", 0);
//得到在屏幕上,让画布居中的左上角点
assert(screen_h >= h);
assert(screen_w >= w);
int my = (screen_h - h) / 2;
int mx = (screen_w - w) / 2;
//printf("NDL_DrawRect fd:%d\n", fd);
for (y = 0; y < h; y++){
//printf("NDL_DrawRect mx: %d, y+my: %d\n", mx, y+my);
lseek(fd, (y + my) * screen_w + mx, SEEK_SET);
write(fd, (void *)&pixels[y * w], w);
}
close(fd);
}
总结:论如何理解虚拟文件系统?
虚拟文件系统,说明这个文件并不是真实的,并没有真正开辟一个空间给其当做文件,用来存放数据
在这个章节我们遇到了许多虚拟文件:
- /dev/events
- /dev/fb
- /proc/dispinfo
他们都是我们为了统一访问在AM中实现的IOE接口,而虚拟出来的文件,我们只是依据这些文件名称,一一对应上不同设备要进行的不同操作
同时我们将访问设备看做访问文件,这样我们可以将接口统一为fs_read/fs_write(int fd, const void *buf, size_t len)
这样一组参数,同时利用Finfo
中的 ReadFn read;WriteFn write;
这两个函数指针
当对于不同设备,两个函数指针注册不同的处理函数,然后在fs_read/fs_write
中直接调用read/write就能够处理,我们再也不用为应用程序,对不同设备提供不同的接口,十分方便。
然后真正访问设备时,是通过AM中提供的IOE接口:io_write
和io_read
,将buf中len个字节通过io_write写入设备的I/O映射内存,或读取设备I/O映射内存中len个字节内容到buf中.
当然还有真实文件系统,这些文件(nanos-lite/src/files.h
)我们是真的开辟的内存给他们用:
所谓的真实文件系统, 其实是指具体如何操作某一类文件. 比如在Nanos-lite上, 普通文件通过ramdisk的API进行操作
VFS(虚拟文件系统)其实是对不同种类的真实文件系统的抽象, 它用一组API来描述了这些真实文件系统的抽象行为
屏蔽了真实文件系统之间的差异, 上层模块(比如系统调用处理函数)不必关心当前操作的文件具体是什么类型, 只要调用这一组API即可完成相应的文件操作.
精彩纷呈的应用程序
在Linux中, 有一批GUI程序是使用SDL库来开发的. 在Navy中有一个miniSDL库, 它可以提供一些兼容SDL的API, 这样这批GUI程序就可以很容易地移植到Navy中了
我们可以通过NDL来支撑miniSDL的底层实现, 让miniSDL向用户程序提供更丰富的功能
运行NSlider
在运行脚本
navy-apps/apps/nslider/slides/convert.sh
可能会有报错,这是有相关库没有安装,以及一些在navy-apps/apps/nslider/README.md
提到的文件,看navy-apps/apps/nslider/README.md
一切都会解决了~
//navy-apps/libs/libminiSDL/src/video.c
void SDL_UpdateRect(SDL_Surface *s, int x, int y, int w, int h) {
NDL_DrawRect((uint32_t *)s->pixels, x, y, s->w, s->h);
}
//navy-apps/libs/libminiSDL/src/event.c
int SDL_WaitEvent(SDL_Event *event) {
char buf[64];
int ret;
while ((ret = NDL_PollEvent(buf, 64)) != 0){
char keytype[4];
char keycode[32];
sscanf(buf, "%s %s", keytype, keycode);
if (strcmp(keytype, "kd") == 0) event->type = SDL_KEYDOWN;
else if (strcmp(keytype, "ku") == 0) event->type = SDL_KEYUP;
else assert(0);
for (int i = 0; i < sizeof(keyname) / sizeof(keyname[0]); i++){
if (strcmp(keycode, keyname[i]) == 0){
//printf("SDL_WaitEvent ret: %d %s\n", ret, keycode);
event->key.type = event->type;
event->key.keysym.sym = i;
assert(i != 0);
return 1;
}
}
assert(0);
}
return 0;
}
如何编译链接Nslider到nanos-lite上?
在navy-apps/Makefile
中169行左右:
APPS = nslider
MENU (开机菜单)
开机菜单是另一个行为比较简单的程序, 它会展示一个菜单, 用户可以选择运行哪一个程序. 为了运行它, 你还需要在miniSDL中实现两个绘图相关的API:
- SDL_FillRect(): 往画布的指定矩形区域中填充指定的颜色
- SDL_BlitSurface(): 将一张画布中的指定矩形区域复制到另一张画布的指定位置
成功实现API的关键是认识到各个API的作用,以及认识到SDL_Surface
中记录的一个画布的信息:
typedef struct SDL_Surface {
Uint32 flags; // 表面的标志
SDL_PixelFormat *format; // 表面的像素格式
int w, h; // 表面的宽度和高度
int pitch; // 表面的行字节数
void *pixels; // 指向像素数据的指针
SDL_Rect clip_rect; // 裁剪矩形
int refcount; // 引用计数
} SDL_Surface;
其中pixels
记录了一整个画布的像素信息,和以前一样,图像像素按行优先方式存储在pixels
中, 每个像素用32位整数以00RRGGBB
的方式描述颜色
w,h是画布的高和宽
我们在实现讲义要求的API SDL_FillRect()
和SDL_BlitSurface()
时,要注意对pixels
中的偏移量进行调整
因为我们是在画布中某一个矩形区域内进行操作,那么我们就获得到这个矩形区域中的像素数据
而且注意在实现SDL_BlitSurface
的时候,在SDL手册上有一个假设:
This assumes that the source and destination rectangles are the same size.
//navy-apps/libs/libminiSDL/src/video.c
void SDL_FillRect(SDL_Surface *dst, SDL_Rect *dstrect, uint32_t color) {
assert(dst);
uint32_t *pixels = (uint32_t *)dst->pixels;
int w;
int h;
int x;
int y;
if (dstrect == NULL){
w = (int)dst->w;
h = (int)dst->h;
x = 0;
y = 0;
} else {
w = (int)dstrect->w;
h = (int)dstrect->h;
x = (int)dstrect->x;
y = (int)dstrect->y;
}
color = SDL_MapRGBA(dst->format,color >> 16 & 0xFF, color >> 8 & 0xFF, color & 0xFF, color >> 24 & 0xFF);
for (int i = 0; i < h; i++)
for (int j = 0; j < w; j++)
pixels[(y + i) * dst->w + x + j] = color;
NDL_DrawRect(pixels, x, y, w, h);
}
void SDL_BlitSurface(SDL_Surface *src, SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect) {
assert(dst && src);
assert(dst->format->BitsPerPixel == src->format->BitsPerPixel);
int sw, dw;
int sh, dh;
int sx, dx;
int sy, dy;
uint32_t *sp = (uint32_t *)src->pixels;
uint32_t *dp = (uint32_t *)dst->pixels;
if (srcrect == NULL && dstrect == NULL) {
//要将 完全的src画布 复制到 完整的dst画布上
sw = (int)src->w;
sh = (int)src->h;
sx = 0;
sy = 0;
dw = (int)dst->w;
dh = (int)dst->h;
dx = 0;
dy = 0;
assert(dw == sw && dh == sh);
} else if (srcrect == NULL && dstrect != NULL){
//要将 完全的src画布 复制到 dstrect指定的dst画布上
sw = (int)src->w;
sh = (int)src->h;
sx = 0;
sy = 0;
//画的高和宽是由源画布决定的
dw = sw;
dh = sh;
dx = (int)dstrect->x;
dy = (int)dstrect->y;
//assert(dstrect->w >= sw && dstrect->h >= sh);
} else if (srcrect != NULL && dstrect == NULL) {
//要将 srcrect指定的src画布 复制到 完全的dst画布上
sw = (int)srcrect->w;
sh = (int)srcrect->h;
sx = 0;
sy = 0;
//画的高和宽是由源画布决定的
dw = sw;
dh = sh;
dx = 0;
dy = 0;
//assert(dst->w >= sw && dst->h >= sh);
} else if (srcrect != NULL && dstrect != NULL){
//要将 srcrect指定的src画布 复制到 dstrect指定的dst画布上
sw = (int)srcrect->w;
sh = (int)srcrect->h;
sx = (int)srcrect->x;
sy = (int)srcrect->y;
dw = (int)dstrect->w;
dh = (int)dstrect->h;
dx = (int)dstrect->x;
dy = (int)dstrect->y;
assert(dw == sw && dh == sh);
}
for (int i = 0; i < sh; i++)
for (int j = 0; j < sw; j++)
dp[(dy + i) * dst->w + dx + j] = sp[(sy + i) * src->w + sx + j];
NDL_DrawRect(dp, dx, dy, dw, dh);
}
NTerm (NJU Terminal)
SDL_GetTicks的小不同我猜应该是:'Returns an unsigned 32-bit value representing the number of milliseconds since the SDL library initialized.', 要在
NDL_Init
初始化一个NDL开始时间
这个我已经实现了
//navy-apps/libs/libminiSDL/src/timer.c
uint32_t SDL_GetTicks() {
return NDL_GetTicks();
}
//navy-apps/libs/libminiSDL/src/event.c
int SDL_PollEvent(SDL_Event *event) {
char buf[64];
if (NDL_PollEvent(buf, 64) == 0) return 0;
else {
char keytype[4];
char keycode[32];
sscanf(buf, "%s %s", keytype, keycode);
if (strcmp(keytype, "kd") == 0) event->type = SDL_KEYDOWN;
else if (strcmp(keytype, "ku") == 0) event->type = SDL_KEYUP;
else assert(0);
for (int i = 0; i < sizeof(keyname) / sizeof(keyname[0]); i++){
if (strcmp(keycode, keyname[i]) == 0){
event->key.type = event->type;
event->key.keysym.sym = i;
assert(i != 0);
return 1;
}
}
assert(0);
}
return 0;
}
Flappy Bird
//navy-apps/libs/libSDL_image/src/image.c
SDL_Surface* IMG_Load(const char *filename) {
FILE *file;
long size;
SDL_Surface *ret;
//用libc中的文件操作打开文件
file = fopen("example.txt", "rb");
assert(file != NULL);
//获取文件大小size
fseek(file, 0, SEEK_END);
size = ftell(file);
//申请一段大小为size的内存区间buf
unsigned char *buf = (unsigned char *)malloc(size);
assert(buf != NULL);
//将整个文件读取到buf中
fseek(file, 0, SEEK_SET);
size_t bytesRead = fread(buf, 1, size, file);
assert(bytesRead == size);
//将buf和size作为参数, 调用STBIMG_LoadFromMemory(), 它会返回一个SDL_Surface结构的指针
ret = STBIMG_LoadFromMemory(buf, bytesRead);
//关闭文件, 释放申请的内存
fclose(file);
free(buf);
return ret;
}
展示你的批处理系统
可以运行其它程序的开机菜单
"执行其它程序"需要一个新的系统调用来支持, 这个系统调用就是SYS_execve, 它的作用是结束当前程序的运行, 并启动一个指定的程序.
这个系统调用比较特殊, 如果它执行成功, 就不会返回到当前程序中
//nanos-lite/src/syscall.c
static void sys_execve(Context *c){
char *fname = (char *)c->GPR2;
naive_uload(NULL, fname);
c->GPRx = 0;
}
static void sys_exit(Context *c){
const char *menu = "/bin/menu";
c->GPR2 = (intptr_t)menu;
sys_execve(c);
//halt(c->GPRx);
}
展示你的批处理系统
这里我的做法和nemu中sdb的很像:
//navy-apps/apps/nterm/src/builtin-sh.cpp
static struct {
const char *name;
const char *description;
int (*handle)(int, char *);
} cmd_table [] = {
{"nterm", "a simulated terminal", cmd_execve},
{"bmp-test", "a small test program", cmd_execve},
{"hello", "a small test program", cmd_execve},
{"timer-test", "a small test program", cmd_execve},
{"nslider","The simplest displayable application in Navy", cmd_execve},
{"file-test", "a small test program", cmd_execve},
{"event-test", "a small test program", cmd_execve},
{"dummy", "a small test program", cmd_execve},
{"menu", "Display an application menu", cmd_execve},
{"help", "Shows the commands that nterm can execute", cmd_help},
};
static void sh_handle_cmd(const char *str) {
char *clstr = (char *)str;
if (clstr[strlen(clstr) - 1] == '\n') clstr[strlen(clstr) - 1] = '\0';
char *str_end = clstr + strlen(clstr);
char *cmd = strtok(clstr, " ");
if (cmd == NULL) return;
char *args = cmd + strlen(cmd) + 1;
if (args >= str_end) args = NULL;
int i;
for (i = 0; i < NR_CMD; i++){
if (strcmp(cmd, cmd_table[i].name) == 0){
if (cmd_table[i].handle(i, args) < 0) return ;
break;
}
}
if (i == NR_CMD) sh_printf("Unknown command '%s'\n", cmd);
}
static int cmd_execve(int idx, char *args){
setenv("PATH", "/bin", 0);
if (execvp(cmd_table[idx].name,(char * const *)args) == -1) return -1;
return 0;
}
static int cmd_help(int idx, char *args){
char *arg = strtok(NULL, " ");
int i;
if (arg == NULL) {
/* no argument given */
for (i = 0; i < NR_CMD; i++)
sh_printf("%s - %s\n", cmd_table[i].name, cmd_table[i].description);
}
else {
for (i = 0; i < NR_CMD; i++) {
if (strcmp(arg, cmd_table[i].name) == 0) {
sh_printf("%s - %s\n", cmd_table[i].name, cmd_table[i].description);
return 0;
}
}
sh_printf("Unknown command '%s'\n", arg);
}
return 0;
}
我突然有个好想法:
我把开机界面的/bin/bmp-test程序作为最先运行的程序,然后改动下bmp-test程序,当有任何按键时,bmp-test程序结束
这个时候SYS_exit会执行/bin/nterm程序!
总结
FIN
我的NEMU PA之旅结束了~
你的仙剑奇侠传呢?你的PA4呢?
我其实对仙剑奇侠传不太感兴趣,我感兴趣的是PA4中可以实现游玩Clanned
!
但是本人不是NJU的学生,同时也是一个大三下人了,所实话,我的时间不多了😭,对应我这个四非的计科人还有面对升学压力和未来迷茫等😶🌫️~
这次的PA0~PA3是历经28天完成的, 我记得我大二下的时候第一次在操作系统课上通过网课的方式遇到的jyy老师,然后就自然遇到了PA,在大二的暑假做了一半的PA1
但是因为算法比赛和数学建模,没有时间做了,便放弃了...这次在做PA之前我学完了CSAPP,信心满满又来做PA了<゜)))彡
说实话还有很多有意思的源码没有看...但是我依旧收获了很多,完结撒花吧🥳
话说,夏天快来了,希望有个好的夏天吧
非源代码改动,配置记录
最简单的操作系统
//我们为大家准备了Nanos-lite的框架代码, 通过执行以下命令获取:
cd ics2023
bash init.sh nanos-lite
//解开nanos-lite/include/common.h下的宏
#define HAS_CTE
加载第一个用户程序
//我们准备了一个新的子项目Navy-apps, 专门用于编译出操作系统的用户程序. 通过执行以下命令获取Navy的框架代码:
cd ics2023
bash init.sh navy-apps
strace工具
简易文件系统
运行NSlider
sudo apt update
sudo apt install imagemagick
Flappy Bird
网友开发了一款基于SDL库的Flappy Bird游戏sdlbird, 我们轻松地将它移植到Navy中.
在navy-apps/apps/bird/目录下运行make init, 将会从github上克隆移植后的项目.
这个移植后的项目仍然可以在Linux native上运行: 在navy-apps/apps/bird/repo/目录下运行make run即可 (你可能需要安装一些库, 具体请STFW).
这样的运行方式不会链接Navy中的任何库, 因此你还会听到一些音效, 甚至可以通过点击鼠标来进行游戏.
我的linux中安装了最新的SDL库,但是我们代码上#include<SDL.h>
在最新的库下找不到,因为在usr/include
中我们有的是SDL2
,我们要下过一个旧点版本的SDL库
sudo apt update
sudo apt install libsdl1.2-dev
sudo apt update
sudo apt install libsdl-image1.2-dev
另外, Flappy Bird默认使用400像素的屏幕高度, 但NEMU的屏幕高度默认为300像素, 为了在NEMU运行Flappy Bird, 你需要将navy-apps/apps/bird/repo/include/Video.h中的 SCREEN_HEIGHT修改为300.