实验:Lab: System calls

实验开始之前需要将git分支切换到syscall分支不然有些文件你是没有的

1
2
3
$ git fetch
$ git checkout syscall
$ make clean

1.GDB:

参考链接

我的实验环境为ubuntu20.04,只有gdb-multiarch这个gdb调试工具,建议不要安装riscv64-linux-gdb因为特别难以下载且不好安装。

连接GDB Server

  1. 打开一个terminal,在xv6的文件夹中键入make qemu-gdb打开gdbserver。在课程演示中我们使用了CPUs=1这个选项以一个cpu运行xv6
    image.png

  2. 再打开一terminal,同样在xv6文件夹中,键入gdb-multiarch这个命令就可以链接到gdb-server了,链接成功后如图所示
    image.png

实验

  1. 如上述所说将gdb-server连接完毕

  2. 在syscall.c文件的syscall函数处添加断点,b=break
    2.1

  3. 让程序运行,使用c=continue,当运行到syscall函数处就会发生中断。
    2.2

  4. 使用backtrace查看栈信息,栈顶为syscall(),那么调用syscall的函数就是usertrap函数,问题1
    2.3

  5. 使用layout src,更改gdb布局可以查看源代码位置
    2.8

  6. 使用p /x *p打印proc结构体的指针变量,其中/x是以十六进制打印
    2.4

打印p->trapframe->a7的值是多少?参考initcode.S文件

p->trapframe->a7 = 7,在后续的实验中也会知道这个值实际上是系统调用的编号,等于7说明我们调用了exec系统调用

  1. 同样我们可以打印$+寄存器名称打印寄存器值。

2.5

CPU在此之前是从什么模式进入管理者模式的?riscv-privileged

sstatus:是supervisor mode的状态寄存器,特权寄存器,功能主要是跟踪当前的处理器的操作状态。32位的sstatus寄存器的spp位(第8位):指示硬件线程的在进入supervisor mode之前的特权级。0表示从用户模式陷入(trap),1反之是从机械模式陷入。

sstatus
如上图中得到sstatus中的值为0x22,说明第8位是为0的,则是从用户模式陷入管理者模式

  1. 当我们将 num = p->trapframe->a7; 替换为 num = * (int *) 0;,那么xv6的内核崩溃,如下图所示是kernel.asm打印的sepc(Supervisor Exception Program Counter)寄存器的值可以帮助我们找到内核页错误

2.7

写下内核正在执行的汇编指令。哪个寄存器对应变量num?

stval(Supervisor Trap Value)寄存器保存num变量为0,让内核崩溃汇编指令的地址是sepc保存的。

通过上图得知sepc所报错的地址为0x000000008000207c,那么要跟踪这个错误,就可以使用地址作为断点,并使用c运行程序,再使用layout asm查看断点所对应的汇编代码

1
2
$ gdb-multiarch
(gdb) b *0x000000008000207c

2.10

通过打印汇编指令可以得知(lw-loading word是risv-v加载物理内存的字到寄存器的指令),CPU将s2寄存器的值加载到了物理地址=0处,由于0x0-0x80000000(xv6 图3.3)处是IO设备的地址因此会导致内核崩溃。

scause(Supervisor Cause Register)寄存器,当从一个陷阱进入管理者模式,scause被写入并指示是哪一个事件导致了陷阱。

查看riscv-privileged的图4.1、4.2,其低31位是异常代码,由于在报错中scause=0xd
转为十进制等于13,且没有发生中断,找到对应的描述可知,错误是加载了错误的页表

scause1

scause

请注意,scause是由上面的内核崩溃打印的,但您通常需要查看其他信息来跟踪导致崩溃的问题。例如,为了找出内核崩溃时正在运行的用户进程,可以打印出进程的名称:

内核奔溃时正在运行的二进制文件的名称是什么?它的进程ID(ID)是多少?

2.12

System call tracing (moderate)

系统调用实现

  1. 查看kernel/syscall.h代码,其中定义了每一个系统调用的宏
  2. 查看user/usys.pl的perl代码,这是一个脚本文件主要是生成usys.S这个汇编文件,这个汇编文件中包含了每一个系统调用的接口,因此需要在usys.pl文件中加入entry("trace")用于生成trace系统调用接口,在usys.S会生成如下4行代码。
1
2
3
4
5
.global trace
trace:
li a7, SYS_trace
ecall
ret
  1. 查看kernel/syscall.c代码,在syscall(void)函数中,通过p->trapframe->a7获取系统调用号,接着进行系统调用syscalls[num](),其中syscalls是一个系统调用函数指针数组,系统调用的返回值保存于p->trapframe->a0中。

以上便是系统调用的大致过程了。

实验要求

本次实验我们需要再用户空间中实现一个trace应用,这个应用可以跟踪打印程序中系统调用。trace函数的参数是系统调用syscall_id的掩码(mask),在掩码中将对应的位设为1则trace函数就会打印相应的系统调用。

例如我们调用trace(6),其中6 = 0110,那么就会打印syscall_id=2,3的系统调用,相当于1<<SYS_exit,1<<SYS_wait

trace程序示例

调用用户空间的trace程序,接着是掩码,最后则是跟踪的程序与跟踪调用程序的参数(通过exec系统调用运行的程序),打印的格式为:进程的pid:syscall 系统调用名称 -> 系统调用返回值

1
2
3
4
5
6
$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
$

我们可以调用预先设定好的程序usertests,进行测试,值得注意的是fork创建的子进程也是需要复制掩码的,具体代码可以看proc.c中的fork函数实现

1
2
3
4
5
6
7
8
9
10
11
12
$ trace 2 usertests forkforkfork
usertests starting
test forkforkfork: 407: syscall fork -> 408
408: syscall fork -> 409
409: syscall fork -> 410
410: syscall fork -> 411
409: syscall fork -> 412
410: syscall fork -> 413
409: syscall fork -> 414
411: syscall fork -> 415
...
$

实现

HINS

  1. 修改makefile在用户空间添加trace程序

  2. 修改usys.pl,添加trace系统调用入口

  3. 修改user.h,添加trace系统调用函数接口

  4. 修改syscall.hsyscall.c文件,添加trace系统调用函数接口定义,系统调用的名称。

  5. 修改proc.h,在proc结构体中添加trace_mask字段

  6. 修改sysproc.c,添加trace系统调用的处理函数sys_trace,其中要获得系统调用的参数可以参考sleep函数以及exit函数。

  7. 修改proc.c,在fork函数中添加子进程继承父进程掩码字段

主要文件修改

  1. sysproc.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    uint64
    sys_trace(void){
    int mask;

    argint(0,&mask);
    //swtich on the trace_syscall
    myproc()->trace_mask |= mask;

    return 0;
    }
  2. syscall.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37

    ....
    extern uint64 sys_trace(void);

    static uint64 (*syscalls[])(void) = {
    ...
    [SYS_trace] sys_trace,
    };

    char* syscall_names[] = {
    " ", "fork", "exit", "wait", "pipe", "read",
    "kill", "exec", "fstat", "chdir", "dup",
    "getpid", "sbrk", "sleep", "uptime", "open",
    "write", "mknod", "unlink", "link", "mkdir",
    "close", "trace",
    };

    void
    syscall(void)
    {
    int num;
    struct proc *p = myproc();

    num = p->trapframe->a7;
    if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    // Use num to lookup the system call function for num, call it,
    // and store its return value in p->trapframe->a0
    p->trapframe->a0 = syscalls[num]();
    if((1<<num) & p->trace_mask){
    printf("%d: syscall %s -> %d\n",myproc()->pid,syscall_names[num],p->trapframe->a0);
    }
    } else {
    printf("%d %s: unknown sys call %d\n",
    p->pid, p->name, num);
    p->trapframe->a0 = -1;
    }
    }
  3. proc.c

    1
    2
    3
    4
    5
    6
    7
    int
    fork(void)
    {
    ...
    np->trace_mask = p->trace_mask;
    ...
    }

Sysinfo (moderate)

系统调用实现

在该实现中我们需要实现一个叫做sysinfo的的系统调用,这个系统调用主要是返回一个sysinfo结构体,其中包含两个字段:1.nproc:不是UNUSED的进程数量,2.freemem :空闲物理空间的字节。用户程序通过调用sysinfo(struct sysinfo *info),那么info参数中就会返回对应的字段。

实验中提供一个用户程序sysinfotest进行测试,最后打印OK则实验成功

HINS

  1. 添加 $U/_sysinfotest到 Makefile的UPROGS中。

  2. 在user.h中添加以下声明
    struct sysinfo;
    int sysinfo(struct sysinfo *);

  3. 参考sys_fstat() (kernel/sysfile.c)和filestat() (kernel/file.c)这两个函数学习如何使用copyout函数将sysinfo结构体内容传回用户空间。

  4. 在kalloc.c中添加函数,统计空闲物理内存字节

  5. 在proc.c中添加函数,统计不是UNUSED进程的数量

实现

  1. sysproc.c:需要注意的是获取到用户的参数地址后需要判断是是否超出物理内存的最大值,超出了说明其是错误参数,返回0xffffffffffffffff
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "sysinfo.h"

uint64 n_proc(void);
uint64 free_memory(void);
....
uint64
sys_sysinfo(void){
struct proc *p = myproc();
struct sysinfo kinfo;
uint64 uinfo;

argaddr(0, &uinfo);//user pointer to struct sysinfo

if(uinfo > PHYSTOP){//error arugement
return 0xffffffffffffffff;
}

kinfo.nproc = n_proc();
kinfo.freemem = free_memory();

//copy info to u_info
if(copyout(p->pagetable, uinfo, (char *)&kinfo, sizeof(kinfo)) < 0)
return -1;

return 0;
}
  1. proc.c:文件开始定义了一个proc数组,说明xv6中最大进程量为NPROC,再到allocpid中查看,我们便可以得知nextpid是定义了下一个进程的id,因此只需遍历这个proc数组并判断进程状态,就可以得到满足条件的进程数了。
1
2
3
4
5
6
7
8
9
10
11
12
uint64 
unused_proc(void){
uint64 nproc = 0;
int i;
for(i = 0;i < nextpid && i < NPROC;i++){
if(proc[i].state != UNUSED ){
nproc++;
}
}

return nproc;
}
  1. kalloc.c:物理内存分配器,给用户进程、内核栈、页表、管道分配内存

kmem结构体中包含了两个结构体

  • run:一个没有数据域的链表,主要作用是记录未分配的物理页表的节点
  • spinlock:内存分配与删除时的锁
1
2
3
4
5
6
7
8
struct run {
struct run *next;
};

struct {
struct spinlock lock;
struct run *freelist;
} kmem;

由于freelist指向空页表的个数,我们只需要遍历freelist统计其有多少个节点即可,最后乘以页表的字节数,就可以得到空闲内存的字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uint64
free_memory(void){
uint64 freemem = 0;

struct run *free = kmem.freelist;

while(free){
free = free->next;
freemem++;
}

freemem *= PGSIZE;

return freemem;
}

实验结果

2.11