提示:这是操作系统本人对 MIT 6.S081 的 lab2 实验课的笔记,仅供参考。
前言
提示:以下是本篇文章正文内容,仅供参考
一、总结
在做该实验时需要阅读第2章、第4章的4.3小节以及4.4小节。
在阅读第4章需要记住如下要点:
(1) 系统调用返回负数表示失败;返回0或者正数表示成功。
(2) 用户执行系统调用的时候,系统调用函数需要的参数放在 寄存器a0到a5中(有可能比这多,但本次实验只用到了a0),系统调用编号放在 寄存器a7中。注意!!!!我们这里不需要考虑系统调用函数参数多域寄存器的问题。。。
(3) 在本次实验中我们可以通过函数argint、argaddr。其中artint以整数形式保存,artaddr以指针形式保存。
argint和argaddr函数都是以argraw为基础来实现的。具体函数内容在文件kernel/syscall.c 中。这个函数如果你没做过更改,只能获取到寄存器a0、a1、a2、a3、a4以及a5的值。
(4) 系统调用号通过寄存器a7获得。
(5) 内存拷贝函数 copyout 用于将内核数据复制到用户提供的地址。
1.1 进程概述小结
如图是xv6操作系统的进程虚拟地址分配图。总共分为如下几段:
程序段(user text)
:存储用户的应用程序代码。数据段(user data)
:已初始化的全局变量和静态变量,通常是可读写的。一般来说还包括一个BSS段,用来存储未初始化的全局变量和静态变量,程序开始执行时,这些变量会被初始化为0。用户栈段(user stack)
:用于函数调用的临时数据,包括函数参数、返回地址、本地变量等。在linux系统可以用" ulimit -s "查看当前栈大小限制。堆段(heap)
:动态内存分配区域,用于在运行时分配的内存(比如通过 C语言中malloc 函数或者C++的关键字new)。trapframe
:该部分虚拟地址用于存储从内核空间返回到用户空间时保存和恢复寄存器的值。在该实验中,空间大小为一页。trampoline
:该部分虚拟地址存储了用于从用户空间到内核空间转换的汇编代码,所有进程该部分虚拟地址都指向相同的物理地址。
相关指令:
ecall
: 进程通过该指令进行系统调用,该指令提升硬件特权级别,并将程序计数器(PC)更改为内核定义的入口点,入口点的代码切换到内核栈,执行实现系统调用的内核指令。
sret
: 当系统调用完成时,内核切换回用户栈,并通过调用sret指令返回用户空间,该指令降低了硬件特权级别,并在系统调用指令刚结束时恢复执行用户指令。
1.2 XV6启动流程小结
第一步,机器模式,xv6上电运行一个存储在只读内存中的引导加载程序来将xv6内核程序加载到内存中。在机器模式下,具体是从_entry
(kernel/entry.S:6
)开始运行xv6。此时,MMU处于禁用模式,虚拟地址直接映射物理地址。由于0x0:0x80000000
包含了I/O设备,加载程序将xv6内核程序加载到地址为0x80000000
的内存中。此外,_entry
的指令设置了一个栈区,这样xv6就可以运行C代码,调用C代码的start
函数执行一些仅在机器模式下允许的配置,便回到管理模式。
函数start执行的功能:
在寄存器mstatus中将先前的运行模式改为管理模式
,并将main函数的地址写入寄存器mepc,即将返回地址设为main,同时通过向页表寄存器satp写入0来在管理模式下禁用虚拟地址转换,并将所有的中断和异常委托给管理模式。此外,在进入管理模式之前,start
函数对时钟芯片进行编程以产生计时器中断。处理完成后,start
通过调用mret“返回”到管理模式。这将导致程序计数器(PC)的值更改为main(kernel/main.c:11)
函数地址。
第二步,管理模式,通过指令mret
从机器模式返回管理模式,在main(kernel/main.c:11)
初始化几个设备和子系统后,便通过调用userinit (kernel/proc.c:212)
创建第一个进程,第一个进程执行一个用RISC-V程序集写的小型程序:initcode. S (user/initcode.S:1)
,它通过调用exec
系统调用重新进入内核。exec
用一个新程序(本例中为 /init
)替换当前进程的内存和寄存器。一旦内核完成exec
,它就返回/init
进程中的用户空间。
1.1 trace 实验
步骤1:
### 跟第一章一样,首先在Makefile文件中的UPROGS中添加“$U/_trace\”
步骤2:
### 在user/user.h文件中添加系统调用原型 “int trace(int);”,如下图所示,调用原型可根据“trace.c”中的trace函数调用示例,进行推断。
步骤3:
### 添加存根,在文件user/usys.pl文件中添加存根,仿照其文件内容,添加“entry("trace");”
### 添加系统调用编号,在文件kernel/syscall.h文件中添加,需要添加一个未被使用的系统调用编号,我添加为“#define SYS_trace 22”
至此为止,你使用“make qemu”命令应该可以正常编译不报错了。
后续编写思路:
1、首先需要在进程的结构体中添加一个成员变量,用来表示需要追踪的系统调用函数,在kernel/proc.h文件中的struct proc
添加成员变量; 同时注意到在fork子进程中也需要修改这部分的代码,这部分在代码在kernel/proc.c文件的fork
函数中
2、实现trace
函数,trace
系统调用函数功能就是将用户指定跟踪的系统调用函数系统编号,添加进特定进程中。这里就是实现trace
函数的系统调用函数sys_trace
,直接在kernel/sysproc.c实现该函数。
3、在kernel/syscall.c文件中修改syscall
函数,因为所有系统调用函数最终都会通过该函数来执行,当syscall
检查到系统调用函数是指定需要追踪的系统调用函数时,就会按照题目要求输出特定信息。
后续步骤可直接参考—> 资料
1.2 sysinfo实验
步骤1:
### 跟第一章一样,首先在Makefile文件中的UPROGS中添加“$U/_sysinfotest\”
步骤2:
### 在user/user.h文件中添加系统调用原型 “int sysinfo(struct sysinfo *);”,如下图所示,调用原型可根据“trace.c”中的trace函数调用示例,进行推断。
步骤3:
### 添加存根,在文件user/usys.pl文件中添加存根,仿照其文件内容,添加“entry("sysinfo");”
### 添加系统调用编号,在文件kernel/syscall.h文件中添加,需要添加一个未被使用的系统调用编号,我添加为“#define SYS_sysinfo 23”
至此为止,你使用“make qemu”命令应该可以正常编译不报错了。
后续编写思路:
1、在kernel/kalloc.c文件中添加一个函数用于获取空闲内存量,思路为:
在改文件中,内存是用链表进行管理的,因此遍历kmem中的空闲链表就能够获取所有的空闲内存。同时需要再kernel/defs.h文件中添加函数声明。
2、在kernel/proc.c文件中遍历proc数组,统计处于活动状态的进程。同时需要再kernel/defs.h文件中添加函数声明。
3、在kernel/sysproc.c文件中添加系统调用函数“sys_sysinfo”的实现即可。并在kernel/syscall.c文件添加函数声明。
后续步骤可直接参考—> 资料
1.3 小结
小结:所有在user/user.h文件中声明的系统调用函数都会在user.pl文件中进行注册,同时在内核空间会进行相对应的函数实现。在进行编译之后,当用户调用相关函数后,便会从用户空间陷入到内核空间,实现系统调用。