操作系统 MIT6.S081 Lab2 System Call

操作系统 MIT6.S081 Lab2 System Call

实验原理

① Xv6 运行在多核的 RISC-V 微处理器:

  • RISC-V 处理器提供硬件级别的隔离,包括三种执行指令的模式:机器模式、内核模式和用户模式;内核模式下,CPU 可以运行如开关中断、读写寄存器等特权指令,并且是在内核空间上运行的,这些程序就是内核;
  • RISC-V 为进程提供指令 ecall 用于将用户态转为内核态;执行 ecall 以后,PC 将会知道内核指定的入口。该入口的代码切换到内核栈上运行内核指令;当系统调用结束时,内核通过 sret 指令切换回用户栈和用户空间,继续之前的用户程序。

② Xv6 的内核组织:

  • Xv6 采用单体内核的内核组织形式,内核接口对应操作系统的接口,由内核完成整个操作系统;
  • 本次实验涉及到的文件(kernel 目录下):
    • defs.h :模块间的接口;
    • proc.c :进程和调度相关的程序;
    • syscall.c :用于将系统调用的请求分派给不同的具体实现的函数;
    • sysfile.c:文件相关的系统调用的实现;
    • sysproc.c:进程相关的系统调用的实现;

③ Xv6 实现隔离机制的基本单元是进程:

  • 使用一个 struct proc 保存进程的各类信息,系统调用遵循如下 calling convention:

    • 调用系统调用时, proc 中寄存器 a7 保存的序号确定调用的系统调用序号及对应函数,对应kernel/syscall.c 中保存的一个函数指针数组;将传递个系统调用的参数放在寄存器 a0a1 … 中;
    • 系统调用结束时,返回值保存在寄存器 a0 中,返回非负整数表示成功,否则失败
  • 在一个进程的虚拟空间中,trampoline 保存了进出内核的代码,trapframe 保存了从用户态到内核态转换时的现场:

    • 系统调用在 trapframe 中查找相关寄存器来得到用户进程传给内核的参数,内核可调用接口 argintargaddrargfd 来得到不同类型的参数

Part A

**实验目标:**实现系统调用 trace ,用于跟踪系统调用,打印PID、系统调用名称、返回值等信息;通过 usertests 测试。

实验步骤:

① 在 Makefileuser/user.huser/usys.pl 中添加相关声明:

# Makefile
UPROGS=
	...
	$U/_trace\
// user/user.h
// systemcalls
int trace(int);
# user/usys.pl
entry("trace");

trace() 应当接受一个整数,作为被追踪的系统调用代码的掩码;

② 在 kernel/proc.h 保存进程信息的结构体中新增 mask 成员变量,用于保存该进程需要追踪的系统调用代码的掩码:

// kernel/proc.h
struct proc {
    ...
    int mask;
}

③ 在 kernel/sysproc.c 中实现函数 uint64 sys_trace(void)

// kernel/sysproc.c
uint64
sys_trace(void) {
    int mask;
    argint(0, &mask);
    myproc()->mask = mask;
    return 0;
}

系统调用 trace 被调用时需要传递一个整型变量 mask ,使用接口 argint(n, addr) 以整数的形式获得第 0 个参数传到 addr 中,设置成当前进程的 mask

④ 在 kernel/syscall.hkernel/syscall.c 中添加系统调用 trace 的相关代码;需要增加一个保存所有系统调用名称的字符串数组:

// kernel/syscall.h
#define SYS_trace 22
// kernel/syscall.c
extern uint64 sys_trace(void);
static uint64 (*syscalls[])(void) = {
    ...
    [SYS_trace]  sys_trace
}
char* syscall_name[] = {
    "fork",
    "exit",
    ...
}
void
syscall(void) {
    ...
    if (((p->mask) & (1 << num)) > 0)
        printf("%d: syscall %s -> %d\n", p->pid, syscall_name[num - 1], p->trapframe->a0);
    ...
}

通过掩码判断当前系统调用是否需要被追踪;根据 RISC-V 的 C 的 calling convention,系统调用的返回值存在寄存器 a0

⑤ 修改 fork() 使得掩码可以从父进程传递给子进程:

// kernel/proc.c
int
fork(void) {
    ...
    acquire(&np->lock);
   	np->mask = p->mask;
    acquire(&np->lock);
}

不清楚是否涉及到临界资源的问题,但是上锁总是安全的

测试:

trace 32 grep hello README 用于跟踪 grep hello README 执行时系统调用 read 的执行情况( 32 = 2 5 32=2^5 32=25SYS_read = 5):

请添加图片描述

trace 2147483647 grep hello README 用于跟踪 grep hello README 执行时所有系统调用的执行情况(2147483647 的二进制表示全是 1):
请添加图片描述

trace 2 usertests forkforkfork 用于跟踪 usertest forkforkfork 执行时系统调用 fork 的执行情况( 2 = 2 1 2=2^1 2=21SYS_fork = 1):

A_test31A_test32
A_test33A_test34

其中会出现 -1 的原因是,Xv6 最多允许 64 个进程同时存在,可以在 proc.c 中观察到进程数组值为 NPROC (64)因此当进程数超过 64 时就会 fork 失败,返回 -1 :

A_NPROC

Part B

**实验目标:**实现系统调用 sysinfo ,实现系统信息收集功能;通过 sysnifotest 测试。

实验步骤:

① 在 Makefileuser/user.huser/usys.pl 中添加相关声明:

# Makefile
UPROGS=
	...
	$U/_sysinfotest\
// user/user.h
// systemcalls
struct sysinfo;
int sysinfo(struct sysinfo*);
# user/usys.pl
entry("sysinfo");

系统调用 sysinfo 接受一个指向 strcut sysinfo 的指针,并将相关信息填充到这个结构体中

② 在 kernel/kalloc.ckernel/proc.c 中添加辅助函数,用于获得空闲空间的大小以及正在使用的进程的数目,并将接口添加到 kernel/defs.h ,供 sys_sysinfo() 的实现使用:

// kernel/kalloc.c
uint64
free_memory_number(void) {
    uint64 num = 0;
    struct run* r;
    acquire(&kmem.lock);
    r = kmem.freelist;
    while (r) {
        num += PGSIZE;
        r = r->next;
    }
    release(&kmem.lock);
    return num;
}

计算空闲空间的大小,即遍历空闲列表,每块空间的大小为 PGSIZE(4096B);由于空闲列表是临界资源,因此需要上锁

// kernel/proc.c
uint64
used_process_number(void) {
	uint64 num = 0;
    for (int i = 0; i < NPROC; ++i) {
        if (proc[i].state != UNUSED) {
            ++num;
        }
    }
    return num;
}

遍历进程数组中所有进程,统计不为 UNUSED 状态的所有进程数

// kernel/defs.h
// kalloc.c
uint64 free_memory_number(void);
// proc.c
uint64 used_process_number(void);

③ 在 kernel/sysinfo.h 中添加包含一个字符串类型成员的结构体,包含学号信息:

// kernel/sysinfo.h
struct {
    char* stuID;
} stuinfo = {"xxxxxxxxxxx"};

④ 在 kernel/sysproc.c 中实现函数 uint64 sys_sysinfo(void)

// kernel/sysproc.c
uint64
sys_sysinfo(void) {
    // stuinfo is defined in sysinfo.h
    printf("my student number is %s\n", stuinfo.stuID);
    uint64 ps;
    argaddr(0, &ps);
    struct sysinfo temp;
    temp.freemem = free_memory_number();
    temp.nproc = used_process_number();
    if (copyout(myproc->pagetable, ps, (char*)(&temp), sizeof(temp)) < 0)
        return -1;
    return 0;
}

copyout(pagetable_t pagetable, uint64 dstva, char* src, uint64 len) 将来自 src 的内容拷贝到虚拟地址 dstva 中,成功返回 0,失败返回 -1;

⑤ 在 kernel/syscall.hkernel/syscall.c 中添加系统调用 sysinfo 的相关代码:

// kernel/syscall.h
#define SYS_sysinfo 23
// kernel/syscall.c
extern uint64 sys_sysinfo(void);
static uint64 (*syscalls[])(void) = {
    ...
    [SYS_sysinfo]  sys_sysinfo
}
char* syscall_name[] = {
    ...
    "sysinfo"
}

测试:

sysinfotest 用于测试系统调用 sysinfo 是否正确,包括测试空闲内存空间大小计算是否正确和 UNUSED 进程数是否正确;每测试一部分就会调用一次 sysinfo ,同时打印一遍学号:

B_test

问题回答

System calls Part A 部分,简述一下 trace 全流程
  • shell 由 user/sh.c 实现,输入的指令交给 parsecmd() 解析,返回指令类型为 EXEC;再交给 runcmd() 分派,发现是 EXEC 类型,就调用 exec() 并传递相关参数;

  • exec() 根据解析出来的第一个字符串为 ”trace“ 而将 user/trace.c 的可执行代码加载到内存中执行,并传递相关参数;

  • user/trace.c 检查参数的有效性之后,分为两步:

    ① 首先调用 trace() 函数:

    • trace() 函数在 user/user.h 中声明,由 user/usys.pl 提供入口; user/usys.S 根据 calling convention 设置好相关参数,并且执行 ecall 进入内核态:
    usys_pl
    • 在内核中,kernel/syscall.c/syscall() 函数根据 p->trapframe->a7 调用 syscalls[SYS_trace] ,即 kernel/sysproc.c 中的 systrace() 函数

      • kernel/sysproc.c/systrace() 函数通过 p->trapframe->a0 获得掩码,并设置为当前进程的掩码;

      执行完后,将返回值放入 p->trapframe->a0 ,并执行指令 sret 返回用户态

    trace() 函数返回后,由返回值判断是否成功;若成功,再获取 argv[2] 中存储的需要被追踪的系统调用名,调用 exec() 调用该系统调用

    • kernel/syscall.c/syscall() 中会判断当前掩码中该系统调用的编码位是否为 1;若为 1,则打印出 PID、进程名以及返回值

    请添加图片描述

    • 每次 fork 出子进程时,子进程将会继承父进程的掩码,因此可以追踪由原系统调用所引起的所有掩码中包含的系统调用
kernel/syscall.h 是干什么的,如何起作用的?
  • kernel/syscall.h 对每个系统调用都进行编号;

  • 当用户进程需要调用系统调用时,依据 calling convention,把即将调用的系统调用的编号存入寄存器 a7,并将传递个系统调用的参数放在寄存器 a0a1 … 中;接着调用 ecall 指令切换到内核态

  • 进入内核态时,现场被保存在 trapframe 中,因此内核从 trapframe 中获取寄存器 a7a0a1

  • syscall.c 中保存了一个函数指针数组,里边存的是系统调用的具体实现,根据 a7 的值确定实际调用哪个函数

命令 trace 32 grep hello README 中的 trace 字段是用户态下的还是实现的系统调用函数 trace?

用户态下的,在 user/usys.S 中实现;传递给 int trace(int) 的参数(即 mask / 掩码)在寄存器 a0 中,所作的事情是将系统调用 trace 的编码 SYS_trace 放入寄存器 a7 中,然后才调用 ecall 指令进入内核态,所以前面做的事情都是在用户态下完成的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Air浩瀚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值