文章目录
- 1 System call tracing
- 1.1 主要思路
- 1.2 系统调用流程
- 2 Sysinfo
- 2.1 kernel/kalloc.c 此文件用于实现分配物理空间的函数
- 2.1.1 结构体定义
- 2.1.2 空闲链表初始化
- 2.1.3 内存的分配和释放
- 2.1.4 获取空闲内存字节数
- 2.2 kernel/proc.c 此文件用于进程管理
- 2.3 sys_Sysinfo
- 2.3.1读取用户传递的参数
- 2.3.2 数据复制到用户态
- sys_sysinfo具体实现
- 3 总结
实验链接
1 System call tracing
写一个系统调用追踪器,作用是对于指定的系统调用号,对进程号,系统调用名称,返回值进行打印。
1.1 主要思路
- 每个系统调用都需要经过
syscall
函数,此函数从进程的trapframe
取得将要进行调用的系统调用号num
,因此思路就是在进行系统调用后,若num
是否在进程指定的调用号中,则将以上信息进行打印。 - 完成trace系统调用:在1.中需要知道进程要追踪的指定系统调用号是什么,因此trace系统调用就是来指定系统调用号的,因此对于每个进程,为进程结构体中新增属性
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; }
1.2 系统调用流程
根据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
- 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); }
2 Sysinfo
新增一个系统调用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
中新增统计空闲进程的函数。
2.1 kernel/kalloc.c 此文件用于实现分配物理空间的函数
主要是定义了一个空闲链表用来管空闲内存,包括初始化,内存回收,内存分配,以下是具体函数功能介绍,非常非常简单。
2.1.1 结构体定义
首先是定义了1个run
的极简链表,用来串起空闲内存,其次是kmem
结构体,用来放1个锁,还有空闲内存链表的表头。
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
2.1.2 空闲链表初始化
-
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); }
2.1.3 内存的分配和释放
- kfree 此函数将1个物理页的首地址存放到空闲链表中
实现上:- 先判断地址是否越界
- 不越界则先将该页填满数据垃圾,避免数据泄露
- 进行隐式链接:直接在该地址创建1个空闲链表节点,采用头插的方法将该节点作为新的空闲链表节点。这种链接方法为隐式链接,即直接在空闲的地址创建节点,这样的好处是空闲节点不用额外占据空间,可节约空间;坏处是隐式链接的稳定性较差,若某个链接断开或错漏,则整个链表失效。注意在链接前需要获得锁,在链接后释放锁,因为空闲链表是临界资源,同一时间只能由1个进程访问。
// 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); }
- kalloc 此函数将空闲链表中的一页分配出去
// 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; }
2.1.4 获取空闲内存字节数
- 自实现get_freemem,用于返回当前内存空闲字节数
根据以上分析,显然内存空闲字节数就是空闲链表的节点数量乘上每个物理页的大小,注意在访问空闲链表的时候需要加锁,访问结束后释放锁,因为可能出现以下场景:- 进程A调用
sysinfo
,并获取了kmem.freelist
- 进程B调用
malloc
,并触发了kalloc
,若未加锁,则kalloc
将链表头部的节点kmem.freelist
分配给进程B - 进程A尝试通过
run
链表的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; }
- 进程A调用
2.2 kernel/proc.c 此文件用于进程管理
因为这个文件的体量较为庞大,后续还有和进程管理更强相关的如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;
}
2.3 sys_Sysinfo
提示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.3.1读取用户传递的参数
此系统调用有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;
}
2.3.2 数据复制到用户态
参考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;
}
sys_sysinfo具体实现
- 读取用户传递参数:用户传递进来的是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;
}
3 总结
通过这个实验,我了解了:
- 系统调用的流程,但还不了解具体是如何从用户态陷入内核态(TODO)
- xv6使用隐式链表数据结构来进行空闲内存的分配和回收,同时加锁防止多个进程使用此临界资源
- 进程管理是是1个进程结构体数组维护,可通过遍历获取每个进程的状态
- 内核态要将数据返回给用户态,因为用户态有自己的进程空间,使用的虚拟地址无法直接在内核态解引用,所以在内核态需要实现
copyout
函数,利用用户态进程的页表来进行虚拟地址到物理地址的寻址,再直接将数据拷贝到用户态进程的虚拟地址映射的物理地址上。