NJU计算机课程基础实验 (完) 2022-09-16
虚实交错的魔法: 分时多任务
从这我越来越感受到系统复杂度上升带来的挑战,也明白了抽象的根本目的。
抽象是为了降低复杂度,为了系统能够更好的做大以及稳健和排错。
而且抽象能让你切换不同的“后续程序”进行测试,甚至在大概率正确的载体上进行diff查看到底是哪一层出现了问题(参考各种native)
如何相信抽象是对的?——对每一个抽象层完成后的充分测试。
善用assert是魔法!
(个人能力受限,挂在了2阶的最后pal阶段。。。其实基本也算是做完了.也许有机会能请教大佬解决一下,这样就可以做第三阶段了,很可惜,但也只能这样了。)
2022年9月16日记
上下文切换
自从有了上下文切换后,程序也就有了进程的概念。(从静止到运动的飞跃 ~~·)
假设进程A运行的过程中触发了系统调用, 陷入到内核. 根据
__am_asm_trap()
的代码, A的上下文结构(Context
)将会被保存到A的栈上. 在PA3中, 系统调用处理完毕之后,__am_asm_trap()
会根据栈上保存的上下文结构来恢复A的上下文.
如果我们先不着急恢复A的上下文, 而是先将栈顶指针切换到另一个进程B的栈上, 那会发生什么呢? 由于B的栈上存放了之前B保存的上下文结构, 接下来的操作就会根据这一结构来恢复B的上下文. 上下文切换其实就是不同进程之间的栈切换!
进程控制块
有不少信息都是进程相关的, 除了刚才提到的上下文指针
cp
之外, 上文提到的栈空间也是如此. 为了方便对这些进程相关的信息进行管理, 操作系统使用一种叫进程控制块(PCB, process control block)的数据结构, 为每一个进程维护一个PCB. Nanos-lite使用一个联合体来把其它信息放置在进程堆栈的底部. 代码为每一个进程分配了一个32KB的堆栈, 已经足够使用了, 不会出现栈溢出导致UB. 在进行上下文切换的时候, 只需要把PCB中的cp
指针返回给CTE的__am_irq_handle()
函数即可, 剩余部分的代码会根据上下文结构恢复上下文.
内核线程
对于刚刚加载完的进程, 我们要怎么切换到它来让它运行起来呢?? 答案很简单, 我们只需要在进程的栈上人工创建一个上下文结构, 使得将来切换的时候可以根据这个结构来正确地恢复上下文即可. 我们先把Nanos-lite中直接定义的一些测试函数作为程序. Nanos-lite提供了一个测试函数
hello_fun()
(在nanos-lite/src/proc.c
中定义), 我们接下来的任务就是为它创建一个上下文, 然后切换到它来执行. 这样的执行流有一个专门的名称, 叫"内核线程"(kernel thread).创建内核线程的上下文是通过CTE提供的
kcontext()
函数 (在abstract-machine/am/src/$ISA/nemu/cte.c
中定义)来实现的, 在Nanos-lite中, 我们可以通过一个context_kload()
函数来进行进一步的封装: 它会调用kcontext()
来创建上下文, 并把返回的指针记录到PCB的cp
中上下文的创建和切换是CTE的工作, 而具体切换到哪个上下文, 则是由操作系统来决定的, 这项任务叫做进程调度 进程调度是由
schedule()
函数(在nanos-lite/src/proc.c
中定义)来完成 的, 它用于返回将要调度的进程上下文. 因此, 我们需要一种方式来记录当前正在运行哪一个进程, 这样我们才能在schedule()
中返回另一个进程的上下文我们让
schedule()
总是切换到pcb[0]
. 注意它的上下文是通过kcontext()
创建的, 在schedule()
中才决定要切换到它, 然后在CTE的__am_asm_trap()
中才真正地恢复这一上下文.
努力理解了一下,然后写了个汇编,成功点亮~(主要卡在kcontex如何传送area)
这里还要思考一下area到底怎么才是对的。开头地址kstack.start应该拿谁?那段空间应该从哪边开始界定(这也是需要理解的坑)
内核线程的参数
我们来创建两个内核线程, 给它们传递不同的参数, 然后在输出的信息中把参数也一同输出, 这样我们就能看到执行流在两个内核线程之间来回切换了! 我们只需要让
kcontext()
按照调用约定将arg
放置在正确的位置, 将来hello_fun()
执行的时候就可以获取正确的参数了.
感叹这个设计真聪明~
实现上下文切换(2)
根据讲义的上述内容, 实现以下功能:
修改CTE的
kcontext()
函数, 使其支持参数arg
的传递通过
kcontext()
创建第二个以hello_fun()
为入口的内核线程, 并传递不同的参数修改Nanos-lite的
schedule()
函数, 使其轮流返回两个上下文你可以自行约定用何种类型来解析参数
arg
(整数, 字符, 字符串, 指针等皆可), 然后修改hello_fun()
中的输出代码, 来按照你约定的方式解析arg
. 如果你的实现正确, 你将会看到hello_fun()
会轮流输出不同参数的信息.
这里要想想,riscv用什么方法传参呢?然后思考一下会不会和我们之前的操作有没有冲突(本质是执行顺序的问题,实际上没任何问题)
为什么这里叫做内核线程?我的想法是因为他在nanos内部调用实现,并且可以达到快速的执行流切换的效果。
在真实的操作系统中, 内核中的很多后台任务, 守护服务和驱动程序都是以内核线程的形式存在的. 如果你执行
ps aux
, 你就会看到系统中有很多COMMAND中带有中括号的内核线程(例如[kthreadd]
). 而创建和执行它们的原理, 也是和上面的实验内容非常相似(当然具体实现肯定会有所不同).
有关一些调用约定: