0%

Lab Syscall

Lab Syscall

前置知识

一、添加一个syscall的流程

  1. 当你想要添加编译一个user下的源文件,你需要把它加到Makefile中
  2. 为了生成进入中断的汇编.s文件,需要在user/user.pl中加入进入内核态的入口函数声明,以便使用ecall中断user进入kernel
    1
    entry(“trace”);
  3. 需要在kernel/syscall.h中添加系统调用指令码
    1
    2
    3
    4
    5
    ...
    #define SYS_mkdir 20
    #define SYS_close 21
    #define SYS_trace 22
    #define SYS_sysinfo 23
  4. 需要在kernel/syscall.c中添加定义和入口
    1
    2
    3
    4
    5
    6
    7
    8
    extern uint64 sys_trace(void);
    ...
    static uint64 (*syscalls[])(void) = {
    [SYS_fork] sys_fork,
    ...
    [SYS_trace] sys_trace,
    [SYS_sysinfo] sys_sysinfo,
    };
  5. 如果user下的程序需要调用这个syscall,那么就需要在user/user.h中加入syscall函数声明

二、阅读内存分配的代码实现kernel/kalloc.c

  1. xv6拥有一个链表来记录所有的分页,每一个节点都是一个page,头节点储存了最后一个空page的地址,同时,它拥有一个end数组来记录kernel后的第一个地址。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct run {
    struct run *next;//linked list pointer
    };

    struct {
    struct spinlock lock;
    struct run *freelist;
    } kmem;//linked list, the implementation of the pages
    //这里直接声明了一个实例kmem,因此可以直接用
  2. 首先,它会初始化一个lock来避免多核时共享资源出现死锁,然后调用freerange
  3. 在freerange里,它调用kfree把所有pages清空
    1
    2
    3
    4
    5
    6
    7
    8
    void
    freerange(void *pa_start, void *pa_end)
    {
    char *p;
    p = (char*)PGROUNDUP((uint64)pa_start);
    for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
    kfree(p);//通过这个循环将所有pages初始化为1(free all the pages)
    }
  4. 在kfree中,它把需要free的page填充为1,然后更新头节点
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // Fill with junk to catch dangling refs.
    memset(pa, 1, PGSIZE);

    r = (struct run*)pa;

    acquire(&kmem.lock);//r->next: 上一个节点的指针
    r->next = kmem.freelist;//freelist: 可用的最后一个page的地址
    kmem.freelist = r;
    release(&kmem.lock);
  5. 在kalloc(分配内存函数)中,它把最后的空page拿出,更新头节点,然后把这个page填充为5并返回它的指针
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    void *
    kalloc(void)
    {
    struct run *r;

    acquire(&kmem.lock);
    r = kmem.freelist;
    if(r)
    kmem.freelist = r->next;//可用page指针前移
    release(&kmem.lock);

    if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
    return (void*)r;
    }
  6. 注意:在一个进程的pages中,不可用页的freelist默认被初始化为0,而可用空间的逻辑地址在分配时是连续的,所以可以通过遍历链表来找出所有的空page

三、阅读进程管理部分代码实现kernel/proc.c,proc.h

  1. 在proc.h中,定义了一个proc结构体,它储存了一个进程的各种信息,当你想记录一个进程的新的信息时,就可以在这里面直接添加变量。proc.h中还有一个enum,定义了一个进程的五种状态。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };//all the process state in struct proc

    // Per-process state
    struct proc {
    struct spinlock lock;

    // p->lock must be held when using these:
    enum procstate state; // Process state
    struct proc *parent; // Parent process
    void *chan; // If non-zero, sleeping on chan
    int killed; // If non-zero, have been killed
    int xstate; // Exit status to be returned to parent's wait
    int pid; // Process ID

    ...

    int mask; // The "trace"'s argument
    //注意:你不能在结构体里给它初始化,需要去allocproc里面赋值
    };
  2. 在proc.c中,声明了一个记录所有cpu状态的struct cpus,一个记录所有进程信息的proc数组。注意:在proc里,空的数组单元中state变量是UNUSED(0),代表当前单元没有储存进程信息。
    1
    2
    3
    struct cpu cpus[NCPU];

    struct proc proc[NPROC];//存储了所有的process信息
  3. allocproc: 这个函数会从头开始寻找一个空的process table,如果找到的这个page table是合法的,那么将它的内容初始化
  4. freeproc: 这个函数会将一个page table free掉

四、阅读kernel/syscall.c和部分汇编

  1. 在usys.S中,可以看到所有的系统调用号都储存在a7寄存器中
  2. 在argaddr和argint中,都是根据给定的序号来从register中获取数据,区别是返回int还是unsigned long,因为kernel和user的page table映射不一样,寄存器也不互通,所以需要类似的函数传参
  3. 在syscall函数中,先获取当前进程,然后获取当前进程所使用的系统调用号。如果系统调用执行成功,那就把返回值记录到a0寄存器中,如果失败,a0被设置为-1

task: system call tracing

  1. 根据添加syscall的流程添加syscall sys_trace
  2. 在PCB中加入一个mask掩码变量来储存系统调用号(kernel/proc.h)
  3. 对fork()进行修改,通过让子进程继承父进程的mask来追踪子进程的mask(kernel/proc.c)
  4. 将mask初始化为0,同时还可以在销毁page时把它置为0(kernel/proc.c, allocproc)(kernel/proc.c, freeproc)
  5. 实现sys_trace具体代码。先获取当前进程的mask,然后将该mask传递给trace函数(kernel/sysproc.c)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    uint64
    sys_trace(void)
    {
    int mask;
    if(argint(0, &mask) < 0)
    return -1;//获取mask
    myproc()->mask = mask; // 传参
    return 0;// myproc()获取当前进程的PCB
    }
  6. 加入一个字符串数组,用来输出syscall name(kernel/syscall.c)
    1
    2
    3
    4
    5
    6
    7
    8
    static char *syscalls_name[] = {
    "fork", "exit", "wait", "pipe",
    "read", "kill", "exec", "fstat",
    "chdir", "dup", "getpid", "sbrk",
    "sleep", "uptime", "open", "write",
    "mknod", "unlink", "link", "mkdir",
    "close", "trace", "sysinfo",
    };
  7. 在syscall函数中进行输出(kernel/syscall.c)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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]();//注意:这是将系统调用的返回值存储在a0中
    if(p->mask & (1 << num)) {//mask的对应位为1(仔细读lab要求)
    printf("%d: syscall %s -> %d\n", p->pid, syscalls_name[num - 1], p->trapframe->a0);
    }
    } else {
    printf("%d %s: unknown sys call %d\n",
    p->pid, p->name, num);
    p->trapframe->a0 = -1;
    }
    }

task: sysinfo

  1. 根据添加syscall的流程添加syscall sys_sysinfo,注意,在makefile中添加的时sysinfotest,同时声明该函数之前要声明它的结构体类型的参数sysinfo(user/user.h)
  2. 在kernel/proc.c中添加一个函数来获取非空闲进程个数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    int free_proc(void)
    {
    uint64 cnt = 0;
    struct proc *p;
    for(p = proc; p <= &proc[NPROC]; p++)//遍历proc数组
    {
    acquire(&p->lock);//暂时不太清楚锁的用途
    if(p->state != UNUSED)
    cnt ++;
    release(&p->lock);
    }
    return cnt;
    }
  3. 在kernel/kalloc.c中添加一个函数来获取当前空闲内存大小
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int free_memory(void)
    {
    struct run * r;
    r = kmem.freelist;
    uint64 cnt = 0;
    while(r)//为什么第一个不可用页是0呢?难道是默认初始化?//是的,具体请看后记
    {
    cnt ++;
    r = r->next;
    }
    return cnt * PGSIZE;
    }
  4. 在kernel/sysproc.c中声明以上两个函数,并实现sys_sysinfo系统调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    uint64
    sys_sysinfo(void)
    {
    struct sysinfo info;
    uint64 addr;
    struct proc *p = myproc();
    if(argaddr(0, &addr) < 0)
    return -1;

    info.freemem = free_memory();
    info.nproc = free_proc();

    if(copyout(p->pagetable, addr, (char *)&info, sizeof info) < 0)
    return -1;
    return 0;
    }

后记

  1. 关于链表尾节点的初始化问题,后来发现的确是这样。在声明kmem时,kmem的freelist初始化为0,第一次插入空闲页表的时候,0自然会传递给run的next,因此,链表尾部的next一定为0。