好久没写Blog了,来讨论一下CS162的Project 1: User Programs吧!(古旧文档,一直忘了发
Base on UCB CS162 Spring2024
在此之前
- 建议先阅读Pintos Doctumentation,想起来就翻一翻,注意Debug的部分,有很多好用的gdb命令
- 从24Sp开始,不再依赖Vagrant搭建环境,转而使用Docker,更方便配置
- 我使用M1 Mac作为主力机,想有个x86(非模拟的环境)只能借助本地搭建的另一个服务器(其实就是连了局域网的路由器,用VSCode/SSH协作),当时跑Vagrant,改配置文件很头痛。也是我没继续写162的原因(?
- 现在提供了aarch64和x86的Docker环境,very good!
- 自己写注释很重要
- 编写合适的helper_function:能不在大段的代码下实现更复杂的操作,将其隔离开来,能大大优化代码的可读性
开胃菜
回想下proj0让我们做了什么,执行do-nothing却发生page fault,主要源于权限的划分,User身份的进程不应该越界访问属于Kernel的地址,通过Pintos Doc/Virtual Memory Layout中知道
User virtual memory ranges from virtual address
0
up toPHYS_BASE
, which is defined inthreads/vaddr.h
and defaults to0xc0000000
(3 GB). Kernel virtual memory occupies the rest of the virtual address space, fromPHYS_BASE
up to 4 GB.
发生中断的位置正好在0xc0000000!
从Program startup details中我们知道,主要是没有正确初始化堆栈或相关寄存器的状态,为了通过这个部分,偷偷修改esp就好了。
别忘了对齐:
The x86 ABI requires that
%esp
be aligned to a 16-byte boundary at the time thecall
instruction is executed
然后其他测试可能还是过不了🥲
进入正题
建议按照Project1提供的Plan思路进行实现
Argument Passing
- 传入thread_create的参数,没有经过预处理
tid_t thread_create(const char* name, int priority, thread_func* function, void* aux)
- 我们需要从整串命令中切割出执行程序
- 关于切割,自然而然会想到使用strtok_r,以空格作为split的字符,会有问题吗?
char* strtok_r(char* s, const char* delimiters, char** save_ptr)
- 会,遇到引号内的空格怎么办?
- 在现实中有相关的库实现,可以直接切割命令行参数,考虑的比我们仔细,很可惜,这是Pintos,它不依赖与外部库实现编译
- (wordexp) https://linux.die.net/man/3/wordexp
- 建议自己搭几个能切割参数的helper function,在strtok_r外套个壳也不错
- 在设计之初我就开始考虑效率的问题,纠结了很久的实现,比如要不要省去命令行中多余的空格,减少空间的浪费,在代码什么位置进行命令行的处理
- 最终我还是直接进行了整块内存的copy,取巧的是可以提前计算复制到栈空间的offset(argv[i])
- 建议计算esp时,对应原因的minus与对应的type要有关系
- 强制转化指针类型并赋值时也要对应起来(主要是规范与易读
- 不要忘了align
Process Control Syscalls
- 这个部分最重要的就是无效地址的识别。看起来很简单,坑小多
- argv[i]之前,应该检查&argv[i]的权限,argv[i]会返回地址argv+i*sizeof(type)的内容
- 我比较喜欢使用switch case的形式,只要记得break就好
-
这个部分最重要的是exec及wait的实现
pid_t exec (const char *cmd_line)
- If the program cannot load or run for any reason, return -1. Thus, the parent process cannot return from a call to
exec
until it knows whether the child process successfully loaded its executable. - 重要的是选择合适的同步工具,分析一下:一个父进程通过process_execute调用thread_create,子线程会通过start_process初始化(thread_create也初始化了一部分),在题目要求下,不会出现两个exec的系统调用同步进行,选择lock或sema都ok,我选择了sema实现
- 若为thread的结构体添加sema变量,要注意初始化
- sema_up 和 sema_down的位置要想好
- 既然能自己设计结构体,在初始化时我也会设置其parent的thread指针
- If the program cannot load or run for any reason, return -1. Thus, the parent process cannot return from a call to
int wait (pid_t pid)
- 为了简化这一块的实现,我创建了新的children_info结构体,主要有两个作用
- 作为父进程寻找子进程的手段,在thread中维护一个list即可
- 记录其进程情况,方便父进程查看信息,例如返回值或是否被等待
- 这里的同步原语我都采用了sema
- 被等待过的pid再次wait直接返回-1
- 仍然存活的child会将其设置为被等待的状态,并通过sema_down等待children结束
- exit之前所有的wait都应该执行完,因此也要实现相应的同步原语
- 为了简化这一块的实现,我创建了新的children_info结构体,主要有两个作用
File Operation Syscalls
- 可以先实现最基本的,再优化同步的问题
- 或许可以用上filesys/file.h中的file_deny_write及file_allow_write
Floating Point Operations
- 这块的代码量不大,主要是学习几个基本的汇编指令,并修改switch/interrupt相关的结构体,使其能保存fpu的status
Other
- 最好检查一下内存的管理,multiple-oom不能过有可能是有些内存没有free?也可能是同步原语导致的死锁
- 我在float-point部分的实现下,有些测试的输出没有问题,但仍然会报错
Bail out! ERROR:../../accel/tcg/tcg-accel-ops.c:79:tcg_handle_interrupt: assertion failed: (qemu_mutex_iothread_locked())
- 去翻了一下以为是qemu的问题
- 尝试了n种姿势的排查,有且不限于本地build qemu,结果还是不行等…
- 在找到没有实现的点后便能通过测试,过不了大抵不是电脑的问题。。。。
- 破案:就是我的实现有问题。
推荐阅读
PREVIOUS对象引用、可变性、垃圾回收