
Essay2026 / 09 / 09
XV6实验2-实现用户看到的Syscall
0-如何添加一个系统调用
0.1-一个系统调用的过程
用户在user空间内进行编程,包含user.h这个头文件后,更新makefile便可以生成程序,与操作系统交互。 那么一个调用的构成是这这样的。
- 用户在
user/user.h内看到了操作系统提供的接口。 user/usy.pl这个脚本会生成一个user/usys.S汇编程序。该汇编文件就是用户到对应系统调用的汇编程序。这个汇编程序就相当于是个钩子,把内核上系统接口的实现与用户看到的系统接口之间连接起来。它做两件事情,第一是将定义在/kernel/syscall.h的系统调用号放在a7寄存器内,随后ecall进入内核- 进入内核。内核内的
kernel/syscall.c内的syscall会被执行。其逻辑很简单。在文件内定义了一个系统调用号与系统调用函数映射关系的 映射表 。所以syscall就会真正把用户看到的,传进来的参数传递给内核里的那个真正干事的系统调用函数。 - 通过映射表调用相应的系统函数,并传递参数进去。内核中的系统函数被执行。然后逐层返回用户看到系统接口。一次系统调用完成
0.2-新增系统调用需要修改的地方
所以如果要无中生有一个系统调用。那么从顶至下需要依次修改
user/user.h定义给用户使用的系统接口函数user/usys.pl产生接口函数所需的用户到内核的汇编程序kernel/syscall.h定义内核该系统调用的调用号kernel/syscall.c在映射表内新增调用号到真正系统调用函数的记录kernel/xxx.c实现该系统调用- 一些其他的内核文件,以实现系统调用的正确逻辑。如read不能只是关于文件,进程的fork,alloc等需要考虑其是否被复制继承fd表等等
1-System Call Tracing
1.1-需求分析
这里我们被要求添加一个新的系统调用 trace
其中这个系统调用被定义为
int trace(int SYS_CALL_MASK)
描述 trace接收SYS_CALL_MASK掩码得知要追踪哪些系统调用并输出。其中进程调用trace后,其自身与子进程的相关系统调用将会被跟踪,并打印在屏幕上。其中打印的格式为
[pid]: syscall [syscall_name] -> [return_value]
返回值 其中正常情况下trace将会返回0,异常情况下返回-1
1.2-预备的修改
在user/user.h内添加
int trace(int);
在user/usys.S内添加
entry("trace");
在kernel/syscall.h内添加
#define SYS_trace 22
在kernel/syscall.c的映射表内添加
extern uint64 sys_trace(void);
//在syscalls映射表内添加
[SYS_trace] sys_trace;
1.3-实现
首先我们要明确我们正在哪里捕获系统调用
在xv6内,一个进程,他的所有系统调用进入内核态的第一件事情就是被syscall这个函数接管。其会解析传递过来的系统调用号,然后调用相应的正在内核里面写的实现的系统调用函数
所以第一件事情很明确,就是在syscall内去检查该系统调用是否被trace,如果被trace就输出
第二件事情就是trace的子进程默认也捕获的功能。这里其实很简单。就是我们在procceess的数据结构内添加trace的掩码信息来记录该进程哪些系统调用正在被trace。然后同步修改fork,保证子进程被创建时其trace掩码信息会继承自父进程。
那么第一步就是给进程的数据结构新增一个信息以记录trace掩码
// kernel/proc.h
struct proc {
//省略代码
int tmask;//新开一行,写下
};
接下来修改syscall,让他在系统调用号合理时。读取进程的tmask以确定该是同调用是否被trace
// kernel/syscall.c
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->tmask) & (1<<num)){//syscall had be traced
printf("%d: syscall %s -> %d\n",p->pid,syscall_names[num],p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
同样修改fork
// kernel/proc.c
int
fork(void)
{
//省略代码
np->tmask = p->tmask;//新增代码
pid = np->pid;
np->state = RUNNABLE;
release(&np->lock);
return pid;
}
最后实现下sys_trace,他的任务就是获取参数,然后写入到进程的tmask当中
// kernel/sysproc.c
uint64
sys_trace(void){
int mask;
struct proc* p = myproc();
if(argint(0,&mask) < 0){
return -1;
}
p->tmask = mask;
return 0;
}
2-Sysinfo
2.1-需求分析
这里我们被要求实现一个系统调用函数 用户会传递一个sysinfo结构体指针,sysinfo函数需要获取空闲的内存容量与进程表中非UNUSED的进程数,并放置到用户给的结构体内
int sysinfo(void*);
返回值 其将在正常工作时返回0,异常时返回-1
2.2-准备工作
在user/user.h内新增
int sysinfo(struct sysinfo*);
在user/usys.pl内新增
entry("sysinfo");
在kernel/syscall.h内添加
#define SYS_sysinfo 23
在kernel/syscall.c内的映射表内添加相应的记录
2.3-实现
所以对于sysinfo我们的实现为
- 获取参数,得到用户给的addr
- 获取空闲内存量
- 获取非UNUSED的进程数
- 在内核态的情况下,把数据拷贝到用户addr上
所以下面即代码实现
#include "types.h"
#include "riscv.h"
#include "sysinfo.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "proc.h"
uint64
sys_sysinfo(void)
{
struct sysinfo sinfo;
uint64 addr;
struct proc* p = myproc();
if(argaddr(0,&addr)< 0){
return -1;
}
//get the free memory
sinfo.freemem = kcount();
//get processors
sinfo.nproc = proc_count();
//copy out from kernel to user
if(copyout(p->pagetable,addr,(char*)&sinfo,sizeof(sinfo)) < 0){
return -1;
}
return 0;
};
接下来就是实现kcount,proc_count
在kernel/kalloc.c内实现kcount
int
kcount(void){
int n=0;
struct run* p;
acquire(&kmem.lock);//在计数完成前,确保内存布局不变
p = kmem.freelist;
while(p){
p = p->next;
n++;
}
release(&kmem.lock);
return n*PGSIZE;
}
在kernel/proc.c内实现proc_count
int
proc_count(void){
struct proc* p;
int count;
count = 0;
for(p = &proc[0];p<&proc[NPROC];p++){
acquire(&p->lock);
if(p->state != UNUSED){
count++;
}
release(&p->lock);
}
return count;
}