上周打了一场数模国赛,打前元气超人,打完葛优躺平。Lab2只有两个小实验,聚焦于如何创建系统调用命令,也并不是那么简单,实验文档戳这。
❖ Coding
☑︎ System call tracing (moderate)
我们需要创建一个trace系统调用来追踪一个命令中指定的系统调用,并打印出相应的返回值。
$ trace 2147483647 grep hello README
4: syscall trace -> 0
4: syscall exec -> 3
4: syscall open -> 3
4: syscall read -> 1023
4: syscall read -> 966
4: syscall read -> 70
4: syscall read -> 0
4: syscall close -> 0
指令格式:trace mask cmd
。mask用来指定需要追踪的系统调用,在操作系统中每个系统调用都会有自己唯一的编号SYS_syscall(在kernel/syscall.h中的宏定义),比如说fork的编号就是SYS_fork,如果mask&(1<<SYS_syscall)!=0
就说明需要追踪该syscall,需要打印出其返回值,本质上如果假设
代表其二进制低
位数字的话,那么
就代表需要追踪SYS_syscall=
的系统调用。
那么我们就需要在进程结构体proc中加入int mask
,注意到一个进程中执行不同系统调用是通过fork来实现的,本质上又创建了一个proc实体,因此在fork的实现(kernel/proc.c)中我们需要实现mask的传递:
np->mask = p->mask;
那么接下来就可以打印信息pid: syscall name -> return_value
。我们可以在每个系统调用都必经的函数syscall(kernel/syscall.c)里面实现,首先观察syscall函数:
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]();
}
else
{
printf("%d %s: unknown sys call %d\n",p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
可以看出p里面存储了我们需要的pid,但p->name并不是我们想要的系统调用名称,注意到fork实现中(kernel/proc.c:304)是直接拷贝原proc的name,即name里存储的其实是进程名:
safestrcpy(np->name, p->name, sizeof(p->name));
这里需要注意进程与线程之间的差别。虽然fork完后也是一个proc结构体,但事实上只是p->name进程下的一个线程而已。进程是对运行程序的封装,是系统进行资源调度和分配的基本单位,各进程之间相互隔离不共享数据,实现了操作系统的并发性;而线程则是一个进程中的子任务,是CPU调度的最小单位,一个进程中的所有线程共享该进程的数据资源。那么每个命令就是一个进程,里面会执行多个系统调用,每个系统调用都是一个线程。
虽然p里面没有线程名称的直接信息,但肯定有其他相关信息。注意到
p->trapframe->a0 = syscalls[num]();
这里调用了syscalls[num]
,然后将返回值传递给了a0寄存器(RISC-V的C规范是把返回值放在a0中)。syscalls是一个无符号整型指针数组,即函数指针。那么可以肯定这个num将是一个重要的突破口,我们只要按照syscalls中系统调用的顺序定义一个字符串数组sysname,sysname[num]
就是系统调用的名称了。同时p->trapframe->a0
就是函数返回值。
107 + │ extern uint64 sys_trace(void);
132 + │ [SYS_trace] sys_trace, //static uint64 (*syscalls[])(void)
136 + │ char *sysname[] = {
137 + │ [SYS_fork] "fork",
138 + │ [SYS_exit] "exit",
139 + │ [SYS_wait] "wait",
140 + │ [SYS_pipe] "pipe",
141 + │ [SYS_read] "read",
142 + │ [SYS_kill] "kill",
143 + │ [SYS_exec] "exec",
144 + │ [SYS_fstat] "stat",
145 + │ [SYS_chdir] "chdir",
146 + │ [SYS_dup] "dup",
147 + │ [SYS_getpid] "getpid",
148 + │ [SYS_sbrk] "sbrk",
149 + │ [SYS_sleep] "sleep",
150 + │ [SYS_uptime] "uptime",
151 + │ [SYS_open] "open",
152 + │ [SYS_write] "write",
153 + │ [SYS_mknod] "mknod",
154 + │ [SYS_unlink] "unlink",
155 + │ [SYS_link] "link",
156 + │ [SYS_mkdir] "mkdir",
157 + │ [SYS_close] "close",
158 + │ [SYS_trace] "trace",
159 │ };
160 │
161 │ void
162 │ syscall(void)
163 │ {
164 │ int num;
165 │ struct proc *p = myproc();
166 │
167 │ num = p->trapframe->a7;
168 │ if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
169 │ p->trapframe->a0 = syscalls[num]();
170 + │ if(1<<num & p->mask)
171 + │ printf("%d: syscall %s -> %d\n",p->pid,sysname[num],p->trapframe->a0);
172 │ } else {
173 │ printf("%d %s: unknown sys call %d\n",
174 │ p->pid, p->name, num);
175 │ p->trapframe->a0 = -1;
176 │ }
177 │ }
o对,我们甚至还没实现trace函数💦。你可能会觉得上面的做法已经间接实现了trace,但事实上trace是在用户模式(user mode)调用的,因此传入的参数还在用户空间,而系统调用都是在内核空间内执行的,为了操作系统不受进程影响两者是分隔的。
用户态的trace函数已经帮我们实现:
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
int i;
char *nargv[MAXARG];
if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){
fprintf(2, "Usage: %s mask command\n", argv[0]);
exit(1);
}
if (trace(atoi(argv[1])) < 0) {
fprintf(2, "%s: trace failed\n", argv[0]);
exit(1);
}
for(i = 2; i < argc && i < MAXARG; i++){
nargv[i-2] = argv[i];
}
exec(nargv[0], nargv);
exit(0);
}
不难看出关键函数是int trace(int)
,那我们需要先在user/user.h中声明该系统调用,同时在user/usys.pl(生成汇编文件user/usys.S)中加入entry("trace")
使trace函数可以进入内核模式,并在kernel/syscall.h
中添加一个系统调用号,整个系统调用过程如下所示
:
user/user.h:用户态程序调用跳板函数 trace()
user/usys.S:跳板函数 trace 使用 CPU 提供的 ecall 指令,进入内核模式
kernel/syscall.c:到达内核模式统一执行系统调用处理函数syscall,所有系统调用都会跳到这里来处理
kernel/syscall.c:syscall根据跳板传进来的系统调用编号,查询 syscalls[] 表,找到对应的内核函数并调用
kernel/sysproc.c:到达 sys_trace 函数,执行具体内核操作
接下来只需在kernel/sysproc.c中实现sys_trace,该函数只要把用户空间的mask拿进来就行,cmd在执行时事实上可以认为是trace的多个子线程,因此mask会一直传递下去。
uint64
sys_trace(void)
{
int mask;
if(argint(0, &mask) < 0)
return -1;
myproc()->mask=mask;
return 0;
}
☑︎ Sysinfo (moderate)
实现一个系统调用sysinfo,它接受一个指向struct sysinfo的指针,然后向这个结构体写入剩余空间的字节数和状态非”UNUSED”的进程数。
───────┬─────────────────────────────────────────────────────────
│ File: kernel/sysinfo.h
───────┼─────────────────────────────────────────────────────────
1 │ struct sysinfo {
2 │ uint64 freemem; // amount of free memory (bytes)
3 │ uint64 nproc; // number of process
4 │ };
───────┴─────────────────────────────────────────────────────────
首先我们需要实现统计剩余空间字节数和已使用进程数的函数。关于空间分配的实现在kernel/kalloc.c,注意到kmem中有一项freelist链表,然后看kfree函数
46 │ void
47 │ kfree(void *pa)
48 │ {
49 │ struct run *r;
50 │
51 │ if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
52 │ panic("kfree");
53 │
54 │ // Fill with junk to catch dangling refs.
55 │ memset(pa, 1, PGSIZE);
56 │
57 │ r = (struct run*)pa;
58 │
59 │ acquire(&kmem.lock);
60 │ r->next = kmem.freelist;
61 │ kmem.freelist = r;
62 │ release(&kmem.lock);
63 │ }
kfree函数将pa地址开始PGSIZE大小的空间都初始化为1,然后将pa挂到freelist的头上,再将freelist指向pa,而kalloc函数则是每次都使用freelist的第一块PGSIZE空间。说明freelist是可用空间的链表,而且每次分配的最小单元是PGSIZE字节,那么我们只要数一下freelist的元素个数然后乘个PGSIZE即可。
uint64
count_freemem(void)
{
acquire(&kmem.lock);
struct run *r = kmem.freelist;
uint64 cnt = 0;
while (r)
{
++cnt;
r = r->next;
}
release(&kmem.lock);
return cnt * PGSIZE;
}
为防止竞态条件产生,在统计时需要先给kmem上锁。
对于统计状态为非”UNUSED”的进程数也不难,注意到kernel/proc.c中
11 │ struct proc proc[NPROC];
可知最大进程数即为NPROC,那么我们只要遍历proc,然后依次检查状态即可。
uint64
count_process(void)
{
uint64 cnt = 0;
for (int i = 0; i < NPROC; ++i)
if (proc[i].state != UNUSED)
++cnt;
return cnt;
}
最后我们只要将这个sysinfo结构体拷贝至用户空间即可。首先通过argaddr函数获取用户态int sysinfo(struct sysinfo *)
传入的sysinfo虚地址,然后结合该进程的页表pagetable可以得到该地址对应的物理地址,最后将内核中的sysinfo结构体拷贝过去。
uint64
sys_sysinfo(void)
{
struct proc *p = myproc();
struct sysinfo sf;
uint64 addr;
sf.freemem = count_freemem();
sf.nproc = count_process();
if (argaddr(0, &addr) < 0 || copyout(p->pagetable, addr, (char *)&sf, sizeof(sf)) < 0)
return -1;
return 0;
}
argaddr第一个参数0是指从a0寄存器获取,我特地回去翻了一下计组PPT
果然,a0、a1存储函数参数,as I guess。
其余声明之类的与上面的trace类似。另外别忘了在kernel/sysproc.c中加入
9 + │ #include "kernel/sysinfo.h"
如果一切正常,在xv6启动后输入sysinfotest会输出OK。
$ sysinfotest
sysinfotest: start
sysinfotest: OK