
Lab2 实现 2 个 syscall。
Using gdb
第一部分是了解使用 gdb 调试 xv6(我使用的是 Debian)
其中一个窗口启动 qemu-gdb |
xv6 项目,为我们提供了便捷的 .gdbinit
脚本,默认情况下(下图), GDB 好像不为我们自动加载这个脚本,
有两种方式可以让图中的提示消失,
第一种是将 xv6 的 .gdbinit 文件设置为 auto-load
mkdir -p /root/.config/gdb |
第二种是直接执行 source .gdbinit
手动加载,第一种设置 auto-load 的方式一劳永逸,下次 gdb 的时候,不用敲 source 了。
下面查看一下.gdbinit
里写了什么,打开 .gdbinit
发现它会帮我们,
- 设置远端机器的架构 set architecture
- 自动连接远端机器 target remote
- 加载调试符号等操作 symbol-file
- 其他
这些配置对于 GDB 远端调试都是必要的。特别解释一下 set architecture 的原因,现在市面上没有厂家量产 riscv 架构的机器,我们通常都是用 x86/aarch64 架构,存在一个跨架构调试的问题。
set confirm off |
System call tracing
如何创建一个 syscall
user/user.h
中添加系统调用定义user/usys.pl
中定义系统调用的入口,usys.pl 是一个脚本,会将所有的系统调用 entry 生成和对应的汇编 usys.S,将系统调用号存到 a7 寄存器,然后调用 ecall 指令进行系统调用,接着返回
.global trace
trace:
li a7, SYS_trace
ecall
ret
kernel/syscall.h
中新增一个新的系统调用号。kernel/syscall.c
中 syscalls 数组新增 syscall 的响应函数,会在 syscall() 函数中以函数指针的方式被使用。kernel/sysproc.c
中新增对应 syscall 的实际响应函数。以 sys_ 开头。 sys_ 开头的函数是个包装函数,先对应的寄存器中读取系统调用的参数,然后调用实际的函数完成系统调用。
实现原理
先在 struct proc (代表进程 process) 中添加 tracemask 字段,代表当前进程需要跟踪的 syscall。
diff --git a/kernel/proc.h b/kernel/proc.h |
当进程发起 syscall 时,会检测当前进程的发起的 syscall 的 tracemask 是否被置位。置位则代表当前进程被 trace,输出当前发起 syscall 进程的 pid,syscall_name,syscall 返回值。
syscall(void) |
调用路径
用户态同名函数 trace -> syscall -> sys_trace -> 内核态同名函数 trace
- trace(userspace):发起 syscall
- syscall:系统调用函数,通过 syscalls 数组中定义的函数指针,根据系统调用号获得对应的函数指针,调用对应的 sys_ 函数
- sys_trace:包装函数,先通过寄存器读取用户态传入的系统调用参数,接着调用实际的 trace()
- trace(kernelspace):设置当前进程的 tracemask 值为要追踪的系统调用号。
uint64 |
// trace syscall |
void |
tracemask 变量初始化、销毁
在 struct proc
新增了 tracemask 字段需要正确初始化以及销毁,fork 也需要复制将字段复制到子进程里才能符合 fork 的语义。
diff --git a/kernel/proc.c b/kernel/proc.c |
Show me the code
Talk is cheap. Show me the code.
diff --git a/Makefile b/Makefile |
思考
实现完 trace syscall 后,总觉得好像少点什么,不完整。仔细想想,我们实现的 trace 只能 trace 进程本身以及子进程?
Q: 那我们在现有的基础设施上,能否实现 trace 别的进程,也就是说给 trace 命令添加一个 -p 参数,能 trace 别的进程?
A: 实际上是可以的,而且在现有的基础设施上支持 -p 参数没有工作量,只需要重新实现 trace,将其中 myproc() 替换为遍历所有的进程,将其中某个 pid 进程的 tracemask 置位即可。
Sysinfo
实现一个 sysinfo 系统调用,获取 xv6 内核的空闲内存以及当前进程数,将数据从内核态拷贝到用户态。
sysinfo 系统调用
有两点需要注意,
第一点是向用户态拷贝数据时候,只需要将结构体的地址传进来,内核从首地址往后写数据,用户态收到数据后根据定义的结构体解析数据。所以,只需要一个地址 addr 就行了。
第二点是返回值,当 copyout 失败的时候,需要返回 -1。sysinfotest.c 里测试用例会构造一个错误的 addr 作为 sysinfo 的参数传进来,此时 sysinfo 会失败,测试用例会对比返回值是不是 0xffffffffffffffff
,也就是 -1 。这里涉及的是 C 语言负数的存储方式,补码。
// sysinfo syscall |
nr_freemem 和 nr_processes 两个函数的定义写在 kernel/defs.h 就行了, xv6 内核态所有的函数定义都放在此文件里。
获取空闲内存
xv6 的空闲内存使用链表管理的,通过将指针 r 指向当前 kmem.freelist 即空闲内存的头部,遍历直到访问为 NULL 时,期间访问的总数及空闲内存的个数。再乘上每页的大小可得空闲内存容量。
int nr_freemem(void) { |
获取进程数
xv6 内核进程使用全局维护的 proc 数组来表示,遍历它即可。
int nr_processes(void) { |
Show me the code
Talk is cheap. Show me the code.
diff --git a/Makefile b/Makefile |
测试结果
最后 make grade
查看结果,
== Test answers-syscall.txt == |