lab2

TOPIC:

  • ISOLATION:将资源抽象为服务。我们需要控制应用权限,防止应用程序间的相互影响,定期让应用程序让出cpu…

  • KERNEL/USER MODE:隔离操作系统内核和用户应用程序。

  • SYSTEM CALL:从用户态转到内核态。通过ECALL指令并传入系统调用号实现。

RISCV模式:用户模式,监督者模式,机器模式。

硬件对隔离的支持

  • 硬件可以通过寄存器的一个位判断当前模式,以允许执行特权指令。
  • 硬件通过虚拟内存限制进程可以访问的内存空间。

宏内核与微内核:

  • 宏内核:将所有操作系统服务都运行在内核模式中。

    • 优点:较好的性能,如应用程序与文件系统交互,只需进入一次内核。
    • 缺点:易产生BUG,且一旦产生bug极可能导致内核崩溃。
  • 微内核:内核模式只提供基础支持,将文件系统等功能作为普通应用程序运行

    • 优点:较少的BUG,某功能崩溃可尝试重启。
    • 缺点:性能欠佳,如应用程序与文件系统交互,应用程序需要先跳转进内核,再从内核跳转进文件系统用户程序。

gdb配置

首先将gdb配置输出到~/.gdbinit。

1
2
echo "add-auto-load-safe-path /home/randolfluo/Desktop/xv6/xv6-labs-2020/.gdbinit " >> ~/.gdbinit
gdb-multiarch

然后tmux打开两个窗口,分别运行xv6和gdb即可进行远程调试。

1
2
[0]:make qemu-gdb
[1]:gdb-multiarch

System calls

下面两个实验主要是让我们熟悉系统调用的过程:

System call tracing

在usyc.pl添加进入syscall的入口,编译时会将该入口编译为汇编指令

1
entry("trace");

在user.h添加系统调用

1
int trace(int);

在sysproc.c添加,参数获取用argint函数,获取保存在trapframe寄存器的值。

1
2
3
4
5
6
7
8
uint64
sys_trace(void)
{
int n;
argint(0,&n); //接收参数
myproc()->mask = n;
return 0;
}

在proc.h中的进程结构体添加掩码,记录该进程是否需要追踪

1
2
3
4
struct proc {
...
int mask;
}

在syscall.h添加宏定义,定义系统调用号

1
#define SYS_trace  22

在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
38
39
40
41

extern uint64 sys_trace(void);


static uint64 (*syscalls[])(void) = {
......
[SYS_trace] sys_trace,
};
char *sys_name[]={
[SYS_fork] "fork",
[SYS_exit] "exit",
[SYS_wait] "wait",
[SYS_pipe] "pipe",
[SYS_read] "read",
[SYS_kill] "kill",
[SYS_exec] "exec",
[SYS_fstat] "fstat",
[SYS_chdir] "chdir",
[SYS_dup] "dup",
[SYS_getpid] "getpid",
[SYS_sbrk] "sbrk",
[SYS_sleep] "sleep",
[SYS_uptime] "uptime",
[SYS_open] "open",
[SYS_write] "write",
[SYS_mknod] "mknod",
[SYS_unlink] "unlink",
[SYS_link] "link",
[SYS_mkdir] "mkdir",
[SYS_close] "close",
[SYS_trace] "trace",
};


//修改syscall添加掩码判断
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
if((1 << num) & p->mask)
printf("%d: syscall %s -> %d\n", p->pid, sys_name[num], p->trapframe->a0); //a0寄存器存储返回值
} else {

在proc.c中的fork()函数设置子进程继承掩码

1
2
3
4
5
6
int fork(voide)
{
// Cause fork to return 0 in the child.
np->trapframe->a0 = 0;
np->mask = p->mask; //继承掩码
}

最后在makefile中将trace添加进编译选项。

1
$U/_trace\

通过实验总结,我们可以发现trace实际是通过设置进程结构体中的标志位来标记进程中需要跟踪的系统调用,并通过在fork()中继承标记位来实现对子进程的追踪。最后在每次调用syscall检查对应掩码输出调用信息。

Sysinfo

和trace差不多的修改程序,但是我们需要自己创建两个函数

先修改系统调用入口usys.pl

1
entry("sysinfo");

在user.h添加添加声明,提供用户接口

1
2
struct sysinfo;
int sysinfo(struct sysinfo *);

在syscall.h添加系统调用号

1
#define SYS_sysinfo 23

在syscall.c添加SYS_sysinfo到trace追踪里

1
2
3
4
5
6
7
8
9
10
11
12
13
extern uint64 sys_sysinfo(void);

static uint64 (*syscalls[])(void) = {
....
[SYS_sysinfo] sys_sysinfo,
};


char *sys_name[]={
...
[SYS_sysinfo] "sysinfo",
};

在proc.c添加获取进程数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
n_proc_num(void)
{

struct proc *p;
int n = 0;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state != UNUSED)
n++;
release(&p->lock);

}
return n;
}

在kalloc.c获取空闲内存大小,由于空闲列表是通过链表实现的且每次分配内存都为PGSIZE,我们可以计算空闲块数*PGSIZE得到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
free_mem(void)
{
uint64 n=0;
struct run *r;

acquire(&kmem.lock);
r = kmem.freelist;
while (r) {
n += PGSIZE;
r = r->next;
}
release(&kmem.lock);
return n;
}

在defs.h添加上述函数,方便其他系统模块调用。

1
2
3
4
5
6
// kalloc.c
int free_mem(void);


// proc.c
int n_proc_num(void);

在sysproc.c添加sys_sysinfo系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "sysinfo.h"

...

uint64
sys_sysinfo(void)
{
struct proc *p = myproc(); //获取进程信息
uint64 info;
argaddr(0, &info); //获取参数
struct sysinfo sys_info;
sys_info.freemem = free_mem();
sys_info.nproc = n_proc_num();
if(copyout(p->pagetable, info, (char *)&sys_info, sizeof(sys_info)) < 0) //将参数传递给调用者
return -1;
return 0;
}

别忘了在makefile添加测试文件

1
$U/_sysinfotest\