BUAA OS LAB4 实验笔记目录
源码移步GitHub https://github.com/FFChyan/BUAA_OSLAB
BUAA OS LAB4 实验笔记
lab4-1
lab4前半部分的主题是系统调用。
user文件夹里的函数是用户态的,lib是内核态的。
系统调用的核心文件
- user/syscall_lib.c --> 用户态的系统调用接口
- user/syscall_wrap.S --> 执行特权指令syscall的汇编
- lib/syscall.S --> 内核的系统调用中断入口
- lib/syscall_all.c -->具体的系统调用的实现
系统调用的流程
- 调用一个封装好的用户空间的库函数(如writef)
- 调用用户空间的syscall函数,在syscall_lib.c里
- 调用msyscall,函数的第一个参数是系统调用号,用于陷入内核态。源码如下:
- 调用msyscall
// lib.h
extern int msyscall(int, int, int, int, int, int); // 有六个参数
- 第一个参数是系统调用号,其值位于unistd.h
// unistd.h
#ifndef UNISTD_H
#define UNISTD_H
#define __SYSCALL_BASE 9527
#define __NR_SYSCALLS 20
#define SYS_putchar ((__SYSCALL_BASE ) + (0 ) )
#define SYS_getenvid ((__SYSCALL_BASE ) + (1 ) )
#define SYS_yield ((__SYSCALL_BASE ) + (2 ) )
#define SYS_env_destroy ((__SYSCALL_BASE ) + (3 ) )
#define SYS_set_pgfault_handler ((__SYSCALL_BASE ) + (4 ) )
#define SYS_mem_alloc ((__SYSCALL_BASE ) + (5 ) )
#define SYS_mem_map ((__SYSCALL_BASE ) + (6 ) )
#define SYS_mem_unmap ((__SYSCALL_BASE ) + (7 ) )
#define SYS_env_alloc ((__SYSCALL_BASE ) + (8 ) )
#define SYS_set_env_status ((__SYSCALL_BASE ) + (9 ) )
#define SYS_set_trapframe ((__SYSCALL_BASE ) + (10 ) )
#define SYS_panic ((__SYSCALL_BASE ) + (11 ) )
#define SYS_ipc_can_send ((__SYSCALL_BASE ) + (12 ) )
#define SYS_ipc_recv ((__SYSCALL_BASE ) + (13 ) )
#define SYS_cgetc ((__SYSCALL_BASE ) + (14 ) )
#endif
- 除此之外msyscall 函数还有5 个参数,这些参数是系统调用实际需要使用的参数。
6个参数的存储和传递:
前4 个参数会被syscall 开头的函数分别存入 a 0 − a0- a0−a3($4~$7) 寄存器。
同时栈帧底部保留16 字节的空间。
后2 个参数只会被存入在前4 的参数的预留空间之上的8 字节空间内。
- 陷入内核,处理器将PC 寄存器指向一个相同的内核异常入口。内核取得信息,执行对应的内核空间的系统调用函数 (sys_*)
首先
// syscall_wrap.S
LEAF(msyscall)
// TODO: execute a `syscall` instruction and return from msyscall
move v0, a0 // 传递系统调用号,a0是第一个用来保存参数的寄存器,也就是保存的是系统调用号
syscall // 执行相应系统调用
jr ra
nop
END(msyscall)
syscall.S
其次,用户态函数—>调用系统函数,此处handle_sys
函数是用户态和内核态的交接。
大体流程是
- 保存用户态寄存器到栈中(之后要访问用户态的寄存器必须通过栈顶+偏移,即形如
(TF_REG4)sp
) - 找到系统调用的入口
- 传参(将参数放到内核态的栈顶,并将前四个参数放到a0~a3,后两个在内核态的栈中)
- 跳转
- 返回,并将返回值传给用户态
- 还有一些pc+4之类的小操作
// syscall.S
#include <asm/regdef.h>
#include <asm/cp0regdef.h>
#include <asm/asm.h>
#include <stackframe.h>
#include <unistd.h>
// 此处的栈指针是内核空间的栈指针
NESTED(handle_sys,TF_SIZE, sp)
// 把用户态的所有寄存器存进栈帧,sp相当于栈顶
// 之后要访问用户态的寄存器必须通过栈顶+偏移,即(TF_REG4)sp,像这样
SAVE_ALL // Macro used to save trapframe
CLI // Clean Interrupt Mask /* 用于屏蔽中断位的设置的汇编宏*/
nop
.set at // Resume use of $at
// TODO: Fetch EPC from Trapframe, calculate a proper value and store it back to trapframe.
/* TODO: 将Trapframe的EPC寄存器取出,计算一个合理的值存回Trapframe中*/
// PC + 4
lw t1,TF_EPC(sp)
addiu t1, 4
sw t1, TF_EPC(sp)
// TODO: Copy the syscall number into $a0.
/* TODO: 将系统调用号“复制”入寄存器$a0 */
lw a0, TF_REG2(sp) // v0 is $2
addiu a0, a0, -__SYSCALL_BASE /* a0 <- “相对”系统调用号*/ // a0 <- relative syscall number
sll t0, a0, 2 /* t0 <- 相对系统调用号* 4 */ // t0 <- relative syscall number times 4
la t1, sys_call_table /* t1 <- 系统调用函数的入口表基地址*/ // t1 <- syscall table base
addu t1, t1, t0 /* t1 <- 特定系统调用函数入口表项地址*/ // t1 <- table entry of specific syscall
lw t2, 0(t1) /* t2 <- 特定系统调用函数入口函数地址*/ // t2 <- function entry of specific syscall
lw t0, TF_REG29(sp) /* t0 <- 用户态的栈指针*/ // t0 <- users stack pointer
// 第5,6两个参数从用户栈中取
lw t3, 16(t0) /* t3 <- msyscall的第5个参数*/ // t3 <- the 5th argument of msyscall
lw t4, 20(t0) /* t4 <- msyscall的第6个参数*/ // t4 <- the 6th argument of msyscall
// TODO: Allocate a space of six arguments on current kernel stack and copy the six arguments to proper location
/* TODO: 在当前栈指针分配6 个参数的存储空间,并将6 个参数安置到期望的位置*/
// 从a0~a3中取出前四个参数
lw t5, TF_REG4(sp) // a0 is $4
lw t6, TF_REG5(sp) // a1 is $5
lw t7, TF_REG6(sp) // a2 is $6
lw t8, TF_REG7(sp) // a3 is $7
addiu sp,sp,-20
// 将这6个参数都放到新的栈里
sw t5, (sp) // 参数1,a0
sw t6, 4(sp) // 参数2,a1
sw t7, 8(sp) // 参数3,a2
sw t8, 12(sp) // 参数4,a3
sw t3, 16(sp) // 参数5,从用户栈中取出的
sw t4, 20(sp) // 参数6,同上
// 将前四个参数放进内核态的寄存器a0~a3,后两个继续在16(sp)和20(sp)中
move a0, t5
move a1, t6
move a2, t7
move a3, t8
jalr t2 /* 调用sys_*函数*/ // Invoke sys_* function
nop
// TODO: Resume current kernel stack
/* TODO: 恢复栈指针到分配前的状态*/
addu sp, 20
// Store return value of function sys_* (in $v0) into trapframe
/* 将$v0中的sys_*函数返回值存入Trapframe,此时已经从系统调用中返回,得到return的结果在v0中,需要存入用户态的2号寄存器 */
sw v0, TF_REG2(sp)
// Return from exeception
/* 从异常中返回(恢复现场) */
j ret_from_exception
nop
END(handle_sys)
sys_call_table: /* 系统调用函数的入口表*/ // Syscall Table
.align 2
.word sys_putchar
.word sys_getenvid
.word sys_yield
.word sys_env_destroy
.word sys_set_pgfault_handler
.word sys_mem_alloc
.word sys_mem_map
.word sys_mem_unmap
.word sys_env_alloc
.word sys_set_env_status
.word sys_set_trapframe
.word sys_panic
.word sys_ipc_can_send
.word sys_ipc_recv
.word sys_cgetc
syscall_all.c
具体系统调用的实现。
int sys_mem_alloc()
功能:分配内存。用户程序可以通过这个系统调用给 该程序所允许的虚拟内存空间 显式地分配实际的物理内存。
步骤:
- envid2env,找到进程
- page_alloc,新开一页
- page_insert,把页给进程
int sys_mem_map()
功能:将源进程地址空间中的相应内存映射到目标进程的相应地址空间的相应虚拟内存中去。换句话说,此时两者共享着一页物理内存。
步骤:
- 找到源env和目标env
ret = envid2env(srcid, &srcenv, 1);
if(ret != 0) return -E_BAD_ENV;
ret = envid2env(dstid, &dstenv, 1);
if(ret != 0) return -E_BAD_ENV;
- 获取srcva映射的页
if ((ppage = page_lookup(srcenv -> env_pgdir, round_srcva, &ppte)) == 0)
return -E_UNSPECIFIED;
- 给目标env
ret = page_insert(dstenv->env_pgdir, ppage, round_dstva, perm);
if(ret != 0) return -E_NO_MEM;
int sys_mem_unmap()
步骤:
- 找到对应env
- 直接调用page_remove的unmap功能
page_remove(env->env_pgdir, va);
void sys_yield()
// 类似于env_destroy,保存kernel_sp中的Trapframe,随后执行sched_yield;
struct Trapframe *src = (struct Trapframe *)(KERNEL_SP - sizeof(struct Trapframe));
struct Trapframe *dst = (struct Trapframe *)(TIMESTACK - sizeof(struct Trapframe));
bcopy((void *)src, (void *)dst, sizeof(struct Trapframe));
sched_yield();
进程间通信
void sys_ipc_recv(int sysno, u_int dstva)
{
if(dstva >= UTOP) return;
// 首先要将env_ipc_recving 设置为1, 表明该进程准备接受其它进程的消息了
curenv->env_ipc_recving = 1;
// 之后阻塞当前进程,即将当前进程的状态置为不可运行
curenv->env_status = ENV_NOT_RUNNABLE;
// env_ipc_dstva 则说明了接收到的页需要被映射到哪个虚地址上
curenv->env_ipc_dstva = dstva;
LIST_REMOVE(curenv, env_sched_link);
// 之后放弃CPU(调用相关函数重新进行调度)
sys_yield();
}
int sys_ipc_can_send(int sysno, u_int envid, u_int value, u_int srcva,
u_int perm)
{
int r;
struct Env *e;
struct Page *p;
Pte *ppte;
perm |= PTE_V;
// 检查地址
if (srcva >= UTOP || srcva < 0) return -E_INVAL;
// 检查进程号是否正确
if (envid2env(envid, &e, 0) < 0) return -E_INVAL;
// 检查接收进程是否接收
if (e->env_ipc_recving == 0) return -E_IPC_NOT_RECV;
if (srcva != 0) {
p = page_lookup(curenv->env_pgdir, srcva, &ppte);
if((*ppte & PTE_R)==0 && (perm & PTE_R))
printf("Permission Denied\n");
if((r = page_insert(e->env_pgdir, p, e->env_ipc_dstva, perm) < 0)){
printf("page insert failed in ipc_send");
return r;
}
}
e->env_ipc_perm = perm;
e->env_ipc_recving = 0;
e->env_status = ENV_RUNNABLE;
e->env_ipc_value = value;
e->env_ipc_from = curenv->env_id;
LIST_INSERT_HEAD(&env_sched_list[0], e, env_sched_link);
return 0;
}
lab4-2
lab4的后半部分是fork。大概要做的工作就是为子进程的创建提供“如何创建”,“如何安排子进程与父进程不一样的特性”,作为一个子进程或者父进程需要自觉调用的函数。然后在fktest里真实创建。
fork()
fork函数的大概流程如下。
根据fork()这个函数中的函数顺序,流程大致如上。几个关键步骤详述如下:
创建子进程以及子进程的返回值
创建子进程的函数是sys_env_alloc()
,需要做的事情是:
- alloc一个进程(此时
status==ENV_RUNNABLE
),并且这个进程的sp指向USTACKTOP
- 将进程的状态设置为
ENV_NOT_RUNNABLE
- 将栈KERNEL_SP中的上下文搬到进程的tf中
- 设置pc,v0(返回值),优先级
此处的返回值是2号寄存器,v0,设置为0
子进程获得他的parent_id
if((newenvid = syscall_env_alloc()) == 0){
// 子进程进入到这个if,然后找到自己是谁。
env = &envs[ENVX(syscall_getenvid())];
env->env_parent_id = parent_id;
return 0; // 子进程 return 0
}
在fork.c的fork()函数中,只有子进程可以进入这个if,然后因为parent_id是static,所以会获得父进程之间get到的id,其实就是父进程的id。
写时复制
子进程在写时,需要将相应的内容拷贝一份到自己的页目录,然后在自己的页里写,而父进程那里不变。所以需要保护位PTE_COW
,用于标志可写页。其实就是写时复制。
在函数fork()中,先判断父进程的这个页是否有效,如果连父进程都不存在这个页了,子进程就更加不用写时复制了。相关代码片如下:
for(i = 0;i < USTACKTOP;i += BY2PG)
{
// 页目录的入口 // 页表的入口
if(((*vpd)[VPN(i)/1024]) != 0 && ((*vpt)[VPN(i)]) != 0)
// 如果父进程中这个页目录有效,且这个页表项有效,再duppage这个页
{
duppage(newenvid, VPN(i));
}
}
在函数duppage()中,分情况处理这个页。
情况有以下几种:
- 有效共享可写
- 有效不共享可写
- 不有效,或者不可写
对于第一种,共享可写,就设置有效位PTE_LIBRARY,表示共享。
对于第二种,不共享,那么子进程如果要写就必须复制一份,即写时复制位PTE_COW。
对于第三种,不有效,或者只读,就保留自己原先就有的一些有效位perm不变。
有效位的设置对于子进程和父进程都要执行。(我的理解是,如果父子进程共享一个页,那么谁写谁复制,所以二者都需要写时复制有效位。)
异常处理的栈
写时复制是依赖于缺页中断实现的。中断处理函数是handle_mod。发生中断时,调用汇编函数except_vec3。
mfc0 k1,CP0_CAUSE
la k0,exception_handlers
andi k1,0x7c
addu k0,k1
lw k0,(k0)
NOP
jr k0
nop
在这几句中,先加载异常处理函数数组exception_handlers
的首地址到k0,然后用andi取CAUSE寄存器中表示原因的5位给k1,又因为0xc自带末尾两个0,所以不需要左移,直接k0+k1,其实就是数组首地址+偏移量了。也就是addu这句之后的k0,表示对应异常处理函数所在位置的索引,然后lw
取其上的内容,应该存的是异常处理函数的地址,然后jr跳转到相应函数去执行中断处理流程。
以及还有一个函数page_fault_handler()
,在全文中只有一处汇编有调用的痕迹:
BUILD_HANDLER mod page_fault_handler cli
所以这个函数肯定和mod有关。也就是和写时复制时的缺页中断有关。
在先前,设置父子进程的异常处理栈和入口的时候已经给异常处理开辟一个新的栈,与平时在用的普通的栈不同,cscore上的解释是“因为发生缺页错误的也可能是正常堆栈的页面……我们把这个堆栈称作异常处理栈,它的栈顶对应的是宏UXSTACKTOP”。
在page_fault_handler()
这个函数中,首先他将当前进程的tf(就是上下文),复制到PgTrapFrame
中,这是一个临时变量。然后判断当前进程的异常处理栈顶的位置。
如果栈顶指针(tf_regs[29])在异常处理栈的最上面一个BY2PG的空间内,也就是最上面一页,那就继续向下进栈。
if(tf->regs[29] >= (curenv->env_xstacktop - BY2PG) && tf->regs[29] <= (curenv->env_xstacktop - 1))
tf->regs[29] = tf->regs[29] - sizeof(struct Trapframe);
否则(这种否则的情况只能是上下文进栈已经超了一个BY2PG的数据量,因为从一开始就是初始化栈顶xstacktop的,所以不可能会大于xstacktop)就重新回到栈顶,然后分配一个trapframe大小的空间。
tf->regs[29] = curenv->env_xstacktop - sizeof(struct Trapframe);
也就是异常处理的栈始终只能占据异常处理栈的栈顶一个页的空间,不可再向下开空间,因为第二个页要放别的东西,具体见下文。
真正进行处理的函数:user/fork.c 中的pgfault
cscore上对pgfault的安排如下:
- 判断页是否为写时复制的页面,是则进行下一步,否则报错
- 分配一个新的内存页到临时位置,将要复制的内容拷贝到刚刚分配的页中
- 将临时位置上的内容映射到发生缺页中断的虚拟地址上,然后解除临时位置对内存的映射
这个函数不需要我们自己写,它和duppage很像。我觉得也是在准备关于写时复制的一些机制。
在这个函数中的temp是一个临时的栈顶指针,他指向的是异常处理的栈的栈顶之下的第二个BY2PG大小的页。第一个页如上所述是用来存储异常发生时的寄存器的。
也是分几种情况,首先是可写且有写时复制保护位,那么进行写时复制操作。否则不可写,或者没有写时复制保护位,就panic。
实现写时复制:
- 首先给temp这个虚拟地址,分配一页物理内存,用系统函数
syscall_mem_alloc
。 - 然后将虚拟地址va的内容拷贝到temp上。
- 然后将temp和va的两个虚拟地址,都指向新开的temp的这个物理地址上,用系统函数
syscall_mem_map
。 - 然后让虚拟地址temp与他的物理页unmap。这一点说明temp真的只是个中转页。
- 最后,只有va指向新开的这一页了。va是原先的那个虚拟地址,指向新的物理页,这一页存的是原先的内容,但是是不一样的物理页了。也就是说,进程在写东西的时候,没改动最原先的物理页上的东西,但它看到的内容还是原先的那些内容,而且他的虚拟地址好像也没变……