[mit6.s081] 笔记 Lab2:system calls
前言
这个实验相较于上一个实验,难度加大了好多,主要难点就在于阅读源码。刚开始做lab1时很蒙,不知道要干哈。在看了相关的xv6文档,视频课程,阅读部分源码和查阅资料后,才有头绪完成了该实验,收货还是很大的。后面实验肯定会越来越难,要继续坚持下去。
xv6-RISCV 的系统调用
首先用户态程序通过系统调用 ecall 进入内核态,硬件做好一些简单的环境配置,然后进入 trampoline.S/uservec() 程序段,接着通过 syscall() 函数选择具体的系统调用,接着是硬件实现或者软件实现(我们这里做的功能)具体功能。
调用链
- user/user.h: 用户态程序调用跳板函数(也就是系统调用在用户态的声明)
- user/usys.S: 跳板函数使用 CPU 提供的 ecall 指令,调用到内核态
- kernel/syscall.c 到达内核态统一系统调用处理函数 syscall(),所有系统调用都会跳到这里来处理。
- kernel/syscall.c syscall() 根据跳板传进来的系统调用编号,查询 syscalls[ ] 表,找到对应的内核函数并调用。
- kernel/sysproc.c 到达内核中的系统调用函数,执行具体内核操作
System call tracing (moderate)
In this assignment you will add a system call tracing feature that may help you when debugging later labs. You’ll create a new trace system call that will control tracing. It should take one argument, an integer “mask”, whose bits specify which system calls to trace. For example, to trace the fork system call, a program calls trace(1 << SYS_fork), where SYS_fork is a syscall number from kernel/syscall.h. You have to modify the xv6 kernel to print out a line when each system call is about to return, if the system call’s number is set in the mask. The line should contain the process id, the name of the system call and the return value; you don’t need to print the system call arguments. The trace system call should enable tracing for the process that calls it and any children that it subsequently forks, but should not affect other processes.
1.准备
- 在 Makefile 中把 $U/_trace 添加到 UPROGS 里
2.用户态准备工作
2.1.user/user.h
- 在user/user.h添加函数原型(跳板函数)
int trace(int);
2.2.user/usys.pl
- 在 user/usys.pl 中添加入口
entry(“trace”);
- usys.pl 的作用是生成 usys.S 汇编代码
生成用户态的系统调用汇编代码
如下是生成的 trace 系统调用汇编代码
.global trace
trace:
li a7, SYS_trace
ecall
ret
3. 内核态准备工作
3.1.kernel/syscall.h
- 在kernel/syscall.h中添加下列宏(系统调用号)
#define SYS_trace 22
3.2.kernel/syscall.c
- 添加sys_trace的定义和入口
extern uint64 sys_trace(void);
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
................//中间入口地址
[SYS_trace] sys_trace,
};
注意
:
-
- 在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。
-
syscalls
是一个函数指针数组,[SYS_trace] sys_trace
是c语言的一个语法
表是下表为SYS_trace(系统调用号) 的元素值为sys_trace(函数指针)
3.3. kernel/sysproc.c
- 在该文件下添加sys_trace的具体实现(下面是测试)
uint64 sys_trace(void) {
// 这里没有做具体的实现
// 只是测试
// 具体实现见后面
struct proc *p = myproc(); //获取该进程的PCB
printf("syscall: trace(pid: %d)\n", p->pid);
return 0;
}
- 做到这个时候,执行
make qemu
就可以将xv6跑起来了,可以执行trace系统调用
$ trace 32 grep hello README
结果
syscall: trace(pid: 3)
4.sys_trace代码具体实现
4.1.kernel/proc.h
- 为了要让系统在调用trace后,当产生系统调用时,输出相关信息,我们需要在进程pcb中增添一个成员trask_mask用来存放mask
// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
```````````````//中间部分代码省略
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
uint64 trace_mask;//存放掩码
};
4.2.kernel/proc.c
- fork时,应实验要求,也要对子进程进行追踪,所以需要让子进程继承mask,
// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
// Allocate process.
if ((np = allocproc()) == 0)
{
return -1;
}
// Copy user memory from parent to child.
`````````````//中间部分代码省略
np->trace_mask = p->trace_mask; //子进程继承父进程的掩码
release(&np->lock);
return pid;
}
- 进程创建时,应将trace_mask初始化为0
// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, or a memory allocation fails, return 0.
static struct proc *
allocproc(void)
{
struct proc *p;
`````````````//中间部分代码省略
// Set up new context to start executing at forkret,
// which returns to user space.
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
p->trace_mask=0; //将mask初始化为0
return p;
}
- 进程销毁时将其置为0
// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
if (p->trapframe)
kfree((void *)p->trapframe);
p->trapframe = 0;
if (p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->trace_mask = 0; //化掩码置为0
p->state = UNUSED;
}
4.3.kernel/sysproc.c
- sys_trace具体实现代码,获取mask存放在trace_make当中
uint64
sys_trace(void)
{
int mask;
if (argint(0, &mask) < 0) //获取掩码
return -1;
myproc()->trace_mask = mask;
return 0;
}
注意
:
- 并且由于内核与用户进程的页表不同,寄存器也不互通,所以参数无法直接通过 C 语言参数的形式传过来,而是需要使用 argaddr、argint、argstr 等系列函数,从进程的 trapframe 中读取用户进程寄存器中的参数。
4.4.kernel/syscall.c
- 加入一个字符数组,通过系统调用号来找到名字
static char *syscall_names[] = {
"fork", "exit", "wait", "pipe", "read", "kill",
"exec", "fstat", "chdir", "dup", "getpid", "sbrk",
"sleep","uptime", "open", "write", "mknod",
"unlink", "link", "mkdir", "close", "sysinfo", "trace"};
- 在syscall中进行信息输出
void syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7;
if (num > 0 && num < NELEM(syscalls) && syscalls[num])
{
p->trapframe->a0 = syscalls[num]();
if ((p->trace_mask >> num) & 1) //判断是否调用了trace
{
printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num - 1], p->trapframe->a0);
}
}
else
{
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
5.测试
以上就完成了trace的编写
$ trace 32 grep hello README
执行结果:
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
Sysinfo (moderate)
In this assignment you will add a system call, sysinfo, that collects information about the running system. The system call takes one argument: a pointer to a struct sysinfo (see kernel/sysinfo.h). The kernel should fill out the fields of this struct: the freemem field should be set to the number of bytes of free memory, and the nproc field should be set to the number of processes whose state is not UNUSED. We provide a test program sysinfotest; you pass this assignment if it prints “sysinfotest: OK”.
实现该系统调用在用户态和内核态的准备工作和实现trace的准备工作一样,在这就不过多赘述
1.sys_sysinfo代码实现
1.1.kernel/proc.c
- 在proc.c添加函数 acquire_nproc,该函数是用来获取当前的非空闲进程个数
uint64 acquire_nproc() //获取正在被使用的线程数
{
struct proc *p;
uint64 cnt = 0;
for (p = proc; p < &proc[NPROC]; p++)
{
acquire(&p->lock);
if (p->state != UNUSED) //判断该进程是否正在被使用
{
cnt++;
}
release(&p->lock);
}
return cnt;
}
注意:
- 我们在这只进行的操作,所以在获取进程时不需要加锁
struct proc proc[NPROC];
是一个全局变量,用来存放所有的进程。在xv6中,支持最大进程数为64 ,数量较少,所以采用了数组的方式存放所有进程,在Linux当中,采用的是双向循环链表的形式。
1.2.kernel/kalloc.c
- 在kernel/kalloc.c添加函数acquire_freemem(),该函数可获取当前空闲内存大小
uint64 acquire_freemem()//获取空闲内存
{
struct run *r;
uint64 cnt=0;
acquire(&kmem.lock);
r = kmem.freelist;
while(r)
{
r = r->next;
cnt++;
}
release(&kmem.lock);
return cnt*PGSIZE;
}
注意:
xv6采用的就是空闲链表法,上面代码就是先计算出空闲页个数,然后个数乘以每个页的大小。
2.kernel/sysproc.c
*对上面两个函数进行声明, 实现了上面两个函数,再来实现sys_sysinfo就简单很多了,
uint64 acquire_freemem();
uint64 acquire_nproc();
uint64
sys_sysinfo(void)
{
struct sysinfo info;
uint64 addr;
info.freemem = acquire_freemem();
info.nproc = acquire_nproc();
struct proc *p = myproc();
if (argaddr(0, &addr) < 0)
return -1;
// 将页表 p->pagetable 中起始地址为 &info 长度为 sizeof(info) 的内存
// 复制到地址 addr,以供用户层使用
if (copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)
return -1;
return 0;
}
测试
以上就完成编写
$ sysinfotest
结果
sysinfotest: start
sysinfotest: OK