赵岳 14231027
move v0,a0
sw a0,0(sp)
sw a1,4(sp)
sw a2,8(sp)
sw a3,12(sp)
syscall
jr ra
mysyscall主要完成的功能是,设置syscall的各种参数,包括系统调用号(传入v0),和其他参数。由于寄存器的个数限制,只是将a0~a3四个参数使用寄存器进行传递,其余的参数使用栈传递。因此,结合注释,不难写出如上代码。值得注意的是,始终需要记得填写.s汇编函数时,要使用jr ra返回。
实现 lib/syscall_all.c 中的 void sys_ipc_recv(int sysno,u_int dstva) 函数和 int sys_ipc_can_send(int sysno,u_int envid, u_int value, u_int srcva, u_int perm) 函数。
void sys_ipc_recv(int sysno,u_int dstva)
{
if ((unsigned int)dstva >= UTOP || dstva != ROUNDDOWN(dstva, BY2PG)){
return;
}
curenv->env_ipc_dstva = dstva;
curenv->env_ipc_recving = 1;
curenv->env_status = ENV_NOT_RUNNABLE;
sys_yield(); //not sched yield!!
return 0;
}
这个函数的主要目的如下:设置当前进程为可接收的状态,然后使当前运行的进程不可运行,暂停当前进程调度,调度其它进程进入cpu。其中参数 dstva是收到的页面的虚拟地址。暂停当前进程调度后,需要使用进程调度函数重新进行进程调度。值得注意的是,这里我认为应该使用在syscallall.c中给出的sys_yield()函数,而不是我们在lab3中实现的sched_yield函数。实际上,这两个函数的唯一区别就是 bcopy((int)KERNEL_SP-sizeof(struct Trapframe),TIMESTACK-sizeo f(struct Trapframe),sizeof(struct Trapframe));
这一句话。这句代码的目的是将当前进程的运行环境保存到TIMESTACKsizeof(struct Trapframe)。在理论课中我们知道进程的切换很关键的步骤就是保存线程,既然我们需要将cpu交给其他进程,那这个暂时被搁置的进程就不能不管。因此这一步是很关键的
int sys_ipc_can_send(int sysno,u_int envid, u_int value, u_int srcva, u_int perm)
{
struct Env *target;
struct Page *page;
Pte *pte;
int r, ret = 0;
if ((r = envid2env(envid, &target, 0)) < 0)
return -E_BAD_ENV;
if (!target->env_ipc_recving)
return -E_IPC_NOT_RECV;
if (srcva) {
if (((unsigned int)srcva >= UTOP)||(srcva != ROUNDDOWN(srcva, BY2PG))||((page = page_lookup(curenv->env_pgdir, srcva, &pte)) == NULL))
return -E_INVAL;
//illegal address or not rounded or no such a page return -E_INVAL
if (page_insert(target->env_pgdir, page, target->env_ipc_dstva, perm) < 0)
return -E_NO_MEM;
ret = 1; //success = 1
}
target->env_ipc_recving = 0;
target->env_ipc_value = value;
target->env_ipc_from = curenv->env_id;
target->env_ipc_perm = ret? perm : 0 ;
target->env_status = ENV_RUNNABLE; //runnable again
return 0;
}
这个函数的目的是: 尝试向目标进程(env) 'envid'发送'value' ,如果目标进程没有使用sys_ipc_recv请求IPC,那么发送会失败,并且返回-E_PC_NOT_RECV。 否则,发送成功,目标进程的ipc相关的域被按照如下方式设置:env_ipc_recving 被设置为 0 以阻塞之后发送的值,env_ipc_from 被设置为发送者的 envid,env_ipc_value 被设置为'value' 。目标进程被重新设置为可运行的。成功时返回0,错误时返回值小于0。
那么,根据这个详细的步骤,我们不难写出如上代码。当然,目前还存在几个难以回答的问题:
1.sysno参数的作用是什么?该函数中好像并没有使用。
2.难道srcva的作用只是为了判断一下目标地址的正确性吗?
3.在本函数中的ret是什么作用,为了标记进程通信的成功吗?在我的函数中,如果ret成功置1才能对perm进行赋值,否则为0。
最后,仔细理解一下该函数中调用的env2envid函数:
int envid2env(u_int envid, struct Env **penv, int checkperm)
{
struct Env *e;
/* Hint:
* If envid is zero, return the current environment.*/
if (envid == 0) {
*penv = curenv;
return 0;
}
e = &envs[ENVX(envid)];
if (e->env_status == ENV_FREE || e->env_id != envid) {
*penv = 0;
return -E_BAD_ENV;
}
/* Hint:
* Check that the calling environment has legitimate permissions
* to manipulate the specified environment.
* If checkperm is set, the specified environment
* must be either the current environment.
* or an immediate child of the current environment. */
if (checkperm && e != curenv && e->env_parent_id != curenv->env_id){
*penv = 0;
return -E_BAD_ENV;
}
*penv = e;
return 0;
}
这个函数是作用是:传入一个 envid,然后函数会把 envid 所对应的 env 结构体的指针存在传入的 penv参数中。如果出错会返回 -E_BAD_ENV。 那么至此,我们就相对清楚了该函数的作用。我们在ipc_send函数中,我们将&target作为调用该函数的第二个参数,envid作为第一个参数。这里,由于没有看懂checkperm的机制,因此我们直接跳过了checkperm这一阶段,将0传入。最后,我们将该函数的错误返回值作为调用该函数时的错误返回值即可。 至此,进程通信部分已经完成。
子进程完全按照父进程fork之后的代码执行,说明子进程的代码段和父进程的代码段是一致的。他们是同一个程序,只不过是两个不同的进程。然而子进程是“半路杀出”,从一半开始执行,说明子进程虽然和父进程代码一样,可是开始的位置却不同。具体一点,子进程执行的代码的起始位置应该是父进程的epc。这一点在我们的代码中也有体现:child->env_tf.pc = child->env_tf.cp0_epc;
。
C.
int sys_env_alloc(void)
{
struct Env *child;
if (env_alloc(&child, curenv->env_id) < 0)
return -E_NO_FREE_ENV;
bcopy(KERNEL_SP - sizeof(struct Trapframe), &child->env_tf,sizeof(struct Trapframe));
child->env_status = ENV_NOT_RUNNABLE;
child->env_tf.pc = child->env_tf.cp0_epc;
child->env_tf.regs[2] = 0;
return child->env_id;
}
这个函数我认为有如下几点需要注意:
1.首先在申请新的进程控制块时判断一下能否申请新的进程,不能的话返回-E_NO_FREE_ENV,尽管这种情况出现的较少。
2.当前运行进程的现场保存在了(int)KERNEL_SP-sizeof(struct Trapframe)
(这是有证据可循的,比如sys_yield函数),因此我们需要将其拷贝到新创建进程的tf中。
3.将新申请的进程状态标记为env_status = ENV_NOT_RUNNABLE,这也是符合逻辑的,我们只是创建了一个新的进程控制块,其数据、栈、代码等其他还未设置好。
4.在将父进程的运行环境拷贝到子进程后,还需要调整一些东西,首先,使得子进程的调用返回0。由于返回值v0是2号寄存器,因此我们还需要一句child->env_tf.regs[2] = 0
。
5.根据实验前的父子进程fork小实验,我们知道子进程是从父进程fork它的地方继续执行,而不是代码的开头。因此我们要在创建子进程时的pc置为父进程的epc,因此我们需要child->env_tf.pc = child->env_tf.cp0_epc;
6.父子进程在fork函数中envid = sys_env_alloc()
后便“分道扬镳”了,那么我们设置了子进程的返回值为0后,我们还需要管一下父进程。为了区别父子进程,将父进程的返回值设置为子进程的env_id。
如上,不再赘述。
int fork(void)
{
u_int envid;
int pn;
extern struct Env *envs;
extern struct Env *env;
set_pgfault_handler(pgfault);
if((envid = syscall_env_alloc()) < 0)
user_panic("syscall_env_alloc failed!");
if(envid == 0){
env = &envs[ENVX(syscall_getenvid())];
return 0;
}
for(pn = 0; pn < ( UTOP / BY2PG) - 1 ; pn ++){
if(((*vpd)[pn/PTE2PT]) != 0 && ((*vpt)[pn]) != 0)
duppage(envid, pn);
}
if(syscall_mem_alloc(envid, UXSTACKTOP - BY2PG, PTE_V|PTE_R) < 0)
user_panic("In fork! syscall_mem_alloc error!");
if(syscall_set_pgfault_handler(envid, __asm_pgfault_handler, UXSTACKTOP) < 0)
user_panic("In fork! syscall_set_pgfault_handler error!");
if(syscall_set_env_status(envid, ENV_RUNNABLE) < 0)
user_panic("In fork! syscall_set_env_status error!");
return envid;
}
这个函数可以理解为父进程“恋恋不舍地为子进程开辟了闯荡的道路并为其做好出发的准备”。那么我们要做的就是,为子进程设置页面错误的处理函数(我们暂时不需管怎样处理,这个在最后的函数中会写),用sys_env_alloc函数申请一个子进程的进程控制块。然后按页遍历2G的用户空间,如果存在页映射,那就用duppage函数给子进程的页面设置好权限位。调用syscall_mem_alloc函数为子进程申请一个错误栈。最后,将子进程的状态利用syscall_set_env_status设置为可以运行的。至此,子进程就可以正式“出发”了。 这里有一些问题需要注意,首先,我们一再强调了fork是用户态下的函数,那么我们就只能利用大量提供的syscall_x函数来进行一些参数的设置。由于这些syscall函数都是int类型的,所以再调用时进行了一步判断,如果调用错误就会进入user_panic(由于用户态,不能使用panic),方便调试错误。 还有一个非常关键的问题,,我们需要将 _asm_pgfault_handler 设置为子进程的页中断函数入口。我们知道,_asm_pgfault_handler 函数(这是一个汇编函数)将出错的地址提取了出来,并作为参数传递给 pgfault 。也就是说,如果我们将子进程的页中断函数设置为pgfault,那么页中断时,就会直接进入 pgfault 函数。这样的结果就是:从cp0中提取出错的虚地 址,恢复寄存器等工作就没代码来做了。所以,我们需要将_asm_pgfault_handler 设置为子 进程的页中断处理函数。 在后来和其他同学的讨论中,发现错误栈的申请和映射工作可以放在sys_env_alloc函数中进行。但在这里,为了和lab3中env_alloc函数的对应关系,在sys_env_alloc只进行了进程控制块的申请工作,其余fork的额外工作全部在fork函数中实现。
如果仔细阅读上述这一段话, 你应该可以发现, 我们并不是对所有的用户空间页都使用 duppage 进行了保护。那么究竟哪些用户空间页可以保护,哪些不可以呢,请结合 include/mmu.h 里的内存布局图谈谈你的看法。
根据mmu.h中的内存布局,我们可以看到,UTOP是用户空间的极限,duppage肯定为比UTOP小的地址空间服务。其次,我们还注意到,UTOP也叫UXSTACKTOP (#define UXSTACKTOP (UTOP)
)它的下面一页是 exception stack,每个进程的异常栈都是我们单独处理的,因此我认为这一页也不需要duppage。综上,duppage保护的页应该是截至到(UTOP/BY2PG-1)这一页。
static void duppage(u_int envid, u_int pn)
{
u_int perm = ((*vpt)[pn]) & 0xfff;;
if( (perm & PTE_R)!= 0 || (perm & PTE_COW)!= 0){
if(!(perm & PTE_LIBRARY)) {
perm = perm | PTE_V | PTE_R | PTE_COW;
}
else{
perm = perm | PTE_V | PTE_R;
}
if(syscall_mem_map(syscall_getenvid(), pn * BY2PG, envid, pn * BY2PG, perm) < 0)
user_panic("In duppage:mem_map_envid error!");
if(syscall_mem_map(syscall_getenvid(), pn * BY2PG, 0, pn * BY2PG, perm) < 0)
user_panic("In duppage:mem_map_0 error!");
}
else{
if(syscall_mem_map(syscall_getenvid(), pn * BY2PG,envid, pn * BY2PG, perm) < 0)
user_panic("In duppage:mem_map_envid error!");
}
}
这个函数原理较为简单,就是为fork函数填写一个可以正确给出子进程某个页权限位的函数。由于duppage还是在用户态进行的,所以只能使用系统调用提供的函数。我们先利用perm = ((*vpt)[pn]) & 0xfff;
得到当前传入页面的权限位,然后开始判断:如果父进程中的页是可写或COW的,那么再判断一下是否是共享内存的(#define PTE_LIBRARY 0x0004 // share memmory
)如果不是,则COW,否则子进程中该页就不用COW。然后在父子进程中分别调用syscall_mem_map进行页面的映射,均以COW的权限。当然,还存在父进程中那些只读的页,那么只需要不变地将其映射到子进程中即可。
在这里,syscall_mem_map函数的第一个参数,也就是本进程的id的确定值得思考。这里我使用了另一个系统调用来得到本进程函数,但在调试过程中发现传0也可以。猜测可能是由于envid2env函数中,传入0则会得到当前的进程,证据如下:
int envid2env(u_int envid, struct Env **penv, int checkperm)
{
。。。。。。
/* Hint:
* If envid is zero, return the current environment.*/
if (envid == 0) {
*penv = curenv;
return 0;
}
。。。。。。
}
至此,duppage函数就完成了
static void
pgfault(u_int va)
{
int r,i;
va = ROUNDDOWN(va, BY2PG);
if (!((*vpt)[VPN(va)] & PTE_COW ))
user_panic("In pgfault:PTE_COW error!");
if (syscall_mem_alloc(0, PFTEMP, PTE_V|PTE_R) < 0)
user_panic("In pgfault:syscall_mem_alloc error!");
user_bcopy((void*)va, PFTEMP, BY2PG);
if (syscall_mem_map(0, PFTEMP, 0, va, PTE_V|PTE_R) < 0)
user_panic("In pgfault:syscall_mem_map error!");
if (syscall_mem_unmap(0, PFTEMP) < 0)
user_panic("In pgfault:syscall_mem_unmap error!");
}
像前两个函数一样,这个函数还是运行在用户空间的,这对我们函数的实现造成了很大的阻碍。好在丰富的系统调用给了我们足够多的工具。 步骤大致如下:首先判断该虚拟地址对应的页是否是COW的,不是得话则panic一个error(由于用户态,还是需要使用user_panic函数)。然后,在PFTEMP位置申请一个新的物理页,将va这里的页拷贝到这个临时物理页上,最后,把临时位置映射的的物理页映射到va上,并且解除临时位置对内存页的映射。
1.首先,在mmu.h中,我们可以找到如下定义:
extern volatile Pte* vpt[];
extern volatile Pde* vpd[];
查阅资料得知,volatile修饰变量 ,说明这两个变量可能在汇编等C语言之外的地方被改变。那么我们很自然地想到了汇编,因此继续查找.S文件,最终在entry.S发现了这两个变量的定义。
.globl vpt
vpt:
.word UVPT
.globl vpd
vpd:
.word (UVPT+(UVPT>>12)*4)
根据 mmu.h 和 lab3 得知,UVPT是用户空间中页表的位置。那么,vpd就也是页目录的位置。所以,遍历页目录也就是遍历vpd。遍历vpd时,我们将所有分配给内存的物理页映射给子进程。遍历时还需要判断二级页表是否存在,存在则映射。在映射二级页表的同时,二级页表中分配的物理页也需要映射。 二级页表则通过vpt来访问。
2.假设有这样一种需求,我们知道了一个确定的虚拟地址va,需要知道该地址对应的页目录项和页表项的虚拟地址,该如何求?实际上,这个问题的答案就是vpd和vpt。va首先可以拆分为3部分,高10位的PDX,中间10位的PTX,以及低12位的OFFSET。那么要求va对应页目录项的虚拟地址,实际上就是UVPT+(PDX<<2),由于UVPT的低12位是为0的(按页对齐),vpt求法同理。因此:
vpd = UVPT[31:12] | PDX | 00;
vpt = UVPT[31:22] | PDX | PTX | 00;
至于如何使用,那就显而易见了。由于我们的定义方法,vpt里存着UVPT的首地址,所以在lab2中,作如下代换即可:(*vpt)[N] = UVPT[N]
。只不过有一点不同就是lab4我们都是工作在用户空间的,而lab2中大量工作在内核空间。