xv6项目开源—02
参考:28天速通MIT 6.S081操作系统公开课 - 总结帖 - 知乎 (zhihu.com)
理论
1)RISC-V有三种模式,CPU可以执行指令:机器模式、监督者(supervisor)模式和用户模式。
用户态=用户模式=目态
核心态=管理模式=管态
2)CPU提供一个特殊的指令,将CPU从用户模式切换到管理模式,并在内核指定的入口点进入内核(RISC-V为此提供ecall
指令)
3)XV6的源代码位于kernel/*子目录中,模块间的接口都被定义在了def.h*(*kernel/defs.h*)
文件 | 描述 |
---|---|
*bio.c* | 文件系统的磁盘块缓存 |
*console.c* | 连接到用户的键盘和屏幕 |
*entry.S* | 首次启动指令 |
*exec.c* | exec() 系统调用 |
*file.c* | 文件描述符支持 |
*fs.c* | 文件系统 |
*kalloc.c* | 物理页面分配器 |
*kernelvec.S* | 处理来自内核的陷入指令以及计时器中断 |
*log.c* | 文件系统日志记录以及崩溃修复 |
*main.c* | 在启动过程中控制其他模块初始化 |
*pipe.c* | 管道 |
*plic.c* | RISC-V中断控制器 |
*printf.c* | 格式化输出到控制台 |
*proc.c* | 进程和调度 |
*sleeplock.c* | Locks that yield the CPU |
*spinlock.c* | Locks that don’t yield the CPU. |
*start.c* | 早期机器模式启动代码 |
*string.c* | 字符串和字节数组库 |
*swtch.c* | 线程切换 |
*syscall.c* | Dispatch system calls to handling function. |
*sysfile.c* | 文件相关的系统调用 |
*sysproc.c* | 进程相关的系统调用 |
*trampoline.S* | 用于在用户和内核之间切换的汇编代码 |
*trap.c* | 对陷入指令和中断进行处理并返回的C代码 |
*uart.c* | 串口控制台设备驱动程序 |
*virtio_disk.c* | 磁盘设备驱动程序 |
*vm.c* | 管理页表和地址空间 |
4)内核用来实现进程的机制包括用户/管理模式标志、地址空间和线程的时间切片,xv6内核为每个进程维护许多状态片段,并将它们聚集到一个proc
(*kernel/proc.h*:86)结构体中。一个进程最重要的内核状态片段是它的页表、内核栈区和运行状态
5)p->state
表明进程是已分配、就绪态、运行态、等待I/O中(阻塞态)还是退出
实践
自己实现两个新的系统调用trace和sysinfo. 系统调用大部分的工作都需要在有特权的内核态进行操作. 在riscv中, ecall指令使得操作系统由用户态切换至内核态, 我们从特定寄存器里读出究竟是哪个系统调用造成了这次内核态切换并读取, 操作传入的相关参数.
1)trace
在本作业中,您将添加一个系统调用跟踪功能,该功能可能会在以后调试实验时对您有所帮助。您将创建一个新的trace
系统调用来控制跟踪。它应该有一个参数,这个参数是一个整数“掩码”(mask),它的比特位指定要跟踪的系统调用。例如,要跟踪fork
系统调用,程序调用trace(1 << SYS_fork)
,其中SYS_fork
是***kernel/syscall.h***中的系统调用编号。如果在掩码中设置了系统调用的编号,则必须修改xv6内核,以便在每个系统调用即将返回时打印出一行。该行应该包含进程id、系统调用的名称和返回值;您不需要打印系统调用参数。trace
系统调用应启用对调用它的进程及其随后派生的任何子进程的跟踪,但不应影响其他进程。
实现流程:
- 用户呼叫我们提供的系统调用接口 int trace(int)
- 这个接口的实现由perl脚本生成的汇编语言实现, 将SYS_trace的代号放入a7寄存器, 由ecall硬件支持由用户态转入内核态
- 控制转到系统调用的通用入口 void syscall(void) 手上. 它由a7寄存器读出需要被调用的系统调用是第几个, 从*uint64 (*syscalls[])(void)*这个函数指针数组跳转到那个具体的系统调用函数实现上. 将返回值放在a0寄存器里
- 我们从第二步的ecall里退出来了, 汇编指令ret使得用户侧系统调用接口返回
1、首先我们在user/user.h里提供一个用户的接口
# user/user.h
...省略...
int sleep(int);
int uptime(void);
int trace(int); // 新系统调用trace的函数原型签名
2、接着在user/usys.pl里为这个函数签名加入一个entry,作用:把sys_trace的代号放入a7这个寄存器里
# user/usys.pl
...省略...
entry("sleep");
entry("uptime");
entry("trace");
3、user/trace.c的内容,主要的代码如下
if (trace(atoi(argv[1])) < 0) {
fprintf(2, "%s: trace failed\n", argv[0]);
exit(1);
}
for(i = 2; i < argc && i < MAXARG; i++){
nargv[i-2] = argv[i];
}
exec(nargv[0], nargv);
4、首先再proc
结构体中添加一个数据字段,用于保存trace
的参数。并在sys_trace()
的实现中实现参数的保存
// kernel/proc.h
struct proc {
// ...
int trace_mask; // trace系统调用参数
};
// kernel/sysproc.c
uint64
sys_trace(void)
{
// 获取系统调用的参数
argint(0, &(myproc()->trace_mask));
return 0;
}
5、由于struct proc
中增加了一个新的变量,当fork
的时候我们也需要将这个变量传递到子进程中(提示中已说明)
//kernel/proc.c
int
fork(void)
{
// ...
safestrcpy(np->name, p->name, sizeof(p->name));
//将trace_mask拷贝到子进程
np->trace_mask = p->trace_mask;
pid = np->pid;
// ...
return pid;
}
6、考虑如何进行系统调用追踪了,根据提示,这将在syscall()
函数中实现。
void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7; // 系统调用编号,参见书中4.3节
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num](); // 执行系统调用,然后将返回值存入a0
// 系统调用是否匹配
if ((1 << num) & p->trace_mask)
printf("%d: syscall %s -> %d\n", p->pid, syscalls_name[num], p->trapframe->a0);
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
7、在syscall.c文件中定义变量
// ...
extern uint64 sys_trace(void);
static uint64 (*syscalls[])(void) = {
// ...
[SYS_trace] sys_trace,
};
static char *syscalls_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",
};
2)Sysinfo
在这个作业中,您将添加一个系统调用sysinfo
,它收集有关正在运行的系统的信息。系统调用采用一个参数:一个指向struct sysinfo
的指针(参见*kernel/sysinfo.h*)。内核应该填写这个结构的字段:freemem
字段应该设置为空闲内存的字节数,nproc
字段应该设置为state
字段不为UNUSED
的进程数。我们提供了一个测试程序sysinfotest
;如果输出“sysinfotest: OK”则通过。
1、在kernel/kalloc.c中添加一个函数用于获取空闲内存量——物理页面分配器
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
2、内存是使用链表进行管理的,因此遍历kmem
中的空闲链表就能够获取所有的空闲内存,如下
void
freebytes(uint64 *dst)
{
*dst = 0;
struct run *p = kmem.freelist; // 用于遍历
acquire(&kmem.lock);
while (p) {
*dst += PGSIZE;
p = p->next;
}
release(&kmem.lock);
}
3、在kernel/proc.c中添加一个函数获取进程数
void
procnum(uint64 *dst)
{
*dst = 0;
struct proc *p;
for (p = proc; p < &proc[NPROC]; p++) {
if (p->state != UNUSED)
(*dst)++;
}
}
uint64
sys_sysinfo(void)
{
struct sysinfo info;
freebytes(&info.freemem);
procnum(&info.nproc);
// 获取虚拟地址
uint64 dstaddr;
argaddr(0, &dstaddr);
// 从内核空间拷贝数据到用户空间
if (copyout(myproc()->pagetable, dstaddr, (char *)&info, sizeof info) < 0)
return -1;
return 0;
}