Lab2 System calls
向xv6
添加更多的系统调用,加深对xv6
内核的理解!
$ git fetch
$ git checkout syscall
$ make clean
System call tracing (moderate)
在xv6
中每一个系统调用都存在一个mask
值与之对应,而我们需要实现的功能就是追踪被调用的系统调用的mask
值。
在实现该功能之前,我们要了解如何创建一个系统调用。
如何创建一个系统调用
- 在内核中寻找合适的位置,推荐放在
sysproc.c
中,因为我们要实现的系统调用要对进程进行操作,这是是trace
操作,所以放在这里。
uint64
sys_trace(void)
{
int mask;
if (argint(0, &mask) < 0)
{
return -1;
}
struct proc *p = myproc();
p->trace_mask = mask;
return 0;
}
2.在syscall.h
为SYS_trace
创建新的系统调用号。
// 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 // 我在这儿 !!!
3.在syscall.c
中使用extern
全局声明新的内核调用函数,并在syscalls
表中加入新的系统调用。
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语言中数组的表示语法,不过在C++中已经不再使用
4.在usys.pl
中,加入从用户态到内核态的跳板函数。这玩意好像是会生成一个usys.S
的一个汇编文件,调用了ecall
指令,然后ecall
指令才去调用系统调用,此时会跳到内核态的统一系统调用处理函数syscall()
,从而实现系统调用。
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"); // 我在这儿 !!!
5.在用户态的头文件user.h
中加入声明,从而使得用户态程序能找到这个跳板入口函数,也就是trace()
。
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.pl 跳板函数trace() 使用CPU指令ecall,跳转到内核态
kernel/syscall.c 到达内核态的统一系统调用处理函数syscall()进行处理
kernel/syscall.c syscall()会根据传入的系统调用号(在syscall.h中创建),查询syscall[]表,找到对于的内核处理函数并调用
kernel/sysproc.c 到达sys_trace()函数,执行具体操作
这样复杂的调用机制主要目的是实现用户态和内核态的良好隔离。
可这样复杂的操作势必会带来性能的损耗,那为什么要使用如此复杂的一个调用机制呢?
这个问题的答案与之后学习的页表知识相关,这是由于内核和用户进程所使用的页表不同,寄存器也不互通,所以说参数是无法通过函数参数传参的形式从用户态直接传到内核态的,而是需要使用一些特定的函数实现这样的功能,即通过argaddr
,arigint
,argstr
等函数,从进程的trapframe
中读取用户进程寄存器中的参数内容。与此同时,由于页表的不同,指针也是不能直接互通访问的,通俗点说就是:内核不能直接对用户态传入的指针进行解引用操作,而是需要使用copyin
,copyout
的方法结合进程的页表,才能顺利找到用户态的指针所对于的物理内存地址。
该Lab的代码实现
首先在proc.h
中修改结构体proc
的定义,在其中加入trace_mask
用于记录系统调用的编号。
// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
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
// wait_lock must be held when using this:
struct proc *parent; // Parent process
// 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)
int trace_mask; // 我在这儿 !!!
};
其次在proc.c
中的allocproc()
,对trace_mask
进行默认的初始化行为,即赋值0,防止垃圾数据的干扰。
static struct proc *
allocproc(void)
{
......
// 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; // 我在这儿 !!!
return p;
}
然后在sysproc.c
中,加入实现系统调用的具体代码,也就是设置当前进程的syscall_trace mask
。
uint64
sys_trace(void)
{
int mask;
if (argint(0, &mask) < 0)
{
return -1;
}
struct proc *p = myproc();
p->trace_mask = mask;
return 0;
}
接着修改fork
函数,使得子进程可以继承到父进程的syscall_trace mask
.
// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int fork(void)
{
......
safestrcpy(np->name, p->name, sizeof(p->name));
pid = np->pid;
np->trace_mask = p->trace_mask; // 我在这儿 !!!
......
return pid;
}
最后根据上文提到的系统调用的全过程,可知,所有的系统调用到达内核态后,都会进入一个统一的系统调用处理函数syscall()
,所以说我只需要在这个函数中买点就搞定了。
// 这个syscall_name是一个字符串数组,用于映射系统调用的名称
static char *syscall_name[] = {"fork", "exit", "wait", "pipe", "read",
"kill", "exec", "fstat", "chdir", "dup",
"getpid", "sbrk","sleep", "uptime", "open",
"write", "mknod", "unlink", "mkdir", "close", "trace"};
void syscall(void)
{
int num;
struct proc *p = myproc(); // 获取当前进程
num = p->trapframe->a7;
if (num > 0 && num < NELEM(syscalls) && syscalls[num]) // 判断系统调用号是否有效
{
int trace_mask = p->trace_mask;
p->trapframe->a0 = syscalls[num](); // 通过系统调用编号,获取系统调用处理函数的指针,调用并将返回值存到用户进程的 a0 寄存器中
if ((trace_mask >> num) & 1) // 按需要打印
{
printf("%d: syscall %s -> %d\n", p->pid, syscall_name[num-1],p->trapframe->a0);
}
}
else
{
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
Sysinfo(moderate)
添加一个系统调用,获取空余内存和已经创建的进程数量,多数步骤和上述实验相同,这里不做过多赘述,在实现该系统调用的时候,我们需要把sysinfo
这个结构体从内核内存拷贝到用户内存中,即如何实现获取空余内存和如何获取已经创建的进程数量。
获取空余内存
1.首先在sysproc.c
中声明函数。
#include "types.h"
......
uint64 get_freemem(); // 我在这儿 !!!
2.在kalloc.c
中添加计算空余内存的函数。
uint64 get_freemem()
{
struct run *r;
uint64 ret = 0;
acquire(&kmem.lock); // 加锁,防止竞争情况出现
r = kmem.freelist; // 获取空余页表
while (r)
{
r = r->next;
ret++; // 统计空余页表数
}
release(&kmem.lock); // 解锁
return ret * PGSIZE; // 空余页表数 * 页表大小 = 空余内存的字节数
}
在xv6
中,空余内存页的存储方式是,将空余页表页本身直接作为链表节点,形成一个空余页的链表,在分配页表时,直接把该链表的根节点所对应的页表分配出去,在回收时,直接把要回收的页作为新的根节点,接在原本的空余页的链表后。既然是把本身当作链表节点,那么也意味着我们无需额外的存储空间去存储空余链表,最直接的表现就是在kalloc()
中,分配内存的最后一个阶段,是直接将freelist
的根节点返回出去。
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
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; // 返回空余页表的根节点,作为内存页去使用
}
获取已经创建的进程数量
1.首先在sysproc.c
中声明函数。
#include "types.h"
......
uint64 get_freemem();
uint64 get_nproc(); // 我在这儿 !!!
2.在proc.c
中添加获取已经创建的进程数量的函数
uint64 get_nproc(void)
{
struct proc *p;
uint64 ret = 0;
for (p = proc; p < &proc[NPROC]; p++)
{
acquire(&p->lock); // 这里应该是可以不加锁的,因为我们只是进行一个读取进程列表的操作,不需要写
if (p->state != UNUSED) // 标志位不是UNUSED的进程位,就是已经分配的
{
ret++;
}
release(&p->lock);
}
return ret;
}
该Lab的代码实现
1.具体添加系统调用的过程上文已经阐述,这里不进行过多赘述。
uint64
sys_sysinfo(void)
{
struct sysinfo info;
uint64 addr;
struct proc *p = myproc();
info.freemem = get_freemem();
info.nproc = get_nproc();
if (argaddr(0, &addr) < 0)
{
return -1;
}
if (copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)
{
return -1;
}
return 0;
}
2.在user.h
中提供用户态入口
// system calls
.....
int trace(int); // 这是Lab1哦 !!!
int sysinfo(struct sysinfo *); // 我在这儿 !!!