进行此Lab之前,首先需要阅读Chapter2 和 Chapter4 的 4.3 和 4.4 节,包括部分源文件:
- 用户空间代码(关于系统调用)
user/user.h
anduser/usys.pl
. - 内核空间代码kernel/syscall.h, kernel/syscall.c
- 进程相关代码kernel/proc.h and kernel/proc.c
开始此Lab之前,还需要切换分支
git fetch
git checkout syscall
make clean
Lab2:System Calls
System call tracing (moderate)
要求:
添加一个系统调用跟踪特性,在调试以后的实验时可能会有所帮助。
创建一个 trace 系统调用,它接收一个参数,参数是一个整数掩码(mask),掩码的位指定要跟踪哪个系统调用
比如:要跟踪fork系统调用,程序调用 trace(1 << SYS_fork),其中SYS_fork是来自 kernel/syscall.h 的一个系统调用编号
如果掩码中设置了系统调用编号,则必须修改xv6内核,以便 在每个系统调用即将返回时打印一行 ,该行应该包含进程id,系统调用的名称和返回值,不需要打印系统调用参数
trace系统调用应该支持对调用它的进程和它随后派生的任何子进程的跟踪,但不应该影响其他进程。
在命令行进行调用时:trace [tracing_mask] [command]
比如:trace 32 grep hello README
提示:
- 在
kernel/sysproc.c
中添加函数sys_trace()
,通过在proc结构中定义一个新变量来记住该函数的参数(即mask),进行实现。从用户空间检索系统调用参数的函数是int argint(int, int*)
(见kernel/syscall.c
),可以在kernel/sysproc.c
中看到它们的使用示例。 可以通过argint(int, int*)
来获取系统调用参数,即 tracing mask。 - 修改
fork()
(见kernel/proc.c
),从父进程拷贝 trace mask 到子进程 - 修改
syscall()
(见kernel/syscall.c
)打印 trace 输出。你需要添加一个用于索引的系统调用名称数组(syscalls
)。
创建一个新的系统调用
- 在内核中合适的位置,实现内核系统调用(取决于要实现的功能属于什么模块,理论上随便放都可以,只是主要起归类作用),这里由于trace系统调用会对进程进行操作,所以放在 sysproc.c 中比较合适:
// kernel/sysproc.c
uint64 sys_trace(void) {
int mask;
if (argint(0, &mask) < 0) // 通过argint获得参数
return -1;
// 设置当前调用进程的trace_mask为mask
// 通过myproc获得指向进程的地址,trace_mask 是加在进程结构体中的变量用来保存mask
myproc()->trace_mask = mask;
return 0;
}
- 在
kernel/syscall.h
中,添加新的系统调用编号:
// System call numbers
#define SYS_fork 1
#define SYS_exit 2
#define SYS_wait 3
#define SYS_pipe 4
#define SYS_read 5
#define SYS_kill 6
#define SYS_exec 7
#define SYS_fstat 8
#define SYS_chdir 9
#define SYS_dup 10
#define SYS_getpid 11
#define SYS_sbrk 12
#define SYS_sleep 13
#define SYS_uptime 14
#define SYS_open 15
#define SYS_write 16
#define SYS_mknod 17
#define SYS_unlink 18
#define SYS_link 19
#define SYS_mkdir 20
#define SYS_close 21
#define SYS_trace 22 // 添加的trace系统调用编号
- 用
extern
全局声明新的内核调用函数,并且在syscalls
映射表中,加入从前面定义的编号到系统调用函数指针的映射:
// kernel/syscall.c
extern uint64 sys_chdir(void);
extern uint64 sys_close(void);
extern uint64 sys_dup(void);
extern uint64 sys_exec(void);
extern uint64 sys_exit(void);
extern uint64 sys_fork(void);
extern uint64 sys_fstat(void);
extern uint64 sys_getpid(void);
extern uint64 sys_kill(void);
extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_mknod(void);
extern uint64 sys_open(void);
extern uint64 sys_pipe(void);
extern uint64 sys_read(void);
extern uint64 sys_sbrk(void);
extern uint64 sys_sleep(void);
extern uint64 sys_unlink(void);
extern uint64 sys_wait(void);
extern uint64 sys_write(void);
extern uint64 sys_uptime(void);
extern uint64 sys_trace(void); // 添加函数声明
// 函数指针数组
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,
[SYS_trace] sys_trace, // 添加函数编号到函数的映射关系
};
注意:[SYS_trace] sys_trace
是C语言数组的一个语法,方括号内的数作为元素下标。比如int arr[] = {[3] 2333, [6] 6666}
代表 arr 的下标为 3 的元素为 2333,下标为 6 的元素为 6666,其他元素填充为0。(该语法在C++中已不可用)
- 在
user/usys.pl
中注册一个内核态trace的入口。即在user/usys.pl
中,加入用户态到内核态的跳板函数:
// user/usys.pl
entry("fork");
entry("exit");
entry("wait");
entry("pipe");
entry("read");
entry("write");
entry("close");
entry("kill");
entry("exec");
entry("open");
entry("mknod");
entry("unlink");
entry("fstat");
entry("link");
entry("mkdir");
entry("chdir");
entry("dup");
entry("getpid");
entry("sbrk");
entry("sleep");
entry("uptime");
entry("trace"); // 加入用户态到内核态的跳板函数
Makefile调用 perl 脚本 user/usys.pl
,该脚本生成user/usys.S
(实际的系统调用stub),它使用RISC-V的ecall
指令进入内核。
trace: # 定义用户态跳板函数
li a7, SYS_trace # 将系统调用 id 存入 a7 寄存器
ecall # ecall,调用 system call ,跳到内核态的统一系统调用处理函数 syscall() (syscall.c)
ret
- 在
user/user.h
中加入一个trace的函数原型:int trace(int)
:
// user/user.h
// system calls
int fork(void);
int exit(int) __attribute__((noreturn));
int wait(int*);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
int exec(char*, char**);
int open(const char*, int);
int mknod(const char*, short, short);
int unlink(const char*);
int fstat(int fd, struct stat*);
int link(const char*, const char*);
int mkdir(const char*);
int chdir(const char*);
int dup(int);
int getpid(void);
char* sbrk(int);
int sleep(int);
int uptime(void);
int trace(int);
系统调用的具体流程
user/user.h
:用户态程序调用系统调用函数trace()
user/usys.S
:trace()
函数使用CPU提供的ecall
指令,进入内核态kernel/syscall.c
:到达内核态统一系统调用处理函数syscall()
,所有系统调用都会跳到这里来处理。kernel/syscall.c
:syscall()
根据跳板传进来的系统调用编号,查询syscalls[]
表,找到对应的内核函数并调用kernel/sysproc.c
:到达sys_trace()
函数,执行具体的内核操作
通过以上过程,可以实现用户态和内核态的良好隔离。
由于用户态的参数无法直接通过C语言参数的形式传递到内核,所以需要使用argaddr
、argint
、argstr
等系列函数,从进程的trapframe
中读取用户进程寄存器中的参数。
同时,内核对于用户态传进来的指针不能直接解引用,这是因为内核不能信任任何用户空间的指针。必须对用户空间的指针指向的数据进行验证,因为这个指针指向的地址只在这个进程空间是有效的,跨进程是无效的。并且需要通过验证,避免被传进一个内核的地址。
所以需要使用copyin
、copyout
方法结合进程的页表,才能顺利找到用户态指针(逻辑地址)对应的物理地址:
struct proc *p = myproc(); // 获取调用该 system call 的进程的 proc 结构
copyout(p->pagetable, addr, (char *)&data, sizeof(data)); // 将内核态的 data 变量(常为struct),结合进程的页表,写到进程内存空间内的 addr 地址处。
代码:
在kernel/proc.h
中定义一个新变量,用来保存mask:
// kernel/proc.h
// 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
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
uint64 trace_mask; // Trace Mask
};
在kernel/proc.c
中,当创建新进程时,为mask赋一个默认值0:
// kernel/proc.c
static struct proc*
allocproc(void)
{
...
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
p->trace_mask = 0; // 将trace_mask的默认值设置为0
}
在kernel/sysproc.c
中,实现sys_trace()
函数,它是trace系统调用的具体代码:
// kernel/sysproc.c
uint64 sys_trace(void) {
int mask;
if (argint(0, &mask) < 0)
return -1;
myproc()->trace_mask = mask; // 设置当前调用进程的trace_mask为mask
return 0;
}
在kernel/proc.c
中,修改fork()
函数,使得子进程可以继承父进程的 trace_mask
:
// kernel/proc.c
int
fork(void)
{
...
safestrcpy(np->name, p->name, sizeof(p->name));
pid = np->pid;
np->state = RUNNABLE;
np->trace_mask = p->trace_mask; // 从父进程拷贝trace_mask到子进程
release(&np->lock);
return pid;
}
由于所有的系统调用到达内核态后,都会进入syscall()
这个函数,所以为了可以跟踪所有的系统调用,只需要修改syscall()
函数,在该函数中打印输出即可(为了能输出系统调用的名称,还需要建立一个系统调用编号到系统调用名称的映射):
// 系统调用名称数组(用于trace系统调用打印信息)
const 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",
};
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](); // 通过系统调用编号,获取对应的系统调用函数指针,调用该函数并 将返回值存到用户进程的a0寄存器中
if ((1 << num) & (p->trace_mask)) { // 如果mask中设置了该系统调用编号,则打印信息
printf("%d: syscall %s -> %d\n", p->pid, syscalls_name[num], p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
测试:
Sysinfo(moderate)
要求:
添加一个系统调用sysinfo,它收集关于运行系统的信息
sysinfo系统调用有一个参数:一个指向结构体sysinfo的指针(见 kernel/sysinfo.h)
内核应该填充这个结构体的字段:
freemem 字段应该设置为空闲内存的字节数
nproc 字段应该设置为状态 不是UNUSED 的进程的数量
如果测试程序 sysinfotest 输出 "sysinfotest: OK" 说明通过了测试
提示:
- sysinfo系统调用 需要拷贝一个 sysinfo结构体到用户空间(见
sys_fstat()
(位于kernel/sysfile.c
)和filestat()
(位于kernel/file.c
) 查看如何使用copyout()
) - 为了收集空闲内存的数量,在
kernel/kalloc.c
中添加函数 - 为了收集进程的数量,在
kernel/proc.c
中添加函数
获取空闲内存
在内核的头文件中(kernel/defs.h
)对计算空闲内存的函数进行声明:
// kernel/defs.h
// kalloc.c
void* kalloc(void);
void kfree(void *);
void kinit(void);
uint64 cal_free_mem(void);
在kernel/kalloc.c
中有如下两个结构体:
// kernel/kalloc.c
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
xv6中,空闲内存页的记录方式是形成一个空闲页链表,需要分配空闲页时,就把链表头部的页分配出去,需要回收的时候,就将回收的页作为空闲页的新的头节点。并且链表节点就是空闲页本身。
// kernel/kalloc.c
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist; // 空闲页链表的头节点
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r; // 返回空闲页链表的头节点,将其作为内存页使用
}
注意:常见的记录空闲页的方法有:空闲表法、空闲链表法、位示图法、成组链接法。xv6使用的是空闲链表法。
代码:
// kernel/kalloc.c
// 计算空闲内存(计算空闲链表的长度,然后乘以每一页的大小)
uint64 cal_free_mem(void) {
struct run *r;
uint64 mem_size = 0;
acquire(&kmem.lock); // 需要先上锁,防止竞态条件
r = kmem.freelist;
while (r) {
mem_size += PGSIZE;
r = r->next;
}
release(&kmem.lock);
return mem_size;
}
获取不是UNUSED的进程数量
同样需要先在kernel/defs.h
,对函数进行声明:
// kernel/defs.h
// proc.c
...
void userinit(void);
int wait(uint64);
void wakeup(void*);
void yield(void);
int either_copyout(int user_dst, uint64 dst, void *src, uint64 len);
int either_copyin(void *dst, int user_src, uint64 src, uint64 len);
void procdump(void);
uint64 cal_not_unused_proc(void);
代码:
// kernel/proc.c
struct proc proc[NPROC]; // 进程数组
// 获取不是UNUSED的进程数量
uint64 cal_not_unused_proc(void) {
uint64 res = 0;
for (int i = 0; i < NPROC; ++i) { // 遍历进程数组进行统计即可
if (proc[i].state != UNUSED)
++res;
}
return res;
}
实现sysinfo
系统调用:
// kernel/sysproc.c
uint64 sys_sysinfo(void) {
uint64 addr; // 从用户态读入一个地址,存放 sysinfo 结构
if (argaddr(0, &addr) < 0)
return -1;
struct sysinfo info;
info.freemem = cal_free_mem();
info.nproc = cal_not_unused_proc();
// 将sysinfo结构体从内核空间拷贝到用户空间
if (copyout(myproc()->pagetable, addr, (char* )&info, sizeof(info)) < 0)
return -1;
return 0;
}
测试:
总体测试: