目录
- PreRead
- 任务列表
- 4.3 Code: Calling system calls
- 4.4 Code: System call arguments
- System call tracing
- 测试
- 任务
- hints
- 思路
- 先看用户的trace函数
- trace系统调用到底是怎么作用的呢?
- 重新捋一遍系统调用的过程
- Sysinfo
- 任务
- hints
- 思路
PreRead
任务列表
- xv6课本
- 第二章:Operating system organization
- 第四章
- 4.3:Code: Calling system calls
- 4.4:Code: System call arguments
- 源文件
- 系统调用的用户空间代码:user/user.h和user/usys.pl
- 内核空间代码:kernel/syscall.h、kernel/syscall.c
- 与进程相关的代码是kernel/proc.h和kernel/proc.c。
4.3 Code: Calling system calls
exec系统调用在内核中是如何实现的
- 用户的代码将exec函数的参数放在了寄存器a0和a1,并且将系统调用号放在了寄存器a7
- 系统调用号匹配了syscalls数组的某一个项,这个数组是一系列的函数指针
- ecall指令陷入内核,执行uservec,usertrap,然后执行了syscall
- syscall函数根据系统调用号,找到了sys_exec函数,并调用这个函数
- 当sys_exec函数结束之后,syscall会将它的返回值放在p->trapframe->a0,然后会把这个值作为用户调用exec的返回值。一般情况下,risc-v的c语言的函数的返回值都放在a0寄存器,并且0代表成功,-1代表失败
4.4 Code: System call arguments
系统调用是如何找到用户传递的参数呢?
- 参数通常是存放在寄存器中
- 内核trap的代码将用户的寄存器保存到当前进程的trap页面,内核可以找到这个页面
- 通过argint,argaddr,argfd可以分别从trap页面读出int,pointer,fd
- 调用argraw可以取出正确的被保存的用户寄存器
系统调用的挑战
- 用户调用的程序是有问题的
- 内核和用户空间的虚拟地址映射可能不同
fetchstr函数
- 从用户空间读出文件名
- 调用了copyinstr处理hard工作
System call tracing
测试
$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
$
$ trace 2147483647 grep hello README
4: syscall trace -> 0
4: syscall exec -> 3
4: syscall open -> 3
4: syscall read -> 1023
4: syscall read -> 966
4: syscall read -> 70
4: syscall read -> 0
4: syscall close -> 0
$
$ grep hello README
$
$ trace 2 usertests forkforkfork
usertests starting
test forkforkfork: 407: syscall fork -> 408
408: syscall fork -> 409
409: syscall fork -> 410
410: syscall fork -> 411
409: syscall fork -> 412
410: syscall fork -> 413
409: syscall fork -> 414
411: syscall fork -> 415
...
$
任务
-
增加一个
trace
系统调用 -
用法
trace mask 常规的指令
比如
trace 32 grep hello README
-
功能:在
grep hello README
的过程中,使用了很多系统调用通过
mask
为1的pos可以找到我们需要关注的系统调用1 << SYS_fork
,而SYS_fork
记录在kernel/syscall.h
-
我们需要把
mask
标记的系统调用都打印一下打印的格式如下
pid: syscall 系统调用的名字 -> 返回值
比如
3: syscall read -> 1023
-
fork出的子进程也同样要打印,不相关的进程不要打印
hints
-
将
$U/_trace
加入makefiel的UPROGS -
在
user/user.h
增加原型在
user/usys.pl
增加在
kernel/syscall.h
增加系统调用号 -
在
kernel/sysproc.c
中增加sys_trace()
函数实现系统调用这个函数将参数放在
kernel/proc.h
的proc
结构体的一个新的变量中通过查看
kernel/syscall.c
的例子可以看到怎么从用户空间提取系统调用参数 -
修改
kernel/proc.c
中的fork()
函数完成父进程将mask传递给子进程 -
修改
kernel/syscall.c
中的syscall()
去打印我们的输出不懂:You will need to add an array of syscall names to index into
思路
自己是没能独立做出来,一个是对xv6的系统调用机制不熟悉,还有一个是误解了hints的第3点意思,这一点非常关键
这个lab操作起来其实非常简单,代码量非常小,但是需要好好地想清楚
先看用户的trace函数
这个trace函数其实xv6已经提供给我们了,就在user/trace.c
文件中,先看看这个用户的trace函数的结构,有利于理清思路
第一段有用的代码在这里
- 可以发现,这就直接调用了trace函数,就使用了mask一个变量,真正要检测的程序并没有执行。因此,可以猜测,trace并不是边exec边设置,而是提前就设置好。这样在后面exec使用系统调用的时候,就直接输出信息
if (trace(atoi(argv[1])) < 0) {
fprintf(2, "%s: trace failed\n", argv[0]);
exit(1);
}
第二段代码在这里
这个代码其实没什么新奇的,就是调用了exec函数
for(i = 2; i < argc && i < MAXARG; i++){
nargv[i-2] = argv[i];
}
exec(nargv[0], nargv);
trace系统调用到底是怎么作用的呢?
hints的第3,4,5点给出了答案
- 首先,第3点告诉我们,我们需要定义一个
sys_trace
函数,这个函数会将mask存在proc结构体的一个新的变量中。- 这个新的变量就很关键了,意思是让我们自己去修改proc结构体的代码,增加一个新的变量,表示mask。
- 然后我们在sys_trace中设置这个mask。
- 系统提供了argint函数,这个函数可以用来设置proc中的int的值
- 由preread中的4.3节可知,运行到sys_trace中时,这个系统调用的参数存放在寄存器a0中
因此,sys_trace函数如下,其中trace_mask就是在proc结构体中新增的一个int变量
uint64
sys_trace(void) {
// 这里的0代表将a0的值赋给trace_mask
argint(0, &(myproc()->trace_mask));
return 0;
}
-
然后就是hint的第4点,告诉我们,要修改fork函数,使得子进程也继承父进程的trace特性,其实就是继承trace_mask。我觉得
np->trace_mask = p->trace_mask;
放在np被创建之后,np释放锁之前的位置都可以。 -
最后就是hint的第5点,在syscall函数中打印信息,搞了这么久,其实就是为了完成这个。
-
在成功执行了对应的系统调用之后,判断这个系统调用是否需要trace打印
if ((1 << num) & p->trace_mask)
-
如果满足条件的话,那就
printf("%d: syscall %s -> %d\n", p->pid, syscalls_name[num], p->trapframe->a0);
。其中最后一个参数是这个系统调用的返回值 -
为了方便操作,增加了一个syscalls_name数组,这个数组的写法很神奇
static char *syscalls_name[] = { [SYS_fork] "fork", [SYS_exit] "exit", [SYS_wait] "wait", [SYS_pipe] "pipe", [SYS_read] "read", [SYS_kill] "kill", [SYS_exec] "exec", [SYS_fstat] "fstat", [SYS_chdir] "chdir", [SYS_dup] "dup", [SYS_getpid] "getpid", [SYS_sbrk] "sbrk", [SYS_sleep] "sleep", [SYS_uptime] "uptime", [SYS_open] "open", [SYS_write] "write", [SYS_mknod] "mknod", [SYS_unlink] "unlink", [SYS_link] "link", [SYS_mkdir] "mkdir", [SYS_close] "close", [SYS_trace] "trace", };
-
最后,就是hints中1和2的dirtywork,不过也更能学到系统调用的流程,
-
user.h
中增加系统调用int trace(int);
的声明 -
usys.pl
中增加entry("trace");
-
syscall.h
中增\#define SYS_trace 22
-
hints没有提到的,
syscall.c
的syscalls数组中增加一项[SYS_trace] sys_trace,
重新捋一遍系统调用的过程
就从syscall函数开始吧,之前的还没学,我是菜狗
- syscall函数会通过调用它的用户进程的proc结构体获取这个进程存放在trapframe中的各种信息,其中在syscall中使用的是a7寄存器,存放的是系统调用号
- 然后syscall根据这个系统调用号,在syscall.c文件中定义了一个syscalls数组,这个数组可以通过系统调用号找到对应的系统调用函数,而这个系统调用函数是我们在sysproc.c文件中定义的sys_xxx函数。至此,就去执行具体的系统调用函数了
除此之外,还有一些比较隐秘的知识点
-
用户进程在请求系统调用之后,是会把自己的内存和寄存器信息存放在trapframe中。其中比较重要的就是,会把系统调用的参数给放在从a0开始的寄存器,会把系统调用号放在a7寄存器。
-
通过argint,argaddr,argfd argstr可以分别从trap页面读出int,pointer,fd,字符串
调用argraw可以取出正确的被保存的用户寄存器,上面所提的arg系列函数基本都调用了argraw实现功能
-
syscall函数调用的sys_xxx系列的函数都是没有显式的定义参数的,都需要我们自己直接操作trapframe或者间接通过arg获取用户传给我们的信息
留坑
现在还没有怎么很清楚是如何从用户态的trace函数跳到syscall函数的,希望在后面的课程中弄懂
Sysinfo
任务
- 增加一个系统调用sysinfo,收集正在运行的系统的信息
- 这个系统调用的参数是一个kernel/sysinfo.h中定义的struct sysinfo的指针
- 内核需要填充这个struct的这几个部分
- freemem:free memory的字节数
- nproc:状态不是UNUSED的进程的数量
- 如果sysinfotest输出sysinfotest: OK,则代表通过
hints
- 将$U/_sysinfotest添加到Makefile的UPROGS
- 和上一个任务一样完成各自声明,在
user.h
中,需要提前声明
struct sysinfo;
int sysinfo(struct sysinfo *);
-
sysinfo
需要赋值struct回用户空间看
kernel/sysfile.c
的sys_fstat()
以及kernel/file.c
的filestat()
函数学习怎么使用
copyout()
函数copyout(p->pagetable, addr, (char *)&st, sizeof(st)
- 第一个参数是用户进程的页表,p通过muproc得到
- addr表示要用户空间的某个地址
- 第三个参数是要复制的内核数据的地址
- 第四个数据是复制多少字节
-
为了统计内存的数量,在
kernel/kalloc.c
增加一个函数 -
为了统计进程的数量,在
kernel/proc.c
中增加一个函数
补充一个hints,新增加的这两个函数需要在
defs.h
中声明
思路
-
首先可以和第一个任务一样先把sysinfo系统调用的架子给搭起来,然后正式开始写
sys_sysinfo
函数 -
hints里已经提示我们了,分别需要去另外两个文件里添加函数
-
在kalloc.c文件中,可以发现空闲页面被存放在kmem的freelist中,这个freelist是一个链表,每一个结点代表一个大小为PGSIZE的空闲页面,我们可以通过next指针找到下一个结点。因此简单地遍历一遍就可以得到空闲的页面数量。
除了这个写法,还可以在每次成功调用了kalloc和kfree时更新一个全局变量freepage_num,这样可以避免线性遍历一个链表
uint64 get_freemem() { uint64 ans = 0; acquire(&kmem.lock); struct run *p = kmem.freelist; while (p) { ans += PGSIZE; p = p->next; } release(&kmem.lock); return ans; }
-
在proc.c文件中,看起来代码很多,但是可以发现,所有的进程都是存放在proc数组的,这个数组的元素类型是就是struct proc,因此可以直接访问这个进程的state,代码如下
uint64 get_uf_proc() { uint64 ans = 0; struct proc *p; for (p = proc; p < &proc[NPROC]; p++) { acquire(&p->lock); if (p->state != UNUSED) { ans += 1; } release(&p->lock); } return ans; }
-
最后就要完成
sys_sysinfo
函数,这个函数的关键在于copyout函数,在hints里已经给出了一个用法示例,按着这个来就行了。-
copyout函数的第一个参数,是用户进程的页表
-
sysinfo函数的参数是一个struct sysinfo类型的指针,它是一个传出参数,也就是copyout函数的第二个参数。在sys_sysinfo函数中,这个参数就在a0寄存器中,可以通过argaddr访问,也可以直接通过a0寄存器访问
-
第三个参数是我们要往第二个参数表示的地址写入的具体内容,其实就是一个struct sysinfo对象,所以创建一个这个对象,然后用上面写好的两个函数初始化这个对象的值。最后将它的地址放在第三个参数上即可
-
最后一个参数就是sizeof(struct sysinfo)
代码如下
uint64 sys_sysinfo(void) { struct sysinfo ans; ans.freemem = get_freemem(); ans.nproc = get_uf_proc(); uint64 desaddr; argaddr(0, &desaddr); if (copyout(myproc()->pagetable, desaddr, (char *)(&ans), sizeof(ans)) < 0) { return -1; } return 0; }
-
-