NJU PA 2022:计算机系统遨游(四)PA4

PA4 - 虚实交错的魔法: 分时多任务

最难的一part来了,上下文切换来了,虚拟映射来了,只有抢占多任务较为简单,其中虚拟映射虽然可以看手册,但是细节很多,就比如虚拟页和物理页分别对应着什么,怎么进行映射,页表在内存中的位置有什么特点,什么时候需要分配一张新的页表,都是要自己仔细思考的,其中虚拟内存的调试是最痛苦的,因为你不知道是你映射错误还是程序有错误。这也正是考验我们对系统的理解程度,因为虚拟映射是一个软硬件协同的过程,所以跨越的抽象层也很多。不会说做到PA4时候前面有些内容都忘记了

包括后面的并发,是要讲不同进程间的虚拟地址映射需要分开,否则会发生覆盖的情况
思考题

如果不同的进程共享同一个栈空间, 会发生什么呢?

当进程A,B,C压栈进入同一个栈空间,当A需要弹出时,必定会影响BC在栈中存储的数据,而且当bc调用数据时先要弹出A

思考一下, 如何验证仙剑奇侠传确实在使用用户栈而不是内核栈?

看PCB上下文中栈的地址,小于PCB栈顶的地址中是内核栈,其余是用户栈

在main()函数中, argv和envp的类型是char * [], 而在execve()函数中, 它们的类型则是char * const []. 从这一差异来看, main()函数中argv和envp所指向的字符串是可写的, 你知道为什么会这样吗?

execve是系统调用,它需要保证传递的参数和环境变量不变,而main则是可以对这些参数进行修改、调用

我踩的坑:

用户进程的入口不是naive_load,是loader加载出来的entry(耗时7天)
每个进程都有内核栈和用户栈,我以为用户进程只有用户栈

我们之前提到, PIC的其中一个好处是可以将代码加载到任意内存位置执行.

如果配合虚存管理, PIC还有什么新的好处呢?
(Hint: 动态库已经在享受这些好处了)
如果有ABCD四个库,,假设程序P1依赖A B两个动态库,P2依赖C D两个动态库,那么A B和C D的动态库的加载地址有重叠也没有关系,P1和P2可以同时运行。但是如果有一个新的程序P3依赖A B C D四个动态库,那么前面为动态库分配的加载地址就不能正常工作了。当然,重新为这四个动态库分配load address(让地址不重叠)也是ok的,但是这样一来,P1虽然没有使用C D这两个动态库,但是P1的地址空间还是要保留C D动态库的那段地址,浪费资源。

i386不是一个32位的处理器吗, 为什么表项中的基地址信息只有20位, 而不是32位?

因为页表的大小为4KB = 2 ^12且页与页直接没有重叠的区域,所以多个页表的基地址都是互相间隔X个页表的大小(X>=0且为整数),在这种情况下基地址的后12位都为0,所以PTE中可以只用20位存储地址信息而后三位则用来记录PTE的一些属性,

手册上提到表项(包括CR3)中的基地址都是物理地址,物理地址是必须的吗? 能否使用虚拟地址?

CR3是用来提供一级页表的物理地址
用虚拟地址则寄存器存储的是能够映射到一级页表的物理地址的虚拟地址,首先需要通过MMU进行地址翻译,而MMU翻译首先需要获得一级页表的物理地址,再通过一级页表的映射获得二级页表,最后通过二级页表得到物理地址,由于CR3存储虚拟地址 ,MMU在进行翻译时会用把CR3中的虚拟地址当作为物理地址进行映射,会导致映射到其他内存地址,所以不能用虚拟地址

为什么不采用一级页表? 或者说采用一级页表会有什么缺点?

一级页表空间占用太大,因为其必须要将程序所需的页表项包含进去

程序设计课上老师告诉你, 当一个指针变量的值等于NULL时, 代表空, 不指向任何东西. 仔细想想, 真的是这样吗?当程序对空指针解引用的时候, 计算机内部具体都做了些什么?你对空指针的本质有什么新的认识?

等于NULL时指针指向的地址为0x0,其物理存储的内容没有访问权限

对于x86和riscv32, 在protect()中创建地址空间的时候, 有一处代码用于拷贝内核映射:memcpy(updir, kas.ptr, PGSIZE);

尝试注释这处代码, 重新编译并运行, 你会看到发生了错误. 请解释为什么会发生这个错误.
在vme_init中 内核映射是用来设置一级页表的,如果不进行拷贝,则用户进程中的页表指向的就不是操作系统分配给你的一级页表而是一张空表

ucontext()的行为是在内核栈kstack中创建用户进程上下文. 我们是否可以对ucontext()的行为进行修改, 让它在用户栈上创建用户进程上下文? 为什么?

不能,因为用户栈并非固定不变的,具体来说,用户栈在创建之后,会存储参数,指针,环境变量等内容,当这些内容超过一定数量后会将上下文给覆盖导致复原上下文时跑飞
(本人一开始就因为将上下文设置在用户栈中导致PA4.1一直无法正确运行,所以还是得仔细仔细再仔细)

尝试阅读native的VME实现, 你发现native是如何实现VME的? 为什么可以这样做?

在protect中采用哈希表这类数据结构来记录已经分配的进程的地址空间,上下文切换时它先清空之前进程的映射,之后对当前进程进行映射

因为哈希表这个数据结构本身就是一个一对一的映射,符合虚拟内存的存储方式

我们知道, 用户进程从Navy的_start开始运行, 并且在_start中设置正确的栈指针. 如果在用户进程设置正确的栈指针之前就到来了中断, 我们的系统还能够正确地进行中断处理吗?

能,因为上下文会在中断时被保存,并不影响后续的中断处理

假设硬件把中断信息固定保存在某个内存地址(例如0x1000)的位置, AM也总是从这里开始构造上下文. 如果发生了中断嵌套, 将会发生什么样的灾难性后果? 这一灾难性的后果将会以什么样的形式表现出来?

如果你觉得毫无头绪, 你可以用纸笔模拟中断处理的过程.
原先保存的上下文会被破坏,当前中断执行完后会接着继续执行中断嵌套(因为新的中断的上下文保存的内容就是即将执行另一个新中断时的状态),造成死循环

思考一下,riscv32的软硬件该分别如何协同, 来支持中断嵌套?

mstatus的MIE为1表示时钟中断,软件收到这个时钟信号后,时钟信号清零,首先将上下文保存在对应进程的内核态中,如果此时mstatus再次发出时钟信号,让软件将这个时钟信号进行保存等待,直到内核中的上下文保存完毕

如果你觉得上述代码不难实现, 你就太小看系统的复杂性了. 尝试先不阅读下面的内容, 分析一下上述代码在实际的运行过程中可能会出现什么问题? 如果出现问题, 应该如何解决它们?

新建的几个变量存储在哪,这些新建的变量会不会影响程序原本保存在内核栈上的上下文

一般来说, 处理器的特权级也是一种状态. 我们是否可以通过栈指针的值来判断当前位于用户态还是内核态?

可以啊,因为内核态和用户态本身存储的虚拟地址不是在同一个区间段的,相应的栈指针的值不会一样
posted @ 2022-09-28 17:40  不进育碧不改名  阅读(1490)  评论(0)    收藏  举报