MIT 6.828 学习笔记6 Lab4实验报告
2016-06-09 23:31
447 查看
Lab4实验报告
开始之前,为了弄懂mpconfig与
lapic,可能需要阅读
Intel processor manual和
MP Specification,相关资源可以在课程中找到
Execrise 1
Implement mmio_map_region in kern/pmap.c.// mmio_map_region() uintptr_t ret = base; size = ROUNDUP(size, PGSIZE); base = base + size; if (base >= MMIOLIM) { panic("larger than MMIOLIM"); } boot_map_region(kern_pgdir, ret, size, pa, PTE_PCD | PTE_PWT | PTE_W); return (void *) ret;
映射
MMIO的部分空间到指定物理地址,
size需要对齐
Exercise 2
Then modify your implementation of page_init() in kern/pmap.c to avoid adding the page at MPENTRY_PADDR to the free list.void page_init(void) { size_t left_i = PGNUM(IOPHYSMEM); size_t right_i = PGNUM(PADDR(envs + NENV)); for (size_t i = 1; i < npages; i++) { if ((i < left_i || i > right_i) && i != PGNUM(MPENTRY_PADDR)) { pages[i].pp_link = page_free_list; page_free_list = &pages[i]; } } }
在之前的基础上加上
i != PGNUM(MPENTRY_PADDR)就行了
Question
1.Compare kern/mpentry.S side by side with boot/boot.S. Bearing in mind that kern/mpentry.S is compiled and linked to run above KERNBASE just like everything else in the kernel, what is the purpose of macro MPBOOTPHYS? Why is it necessary in kern/mpentry.S but not in boot/boot.S? In other words, what could go wrong if it were omitted in kern/mpentry.S?由于
AP没有设置页表,而
BSP开启了页表,所以需要自己从虚拟地址转换到物理地址
Exercise 3
Modify mem_init_mp() (in kern/pmap.c) to map per-CPU stacks starting at KSTACKTOP, as shown in inc/memlayout.h.// mem_init_mp() for (int i = 0; i != NCPU; ++i) { uintptr_t kstacktop = KSTACKTOP - i * (KSTKSIZE + KSTKGAP); boot_map_region(kern_pgdir, kstacktop - KSTKSIZE, KSTKSIZE, PADDR(percpu_kstacks[i]), PTE_W | PTE_P); }
根据
CPU数目设置相应的内核栈
Exercise 4
The code in trap_init_percpu() (kern/trap.c) initializes the TSS and TSS descriptor for the BSP. It worked in Lab 3, but is incorrect when running on other CPUs. Change the code so that it can work on all CPUs.void trap_init_percpu(void) { thiscpu->cpu_ts.ts_esp0 = KSTACKTOP - cpunum() * (KSTKSIZE + KSTKGAP); thiscpu->cpu_ts.ts_ss0 = GD_KD; gdt[(GD_TSS0 >> 3) + cpunum()] = SEG16(STS_T32A, (uint32_t) (&(thiscpu->cpu_ts)), sizeof(struct Taskstate) - 1, 0); gdt[(GD_TSS0 >> 3) + cpunum()].sd_s = 0; ltr(GD_TSS0 + (cpunum() << 3)); lidt(&idt_pd); }
初始化每个
CPU的
TSS并向
GDT中加入相应条目
Exercise 5
通过源码可以知道,这里lock_kernel的核心是
xchg指令,它可以原子地设置新值并返回原值,以此来判断获取锁是否成功
Apply the big kernel lock as described above, by calling lock_kernel() and unlock_kernel() at the proper locations.
// i386_init() // Your code here: lock_kernel(); boot_aps();
在唤醒其他
CPU前需要
lock,防止唤醒的
CPU启动进程
// mp_main() // Your code here: lock_kernel(); sched_yield();
初始化
AP后,在调度之前需要
lock,防止其他
CPU干扰进程的选择
// trap() // LAB 4: Your code here. lock_kernel(); assert(curenv);
用户态引发中断陷入内核态时,需要
lock
// env_run() lcr3(PADDR(e->env_pgdir)); unlock_kernel(); env_pop_tf(&(e->env_tf));
离开内核态之前,需要
unlock
BPS启动
AP前,获取内核锁,所以
AP会在
mp_main执行调度之前阻塞,在启动完
AP后,
BPS执行调度,运行第一个进程,之后释放内核锁,这样一来,其中一个
AP就可以开始执行调度,若有的话运行进程
Question
2.It seems that using the big kernel lock guarantees that only one CPU can run the kernel code at a time. Why do we still need separate kernel stacks for each CPU? Describe a scenario in which using a shared kernel stack will go wrong, even with the protection of the big kernel lock.如果内核栈中留下不同
CPU之后需要使用的数据,可能会造成混乱
Exercise 6
Implement round-robin scheduling in sched_yield() as described above.// sched.c // sched_yield() idle = curenv; size_t i = idle != NULL ? ENVX(idle->env_id) + 1 : 0; for (size_t j = 0; j != NENV; j++, i = (i + 1) % NENV) { if (envs[i].env_status == ENV_RUNNABLE) { env_run(envs + i); } } if (idle && idle->env_status == ENV_RUNNING) { env_run(idle); }
在
envs中,从当前进程的下一个开始(或从头开始)找状态为
ENV_RUNNABLE的进程,找到则运行该进程,若找了一圈回到当前进程,则判断当前进程的状态,若为
ENV_RUNNING则继续运行当前进程
// syscall.c // syscall() case SYS_yield: sched_yield(); return 0;
在
syscall中加入相应的分支
Question
3.In your implementation of env_run() you should have called lcr3(). Before and after the call to lcr3(), your code makes references (at least it should) to the variable e, the argument to env_run. Upon loading the %cr3 register, the addressing context used by the MMU is instantly changed. But a virtual address (namely e) has meaning relative to a given address context–the address context specifies the physical address to which the virtual address maps. Why can the pointer e be dereferenced both before and after the addressing switch?由于
e变量时保存在内核栈上,而用户页表与内核页表在内核区域的映射是一致的,所以可以正确地访问该地址
4.Whenever the kernel switches from one environment to another, it must ensure the old environment’s registers are saved so they can be restored properly later. Why? Where does this happen?
这个就是进程上下文切换,保存寄存器发生在执行系统调用(例如
sys_yield)或时钟中断时,相关代码在
trapentry.S中
Exercise 7
Implement the system calls described above in kern/syscall.c.static envid_t sys_exofork(void) { struct Env *e; int r = env_alloc(&e, curenv->env_id); if (r < 0) { return r; } e->env_status = ENV_NOT_RUNNABLE; e->env_tf = curenv->env_tf; e->env_tf.tf_regs.reg_eax = 0; return e->env_id; }
这里的重点是将
trapframe中
eax的值设置为
0,由于子进程的
trapframe与父进程的相同,所以运行子进程时,会执行父进程调用
fork时的下一条指令,而此时返回值是存在
eax中的,这样就实现了在子进程中返回
0
static int sys_env_set_status(envid_t envid, int status) { if (status != ENV_RUNNABLE && status != ENV_NOT_RUNNABLE) { return -E_INVAL; } struct Env *e; if (envid2env(envid, &e, 1) < 0) { return -E_BAD_ENV; } e->env_status = status; return 0; }
按照要求,这里的
status只有两种选择
static int sys_page_alloc(envid_t envid, void *va, int perm) { if ((uintptr_t) va >= UTOP || (uintptr_t) va % PGSIZE || (~perm & (PTE_U | PTE_P))) { return -E_INVAL; } struct Env *e; if (envid2env(envid, &e, 1) < 0) { return -E_BAD_ENV; } struct PageInfo *pp = page_alloc(ALLOC_ZERO); if (pp == NULL) { return -E_NO_MEM; } if (page_insert(e->env_pgdir, pp, va, perm) < 0) { page_free(pp); return -E_NO_MEM; } return 0; }
这里要判断的情况比较多,注意在插入页面失败时需要释放页面
static int sys_page_map(envid_t srcenvid, void *srcva, envid_t dstenvid, void *dstva, int perm) { if ((uintptr_t) srcva >= UTOP || (uintptr_t) srcva % PGSIZE || (uintptr_t) dstva >= UTOP || (uintptr_t) dstva % PGSIZE || (~perm & (PTE_U | PTE_P))) { return -E_INVAL; } struct Env *srce, *dste; if (envid2env(srcenvid, &srce, 1) < 0 || envid2env(dstenvid, &dste, 1) < 0) { return -E_BAD_ENV; } pte_t *pte; struct PageInfo *pp = page_lookup(srce->env_pgdir, srcva, &pte); if (pp == NULL || ((~(*pte) & PTE_W) && (perm & PTE_W))) { return -E_INVAL; } if (page_insert(dste->env_pgdir, pp, dstva, perm) < 0) { return -E_NO_MEM; } return 0; }
注意判断页面为只读而
perm具有写权限的情况
static int sys_page_unmap(envid_t envid, void *va) { if ((uintptr_t) va >= UTOP || (uintptr_t) va % PGSIZE) { return -E_INVAL; } struct Env *e; if (envid2env(envid, &e, 1) < 0) { return -E_BAD_ENV; } page_remove(e->env_pgdir, va); return 0; }
这里还需要在
syscall中添加相应分支
// syscall() case SYS_page_alloc: return sys_page_alloc(a1, (void *) a2, a3); case SYS_page_map: return sys_page_map(a1, (void *) a2, a3, (void *) a4, a5); case SYS_page_unmap: return sys_page_unmap(a1, (void *) a2); case SYS_exofork: return sys_exofork(); case SYS_env_set_status: return sys_env_set_status(a1, a2);
至此,
Part A完成,
make grade通过
Exercise 8
Implement the sys_env_set_pgfault_upcall system call.static int sys_env_set_pgfault_upcall(envid_t envid, void *func) { struct Env *e; if (envid2env(envid, &e, 1) < 0) { return -E_BAD_ENV; } e->env_pgfault_upcall = func; return 0; }
做接下来几个练习前,最好先理清下用户异常处理的调用关系
用户程序调用位于
pgfault.c的
set_pgfault_handler(),作用是设置异常处理程序,为用户异常栈分配页面,并将
env_pgfault_upcall设置为
_pgfault_upcall
当在用户态出现页错误时,
trap_dispatch()调用
page_fault_handler(), 然后设置用户异常栈,将
esp指向该栈顶,
eip指向之前设置的
env_pgfault_upcall,即
_pgfault_upcall,并调用
env_run()
根据
eip,执行位于
pfentry.S的
_pgfault_upcall,首先调用之前设置的异常处理程序,完成后恢复
trapframe中的寄存器,继续执行用户态指令
Exercise 9
Implement the code in page_fault_handler in kern/trap.c required to dispatch page faults to the user-mode handler.// page_fault_handler() // LAB 4: Your code here. if (curenv->env_pgfault_upcall != NULL) { uintptr_t esp; if (tf->tf_esp > UXSTACKTOP - PGSIZE && tf->tf_esp < UXSTACKTOP) { esp = tf->tf_esp - 4 - sizeof(struct UTrapframe); } else { esp = UXSTACKTOP - sizeof(struct UTrapframe); } user_mem_assert(curenv, (void *) esp, sizeof(struct UTrapframe), PTE_W | PTE_U | PTE_P); struct UTrapframe *utf = (struct UTrapframe *) (esp); utf->utf_fault_va = fault_va; utf->utf_err = tf->tf_err; utf->utf_regs = tf->tf_regs; utf->utf_eip = tf->tf_eip; utf->utf_eflags = tf->tf_eflags; utf->utf_esp = tf->tf_esp; tf->tf_esp = esp; tf->tf_eip = (uintptr_t) curenv->env_pgfault_upcall; env_run(curenv); }
向用户异常栈中压入
UTrapframe,需要判断可能发生多次异常,这种情况下需要在之前的栈顶后先留下一个空位,再压入
UTrapframe,之后会用到这个空位,然后设置
esp和
eip并调用
env_run()
Exercise 10
Implement the _pgfault_upcall routine in lib/pfentry.S.// LAB 4: Your code here. movl 48(%esp), %eax subl $4, %eax movl %eax, 48(%esp) movl 40(%esp), %ebx movl %ebx, (%eax) // LAB 4: Your code here. addl $8, %esp popal // LAB 4: Your code here. addl $4, %esp popfl // LAB 4: Your code here. popl %esp // LAB 4: Your code here. ret
第一处:往
trap-time esp所指的栈顶(可能是普通栈也可能是异常栈)后面的空位写入
trap-time eip并将
trap-time esp往下移指向该位置
第二处:跳过
fault_va和
err,然后恢复通用寄存器
第三处:跳过
eip,然后恢复
efalgs,如果先恢复
eip的话,指令执行的位置会改变,所以这里必须跳过
第四处:恢复
esp,如果第一处不将
trap-time esp指向下一个位置,这里
esp就会指向之前的栈顶
第五处:由于第一处的设置,现在
esp指向的值为
trap-time eip,所以直接
ret即可达到恢复上一次执行的效果
Exercise 11
Finish set_pgfault_handler() in lib/pgfault.c.// set_pgfault_handler() // LAB 4: Your code here. envid_t id = sys_getenvid(); if ((r = sys_page_alloc(id, (void *) (UXSTACKTOP - PGSIZE), PTE_W | PTE_U | PTE_P)) < 0 || (r = sys_env_set_pgfault_upcall(id, _pgfault_upcall)) < 0) { panic("sys_page_alloc: %e", r); }
为用户异常栈分配页面并设置异常处理函数
Exercise 12
Implement fork, duppage and pgfault in lib/fork.c.// pgfault() // LAB 4: Your code here. if (!(err & FEC_WR) || !(uvpt[PGNUM(addr)] & PTE_COW)) { panic("pgfault: failed!"); } // LAB 4: Your code here. if ((r = sys_page_alloc(0, (void *) PFTEMP, PTE_U | PTE_W | PTE_P)) < 0) { panic("pgfault: %e", r); } memcpy((void *) PFTEMP, ROUNDDOWN(addr, PGSIZE), PGSIZE); if ((r = sys_page_map(0, (void *) PFTEMP, 0, ROUNDDOWN(addr, PGSIZE), PTE_U | PTE_W | PTE_P)) < 0) { panic("pgfault: %e", r); } if ((r = sys_page_unmap(0, (void *) PFTEMP)) < 0) { panic("pgfault: %e", r); }
检查
err和
pte是否符合条件
分配页面到地址
PFTEMP
复制内容到刚分配的页面中
将虚拟地址
addr(需要向下对齐)映射到分配的页面
取消地址
PFTEMP的映射
// duppage() // LAB 4: Your code here. int perm = PGOFF(uvpt[pn]); if (perm & (PTE_W | PTE_COW)) { perm |= PTE_COW; perm &= ~PTE_W; } if ((r = sys_page_map(0, (void *) (pn * PGSIZE), envid, (void *) (pn * PGSIZE), perm)) < 0) { panic("duppage: %e", r); } if ((r = sys_page_map(0, (void *) (pn * PGSIZE), 0, (void *) (pn * PGSIZE), perm)) < 0) { panic("duppage: %e", r); } return 0;
先判断权限,然后建立映射,注意父进程页表也需要重新建立映射
// fork() int r; set_pgfault_handler(pgfault); envid_t envid = sys_exofork(); if (envid == 0) { thisenv = &envs[ENVX(sys_getenvid())]; return 0; } for (uintptr_t va = 0; va < USTACKTOP; va += PGSIZE) { if ((uvpd[PDX(va)] & PTE_P) && (uvpt[PGNUM(va)] & PTE_P)) { duppage(envid, PGNUM(va)); } } if ((r = sys_page_alloc(envid, (void *) (UXSTACKTOP - PGSIZE), PTE_U | PTE_W | PTE_P)) < 0) { return r; } extern void _pgfault_upcall(void); if ((r = sys_env_set_pgfault_upcall(envid, _pgfault_upcall)) < 0) { return r; } sys_env_set_status(envid, ENV_RUNNABLE); return envid;
设置异常处理函数,创建子进程,映射页面到子进程,为子进程分配用户异常栈并设置
pgfault_upcall入口,将子进程设置为可运行的
至此,
Part B完成,
make grade通过
Exercise 13
Modify kern/trapentry.S and kern/trap.c to initialize the appropriate entries in the IDT and provide handlers for IRQs 0 through 15. Then modify the code in env_alloc() in kern/env.c to ensure that user environments are always run with interrupts enabled.在
IDT中加入相应的项,在
Lab 3中设置过,这里只要仿照之前的代码就行了,所以就不贴代码了
// env.c // env_alloc() // LAB 4: Your code here. e->env_tf.tf_eflags |= FL_IF;
由于在运行
bootloader时屏蔽了中断,这里只需要简单地设置
eflags寄存器的
FL_IF位就可以接收中断了
Exercise 14
Modify the kernel’s trap_dispatch() function so that it calls sched_yield() to find and run a different environment whenever a clock interrupt takes place.// trap_dispatch() case IRQ_TIMER + IRQ_OFFSET: lapic_eoi(); sched_yield();
在函数中加入相应分支即可,需要先调用
lapic_eoi,作用是告诉
LAPIC已经收到并调度中断了,于是
LAPIC从中断请求队列中将其删除,这里可以不需要
break,因为不会返回
Exercise 15
Implement sys_ipc_recv and sys_ipc_try_send in kern/syscall.c.Then implement the ipc_recv and ipc_send functions in lib/ipc.c.static int sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm) { struct Env *e; if (envid2env(envid, &e, 0) < 0) { return -E_BAD_ENV; } if (!(e->env_ipc_recving)) { return -E_IPC_NOT_RECV; } if (e->env_ipc_dstva && srcva && (uintptr_t) srcva < UTOP) { int r = sys_page_map(0, srcva, envid, e->env_ipc_dstva, perm); if (r < 0) { return r; } e->env_ipc_perm = perm; } else { e->env_ipc_perm = 0; } e->env_ipc_value = value; e->env_ipc_from = curenv->env_id; e->env_tf.tf_regs.reg_eax = 0; e->env_ipc_recving = false; e->env_status = ENV_RUNNABLE; return 0; }
首先对参数进行一些检查,如果需要发送页面则建立相关映射,在设置完相关的值后,需要将目标进程
trapframe中的
eax设置为
0以作为成功返回值(
sys_ipc_recv若成功由于放弃
CPU所以不会直接返回),然后取消阻塞,设置状态为可运行的以接受调度
说明中的很多情况在这里没有判断,原因是在
sys_page_map函数中会对这些情况进行判断
static int sys_ipc_recv(void *dstva) { if ((uintptr_t) dstva >= UTOP || (dstva && (uintptr_t) dstva % PGSIZE)) { return -E_INVAL; } curenv->env_ipc_recving = true; curenv->env_ipc_dstva = dstva; curenv->env_status = ENV_NOT_RUNNABLE; sched_yield(); }
首先还是检查参数,然后阻塞进程,设置目标地址与进程状态,最后放弃
CPU执行调度函数
还需要在
syscall()函数中加入相应分支,这里就不贴代码了
int32_t ipc_recv(envid_t *from_env_store, void *pg, int *perm_store) { int r; r = sys_ipc_recv(pg); if (from_env_store) { *from_env_store = r < 0 ? 0 : thisenv->env_ipc_from; } if (perm_store) { *perm_store = r < 0 ? 0 : thisenv->env_ipc_perm; } if (r < 0) { return r; } return thisenv->env_ipc_value; }
根据传入的参数来设置值,比较简单
void ipc_send(envid_t to_env, uint32_t val, void *pg, int perm) { while (1) { int r = sys_ipc_try_send(to_env, val, pg, perm); if (r < 0 && r != -E_IPC_NOT_RECV) { panic("ipc_send: %e", r); } sys_yield(); if (r == 0) { break; } } }
不断尝试发送消息,如果目标进程没有设置
ipc_recving则继续发送,直至成功或者发生其他错误
至此,
Part C完成,
make grade通过
相关文章推荐
- 应用领航:盘点那些年我们一起追过的OS
- 无奇不有!盘点各国自己开发的操作系统
- 可自定义oem的萝卜家园 Ghost XP 新春装机版 V200801 下载
- C#实现判断操作系统是否为Win8以上版本
- js获取本机操作系统类型的两种方法
- Linux操作系统添加新硬盘方法
- java如何获取本地操作系统进程列表
- Linux rdesktop操作系统下远程登录Windows XP桌面
- 32位操作系统认出超出4G内存的方法
- Linux rpm tar 操作系统下软件的安装与卸载方法
- JavaScript 获取用户客户端操作系统版本
- jsp 获取客户端的浏览器和操作系统信息
- Windows 操作系统的安全设置
- php判断当前操作系统类型
- PHP获取用户的浏览器与操作系统信息的代码
- Perl操作系统环境变量的脚本代码
- javascript获取本机操作系统类型的方法
- 封装好的js判断操作系统与浏览器代码分享
- javascript实现获取浏览器版本、操作系统类型
- php根据操作系统转换文件名大小写的方法