好久没写Blog了,来讨论一下CS162的Project 1: User Programs吧!(古旧文档,一直忘了发
Based on UCB CS162 Spring 2024.
这是一篇历史课程笔记,环境和项目范围以 Spring 2024 为背景。下文校订补充一般性的内存与同步要求,不将其改写为当前学期的作业说明。
在此之前
- 建议先阅读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
0up toPHYS_BASE, which is defined inthreads/vaddr.hand defaults to0xc0000000(3 GB). Kernel virtual memory occupies the rest of the virtual address space, fromPHYS_BASEup to 4 GB.
发生中断的位置正好在0xc0000000!
从Program startup details中我们知道,主要是没有正确初始化堆栈或相关寄存器的状态,为了通过这个部分,偷偷修改esp就好了。
别忘了对齐:
The x86 ABI requires that
%espbe aligned to a 16-byte boundary at the time thecallinstruction 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
- 是否需要引号解析,应以该学期的项目要求为准,不要把完整 shell 语法当作 Pintos 的默认要求。拆分参数的 helper function 仍然有助于整理实现。
-
在设计之初我就开始考虑效率的问题,纠结了很久的实现,比如要不要省去命令行中多余的空格,减少空间的浪费,在代码什么位置进行命令行的处理
- 最终我还是直接进行了整块内存的copy,取巧的是可以提前计算复制到栈空间的offset(argv[i])
- 建议计算esp时,对应原因的minus与对应的type要有关系
- 强制转化指针类型并赋值时也要对应起来(主要是规范与易读
-
不要忘了align
Process Control Syscalls
-
这个部分最重要的就是无效地址的识别。看起来很简单,坑小多
- 读取
argv[i]前,应检查保存这个指针的整个内存区域;取得指针后,还要检查它所指向的数据,包括跨页情况。C 表达式argv + i已按元素大小缩放,相当于字节地址加上i * sizeof(*argv)。
- 读取
-
我比较喜欢使用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
execuntil it knows whether the child process successfully loaded its executable. - 重要的是选择合适的同步工具,分析一下:一个父进程通过process_execute调用thread_create,子线程会通过start_process初始化(thread_create也初始化了一部分),不同进程的系统调用可能交错,不能假设全局只有一个
exec。每个父子进程之间都需要同步装载结果;我选择用初始值为 0 的 semaphore 传递完成信号- 若为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强制等待所有子进程。
- 为了简化这一块的实现,我创建了新的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,结果还是不行等…
- 在找到没有实现的点后便能通过测试,过不了大抵不是电脑的问题。。。。
- 破案:就是我的实现有问题。
推荐阅读
校订依据
以上链接是校订时可访问的官方文档;当前课程可能已调整项目范围。
