6.S081-Lab2 总结笔记(0基础向)

2. Lab: system calls

笔记目标:

  • 按照11个lab的顺序,整理所涉及到的知识
  • 对每个lab,给出清晰的解答
  • 整理自己在做实验过程中遇到的问题与思考
  • 对于课程中有涉及但是没有相应lab的内容做出补充(比如,后边的那些论文

笔记说明:

  • 针对的是20的课程,其他年份可能会有出入
  • 如果读到一些在本章课程、指导书中完全没提及的内容,那很有可能是引用了后续实验的知识,我会尽量标明,如果遇到看不懂的,可以先跳过
  • 因为自己刚经历了纯小白的苦恼,所以会努力写得让零基础的人也能看懂,但这也会导致笔记比较琐碎

2.1 System call tracing

2.1.1 理论知识

[1] mask:掩码的定义

“It should take one argument, an integer “mask”, whose bits specify which system calls to trace.”

实验要求以及运行示例中都对mask的定义给出了清晰的说明:mask的bits表明了我们要trace的系统调用号

如:

  • mask = 2,转换为二进制为0010,仅第1次位为true,则仅追踪系统调用号为1的系统调用
  • mask = 32, 转换为二进制为100000,仅第5次位为true,则追踪系统调用号为5的系统调用
[2] 用户空间和内核空间之间的消息传递

实验指导书4.3介绍了一种用户和内核之间的通信方式:寄存器

用户将特定信息放入对应的寄存器当中,内核需要时就去特定的寄存器读取。同理,当内核需要向用户返回信息时,也是这样。

用户代码将 exec 的参数放在寄存器 a0 和 a1 中,并将系统调用号放在 a7 中。

syscall (kernel/syscall.c:133)从 trapframe中的 a7 中得到系统调用号,并其作为索引在syscalls 查找相应函数。

当系统调用函数返回时,syscall 将其返回值记录在 p->trapframe->a0 中。用户空间 的 exec()将会返回该值。

2.1.2 实验步骤

在hints中,每一步都给了很详细的提示

[1] 添加trace系统调用的声明

像函数调用一样,我们在调用系统调用前也需要对其进行声明和定义。对系统调用的声明分为两个部分,一个是在用户空间的声明,一个在内核空间的声明:

[1.1] 用户空间的声明
  • user.h

    和往常的头文件的作用类似,保存函数的声明,以便程序知道应该如何调用。通过观察trace.c程序中对trace()的调用:

    if (trace(atoi(argv[1])) < 0) {
        // ...
    }
    

    可知,trace函数的返回值和参数列表均为int类型:

    // user/user.h
    // 打开user.h文件,发现有许多已经声明的系统调用,模仿已有的系统调用声明,对trace系统调用进行声明:
    int trace(int);
    
  • usys.pl

    根据提示,这应该是生成“由用户空间到内核空间”的“入口”的文件,因为系统调用其实是请求内核的特定服务,所以需要在这个文件中设定trace系统调用的“入口”。

    打开文件,模仿其他系统调用即可:

    // user/usys.pl
    entry("trace");
    
[1.2] 内核空间的声明
  • syscall.h

    在内核空间中,也需要对系统调用进行“声明”,这里的声明是给系统调用一个唯一对应的整数,以便内核能够选择正确的系统调用提供给用户。

    // kernel/syscall.h
    #defsine SYS_trace  22
    // 注意系统调用号是唯一对应的,不能重复
    
  • syscall.c

    另外,在syscall.c中也需要对已有的类似于“函数指针的”声明进行模仿:

    // kernel/syscall.c
    // ...
    extern uint64 sys_trace(void);
    // ...
    [SYS_trace]   sys_trace,
    // ...
    

    这是为了后续方便通过系统调用号索引到特定的系统调用。

[2] 添加trace系统调用的定义

前边提到,类比函数调用,除了声明,我们在调用系统调用前还需要对其进行定义,即定义系统调用的功能。

Add a sys_trace() function in kernel/sysproc.c that implements the new system call by remembering its argument in a new variable in the proc structure (see kernel/proc.h)

根据提示,我们需要在 kernel/sysproc.c 中添加 sys_trace() 函数,这个函数通过保存其传入的参数到 proc 结构体中,来实现新的系统调用。

[2.1] 获取参数

根据提示,阅读模仿sysproc.c中的其他示例。我们可以从我们最熟悉的sys_sleep入手,在lab1中我们对sleep有过一次应用:“sleep should pause for a user-specified number of ticks”,显然,sleep系统调用也需要获取用户传入的参数。从sleep的使用情景出发,再去观察sys_sleep会更有针对性。

根据lab2开头让我们阅读的代码kernel/syscall.c(自己仔细完整地看一遍)以及实验指导书4.3、4.4:

// Fetch the nth 32-bit system call argument.
int argint(int n, int *ip);

不难看出sys_sleep中的if(argint(0, &n) < 0) 语句就是在试图保存用户传入的参数。

于是,模仿可得:

int mask;
if(argint(0, &mask) < 0)
    return -1;

这样,传入参数就被获取到了变量mask中。

[2.2] 保存参数

根据提示,需要把获取的参数保存到proc结构体的一个新变量当中。

  • 首先,我们需要知道proc结构体是啥。根据提示打开proc.h,可以看到proc结构体保存了许多进程的信息,如进程运行状态、父进程指针、进程ID等。现在,我们需要其多保存一个信息,也就是我们的mask参数。

    但是结构体中有两个不同的数据段,mask参数应该加到哪个数据段下边呢?

    根据注释,其中一段数据是需要被lock锁保护的,而另一段则不用。什么样的数据需要被lock呢?当一个数据可能会被多个进程读写时,为了保证数据的同步,我们需要对数据进行加锁。可以看到,被lock的数据段如父进程指针、子进程指针等,都是有可能会被多个进程读写的;而未被lock的数据段如进程大小、进程页表等,都只是会被当前进程读写。mask代表着我们要追踪的系统调用号,并不需要被其他进程读取,因此放入不被lock保护的数据段即可。

    // these are private to the process, so p->lock need not be held.
    int mask;
    
  • 其次,如何将参数保存进proc结构体中的变量呢?根据lab2开头提示阅读的kernel/proc.c的函数定义:

    // Return the current struct proc *, or zero if none.
    struct proc* myproc(void);
    

    可知sys_sleep中的if(myproc()->killed)就是在访问当前进程的proc结构体,模仿可得:

    myproc()->mask = mask;
    
[2.3] 总结可得:
// kernel/sysproc.c
uint64 sys_trace(void) {
    int mask;
    if (argint(0, &mask) < 0)
        return -1;
    myproc()->mask = mask;
    return 0;
}
[3] 修改fork()

修改fork,将父进程的mask数据复制到子进程。根据提示,阅读kernel/proc.c中的fork()代码,其中有一段代码是负责copy的,模仿其形式,对mask进行同样的复制即可:

// kernel/proc.c
int
fork(void)
{
  //...

  np->sz = p->sz;
  np->mask = p->mask; // 复制父进程的mask数据到子进程
  np->parent = p;
    
  // ...
}

因为我们需要对子进程的系统调用也进行追踪,而追踪系统调用需要根据proc结构体中保存的mask数据,所以需要进行复制。

[4] 修改syscall()
[4.1] 判断是否是需要trace的系统调用

即,判断正在执行的系统调用是否在用户输入的mask中。2.1.1[1]中有提到,若mask的n次位为1,则n号系统调用需要被追踪。

if(1 << n & mask) /* n为正准备执行的系统调用号,对1做左移运算,得到n次位为1的数,同mask做与运算,判断mask的n次位是否为1 */
[4.2] 打印所要求的信息
  • process id

    有了2.1.2[2.2]保存参数的经验,获取process id已经很轻松了,这些进程的状态信息都保存在了proc结构体中。

    int pid = myproc()->pid;
    
  • the name of the system call

    根据提示,可以增加一个保存系统调用名称的数组,便于通过系统调用号索引输出。

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

    由实验指导书可知,“syscall 将其返回值记录在 p->trapframe->a0 中”,于是:

    int returnValue = p->trapframe->a0;
    
  • 总结得:

    // kernel/sysproc.c
    void syscall(void) {
        int num;
        struct proc *p = myproc();
        char *names[] = {"fork", "exit", "wait", "pipe", "read", "kill", "exec", "fstat", "chdir", "dup", "getpid",
                         "sbrk", "sleep", "uptime", "open", "write", "mknod", "unlink", "link", "mkdir", "close",
                         "trace"};
        num = p->trapframe->a7;
        if (num > 0 && num < NELEM(syscalls) && syscalls[num]) {
            p->trapframe->a0 = syscalls[num]();
            if ((1 << num) & myproc()->mask) {
                printf("%d: syscall %s -> %d\n", p->pid, names[num - 1], p->trapframe->a0);
                // 按照格式打印信息
            }
        } else {
            printf("%d %s: unknown sys call %d\n",
                   p->pid, p->name, num);
            p->trapframe->a0 = -1;
        }
    }
    
[5] 编译、运行
  • Makefile文件中加入trace

    UPROGS=\
            ......
        	$U/_zombie\
       		$U/_trace\
    
  • xv6-labs-2020目录下输入make qemu

  • 在命令行输入trace 32 grep hello README,得到结果:

    其他测试也均能满足要求。

  • 按住Ctrl + a + x,退出xv6,在xv6-labs-2020目录下输入make grade

2.2 Sysinfo

2.2.1 理论知识

没有什么新的理论知识,主要得根据提示看懂所涉及的代码逻辑。

2.2.2 实验步骤

[1] 添加sysinfo系统调用的声明

与上一个实验类似

[1.1] 用户空间的声明
  • user.h

    struct sysinfo;
    int sysinfo(struct sysinfo *);
    
  • usys.pl

    entry("sysinfo");
    
[1.2] 内核空间的声明
  • syscall.h

    #defsine SYS_sysinfo  23
    
  • syscall.c

    // ...
    extern uint64 sys_sysinfo(void);
    // ...
    [SYS_sysinfo] sys_sysinfo,
    // ...
    
[2] 理清实现思路

根据提示,sysinfo系统调用的动作会比较多、比较复杂,建议先用伪代码把实现思路写出来,再模块化地去实现思路中需要的功能。

//1.获取指向sysinfo结构体的指针
//2.计算空闲的memory,并将数据放入sysinfo结构体相应的数据段中
//3.计算unused的process,并将数据放入sysinfo结构体相应的数据段中
//4.将内核中处理好的sysinfo传到用户空间
[3] 获取指向sysinfo结构体的指针

与上一个实验的获取整数参数相似,这里也是获取用户传入的参数,只不过参数类型由整数变为了指针。

uint64 pSysinfo;
if(argaddr(0, &pSysinfo) < 0)
    return -1;
[4] 计算空闲的memory

实验并没有给出如何计算free memory的提示,但是指明了函数要添加到kalloc.c文件中。打开这个文件,可以发现其功能是为进程分配内存,通过观察分配内存的整个过程,获得计算free memory的灵感。

先快速浏览代码,找到分配内存的实现函数: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;
}

可以看到,free memory是保存在一个kmem.freelist的链表结构中的。保险起见,通过观察释放内存的函数来印证我们的想法:kfree()

// 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;

  // ...
  // Fill with junk to catch dangling refs.
  memset(pa, 1, PGSIZE); // 以页为单位进行分配和释放

  acquire(&kmem.lock);
  r->next = kmem.freelist; // 将释放掉的内存放入freelist当中
  kmem.freelist = r;
  release(&kmem.lock);
}

于是,可以确定,kmem.freelist链表中保存了空闲的内存,一个结点表示一页free memory,通过遍历链表即可计算出空闲内存的大小:

// kernel/kalloc.c
uint64 countFreememory() {
    struct run *r;
    uint64 freememory = 0;
    acquire(&kmem.lock);
    r = kmem.freelist;
    while (r) {
        ++freememory;
        r = r->next;
    }
    release(&kmem.lock);
    return PGSIZE * freememory;
}
[5] 计算unused的proc

根据提示,打开proc.c文件,注意到allocproc()函数中有关于unused proc的描述:

// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, or a memory allocation fails, return 0.static struct proc*
allocproc(void)
{
  struct proc *p;
    
  for(p = proc; p < &proc[NPROC]; p++) { // 遍历proc数组(进程表)
    acquire(&p->lock);
    if(p->state == UNUSED) { // 找到state为UNUSED的进程
      goto found;
    } else {
      release(&p->lock);
    }     
  }
// ...
}

于是,通过遍历proc数组统计state为UNUSED的进程数量:

uint64 countUnusedproc() {
    struct proc *p;
    uint64 unusedproc = 0;
    for (p = proc; p < &proc[NPROC]; p++) {
        acquire(&p->lock);
        if (p->state != UNUSED) { 
            ++unusedproc;
        }
        release(&p->lock);
    }
    return unusedproc;
}
[6] 将内核中得到数据的sysinfo传到用户空间

根据提示,观察sys_fstat() (kernel/sysfile.c)filestat() (kernel/file.c),学习如何使用copyout()

uint64
sys_fstat(void)
{
  // ...
  if(argfd(0, 0, &f) < 0 || argaddr(1, &st) < 0) // 获取用户传来的指针参数
  // ...
}

// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
  // ...
  struct stat st;
  // ...
  if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
  // 将从&st开始的sizeof(st)长的数据复制到p->pagetable的addr处
  // ...
}

光看这两个函数,只能连蒙带猜地去理解copyout(),应该顺着defs.h中函数声明的位置找到copyout()的函数定义:

// kernel/vm.c
// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len){
    // 甚至只看函数描述就够了
}

copyout()的功能是将内核中从src开始的len长的数据复制到指定pagetable中的dstva位置处。模仿可得:

copyout(p->pagetable, addr, (char *)&sysinfo, sizeof(sysinfo));
[7] 添加sysinfo系统调用的定义

因为countFreememory()countUnusedpro()都是新定义的函数,所以需要在defs.h中声明函数接口。

kernel中含有多个.c文件,若想要使用其他.c文件中的函数或者变量,就需要在defs.h中设置好接口,defs.h就像是所有函数的头文件。为了便于管理,defs.h中的函数声明是按照函数所处的不同.c文件进行分块放置的,比如,kalloc.c文件中的函数就放在// kalloc.c的注释下方。

// kernel/defs.h
// kalloc.c
uint64          countFreememory(void);
// proc.c
uint64          countUnusedproc(void);

sysproc.c中给出系统调用的定义:

// kernel/sysproc.c
uint64
sys_sysinfo(void) {
    struct sysinfo info;
    uint64 addr;

    info.freemem = countFreememory();
    info.nproc = countUnusedproc();

    if (argaddr(0, &addr) < 0)
        return -1;
    if (copyout(myproc()->pagetable, addr, (char *) &info, sizeof(info)) < 0)
        return -1;
    return 0;
}
[8] 编译、运行
  • Makefile文件中加入sysinfotest

    UPROGS=\
            ......
       		$U/_trace\
            $U/_sysinfotest\
    
  • xv6-labs-2020目录下输入make qemu

  • 在命令行输入sysinfotest,得到结果:

  • 按住Ctrl + a + x,退出xv6,在xv6-labs-2020目录下输入make grade

2.2.3 问题与思考

[1] 为什么sysinfo结构体中的数据不能通过指针直接修改,而是需要借助copyout()函数传到用户空间呢

因为argaddr(0, &addr)获得的地址是用户空间的虚拟地址,并不直接指向数据对象,而是指向用户页表中的页表项,通过页表项才能最终访问到数据对象。copyout其实就是封装好的帮助内核空间访问用户空间虚拟地址的函数。

2.3 Lab2总结

2.3.1 理解系统调用的发生过程

其实就是一个一个的函数调用,只不过有的函数只能在内核空间运行,所以在执行这些函数时,需要有用户空间到内核空间的切换。传递的变量在这个切换的过程中可能会“失去”它的作用域(因为页表的问题),这个时候可以通过寄存器作为用户与内核的消息传递方式,也可以在内核中获取用户页表来通过地址直接访问用户数据,但是用户不能获取内核页表。

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值