TOC
Open TOC
OS Mini Lab
回顾 mini lab 流程
在 .bashrc
中设置 TOKEN 环境变量
git pull origin M1
后,在 pstree 的 Makefile 中添加学号和姓名环境变量
在 ~/os-workbench-2022
中进行 git add .
和 git commit
实际上在
~/os-workbench-2022/pstree
中进行make
就会有 git 记录了
在 ~/os-workbench-2022/pstree
中进行 make submit
M1: 打印进程树 (pstree)
一些手册
一种思路
解析命令行参数
使用 getopt
注意
会写入到 stderr
但是返回值是 EXIT_SUCCESS
而无法识别的命令行参数,需要返回 EXIT_FAILURE
获取进程信息
先找一下映射关系
进程信息在下面的文件中
stat 文件部分格式
示例
获取进程的线程信息
示例
于是只要维护如下的结构体信息
对于进程而言,ptid 为 -1,其余的语义和手册中一致
对于线程而言,为了区分,假定 ppid 为 -1,实际上应该是 creator 的 ppid,而 ptid 为 creator 的 pid
有些线程的名字在 stat 中是不同于进程的,但是 pstree 打印的线程名与进程名相同
使用了手册中提及的 getdents 遍历目录信息
getdents 不会遍历到线程所在的目录,从而简化了逻辑
这里理解错了,
/proc
下不含线程所在的目录
在匹配信息时使用了 regex,因为有些进程名中会有空格
打印
根据命令行参数和之前获得的信息递归打印即可
注意到除了 systemd,其他进程的 ppid 也可能为 0
目前只对相同的线程实现了 compaction
相同进程的 compaction,可能出现下面的情形
若需要显示 pid,则相当于关闭了 compaction
缩进的条件大概是,进程名相同,且线程数目相同
目前尚未实现
另外 kthreadd 作为所有内核线程的祖先,ppid 也为 0
提交
不能使用如下的头文件
也就是说不能使用 getdents 遍历目录信息
应该使用 readdir 系列函数,使用起来也更方便
另外注意字符串应该清零的问题
除非 snprintf 或 strncpy 中参数 count 等于数组的长度
否则都应该使用 memset 先清零
M2: 协程库 (libco)
编译选项
首先编译 co.c 和 co.h,生成共享库 libco-32.so
和 libco-64.so
注意 libco 的 Makefile 里指明了 all
所以在 libco 下 make 只会生成 .so
文件
另外 libco 的 Makefile 里还添加了一些 CFLAGS
-U_FORTIFY_SOURCE
用来防止__longjmp_chk
代码检查到堆栈切换,当成是 stack smashing,然后报错
参考如下
from man 7 feature_test_macros
from man gcc
-DLOCAL_MACHINE
相当于#define LOCAL_MACHINE
然后框架代码提供了一组协程库的测试用例,位于 libco/test 的 main.c 中
需要通过如下方式编译
-I
选项代表 include path,使我们可以#include <co.h>
-L
选项代表增加 link search path-l
选项代表链接某个库,链接时会自动加上lib
的前缀,即-lco-64
会依次在库函数的搜索路径中查找libco-64.so
和libco-64.a
,直到找到为止
生成可执行文件 libco-test-64 后,还需要设置环境变量 LD_LIBRARY_PATH=..
用于指定查找共享库时除了默认路径之外的其他路径
注意这是在 libco/test
中进行的
也可以直接在 libco/test
中 make test
实验指南
http://jyywiki.cn/OS/2022/labs/M2 写的非常详细
注意如下约定,使用协程库的程序应当保证除了初始协程外,其他协程都必须被 co_wait
恰好一次,否则会造成内存泄漏
一些细节
- 为初始协程分配数据结构
需要使用 __attribute__((constructor))
另外使用 __attribute__((destructor))
进行资源回收
似乎无法通过 Valgrind 或 Address Sanitizer 进行检测
- 上下文切换
核心方法是 stack_switch_call
这里寄存器的选择挺随意的……
相当于为每个协程的入口处构造相应的栈帧
对于 __x86_64__
而言,应在寄存器 rsp
的地址处放置返回地址
注意栈的含义,在执行
entry
的第一条指令前和执行entry
的最后一条指令前,寄存器rsp
的值是相同的
对于 __x86__
而言,应在寄存器 rsp
的地址处放置返回地址,并在 rsp + 4
处放置第一个参数
- 堆栈的增长方向
调用 longjmp 出现 segmentation fault
注意堆栈是向低地址增长的,所以赋给 stack_switch_call 应该是堆栈的高地址
否则 context 中的内容会被覆写
yield
在 co_wait
和 ret_entry
中均使用了 co_yield
进行上下文切换
而 co_yield
通过随机调度到非 DEAD
协程,保证了相关的语义
请仔细体会某些函数是永不返回的
- x86-64 堆栈对齐
很怪
这样的话在应用程序中可以使用 printf,但是 co.c 中不能使用 printf
如果改为
这样的话在应用程序中不可以使用 printf,co.c 中似乎也不能使用 printf
Ref.
http://rk700.github.io/2020/01/19/inline-assembly-constraints/
How to Use Inline Assembly Language in C Code
https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
另外在 gdb 中更美观的打印结构体
M3: 系统调用 Profiler (sperf)
strace 参数
观察到 strace 的一些参数
不过 -c
下的粒度不大
考察的是解析 -T
下的输出
环境变量
父进程的环境变量,是 main
函数的 envp
子进程 execve 后的环境变量,是 execve
时传入的 envp
另外需要在子进程 execve 前找到 strace 的正确路径,因为 execve
是不考虑环境变量的
注意 strtok
是破坏性的,不能直接对 envp
的 PATH
进行 strtok
execve 中 sub_argv
的格式如下
注意 COMMAND [ARG]
相对于 argv
有一格偏移
进程间通信
父进程创建一个管道,父进程读取,子进程写入
strace 输出到标准错误流,所以需要将子进程的标准错误流重定向到写端
考虑到 strace 里面的程序会有输出,需要将将子进程的标准输出流重定向到 /dev/null
如果 strace 里面的程序输出到标准错误流,可能会影响解析
另外,重定向放在
execute
函数里面,似乎不起作用
为了方便起见,将父进程的标准输入流重定向到读端
使用
lsof
命令查看进程打开的文件描述符
正则表达式
可以参考 https://ahuigo.github.io/b/code/code-regex#/
读取时似乎正好是一行,所以使用 ^
和 $
来定位
之前一直忘了使用 regfree
释放内存……
计时器
父进程不使用 wait(NULL)
每隔一秒排序并输出
调试选项
调试多进程
尝试下面一些选项
catch exec
用于捕获 execve
测试
TODO
- 注意到找不到
COMMAND
对应的程序时会出现
执行 COMMAND
发生在 strace 内部
考虑在 execve 前确认 COMMAND
的路径是否存在
如果 COMMAND 是以 /
开头的绝对路径,则直接执行
否则在 PATH
环境变量中搜索到第一个存在且可执行的文件
- 绘图
ANSI Escape Code
M4: C Real-Eval-Print-Loop (crepl)
思路
- 若为函数,则将其内容保存到
/tmp/crepl/template-XXXXXX.c
中,使用gcc -c
编译观察是否有语法错误,若没有错误,则编译到共享库/tmp/crepl/libcrepl.so
中 - 若为表达式,在上述步骤的基础上,动态链接到共享库,并调用对应的 wrapper name
共享库的创建
对于使用共享库的程序而言,可以是多个共享库
也可以是一个共享库
但显然一个共享库会方便很多
wrapper
每当我们收到一个表达式,例如 gcd(256, 144)
的时候,我们都可以人工生成一段 C 代码
请记住这个反复使用的小技巧
一些细节
编译选项 -Wno-implicit-function-declaration
从而让函数和表达式均可以调用之前定义过的函数,而不会报错影响合法性判断
后来发现这种行为是掩耳盗铃,实际上需要在每个函数之前显式的添加声明
只不过这样就不能创建互相递归调用的函数了
注意共享库的创建的可移植性 -m64/-m32
创建子进程的核心函数 fork_exec_test
使用 pipe 连接父子进程,关闭子进程的 stdout,重定向子进程的 stderr
父进程根据输出来判断子进程中是否发生了错误
使用函数指针增加灵活性
对于共享库的链接,直接照抄 CSAPP
TODO
- 函数重名
M5: File Recovery (frecov)
sha1sum ref
mkfs.fat
-F 32
表示 FAT-32-S 512
表示 sector 大小是 512 bytes-s 8
表示每个 cluster 有 8 个 sectors
FAT-32
http://jyywiki.cn/pages/OS/manuals/MSFAT-spec.pdf
BPB
记录了一些 metadata
主要关注如下字段
BPB_BytsPerSec
每个 sector 的字节数
BPB_SecPerClus
每个 cluster 包含的 sector
BPB_RsvdSecCnt
Reserved Region 也就是 volume #0 占据的 sector 数量
sector #0 存储了 BPB 信息
通常 sector #6 会对 BPB 进行备份
可以观察 BPB_BkBootSec
字段得知
BPB_NumFATs
FAT 的备份数量
BPB_FATSz32
每个 FAT 包含的 sector
BPB_TotSec32
整个镜像包含的 sector
FAT
FAT Region 中每个 entry 占据 32 字节,记录了 Data Region 对应 cluster 的 ID
通过 cluster ID,可以定位到 Data Region 一段 BPB_BytsPerSec * BPB_SecPerClus
字节长度的空间
这段空间中可能会存储三种数据结构
- data
文件的数据信息
- short entry - 32 字节
主要关注如下字段
DIR_Name - short name
DIR_Attr - 一些属性
通过
可以得到对应数据信息的 cluster ID
- long entry - 32 字节
记录了完整的文件名或目录名
TFM 指出 long entry 应倒序排列在对应 short entry 之前,如图所示
然而似乎存在不满足这个约定的 entry
其中的 checksum 对 short name 的进行了某些计算
另外,根目录 cluster ID 记录在 FAT 的 entry #2 中,对应 Data Region 的 cluster #0
将 cluster ID 转换到 sector 对应空间需要减去这个偏移
BMP
假设所有的 BMP 文件,都是使用 Python PIL 库创建的 24-bit 位图
https://en.wikipedia.org/wiki/BMP_file_format#File_structure
https://zhuanlan.zhihu.com/p/25119530
关键在于计算 image size
可以参考 https://en.wikipedia.org/wiki/BMP_file_format#Pixel_storage
note
- 首先在没有格式化的情形下,尝试得到文件名和校验和
- 这个过程实际上就可以完成本实验的大部分的内容了
- 文件恢复 trivial 算法
- short name 以 BMP 结尾
- 数据连续存储
- 优化算法
- 行间求点积,难以设置阈值
- 考虑每行最后的 padding,猜测 0 出现的次数不能太多
- 进行文件恢复时,注意需要将一些 assertions 转换为条件分支,防止提前 crash
- 注意对于 long name 存储使用的是空终止宽字符串
- 也就是
wchar_t
数组 - 清空时需要使用
wmemset(long_filename, 0, sizeof(long_filename) / sizeof(wchar_t))
- 也就是