CS162Project1

好久没写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 to PHYS_BASE, which is defined in threads/vaddr.h and defaults to 0xc0000000 (3 GB). Kernel virtual memory occupies the rest of the virtual address space, from PHYS_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 the call 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指针
  • int wait (pid_t pid)
    • 为了简化这一块的实现,我创建了新的children_info结构体,主要有两个作用
      • 作为父进程寻找子进程的手段,在thread中维护一个list即可
      • 记录其进程情况,方便父进程查看信息,例如返回值或是否被等待
    • 这里的同步原语我都采用了sema
    • 被等待过的pid再次wait直接返回-1
    • 仍然存活的child会将其设置为被等待的状态,并通过sema_down等待children结束
    • exit之前所有的wait都应该执行完,因此也要实现相应的同步原语

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,结果还是不行等…
    • 在找到没有实现的点后便能通过测试,过不了大抵不是电脑的问题。。。。
    • 破案:就是我的实现有问题。

推荐阅读