[MIT6.S081] 继续向前 · Lab2 · system calls

308 阅读1分钟

Lab2 讲义

源码阅读

在开始coding前,应该先阅读以下源码:

1. user/user.h and user/usys.pl

这是用户空间的代码,与系统调用相关。

user.h内声明了一些系统调用的接口。

usys.pl文件是perl脚本语言文件,用作生成usys.s汇编文件

print "# generated by usys.pl - do not edit\n";
# 包含文件kernel/syscall.h 定义了各个系统调用的编号
print "#include \"kernel/syscall.h\"\n";

# Generate usys.S, the stubs for syscalls

sub entry {
	# 接受一个参数${name}, shift表示从参数列表的开头取出并返回第一个元素,并将该元素从原数组或参数列表中删除
	my $name = shift;
	# 定义一个全局符号,添加到全局符号表中,以便在其它文件中能够调用
	print ".global $name\n";
	# 表示一个代码段的开始,名字为${name}
	print "${name}:\n";
	# 将SYS_${name}对应的值存在a7寄存器中
	print " li a7, SYS_${name}\n"; //
	# 调用ecall,执行a7寄存器保存编号对应的系统调用
	print " ecall\n";
	# 返回到调用者
	print " ret\n";
}

entry("fork");
entry("exit");
entry("wait");
...

注意到一行注释

# Generate usys.S, the stubs for syscalls

注: stub意为“替代物”,是软件开发中的常用词语,在不同的语境中有不同的意义:

  1. 这里指的是系统调用的替代物(system call stubs),我们在用户空间中调用的“系统调用”并非真实的系统调用,真实的系统调用需要通过ecall来调用。
  2. 在软件测试中,某系统存在外部依赖,而这外部依赖不容易获取或获取成本高时(例如,需要收费的API),则在软件进行测试时,先使用本地的模块进行替代,这个模块就叫做stub。

2.kernel/syscall.h and kernel/syscall.c

这是内核空间的代码,与系统调用相关。

syscall.h 定义了一些系统调用的编号,例如SYS_fork1

syscall.c 定义了一些从用户空间取出系统调用参数的函数 ,主要在kernel/proc.c中使用

// 提取地址
int fetchaddr(uint64 addr, uint64 *ip)
// 提取字符串
int fetchstr(uint64 addr, char *buf, int max)
// 获取第n个寄存器中的值
static uint64 argraw(int n)
// 获取第n个寄存器中的值(根据值的数据类型不同,选择下面三者其一),保存在ip对应的地址块中
int argint(int n, int *ip)
int argaddr(int n, uint64 *ip)
int argstr(int n, char *buf, int max)

// 函数指针数组,使用初始化特定元素的数组初始化方式。注意使用了static关键字,本数组只能在本文件中使用
static uint64 (*syscalls[])(void) = { [SYS_fork] sys_fork, [SYS_exit] sys_exit, [SYS_wait] sys_wait, [SYS_pipe] sys_pipe, [SYS_read] sys_read, [SYS_kill] sys_kill, [SYS_exec] sys_exec, [SYS_fstat] sys_fstat, [SYS_chdir] sys_chdir, [SYS_dup] sys_dup, [SYS_getpid] sys_getpid, [SYS_sbrk] sys_sbrk, [SYS_sleep] sys_sleep, [SYS_uptime] sys_uptime, [SYS_open] sys_open, [SYS_write] sys_write, [SYS_mknod] sys_mknod, [SYS_unlink] sys_unlink, [SYS_link] sys_link, [SYS_mkdir] sys_mkdir, [SYS_close] sys_close, };

void syscall(void) {
	int num;
	// myproc()返回当前进程结构体的指针
	struct proc *p = myproc();
	// 获取保存在trapframe中的寄存器a7的内容
	num = p->trapframe->a7;
	if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
		// 调用系统调用 并 将系统调用的返回值保存在a0寄存器中
		p->trapframe->a0 = syscalls[num]();
	} else {
		printf("%d %s: unknown sys call %d\n",
			p->pid, p->name, num);
		p->trapframe->a0 = -1;	
	}
}

3.kernel/proc.h and kernel/proc.c

这是内核空间的代码,与进程相关。

proc.h的主要内容如下:

// 用作在内核上下文切换时保存寄存器的内容
struct context              
// 是每个CPU的状态
struct cpu                  
// 外部声明struct cpu
extern struct cpu cpus[NCPU]
// 陷入帧,每个进程都会保存一份
struct trapframe
// 进程,其中部分变量在使用时需要持有锁,而另外一部分不需要
struct proc                
// 表示进程状态。
enum procstate              

proc.c主要包含一些与进程相关的函数:

// 进程初始化
void procinit(void)
// 返回正在本cpu上运行的进程的结构题指针
struct proc* myproc(void) {
	push_off();
	// 获得当前cpu的
	struct cpu *c = mycpu();
	struct proc *p = c->proc;
	pop_off();
	return p;
}
// pid分配
int allocpid()

// 释放进程
static void freeproc(struct proc *p) 
// 创建用户页表,
pagetable_t proc_pagetable(struct proc *p)

注:"trampoline"意为跳板,在Linux内核中,trampoline通常指的是一个小段代码,作为一个跳板来执行其他函数或指令。在Lecture6中有所解释。

System call tracing (moderate)

在本文第一部分源码阅读中,我们静态地理解系统调用的原理,而在Lab 2的两个assignment中,我们将尝试以动态的视角去理解系统调用的过程,这可能可以很好地帮助我们理解用户程序是如何调用系统调用的。

trace系统调用的作用是:在调用其它系统调用之前,在本进程的结构体struct proc中记录mask的值,并通过fork()传递给所有子进程。后续执行其它系统调用时,syscall()中根据mask的值,输出我们期望观测的系统调用的信息。而不调用trace时,struct proc中mask值默认为0,即所有系统调用的信息都不输出。

user/trace.c中,使用在user/user.h中声明的system call stub接口(关于stub的解释见本文第一部分源码阅读的第二点的最后):

trace(atoi(argv[1]))

user/usys.S中我们可以看到这个stub的具体实现:

.global trace
trace:
 li a7, SYS_trace
 ecall
 ret

执行ecall后,会跳转到内核空间的syscall()函数(kernel/syscall.c),且本cpu的a7寄存器的值存在本进程结构体的trapframe中,以方便内核空间使用。
Q:为什么不在内核空间中直接读取a7寄存器的值?
A:是因为有可能在调用系统调用的过程中会切换到其它进程,这样的话a7寄存器的内容可能会被其它进程改变? 注:在执行ecall到跳转到syscall()函数之间,应该还有一些细节,与前文提到的trampoline相关,但还有待探究。

执行kernel/syscall.csyscall()函数并取出当前进程的trapframe中保存的a7寄存器内容,即系统调用的编号,然后执行对应的系统调用。

Sysinfo (moderate)

与assignment1相反,在本assignment中,我们需要将数据从内核空间传回用户空间。 具体原理有待更新。

在本Lab中学习到的一些命令

gdb

(gdb) layout split
(gdb) stepi # 机器指令层面的单步步入

9218df6f06ea4b4da83ab979586cf6ad~tplv-k3u1fbpfcp-watermark.png

Linux

使用grep的-n参数,从标准输出中搜素关键字

$ cat gdb.sh | grep -n f
1:file kernel/kernel  

qemu

使用一个核运行qemu

make CPUS=1 qemu-gdb

vim

在vim中搜索关键字后,关键字会一直高亮,如何取消高亮?使用vim命令:

# 取消高亮
:nohlsearch # no high light search
:noh 
# 恢复高亮
:hlsearch
:hl