最简单的操作系统
批处理系统
最简单的操作系统:一个后台程序,每次加载一个前台程序运行,结束了之后换另外一个。
目标:
- 用户程序结束执行之后,回到操作系统处继续运行
- 操作系统能加载在某些位置的用户程序执行
对于前者,需要有一个执行流切换机制,但是如果直接使用jal指令跳转的话就显得太随意了,在硬件上有对应的限制:如果执行了非当前权限的代码就会导致陷入操作系统内核然后交由操作系统执行。
穿越时空的旅程
除了被动地进入操作系统之外,还需要主动地陷入操作系统:有的操作不应由用户程序执行,必须请求操作系统。当用户程序执行了一条自陷指令之后,跳转到操作系统预先设定好的位置执行特定的代码。大部分ISA不区分异常和自陷。
对于riscv32:
- 将当前pc写入mepc寄存器
- 在mcause寄存器中设置异常号
- 从mtvec寄存器中取出异常入口地址
- 跳转到异常入口地址
这部分由硬件完成。
特殊的原因? (建议二周目思考)
如果由软件保存,用户程序可能可以写入特定的值来让操作系统执行特定的操作来破坏操作系统。
硬件提供的在操作系统和用户程序之间切换操作流的过程也是上下文切换。为了抽象上下文管理,操作系统的处理过程中需要哪些信息?
- 本次切换的原因,异常或自陷等
- 用户程序的上下文,之后返回用户程序
切换原因:抽象成一个事件。上下文:与isa强相关。
实现异常响应机制
目前的过程是:用户程序->系统调用->执行ecall指令->跳转到__am_asm_trap
->执行流到硬件。
在执行流切换到硬件之前,还有一些事情要处理:程序的执行无可避免的要用到cpu的寄存器,因此需要保存用户程序的寄存器到内存里,然后在硬件层执行完毕后还原现场。riscv32是通过sw指令保存上下文,具体地实现方式是写到sp寄存器指向的内存位置(即内核栈空间)。
:question: 等等,栈空间在哪里
首先栈空间肯定在内存上,其次其具体的位置定义在am的链接脚本里。
栈顶=堆起始位置=
0x1000
,栈底=0x8000
。在am的Start.S
中将_stack_pointer
赋值给sp
寄存器。
重新组织Context结构体
- Trap.S的执行过程:
addi sp, sp, -CONTEXT_SIZE
,让sp指针向下增长CONTEXT_SIZE
的,预留出空位MAP(REGS, PUSH)
,对定义的寄存器列表执行PUSH
,展开之后类似于调用PUSH(1), PUSH(3), PUSH(4)...
。然后将PUSH
展开,就是顺序的写入sp偏移的位置。调用中不存在0和2,其中0号寄存器永远是0,不需要保存或者恢复;2号寄存器是sp自己,也不需要恢复。当然还是预留了这些数据的位置。- 后面是三个CSR寄存器,按偏移量顺序是
CAUSE, STATUS, EPC
- 将寄存器设置好之后,额外的设置一下
mstatus
寄存器以通过diffmv a0, sp
,将sp指针的地址放到参数寄存器上,手动设置函数的参数jal __am_irq_handle
跳转到__am_irq_handle
函数处执行,这个函数有一个类型为Context*
的参数,就是此时a0寄存器的数据- 从
__am_irq_handle
返回后,对称的将sp对应的内存地址上的数据还原出来addi sp, sp, CONTEXT_SIZE
回到一开始的状态等等,所谓的地址空间的信息在哪里?为什么只在Context里面看到了一个指针?文档里说riscv32是将其和0号寄存器放在一起共用地址空间,但是目前看不出来这个事。
异常号的保存
riscv32可以通过软件保存异常号吗?
对比异常处理与函数调用
:question: 内存空间里的数据是随机的
一开始sp移动的过程类似于预留空位,但是这部分的内存数据并不保证直接就是0。因此0号寄存器在这个过程里可能不是0?
必答题(需要在实验报告中回答) - 理解上下文结构体的前世今生
__am_irq_handle
函数里的上下文结构体指针是从哪里来的?上面已经分析了。
实现正确的事件分发
必答题(需要在实验报告中回答) - 理解穿越时空的旅程
实际上上面也差不多了,之后补充吧。
实现etrace
跳过
用户程序和系统调用
目前已经有了跳到特定程序位置的上下文切换机制,还差加载用户程序的部分。
加载实际上就是把可执行文件中的代码和数据加载到合适的地方,然后从一个特定的位置起开始执行指令。
目前规定:加载的程序链接到0x83000000
附近的位置来避免和操作系统冲突。编译的dummy程序非常简单,他的位置直接就在0字节开始处。
:question:堆和栈在哪里
上面提到过这个问题,首先他们最终落脚在内存上,其次他们的定义在am上。对于用户程序,他们目前的存在是和操作系统共用的。之后对这个问题还有更新。
对于操作系统,他可以说只是一个普通的c程序,运行在am提供的运行时环境之上,由am提供了直接的硬件访问支持。对于目前运行多个程序的需求,直接让用户程序操作内存资源显然是不合理的,所以管理资源的职责就给了操作系统,在操作系统上运行的用户程序不能直接操作资源,必须要通过操作系统。
操作系统也提供了运行时环境,他的运行时环境和am提供的不太一样,但是目的是类似的:让用户程序在其上运行。
操作系统需要向用户程序提供访问资源的接口,用户程序也只能通过这个接口来访问资源。这个接口就是系统调用。他和普通的函数调用的区别在于执行流的切换:在系统调用时,上下文会切换到操作系统内部,由操作系统接管。
系统调用的必要性
直接向外暴露am的api就相当于暴露硬件本身。
系统调用将程序的运行时环境分成了两部分:用户态和内核态。用户程序在大部分时间只能在用户态执行非特权级的操作,如果需要调用资源,需要放弃自身的执行由操作系统接管
识别系统调用
目前dummy已经直接调用了系统调用。将数据写到寄存器然后执行
ecall
指令。这一块我觉得比较奇怪的点是,之前已经对事件有上层的抽象了,但是这里实际上完全没用起来:执行系统调用的时候是将系统调用号直接写到a7寄存器里了,而不是有一个抽象统一的标号标记这次异常是一次系统调用,然后读取之后用状态码来区别具体的系统调用。
这里我是在
do_syscall
的内部决定是否让pc+4的,考虑到可能会有特定的系统调用。当然目前已经有不会返回的系统调用了,比如SYS_EXIT
。
实现SYS_yield系统调用
注意实现返回值。
实现SYS_exit系统调用
可以通过让yield返回值发生变化的方式来检查实现的正确性。
RISC-V系统调用号的传递
riscv是用a7寄存器来传递编号的,最后一个参数寄存器。不是a0的原因可能是为了简化实现,a0是第一个参数寄存器。
实现strace
跳过
目前已经实现了两个系统调用,因为操作系统和AM很像,考虑一个TRM提供了哪些功能
- 基本的运算指令,由硬件提供
- 能输出字符
- 有堆区可以动态申请内存
- 可以结束运行,目前已经有系统调用实现了
所以接下来的两个问题是如何输出字符和动态申请内存。后者在之前的时候是直接在硬件内存上划分了足够大的空间,没有管理策略,硬件只负责提供功能实现,具体策略由操作系统提供:操作系统负责提供申请内存的请求处理和管理内存资源如何分配。
在Nanos-lite上运行Hello world
实现之后可以看到一直在输出,可以改一下hello程序让他只输出一次方便检查。
目前还没有引入多程序,所以内存分配只是实现了简单的一个人随意申请的情况。
:question:怎么证明现在的printf没有使用堆区
把hello程序修改一下,然后每次write的时候多putch一下当前的count就好了
实现堆区管理
两部分,实现
_sbrk
对需要的内存量管理;硬件的系统调用,目前只会返回0。
:question:谁调用了七号系统调用
将hello程序里的循环去掉之后重新运行会发现最终调用了一个7号系统调用并
HIT BAD TRAP
了。检查表得知7号是SYS_close
,尝试取消所有的输出之后发现不会触发,猜测可能是内部实现有在程序结束时尝试关闭打开的文件(即使他们是标准输出/标准错误?)。
必答题(需要在实验报告中回答) - hello程序是什么, 它从而何来, 要到哪里去
TODO
文件系统
文件的本质就是一串字节序列。
让loader使用文件
实现几个文件相关的调用就可以了。
在实现的过程里我发现一个问题:我的lsp会给我的文件自动补充头文件,导致重复调用和一些比较奇怪的问题,非常的无语。
实现完整的文件系统
这里我实现的
loader
是借助fs_lseek
定位的,所以一开始就实现完了。在sfs中由于文件定长,我觉得可以直接将越界读写throw出去,
lseek
的manual中是有提到是需要允许其越界的,我这里的实现是直接返回-1认为失败。
把串口抽象成文件
只需要实现写到
serial_write
即可
实现gettimeofday
测试是需要自己实现的
实现NDL的时钟
注意实现的层次,刚刚是在
nanos-lite
上提供了获取当前系统时间的系统调用,实现方式是直接由操作系统对硬件数据的访问。现在需要让这个接口更好用,在nanos-lite
之上的多媒体库中提供功能。这里又发生了我的lsp补全导致重复定义的问题,有点无语。
把按键输入抽象成文件
两个部分:
- 实现
events_read
:当读取虚拟文件/dev/events
的时候,操作系统读取硬件寄存器获取数据,如果有数据就将信息写到buf里。- 实现
NDL
中的poll_events
,该函数返回1/0来表示是否有事件。
用fopen()还是open()?
TODO,有可能是文件偏移量的差别。
在NDL中获取屏幕大小
NDL_OpenCanvas
里的代码比较奇怪,README
中提到4号文件是控制屏幕大小的,5号是屏幕输出文件,但是目前没有提到使用控制屏幕大小的硬件。不过他又提到
NWM
是只能在native
运行的,所以可能是因为这个原因没有做兼容处理。
把VGA显存抽象成文件
这里有一个困惑我很久的问题:当我通过
lseek
写入open_offset
之后,来自NDL
的write
调用要怎么把偏移量给最终的fb_write
?后来想到,一直忽略了是谁调用了
fb_write
,在fs_write
的时候,如果对应的write
函数不为空,我直接写了一个=0进去,所以一直拿不到offset。改成文件自己的open_offset
就可以了。当然
/dev/fb
我没有维护在写的时候的偏移量改变,因为对外只有一个NDL_DrawRect
的接口,每次绘制时都需要lseek
,就不存在问题了。还有一个问题是,
write
和lseek
这些调用里的长度是以字节为单位的,而一个颜色是32位的长度,所以很多地方要乘4来算出字节大小。
实现居中的画布
只需要算出画布自身位置的偏移量就可以了
精彩纷呈的应用程序
神奇的fixedpt_rconst
宏替换,在编译期时借助了编译器的能力把浮点数消除了,不会翻译出带浮点的指令进入
NEMU
。
实现更多的fixedptc API
还是先理一下目前所在的层次,在Nanos-lite
上实现了对硬件文件的写(/dev/fb
),然后在之上有NDL
库提供了更好的api调用:直接对(x,y)
坐标的(w,h)
的矩阵写数据。现在需要做的是移植SDL
库,提供了完整的应用程序支持。
如何将浮点变量转换成fixedpt类型?
TODO
运行NSlider
先准备一个幻灯片,把大小调整到4:3然后再转换成图片bmp。这个脚本会自动把数据写到
fsimg/slides
下。然后需要实现
SDL_UpdateRect
。
文档里说先理解一下Surface
这个SDL
里的概念:文档里说代表了一块可以绘制的内存,然后要求读数据之前先对其上锁。但是Surface
的结构体里本身没有描述上锁这个概念。像素数据由SDL_PixelFormat
和pixels
给出,后者是一个void*
。SDL_PixelFormat
用来描述在Surface
里的pixels
数据的格式(不是数据本身)。
框架代码中已经实现了SDL_CreateRGBSurface
:创建一个空的表面。其中depth
参数是单个像素信息的大小,如果是8则会分配一个空的调色盘platte
供使用,否则会创建一个SDL_PixelFormat
,并记录当前的颜色mask。文档中提供的示例说明了这一点:
1 |
|
框架里提供的代码限制了长度只能是8位或32位,如果是8位则会创建一个长度256的调色板数据,初始化数据全部为0。或者32位记录当前mask和shift。后面就是一些对表面信息的设置,申请pixels
数据所需的内存,将表面返回。
pixels
数组是一个width*height
大小的,单个元素大小为BytesPerPixel
的二维数组,以行优先。所以SDL_UpdateRect
就是将表面上(x,y);(w,h)
指示的像素信息写到设备上。这个方法在NSlider
这个任务中是通过调用SDL_SetVideoMode
调用的,mask使用的是默认数据:
1 |
排列方式是ARGB,和之前的约定一致。因为NSlider
使用的是32位长度的数据,所以直接调用NDL_DrawRect
是可以绘制成功的。具体的流程是:NSlider
中先将bmp文件读出来,然后把像素数据通过SDL_CreateRGBSurface
放进表面里。接下来调用SDL_UpdateRect(slide, 0, 0, 0, 0)
来将整个屏幕重绘。
但是如果长度是8位的话,就不能直接使用当前的数据了,需要使用调色板翻译过来。这里我有一点疑问,如果要翻译的话就需要额外的pixel
空间来储存翻译后的结果,那这样的话无法避免使用malloc
,但是如果每次绘制的时候都malloc
,之前提到过目前的内存实际上没有不会回收的是free
调用,他只管申请不管回收,这样似乎很容易爆掉。klib
中实现的,这个属于nanos-lite
的lib,使用之前实现的系统调用brk
维护可用的内存区域,所以没有问题。但是我还是感觉这样很奇怪。
这里排出一个非常整蛊的bug,具体在loader这里:
1 | - fs_read(fd, (uint32_t*)pheader.p_vaddr, pheader.p_filesz); |
转成32位的定义会导致数据清空的行为异常,最终导致static slider = NULL
这行代码产生的数据不是NULL
最终走到了未知的内存位置导致panic。
还有一个类似的问题,文档里定义的表面的pixels
的类型是void*
,但是框架代码里的是uint8_t
,在执行指针的移动操作之前需要先考虑转指针的类型再移动。
运行NSlider(2)
这里有个问题:之前
NDL
里暴露的事件我的实现是把名字写出来了,在SDL
层我就需要做一个翻译的过程:把按键的名称翻译回编码,这里我选择了跳过这个事情,让下层直接暴露编码。原则上来说还是以输出名称比较稳妥?当然由于操作系统层和应用层都是自己编写的,所以不存在太大的问题。
还有就是渲染速度比较堪忧,甚至会慢慢刷新屏幕看到中间过程。
运行开机菜单
SDL_FillRect
,32位颜色直接写入即可,8位颜色根据文档描述需要过一个转换。SDL_BlitSurface
,复制数据。但是在8位下可能不太一样?需要注意的是32位数据长度的问题,需要先转换指针类型再操作指针移动。
运行NTerm
填充一下api就可以了,不过速度堪忧。
而且echo还要自己实现
文档里说可以在linux native
上运行bird,但是我找了一下似乎没有sdl1的包可以下载,没有继续研究。
运行Flappy Bird
有一个奇怪的问题,
assert
只会往控制台打印一个a就退出了。而且还是小写的a。另外一个问题是bird中有一处代码读取txt时使用了
feof
来检查文件是否已经读取完全,但是会陷入死循环。我怀疑是因为fscanf
的失败导致了EOF
的最终状态没有正常的更新,因为数据量不大我就直接改成了break来跳出。执行速度如果过慢的问题有可能是因为把tracer打开了。
运行仙剑奇侠传
我在这里卡了很久,第一个问题是关于按键状态:实际上不需要实现一个任务队列,只需要维护一个长数组表示各个按键的状态就可以了;第二个是在播放商标图案之后会陷入长久的等待,是因为
SDL_GetTicks
实现导致的错误。pal的代码会在商标播放完成之后等待一小段时间,但是由于实现的错误使得等待的时间非常长。还有一个比较蛋疼的问题是:
SDL_Color
里的val字段实际上不直接对应32位的颜色格式,需要自己转换,否则渲染的数据会类似于反色。
可以运行其它程序的开机菜单
实现系统调用就可以了
展示你的批处理系统
不知道怎么退出当前运行的程序
必答题 - 理解计算机系统
理解上下文结构体的前世今生 (见PA3.1阶段)
理解穿越时空的旅程 (见PA3.1阶段)
hello程序是什么, 它从而何来, 要到哪里去 (见PA3.2阶段)