#Lab2: system calls
I. Source
II. My Code
III. Motivation
Lab2: system calls 主要是想让我们给 xv6 添加几个新的系统调用,在添加的过程中可以学习到一些 kernel 与 user 的交互规则
在开始实验之前,一定要阅读 xv6-6.S081 的第二章节 Operating system organization 及第四章节 Traps and device drivers 的第三小节 Traps from user space 和第四小节 Timer interrupts
IV. System call tracing (moderate)
i. Motivation
主要是为了跟踪 shell 命令是否调用了指定的系统调用,并且还需知道系统调用的返回值。比如,
trace 32 grep hello README
跟踪 ,
grep hello README
因为
32
=
2
5
32=2^5
32=25,5 对应 SYS_read
,所以只关心(跟踪)第 5 个系统调用,具体案例可见 Lab2: system calls 实验主页,
#define SYS_read 5
其中第二个案例,
trace 2147483647 grep hello README
为什么 2147483647 可以跟踪所有系统调用,没搞明白,这或许并不重要,重要的是熟悉系统调用的流程!
ii. Solution
S1 - 添加 sys_trace
首先需要了解 xv6 代码的组织结构,kernel/syscall.h
下保存着所有的系统调用编号,需要为 trace
系统调用添加新的编号,
#define SYS_trace 22
在 kernel/syscall.c
特别需要注意的是,函数指针数组 static uint64 (*syscalls[])(void)
,它将所有系统调用全部序列化,便于 syscall(void)
调用
在添加新的系统调用 trace
时,需要在 kernel/syscall.c
中添加额外的声明,
extern uint64 sys_trace(void);
接着在 kernel/syspro.c
中添加 sys_trace(void)
的具体实现,
uint64
sys_trace(void)
{
int mask = 1;
if(argint(0, &mask) < 0) {
return -1;
}
myproc()->mask = mask;
return 0;
}
其中需要注意 argint(0, &mask)
函数其实就是读取进程地址空间中 trapframe
的 0 号寄存器的值,argint(int, int*)
和 argraw(int)
的具体实现见 kernel/syscall.c
最后需要在 user/user.h
中添加新的系统调用的声明,trace
的调用接口见 user/trace.c
,
int trace(int);
以及在 user/usys.pl
中添加 Perl 命令,
entry("trace");
在 Makefile
的 UPROGS
选项中添加,
$U/_trace
完成上述添加任务之后,才能编译成功
S2 - 设计 trace 系统调用
无编译问题后开始设计 trace
的算法,首先需要跳转到 user/trace.c
中明确 trace
是如何被调用的。当程序运行到 user/trace.c
的第 17 行时,
if (trace(atoi(argv[1])) < 0) {
进程已从用户态进入到内核态了,在状态切换的过程中,进程将 trace
对应的 SYS_trace
系统编号写入进程地址空间的 trapframe
的 7 号寄存器中,具体见 user/usys.pl
,
sub entry {
my $name = shift;
print ".global $name\n";
print "${name}:\n";
print " li a7, SYS_${name}\n";
print " ecall\n";
print " ret\n";
}
翻译一下,第 1 行 ,
sub entry
来到 user 转 kernel 的起始点,之后的几句话都是为状态转换做准备,尤其,
print " li a7, SYS_${name}\n";
将系统调用编号写入 trapframe
的 a7 寄存器中,然后调用 ecall
指令进入内核
需要注意一个细节,trace
接口是有函数参数和返回值的,而 syscall
是无参无返的。我大胆猜测,trace
算法的大致流程可能如下,
int trace(int mask)
{
uint64 x; /** mask = 2^x */
p->trapframe->a7 = x;
p->trapframe->a0 = mask;
syscall();
return 1;
}
因为该函数是不接受参数的,所以我们只能通过地址空间传递参数,即将系统调用的编号写入寄存器中。如此,在执行 syscall(void)
时,通过,
num = p->trapframe->a7;
就能获取用户态的需求(调用哪个系统调用)。根据 trace
的算法目的, syscall()
中调用了 sys_trace()
,修改后的代码如下,
void
syscall(void)
{
int num;
struct proc *p = myproc();
/** 获取系统调用编号,具体见user/usys.pl的line13 */
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
/** 进程的trapframe的a0寄存器存放系统调用的返回值 */
p->trapframe->a0 = syscalls[num]();
/** 输出我所关心的系统调用元数据 */
if((1<<num) & p->mask) {
printf("%d: syscall %s -> %d\n", p->pid, syscalls2strs[num], p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
另外,还需在 fork()
的具体实现中添加 mask
标记位,指明哪个系统调用是我所关心的,
int
fork(void)
{
...
np->state = RUNNABLE;
/** 添加mask标记位 */
np->mask = p->mask;
release(&np->lock);
...
}
必须将 mask
标记位告诉进程控制块(PCB),因为用户输入 trace
命令之后,xv6 会 fork 一个新的进程,让新进程接管跟踪事宜。新进程必须记录 mask
标记位(写入地址空间),只有这样,后续才有可能将标记位传入 kernel
iii. Result
可以通过,
./grade-lab-syscall trace
来验证程序是否正确
V. Sysinfo (moderate)
i. Motivation
主要是为了记录下 xv6 当前的系统信息,包括正在使用的进程数和空闲的内存块数(以 byte 为单位)
ii. Solution
S1 - Makefile 等初始化配置
和 Lab: System call tracing 一样,在实现具体业务逻辑之前需要配置环境,根据 Lab2: system calls 给的提示进行设置
首先在 Makefile
文件的 UPROGS
字段中追加,
$U/_sysinfotest
然后在 user/user.h
中添加 sysinfo
函数声明,
struct sysinfo;
int sysinfo(struct sysinfo *);
同时还要在 kernel/syscall.h
和 kernel/syscall.c
中添加声明和定义(见 Lab: System call tracing )以及追加 user/usys.pl
S2 - 统计进程数和空闲内存块
完成配置,能够顺利编译之后正式进入具体业务逻辑实现环节
首先解决统计 xv6 正在使用的进程数问题,具体表现为遍历整个 proc
数组(添加在 kernel/proc.c
中) ,对其中的每个进程的状态进行判断和累计,代码如下,
uint64
nProcs()
{
uint64 nums = 0;
int i;
for(i=0; i<NPROC; i++) {
struct proc* p = &proc[i];
acquire(&p->lock);
if(p->state != UNUSED)
nums++;
release(&p->lock);
}
return nums;
}
接着解决统计 xv6 空闲的内存块数问题,xv6 有一个空闲链表,具体定义如下,
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
具体表现为顺着 freelist 遍历完所有空闲节点,即可完成统计工作。需要注意的是在操作 kmem
时需要加锁,具体代码如下,
uint64
nFreeMems(void)
{
uint64 nums = 0;
struct run* ptr = kmem.freelist;
struct spinlock* lock = &kmem.lock;
acquire(lock);
while(ptr) {
nums++;
ptr = ptr->next;
}
release(lock);
return nums*PGSIZE;
}
最后还需在 kernel/defs.h
中添加 nProcs()
和 nFreeMems()
函数声明
在完成统计的具体任务之后我们需要将其组合起来,在 kernel/sysproc.c
中编写 sys_sysinfo
系统调用,具体代码如下,
uint64
sys_sysinfo(void)
{
struct proc* proc = myproc();
struct sysinfo sysinfo;
uint64 addr;
if(argaddr(0, &addr) != SYS_OK)
return SYS_ERROR;
sysinfo.nproc = nProcs();
sysinfo.freemem = nFreeMems();
if(copyout(proc->pagetable, addr, (char*)&sysinfo, sizeof(sysinfo)) != SYS_OK)
return SYS_ERROR;
return SYS_OK;
}
首先获取 proc
进程,然后抓取正在使用的进程数和空闲块数,将结果写入 sysinfo
结构体中,最后通过 copyout()
函数将获取的参数从 kernel 传回 user 。其中 copyout()
的大致功能就是将 sysinfo
结构体写入 pagetable
的指定偏移 addr
中
iii. Result
手动进入 qemu,
make qemu
$sysinfotest
或运行脚本,
./grade-lab-syscall sysinfo
qemu 退出命令,
ctrl + a ... x