lab0:环境配置
参照https://github.com/weijiew/everystep/blob/master/src/6.S081/0-summary.md
lab1: Util
值得注意的知识:
1. 文件描述符0通常是进程用来作为输入的,标准输入
文件描述符1通常是进程用来作为输出的(也就是console的输出文件符),标准输出
文件描述符2通常是进程用来作为输出错误的,标准错误
2. 新分配的文件描述符总是当前进程中编号最小的未使用描述符
3. >重定向运算符背后的解析逻辑就是将当前子进程的文件描述符数组的1号位替换为指定文件的描述符。通过 N > &M 的形式,其中 N 和 M 是文件描述符的数字标识符,可以将文件描述符 N 重定向到与文件描述符 M 相同的目标。这样,N 和 M 将引用同一个目标,并共享读写位置和其他属性。重定向的实质相当于两个文件描述符指向同一个文件。
4.分清所谓用户态程序和系统调用之间的区别,用户态下可用的系统调用和功能函数都在user/user.h头文件中。
5.添加用户态程序,需要在makefile中的UPROGS字段中添加,这是注册,以便xc6的命令行中识别执行。
值得注意的函数:
1.fork():
创建了一个新的进程,其内存内容与调用进程(称为父进程)完全相同,称其为子进程
int pid = fork();
if (pid > 0) {...}
else if(pid == 0){...}
//fork() 在父进程中返回子进程的PID号,在子进程中返回0,失败返回-1。
2.exec() :
根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
char* argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0; //标识参数结束
exec("/bin/echo", argv);
//echo就是直接打印到控制台输入的参数,输入的参数是"echo" "hello" "0"
//但是第一个参数自动忽略,因为大多数参数数组argv[0]是程序名。
printf("exec error\n");
3.xv6的shell函数:
主循环使用getcmd函数,getcmd函数从用户的输入中读取一行,然后调用fork创建一个shell进程的副本。父进程调用wait
,子进程执行命令。
int main(void)
{
static char buf[100];
int fd;
// 确保默认的三个文件描述符打开,
while((fd = open("console", O_RDWR)) >= 0){
if(fd >= 3){
close(fd);
break;
}
}
// 循环阻塞等待用户向shell输入命令
while(getcmd(buf, sizeof(buf)) >= 0){
if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){ //对cd命令特殊处理
// Chdir must be called by the parent, not the child.
buf[strlen(buf)-1] = 0; // chop \n
if(chdir(buf+3) < 0) // 在父进程中执行cd命令,进行工作目录的切换
fprintf(2, "cannot cd %s\n", buf+3);
continue;
}
if(fork1() == 0) // fork一个子进程
runcmd(parsecmd(buf)); //父进程等待子进程。
wait(0);
}
exit(0);
}
4.read()/write()
int read(fd,buf,n): 从
文件描述符fd读取最多n字节,将它们复制到buf,并返回读取的字节数,引用文件的每个文件描述符都有一个与之关联的偏移量。
int
write(fd,buf,n):
将buf中的n字节写入文件描述符,并返回写入的字节数
5.open()/close():
int open(const char *pathname, int flags);
第一个参数pathname是指向想要打开的文件路径名,或者文件名。我们需要注意的是,这个路径名是绝对路径名。文件名则是在当前路径下的。
flags参数表示打开文件所采用的操作,我们需要注意的是:必须指定以下三个常量的一种,且只允许指定一个: O_RDONLY:只读模式 。 O_WRONLY:只写模式。O_RDWR:可读可写。选用的常量和上面的必选项进行按位或起来作为flags参数。O_APPEND 表示追加,如果原来文件里面有内容,则这次写入会写在文件的最末尾。O_CREAT 表示如果指定文件不存在,则创建这个文件。等等
int close(int fd);要关闭的文件描述符
6.dup():
dup
系统调用复制一个现有的文件描述符,返回一个引用自同一个底层I/O对象的新文件描述符, 现有的文件描述符与返回的新的文件描述符指向同一个文件。
7.pipe():
管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞。管道只能用于具有关系的进程之间的通信
int p[2];
pipe(p);
// p数组接收创建得到的管道的输入输出文件描述符号。 p0用于读,p1用于写。
// 假设父进程此时只打开了0,1,2三个文件描述符,那么p0和p1分别占用3和4号描述符
在fork
之后,父子进程都有指向管道的文件描述符。可以把管道想象成一个公共文件,fork之后附近的p[0],子进程的p[0]都指向pipe的读端,父子进程的p[1]均指向pipe的写端。
8.atoi(): 字符串转整数
int atoi(const char *s){
int n=0;
while('0' <= *s && *s <= '9')
//每次处理一个字符,n每次乘10进一位,然后*s-'0'计算出当前字符代表数字几
n = n*10 + *s++ - '0';
return n;
}
lab1:until:
1.实现sleep
2.实现pingpong,注意%d: recived ping中的冒号,要完全符合。
lab2: syscall
值得注意的知识:
1. 用户态=用户模式 核心态=管理模式
2. 宏内核:整个操作系统都驻留在内核中,所有系统调用的实现都以管理模式运行
微内核:最大限度地减少在管理模式下运行的操作系统代码量,并在用户模式下执行大部分操作系统。
Xv6是作为一个宏内核实现
3. Xv6为每个进程维护一个单独的页表,定义了该进程的地址空间。如下图:从下(虚拟地址0)到上(虚拟地址MAXVA)。VA(虚拟地址),stack(栈),heap(堆)。trampoline(用于在用户和内核之间切换),trapframe(状态保存)
RISC-V上的指针有64位宽;硬件在页表中查找虚拟地址时只使用低39位;xv6只使用这39位中的38位。因此,最大地址是2^38-1=0x3fffffffff,即MAXVA
4.实现系统调用:
用户态程序与系统调用的实现方式不同,系统调用的具体实现逻辑在内核内部,需要增添修改很多。一个用户态程序虽然复杂,但是均由系统调用组成。
添加一个系统调用必须的步骤:
1.系统调用必须提供用户态下的调用接口,所以必须在user.h文件下添加用户态函数原型。(user.h中包含所有的系统调用的函数原型)。
2.在usys.pl中添加一个stub(存根),usys.pl会用来生成usys.S文件(专门用来生成汇编代码段来将对应的系统调用号读入a7寄存器)。
3.在kernel/syscall.h文件中加入一个新的系统调用号(内核需要借助这个调用号索引到对应的内核例程)如下所示:
#define SYS_fork 1
#define SYS_exit 2
........
4. 在kernel/syscall.c文件中的syscall中加入从系统调用号到函数的索引关系,即上一步中索引号到内核例程的对应。
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
......}
5. 最后在sysproc文件中实现对应的系统调用,完成功能。
值得注意的函数:
lab2 syscall:
1.trace:
实现一个名为trace的系统调用,使之能够追踪当前进程调用的其他系统调用函数,将相关信息打印出来。比如输入trace 32 grep hello README:
$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
trace的后面的数字是要跟踪的系统调用的掩码(mask = 1 << 系统调用号),假设跟踪read系统调用,查找发现read的系统调用号是5(在kernel/syscall.h存放所有的系统调用号),则mask = 1 << 5 = 32(1 左移5位 为10000 = 32)。则trace后面跟着32,在之后的grep hello README则是具体执行的指令,trace 32 grep hello README含义就是在执行grep hello README时,最终都有什么系统调用使用了read系统调用。
注意如果向跟踪所有的系统调用,mask = 2147483647 (即2的31次方,将所有31个低位置为1,跟踪所有系统调用)。
内容解析:
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
int i;
char *nargv[MAXARG]; //MAXARG 32
if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){ //参数个数不小于3个,确保系统调用号是合法数字
fprintf(2, "Usage: %s mask command\n", argv[0]);
exit(1);
}
if (trace(atoi(argv[1])) < 0) {//将第一个参数转换为整数,作为系统调用号传入trace函数
fprintf(2, "%s: trace failed\n", argv[0]);
exit(1);
}
for(i = 2; i < argc && i < MAXARG; i++){//nargv数组持有要追踪的命令,排除了程序名argv[0],系统调用号argv[1]
nargv[i-2] = argv[i];
}
exec(nargv[0], nargv); //执行命令完成系统调用过程追踪
exit(0); //例如trace 32 grep hello README,trace是程序名,32是mask = 1 << SYS_X,grep hello README在这个命令执行期间进行追踪
}
2.argint, argaddr,argstr
用户态向内核态传递参数需要使用一些函数(argint, argaddr,argstr等)从trapframe中将用户态下传递的参数读取到内核态。argint, argaddr等最后都是调用argraw函数。
用户态的系统调用函数有参数,参数为了功能的实现而设置,如user/user.h中的int write(int, const void*, int); 有三个参数。
内核态下的系统调用函数,都是没有参数的(kernel/sysproc.c),因为它们的参数都是从用户态下读进来的。原因是用户态直接传递的数据不可信,需要内核态使用函数(如argitn.argaddr.argraw)自己读取。
argraw: 用户空间向内核空间传递的系统调用参数存储在当前进程p的p->trapframe->a0~p->trapframe->a5寄存器中
// 以64位无符号整数返回寄存器a0-a7中的值
// 当n大于5的时候,函数错误,陷入panic
static uint64
argraw(int n)
{
struct proc *p = myproc();
switch (n) {
case 0:
return p->trapframe->a0;
case 1:
return p->trapframe->a1;
case 2:
return p->trapframe->a2;
case 3:
return p->trapframe->a3;
case 4:
return p->trapframe->a4;
case 5:
return p->trapframe->a5;
}
panic("argraw");
return -1;
}
: argint: 用来获取第n个整型的系统调用参数
// argint函数,有2个参数,第一个参数代表对应系统调用函数的第几个参数。
// 例如我们将来传递进来的那2个参数分别是oldfd和newfd。
// oldfd是第一个,于是argint函数中的0代表了读取oldfd。1代表读取newfd。
int argint(int n, int *ip)
{
*ip = argraw(n);
return 0;
}
argaddr:用来获取第n个地址类型参数
int argaddr(int n, uint64 *ip)
{
*ip = argraw(n);
return 0;
}
argstr:获取string参数同上。
syscall:
// 判断mask的低num位是否位1,若为1,就打印出第num个系统调用函数名字和返回值,进程pid等信息。
// 其中num = p->trapframe->a[7]。用&运算判断对应的位是否同为1
void syscall(void) {
int num;
struct proc *p = myproc();
// 系统调用编号,num值就是当前进程要调用的系统调用函数的编号,它存放在p->trapframe->a7中
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num](); // 执行系统调用,然后将返回值存入a0,系统调用的返回值存放在了p->trapframe->a0中
// 系统调用是否匹配,用&运算判断对应的位是否同为1
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;
}
}
myporc():当前进程的进程结构体
2.Sysinfo
xv6将空闲内存块本身串成了一个链表来管理
kalloc:从一个空闲链表中取一块空闲内存,稍作初始化之后返回
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
// 声明一个run类型的指针r
struct run *r;
// 在从空闲链表中获取空闲内存块时,首先加一把自旋锁
acquire(&kmem.lock);
r = kmem.freelist;
// 如果r不为空,则空闲链表还没有到达结尾
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
// 随意填一些数据将当前页面填满
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
// 返回新申请的页面
return (void*)r;
}
kfree:将空闲页面使用“头插法”插入回空闲列表
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
// 此函数一次释放一整个页面(4096bytes)大小的内存
void
kfree(void *pa)
{
struct run *r;
// 如果pa不能被页面大小整除,或范围超出合法边界,则陷入panic
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
// 随便填一些数字将准备收回的页面填充
memset(pa, 1, PGSIZE);
// 接下来这段代码就是使用“头插法”来将空闲节点插回链表
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}