#Head BLog
本人掘金的专栏文章链接,欢迎阅读!
这是本人的 CSDN 博客之分类专栏链接,欢迎点击阅读!
#Source
#My Code
#Motivation
Lab2: system calls 主要是想让我们给 xv6 添加几个新的系统调用,在添加的过程中可以学习到一些 kernel 与 user 的交互规则
在开始实验之前,一定要阅读 xv6-6.S081 的第二章节 Operating system organization 及第四章节 Traps and device drivers 的第三小节 Traps from user space 和第四小节 Timer interrupts
#System call tracing (moderate)
#Motivation
主要是为了跟踪 shell 命令是否调用了指定的系统调用,并且还需知道系统调用的返回值。比如,
trace 32 grep hello README
跟踪 ,
grep hello README
因为 ,5 对应 SYS_read
,所以只关心(跟踪)第 5 个系统调用,具体案例可见 Lab2: system calls 实验主页,
#define SYS_read 5
其中第二个案例,
trace 2147483647 grep hello README
为什么 2147483647 可以跟踪所有系统调用,没搞明白,这或许并不重要,重要的是熟悉系统调用的流程!
#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
#Result
可以通过,
./grade-lab-syscall trace
来验证程序是否正确
#Sysinfo (moderate)
#Motivation
主要是为了记录下 xv6 当前的系统信息,包括正在使用的进程数和空闲的内存块数(以 byte 为单位)
#Solution
#S0 - 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
#S1 - 统计进程数和空闲内存块
完成配置,能够顺利编译之后正式进入具体业务逻辑实现环节
首先解决统计 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
中
#Result
手动进入 qemu,
make qemu
$sysinfotest
或运行脚本,
./grade-lab-syscall sysinfo