写一个系统调用追踪器,作用是对于指定的系统调用号,对进程号,系统调用名称,返回值进行打印。
syscall函数,此函数从进程的trapframe取得将要进行调用的系统调用号num,因此思路就是在进行系统调用后,若num是否在进程指定的调用号中,则将以上信息进行打印。mask指定系统调用号集合,并且还需要在fork出子进程时,让子进程继承此属性。//kernel/proc.h
struct proc {
int pid; // Process ID
....
// mask that expresses which syscalls will be traced.
int mask;
};
//kernel/proc.c
int
fork(void){
.....
np->mask = p->mask;
}
// kernel/sysproc.c
uint64
sys_trace(void){
int mask;
if(argint(0, &mask) < 0)
return -1;
myproc()->mask = mask;
return 0;
}
根据Lab的介绍,系统调用的流程如下:
/user/user.h中添加系统调用的方法int trace(int);,以供给用户调用user/usys.pl脚本类似其他系统调用添加entry("trace");//user/usys.pl文件
#!/usr/bin/perl -w
print "# generated by usys.pl - do not edit\n";
print "#include \"kernel/syscall.h\"\n";
sub entry {
my $name = shift;
print ".global $name\n";
print "${name}:\n";
print " li a7, SYS_${name}\n";
print " ecall\n";
print " ret\n";
}
entry("fork");
...
entry("trace");
Makefile会执行此脚本,生成user/usys.S//Makefile文件
$U/usys.S : $U/usys.pl
perl $U/usys.pl > $U/usys.S
user/usys.S汇编代码中,用户调用trace,实际上会通过ecall跳转到真正的系统调用,具体的实现很简单,就是为1个a7寄存器赋值一个在头文件define好的系统调用号。# generated by usys.pl - do not edit
#include "kernel/syscall.h"
.....
.global trace
trace:
li a7, SYS_trace
ecall
ret
user/usys.S中,include了"kernel/syscall.h"的头文件,此头文件只有一些define值,因此以上汇编句,其实等价于li a7, 22,即a7 = 22,进行了一下寄存器赋值// System call numbers
...
#define SYS_close 21
#define SYS_trace 22
kernel/syscall.c中的一个系统调用函数数组的下标,在系统调用时,进程取到寄存器a7的值作为系统调用函数数组的下标,至此终于真的调用到了这个函数,当然,最后我们还需要在kernel/sysproc.c中实现对应的函数。//kernel/syscall.c
........
extern uint64 sys_trace(void);
extern uint64 sys_sysinfo(void);
static uint64 (*syscalls[])(void) = {
.........
[SYS_trace] sys_trace,
[SYS_sysinfo] sys_sysinfo,
};
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->mask >> num) & 1)
printf("%d: syscall %s -> %d\n", p->pid, sys_call_name[num], p->trapframe->a0);
}
新增一个系统调用Sysinfo,传入的参数为以下结构体的指针,内核会将结构体填满:freemem表示空闲内存的字节数,nproc表示状态为UNUSED的进程数。
struct sysinfo {
uint64 freemem; // amount of free memory (bytes)
uint64 nproc; // number of process
};
提示1:sysinfo需要将数据复制到用户态,需要使用copy_out函数,可参考sys_fstat() (kernel/sysfile.c) 和 filestat() (kernel/file.c)函数。
提示2:在kernel/kalloc.c中新增统计空闲内存的函数,在kernel/proc.c中新增统计空闲进程的函数。
主要是定义了一个空闲链表用来管空闲内存,包括初始化,内存回收,内存分配,以下是具体函数功能介绍,非常非常简单。
首先是定义了1个run的极简链表,用来串起空闲内存,其次是kmem结构体,用来放1个锁,还有空闲内存链表的表头。
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
kinit 此函数用于空闲链表初始化
先使用initlock初始化锁,然后使用freerange指定初始化的内存范围是从end到PHYSTOP。
extern char end[]; // first address after kernel.
// defined by kernel.ld.
extern char end[]; // first address after kernel.
// defined by kernel.ld.
void
kinit()
{
initlock(&kmem.lock, "kmem");
freerange(end, (void*)PHYSTOP);
}
freerange 此函数用于将连续长度的内存空间释放掉
释放的方法是使用kfree逐页(一页PGSIZE为4096B)将首地址插入到空闲链表中
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
// 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;
}
sysinfo,并获取了kmem.freelistmalloc,并触发了kalloc,若未加锁,则kalloc将链表头部的节点kmem.freelist分配给进程Brun链表的next指针尝试去获取下一个节点,但kmem.freelist已经给进程B在使用,此地址不再存放之前的next地址,而是进程B的私有数据,不应该再在此时访问,这样会导致数据泄露,破坏内存的安全性,并且next在此语义下仍是一个地址,可能会出现访问越界等情况。uint64 get_freemem(){
uint64 freemem = 0;
acquire(&kmem.lock);
struct run *p= kmem.freelist;
while(p){
freemem += PGSIZE;
p = p->next;
}
release(&kmem.lock);
return freemem;
}
因为这个文件的体量较为庞大,后续还有和进程管理更强相关的如Lab thread: Multithreading实现多线程,Lab lock: Parallelism/locking实现并行和进程锁,因此暂不做详细解析。
目前任务是统计空闲进程的数量。
采用的是最简单的遍历思路,在文件中定义了进程结构体数组struct proc proc[NPROC];直接遍历此进程数组,获取进程状态state为UNUSED的数量。参考已有代码中遍历进程数组的某个函数procdump:利用for(p = proc; p < &proc[NPROC]; p++)遍历进程数组,利用p->state判断进程状态。
//Get the num of processes that are not in UNUSED state
int
not_UNUSED_process_num(void){
int count = 0;
for(int i = 0; i < NPROC; i ++)
count += (proc[i].state != UNUSED);
return count;
}
提示1:sysinfo需要将数据复制到用户态,因此需要使用copy_out函数,可参考sys_fstat() (kernel/sysfile.c) 和 filestat() (kernel/file.c)函数。sys_fstat() (kernel/sysfile.c) 函数如下:
// kernel/sysfile.c
uint64
sys_fstat(void)
{
struct file *f;
uint64 st; // user pointer to struct stat
if(argfd(0, 0, &f) < 0 || argaddr(1, &st) < 0)
return -1;
return filestat(f, st);
}
主要完成的是2个工作:
此系统调用有2个参数,参数1为struct file *类型,参数2为uint64类型,但参数不是直接传递的,而是需要通过argxx去读取trapframe寄存器,因为在用户调用参数前,会将参数按顺序存放到trapframe的a0到a6寄存器。
例如static int argfd(int n, int *pfd, struct file **pf)用于读取用户传进来的fd,并从进程的打开文件列表ofile中,根据fd索引到其指向的文件结构体file。
// Fetch the nth word-sized system call argument as a file descriptor
// and return both the descriptor and the corresponding struct file.
static int
argfd(int n, int *pfd, struct file **pf)
{
int fd;
struct file *f;
if(argint(n, &fd) < 0)
return -1;
if(fd < 0 || fd >= NOFILE || (f=myproc()->ofile[fd]) == 0)
return -1;
if(pfd)
*pfd = fd;
if(pf)
*pf = f;
return 0;
}
static uint64
argraw(int n)
{
struct proc *p = myproc();
switch (n) {
case 0:
return p->trapframe->a0;
case 1:
return p->trapframe->a1;
case 2:
return p->trapframe->a2;
case 3:
return p->trapframe->a3;
case 4:
return p->trapframe->a4;
case 5:
return p->trapframe->a5;
}
panic("argraw");
return -1;
}
参考filestat() (kernel/file.c),此处使用copyout函数将数据从内核态复制到用户态。copyout函数原型如下,它从src开始,复制len字节到虚拟地址dstva中。
int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
struct proc *p = myproc();
struct stat st;
if(f->type == FD_INODE || f->type == FD_DEVICE){
ilock(f->ip);
stati(f->ip, &st);
iunlock(f->ip);
if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
return -1;
return 0;
}
return -1;
}
sysinfo结构体的指针,因此先使用argaddr获取此指针copyout将获取的信息传递到用户态。uint64
sys_sysinfo(void)
{
uint64 p;
if(argaddr(0, &p) < 0)
return -1;
struct sysinfo sp;
sp.nproc = not_UNUSED_process_num();
sp.freemem = get_freemem();
if(copyout(myproc()->pagetable, p, (char *)(&sp), sizeof(sp)) < 0)
return -1;
return 0;
}
通过这个实验,我了解了:
copyout函数,利用用户态进程的页表来进行虚拟地址到物理地址的寻址,再直接将数据拷贝到用户态进程的虚拟地址映射的物理地址上。