NJU PA4

未分类
3k 词

多道程序

批处理系统目前的缺陷是同一时刻只能执行一个程序。在一些场景下cpu会浪费,比如等待硬件的数据读取的时间里cpu要一直空转等待。所以很自然的一个新的需求就是:在操作系统上期望能“同时”运行多个程序。

接下来需要实现两个目标:

  • 在内存中可以同时存在多个进程
  • 在满足某些条件的情况下, 可以让执行流在这些进程之间切换

前者是之前在加载阶段做过的,程序就是一截指令序列,通过上下文赋予其意义。既然要运行多个自然就要有能加载多个的能力。

为什么需要使用不同的栈空间?

思考一下目前阶段程序所使用的栈空间,是在am的链接脚本中定义的,他直接划分了硬件提供的内存空间:将前面一部分分配为栈区,后面全部为堆区。这个堆栈是am这个运行环境提供给在上面运行的nanos-lite使用的,而目前nanos-lite加载应用程序是直接的内存拷贝,运行应用程序是简单的函数调用,让pc指针走到内存中特定的位置然后加载指令

上下文切换

之前在CTE里是通过栈指针恢复数据的:将当前的数据保存到栈上,恢复时让函数参数的寄存器指向这里,就能获取到当时保存的数据了。

对于多个进程的切换,在这里可以偷梁换柱:A进程的数据留在那里,将栈指针切换到B进程保留数据的位置,这样还原的上下文就是B进程的了。在之后可以用类似的方式回到A进程的执行。

如果把栈空间看作是一个很大的连续的内存空间,在nanos-lite层从A进程切换到B进程的时候,其实不太好知道B进程的数据此时在哪里:因为当前的栈里保存的数据是A的数据在最顶上,并不知道B进程的数据在哪里;一个更麻烦的事情是,如果B进程已经加载过了,那么他理论上在栈空间里确实会有一个位置保存了当时的数据,但是如果他还没有运行过呢?数据在哪里。这里涉及到一个初始状态的问题。

nanos-lite层引入对进程的抽象:进程控制块PCB

1
2
3
4
5
6
7
8
9
typedef union {
uint8_t stack[STACK_SIZE] PG_ALIGN;
struct {
Context *cp;
AddrSpace as;
// we do not free memory, so use `max_brk' to determine when to call _map()
uintptr_t max_brk;
};
} PCB;

每个进程有自己的栈空间,通过union的特性将一个cp指针写到栈底。

nanos-lite层管理进程,当需要切换到B进程的时候,通过B进程对应的PCB中记录的cp指针信息即可还原。

接下来解决刚刚提到的初始状态的问题:当我们加载了一个进程之后,如何切换到进行运行?

内核线程

只需要对刚刚创建的进程的内存空间里手动放置一个合适的上下文即可。

为什么不叫”内核进程”?

其实我觉得这个问题有点钓鱼,他可以谈很多的东西。之后补吧

实现上下文切换
  • CTE的kcontext函数:内存操作,将mepc写进去就可以了。

  • Nanos-lite的context_kload函数:将kstack提供的栈空间传给kcontext,然后将上下文的位置写到pcb里即可。

  • Nanos-lite的schedule函数:按文档里写,目前只会切换到进程块0。

  • 在Nanos-lite收到EVENT_YIELD事件后, 调用schedule并返回新的上下文:直接c=schedule(c)即可,然后返回出c即可。

  • 修改CTE中__am_asm_trap()的实现, 使得从__am_irq_handle()返回后, 先将栈顶指针切换到新进程的上下文结构, 然后才恢复上下文, 从而完成上下文切换的本质操作:将a0寄存器的值写到sp指针即可。

梳理一下整个过程:

  • Nanos-lite层,声明static PCB pcb[MAX_NR_PROC]数组,用来存放在操作系统层的进程信息,最多MAX_NR_PROC个进程。每个进程块此时已经分配了一段内存空间作内核线程栈,在这段连续的内存上最开始的位置有一些额外的信息。栈是从高地址走到低地址的,在实验中假设不会发生栈溢出。(等等,如果溢出了我们怎么知道?)
  • Nanos-lite层,调用init_proc时,通过context_kload将一个函数的信息加载到一个进程块里。
  • AM层,由上面调用kcontext,传入一段内存空间,帮助构造一个内核线程的初始状态:放置一个合适的Context对象在内核栈的末尾位置。然后将这个上下文数据返回回去。
  • Nanos-lite层,将从AM层获得的上下文指针记录到进程块里。
  • 此时手上的一个进程块就初始化完毕了,它的栈顶位置有一个Context结构,栈底有一个cp指针指向这个Context对象。到这里我们就完成了加载的步骤。
  • Nanos-lite层,执行yield函数时触发系统调用。
  • AM层,通过ecall触发yield的系统调用,进入__am_asm_trap。此时的栈指针属于正在运行的Nanos-lite,保存其运行状态到内核栈后跳转到__am_irq_handle,打包SYS_yield事件。
  • Nanos-lite层,执行回调函数处理事件,偷梁换柱c = schedule(c)将使用的上下文数据切换到进程块0然后返回。
  • AM层,将返回值所在的寄存器a0的数据移动给sp,接下来就恢复了预设好的进程块0的上下文结构。
  • NEMU层,cpu将执行mret指令之后的的mepc指针写到pc上,开始执行预设好的进程块0的指令。

至此就成功地跳转(调用)到了hello函数。

这里在调用kcontext的时候需要为之指定所要用的内存空间,我一开始在思考开头的部分是否要去掉多余的部分,后来想到如果函数栈已经走到这里了,多几个少几个是否发生覆盖已经没有太大意义了。所以实现的时候就直接将整个连续的范围都给了kcontext去使用。

这里放在内存区域的end位置是构造的,因为栈的增长是从内存高的位置走到低的位置。

实现上下文切换(2)

参数写到c->GPR2即可。

因为shcedule的实现,这里会看到交替打印的结果。

用户进程

在之前执行加载的用户程序的时候,使用的是函数直接跳转执行的方式,因此使用的是Nanos-lite的栈,也就是由am提供的。在多道程序的背景下,如果有一个损坏的程序导致了内核栈的数据出错,那么其他共用内核栈的用户程序也会出错。

为了解决这个问题,需要给用户程序提供隔离性:给用户程序分配用户内存空间,代码数据堆栈都应该位于用户区而不是内核区,并且用户程序不能影响其他用户程序的运行。

:question:内核栈的用途

是的,用户程序需要自己的堆栈来实现隔离性,但是每个进程都有的内核栈是给谁用的?Nanos-lite使用的是AM提供的栈区,每个进程分配的内核栈是否落在这个范围里?

PCB中分配的栈是内存栈,位于用户区的栈称作用户栈。目前用户栈的分配方式是直接将堆区末尾的一部分作为用户栈。

:question:哦等等,这样分配内存其他用户进程怎么办

会产生相互覆盖的问题,这里缺少了一个分配内存的方式,只是简单的划分了一下不用的部分给他用而已。

实现多道程序系统

有一说一,之前没提过Navy-apps里的_start啊。

怎么证明确实在使用用户栈而不是内核栈?

一山不能藏二虎?

两个用户程序用的用户栈都是heap.end,互相冲突了。