Part A: Multiprocessor Support and Cooperative Multitasking
- 使JOS支持多处理器(CPU)
- 实现新的系统调用——允许用户级环境创建其他环境
- 实现协作轮询调度——允许内核从一个主动放弃CPU的环境切换到另一个环境
Multiprocessor Support
把SMP技术应用在单个CPU中,为了与SMP区分,单个物理CPU内,等同原来单个CPU的模块称之为Core(核心),这样的CPU称之为多核CPU。
我们将让 JOS 支持对称多处理器(symmetric multiprocessing,SMP),这是一种多处理器模型,其中所有CPU都具有对系统资源(如内存和I / O总线)的等效访问。虽然所有CPU在SMP中功能相同,但在引导过程中它们可分为两种类型:
- 引导处理器(BSP):负责初始化系统和引导操作系统;
- 应用程序处理器(AP):只有在操作系统启动并运行后,BSP才会激活应用程序处理器。
在SMP系统中,每个CPU都有一个附带的本地APIC(LAPIC)单元。
APIC:Advanced Programmable Interrupt Controller高级可编程中断控制器 。APIC 是装置的扩充组合用来驱动 Interrupt 控制器 。在目前的建置中,系统的每一个部份都是经由 APIC Bus 连接的。“本机 APIC” 为系统的一部份,负责传递 Interrupt 至指定的处理器;举例来说,当一台机器上有三个处理器则它必须相对的要有三个本机 APIC。
LAPIC单元负责在整个系统中提供中断。 LAPIC还为其连接的CPU提供唯一标识符。 在本实验中,我们使用LAPIC单元的以下基本功能(在kern/lapic.c
中):
- 根据LAPIC识别码(APIC ID)区别我们的代码运行在哪个CPU上。(
cpunum()
) - 从BSP向APs发送
STARTUP
处理器间中断(IPI)去唤醒其他的CPU。(lapic_startap()
) - 在Part C,我们编写LAPIC的内置定时器来触发时钟中断,以支持抢占式多任务(
pic_init()
)。
MMIO
处理器通过内存映射IO(MMIO)的方式访问它的LAPIC。在MMIO中,一部分物理地址被硬连接到一些IO设备的寄存器,导致操作内存的指令load/store可以直接操作设备的寄存器。
MMIO(Memory mapping I/O)即内存映射I/O,它是PCI规范的一部分,I/O设备被放置在内存空间而不是I/O空间。从处理器的角度看,内存映射I/O后系统设备访问起来和内存一样。这样访问AGP/PCI-E显卡上的帧缓存,BIOS,PCI设备就可以使用读写内存一样的汇编指令完成,简化了程序设计的难度和接口的复杂性。
PortIO和MMIO 的主要区别
1)前者不占用CPU的物理地址空间,后者占有(这是对x86架构说的,一些架构,如IA64,port I/O占用物理地址空间)。
2)前者是顺序访问。也就是说在一条I/O指令完成前,下一条指令不会执行。例如通过Port I/O对设备发起了操作,造成了设备寄存器状态变化,这个变化在下一条指令执行前生效。uncache的MMIO通过uncahce memory的特性保证顺序性。
3)使用方式不同
由于port I/O有独立的64K I/O地址空间,但CPU的地址线只有一套,所以必须区分地址属于物理地址空间还是I/O地址空间。
kern/pmap.c
::mmio_map_region(physaddr_t pa, size_t size)
:
将[pa,pa+size)映射到MMIO区域
在前面的lab中提到虚拟内存的分布图(memlayout.h中),有一部分是[MMIOBASE,MMIOLIM]——这就是内存映射IO区
void *
mmio_map_region(physaddr_t pa, size_t size)
{
// Where to start the next region. Initially, this is the
// beginning of the MMIO region. Because this is static, its
// value will be preserved between calls to mmio_map_region
// (just like nextfree in boot_alloc).
static uintptr_t base = MMIOBASE;
// Your code here:
void *start = (void *)base;
size = ROUNDUP(size, PGSIZE);//对齐
if (base + size >= MMIOLIM || base + size < base)//越界
panic("mmio_map_region failed: size is too big!\n");
boot_map_region(kern_pgdir, base, size, pa, PTE_W | PTE_PCD | PTE_PWT);
base += size;
return start;
// panic("mmio_map_region not implemented");
}
Application Processor Bootstrap
在启动APs之前,BSP应该先收集关于多处理器系统的配置信息,比如CPU总数,CPUs的APIC ID,LAPIC单元的MMIO地址等。在kern/mpconfig.c
文件中的mp_init()
函数通过读BIOS设定的MP配置表获取这些信息。
boot_aps(kern/init.c)
函数驱使AP引导程序。APs开始于实模式,跟BSP的开始相同,故此boot_aps
函数拷贝AP入口代码(kern/mpentry.S)到实模式下的内存寻址空间。但是跟BSP不一样的是,我们需要有一些控制当AP开始执行时。我们将拷贝入口代码到0x7000(MPENTRY_PADDR)。
之后,boot_aps
函数通过发送STARTUP的IPI(处理器间中断)信号到AP的LAPIC单元来一个个地激活AP。在kern/mpentry.S
中的入口代码跟boot/boot.S
中的代码类似。在一些简短的配置后,它使AP进入开启分页机制的保护模式,调用C语言的mp_main
函数进行setup。
为AP的启动代码保存一个物理页,不能被其他使用:
// LAB 4:
// Change your code to mark the physical page at MPENTRY_PADDR
// as in use
else if (i == mpentry)
{
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
整理一下程序运行过程,此过程一直都运行在CPU0,即BSP上,工作在保护模式。
i386_init
调用了boot_aps()
,也就是在BSP中引导其他CPU开始运行boot_aps
调用memmove
将每个CPU的boot代码加载到固定位置- 最后调用
lapic_startap
执行其bootloader启动对应的CPU
Per-CPU State and Initialization
在kern/cpu.h中定义了大部分的per-CPU状态。每个CPU独有的变量应该有:
- 内核栈:不同的核可能同时进入到内核中执行,因此需要有不同的内核栈
- TSS和TSS描述符:每个CPU都需要单独的TSS和TSS描述符来指定该CPU对应的内核栈。
- 进程结构指针:每个CPU都会独立运行一个进程的代码,所以需要Env指针。
- 系统寄存器:比如cr3, gdt, ltr这些寄存器都是每个CPU私有的,每个CPU都需要单独设置。
任务状态段TSS及TSS描述符、局部描述符表LDT及LDT描述符
kern/pmap.c
: :mem_init_mp
:
将内核栈线性地址映射到percpu_kstacks处的物理地址处。
static void
mem_init_mp(void)
{
// LAB 4: Your code here:
int i;
uintptr_t kstacktop_i;
for (i = 0; i < NCPU; ++i)
{
kstacktop_i = KSTACKTOP - i * (KSTKGAP + KSTKSIZE);
boot_map_region(kern_pgdir,
kstacktop_i - KSTKSIZE,
KSTKSIZE,
PADDR(&percpu_kstacks[i]),
PTE_W);
}
}
kern/trap.c
: :trap_init_percpu
:
由于有多个CPU,所以在这里不能使用原先的全局变量ts,应该利用thiscpu指向的CpuInfo结构体和cpunum函数来为每个核的TSS进行初始化
// Initialize and load the per-CPU TSS and IDT
void trap_init_percpu(void)
{
// LAB 4: Your code here:
// Setup a TSS so that we get the right stack
// when we trap to the kernel.
int i = cpunum();
thiscpu->cpu_ts.ts_esp0 = KSTACKTOP - i * (KSTKSIZE + KSTKGAP);
thiscpu->cpu_ts.ts_ss0 = GD_KD;
thiscpu->cpu_ts.ts_iomb = sizeof(struct Taskstate);
// Initialize the TSS slot of the gdt.
gdt[(GD_TSS0 >> 3) + i] = SEG16(STS_T32A, (uint32_t)(&thiscpu->cpu_ts),
sizeof(struct Taskstate) - 1, 0);
gdt[(GD_TSS0 >> 3) + i].sd_s = 0;
// Load the TSS selector (like other segment selectors, the
// bottom three bits are special; we leave them 0)
ltr(GD_TSS0 + (i << 3));
// Load the IDT
lidt(&idt_pd);
}
Locking
在mp_main函数中初始化AP后,代码就会进入自旋。在让AP进行更多操作之前,我们首先要解决多CPU同时运行在内核时产生的竞争问题。最简单的办法是实现1个大内核锁,1次只让一个进程进入内核模式,当离开内核时释放锁。
在kern/spinlock.h中声明了大内核锁,提供了lock_kernel和unlock_kernel函数来快捷地获得和释放锁。总共有四处用到大内核锁:
i386_init()
在启动的时候,BSP启动其余的CPU之前,BSP需要取得内核锁mp_main
中,也就是CPU被启动之后执行的第一个函数,这里应该是调用调度函数,选择一个进程来执行的,但是在执行调度函数之前,必须获取锁trap
函数也要修改,因为可以访问临界区的CPU只能有一个,所以从用户态陷入到内核态的话,要加锁,因为可能多个CPU同时陷入内核态env_run
函数,也就是启动进程的函数,之前在试验3中实现的,在这个函数执行结束之后,就将跳回到用户态,此时离开内核,也就是需要将内核锁释放
加锁后,将原有的并行执行过程在关键位置变为串行执行过程,整个启动过程大概如下:
i386_init–>BSP获得锁–>boot_ap–>(BSP建立为每个cpu建立idle任务、建立用户任务,mp_main)—>BSP的sched_yield–>其中的env_run释放锁–>AP1获得锁–>执行sched_yield–>释放锁–>AP2获得锁–>执行sched_yield–>释放锁……
spin lock
VS sleep lock
- 当一个进程获取
spin lock
时,如果发现锁住的情况,则会进入忙等待,一直占用CPU而不主动让出,知道解锁或时间片用完 - 当一个进程获取
sleep lock
时,如果发现锁住的情况,则会进入阻塞状态,主动让出CPU,等待下次被唤醒,再检查锁情况
Round-Robin Scheduling
现在需要让CPU能在进程间切换,需要完成两件事:
-
实现sched_yield(),该函数选择一个新的进程运行,从当前正在运行进程对应的Env结构下一个位置开始循环搜索envs数组,找到第一个cpu_status为
ENV_RUNNABLE
的Env结构,然后调用env_run()
在当前CPU运行这个新的进程。// Choose a user environment to run and run it. void sched_yield(void) { struct Env *idle; // LAB 4: Your code here. idle = curenv; size_t i = idle != NULL ? ENVX(idle->env_id) + 1 : 0; size_t j; for (j = 0; j != NENV; j++, i = (i + 1) % NENV) { if (envs[i].env_status == ENV_RUNNABLE) { envs[i].env_cpunum = cpunum(); env_run(envs + i); } } if (idle && idle->env_status == ENV_RUNNING) { curenv->env_cpunum = cpunum(); env_run(idle); } // sched_halt never returns sched_halt(); }
-
实现一个新的系统调用sys_yield(),使得用户程序能在用户态通知内核,当前进程希望主动让出CPU给另一个进程。
在
kern/syscall.c
::syscall
中添加:case SYS_yield: sys_yield();
System Calls for Environment Creation
目前,内核可以在多进程间切换,但仅限于内核创建好的进程。
因此,接下来,为进程提供系统调用,从而使进程能创建新进程。
现在需要实现如下系统调用:
-
sys_exofork():
创建一个新的进程,用户地址空间没有映射,不能运行,寄存器状态和父环境一致。在父进程中sys_exofork()
返回新进程的envid,子进程返回0。static envid_t sys_exofork(void) { // LAB 4: Your code here. struct Env *e; int ret = env_alloc(&e, curenv->env_id); if (ret < 0) return ret; e->env_status = ENV_NOT_RUNNABLE; e->env_tf = curenv->env_tf; e->env_tf.tf_regs.reg_eax = 0;//前面的lab说过,eax作为系统调用号,也作为返回值,子进程会返回零 return e->env_id; // panic("sys_exofork not implemented"); }
-
sys_env_set_status:
设置一个特定进程的状态为ENV_RUNNABLE
或ENV_NOT_RUNNABLE
。sys_env_set_status(envid_t envid, int status) { // LAB 4: Your code here. struct Env *e; int ret = envid2env(envid, &e, 1); if (ret < 0) return ret; if (status != ENV_RUNNABLE && status != ENV_NOT_RUNNABLE) return -E_INVAL; e->env_status = status; return 0; // panic("sys_env_set_status not implemented"); }
-
sys_page_alloc:
为特定进程分配一个物理页,映射指定线性地址va到该物理页。static int sys_page_alloc(envid_t envid, void *va, int perm) { // LAB 4: Your code here. struct Env *e; int ret = envid2env(envid, &e, 1); if (ret < 0) return ret; if ((uintptr_t)va >= UTOP || PGOFF(va)) return -E_INVAL; int flag = PTE_U | PTE_P; if ((perm & flag) != flag || (perm & ~(PTE_SYSCALL)) != 0) return -E_INVAL; struct PageInfo *pg = page_alloc(1); if (pg == NULL) return -E_NO_MEM; if (page_insert(e->env_pgdir, pg, va, perm) < 0) { page_free(pg); return -E_NO_MEM; } return 0; // panic("sys_page_alloc not implemented"); }
-
sys_page_map:
拷贝页表,使指定进程共享当前进程相同的映射关系。本质上是修改特定进程的页目录和页表。static int sys_page_map(envid_t srcenvid, void *srcva, envid_t dstenvid, void *dstva, int perm) { // LAB 4: Your code here. struct Env *se, *de; int ret = envid2env(srcenvid, &se, 1); if (ret < 0) return ret; ret = envid2env(dstenvid, &de, 1); if (ret < 0) return ret; if ((uintptr_t)srcva >= UTOP || PGOFF(srcva) || (uintptr_t)dstva >= UTOP || PGOFF(dstva)) return -E_INVAL; int flag = PTE_U | PTE_P; if ((perm & flag) != flag || (perm & ~(PTE_SYSCALL)) != 0) return -E_INVAL; pte_t *entry; struct PageInfo *pg = page_lookup(se->env_pgdir, srcva, &entry); if (pg == NULL) return -E_INVAL; if ((perm & PTE_W) && (*entry & PTE_W) == 0) return -E_INVAL; if (page_insert(de->env_pgdir, pg, dstva, perm) < 0) return -E_NO_MEM; return 0; // panic("sys_page_map not implemented"); }
-
sys_page_unmap:
解除页映射关系。本质上是修改指定用户环境的页目录和页表。static int sys_page_unmap(envid_t envid, void *va) { // Hint: This function is a wrapper around page_remove(). // LAB 4: Your code here. struct Env *e; int ret = envid2env(envid, &e, 1); if (ret < 0) return ret; if ((uintptr_t)va >= UTOP || PGOFF(va)) return -E_INVAL; page_remove(e->env_pgdir, va); return 0; // panic("sys_page_unmap not implemented"); }
Part B: Copy-on-Write Fork
一般fork()会将父进程的地址空间完全复制到子进程中,但子进程通常会立即调用exec()函数族,用新的程序替换复制的内容。因此,复制父进程是非常浪费时间的。
写时复制(COW):将父进程的地址映复制给子进程,即父子进程共享同一个地址空间,将共享的空间标记为只读并设置COW位。当其中一个进程尝试写入共享页面时,该进程会出现页错误,从而进入内核进行处理。
User-level page fault handling
用户级写时复制需要了解写保护页面上的页面错误,写时复制只是用户级缺页中断处理的一种。
通常建立地址空间以便page fault提示何时需要执行某些操作。例如大多数Unix内核初始只给新进程的栈映射一个页,以后栈增长会导致page fault从而映射新的页。一个典型的Unix内核必须记录在进程地址空间的不同区域发生page fault时,应该执行什么操作。例如栈上缺页,会实际分配和映射新的物理内存。BSS区域缺页会分配新的物理页,填充0,然后映射。这种设计在定义他们的内存区域的时候具有极大的灵活度。
Setting the Page Fault Handler
1个用户级写时拷贝的fork函数需要在写保护页时触发page fault,所以我们第一步应该先规定或者确立一个page fault处理例程,每个进程需要向内核注册这个处理例程,只需要传递一个函数指针即可,sys_env_set_pgfault_upcall
函数将当前进程的page fault处理例程设置为func指向的函数。
kern/syscall.c
: :sys_env_set_pgfault_upcall(envid_t envid, void *func)
:
该系统调用为指定的用户环境设置env_pgfault_upcall
。缺页中断发生时,会执行env_pgfault_upcall
指定位置的代码。当执行env_pgfault_upcall
指定位置的代码时,栈已经转到异常栈,并且压入了UTrapframe
结构。
static int
sys_env_set_pgfault_upcall(envid_t envid, void *func)
{
// LAB 4: Your code here.
struct Env *e;
if (envid2env(envid, &e, 1) < 0)
return -E_BAD_ENV;
e->env_pgfault_upcall = func;
return 0;
// panic("sys_env_set_pgfault_upcall not implemented");
}
//还要将其加入到syscall()函数中
case SYS_env_set_pgfault_upcall:
return sys_env_set_pgfault_upcall(a1, (void *)a2);
Normal and Exception Stacks in User Environments
在正常运行期间,用户进程运行在用户栈上,栈顶寄存器ESP指向USTACKTOP处,堆栈数据位于USTACKTOP-PGSIZE 与USTACKTOP-1之间的页。当在用户模式发生1个page fault时,内核将在专门处理page fault的用户异常栈上重新启动进程。
到目前为止出现三个栈:
[KSTACKTOP-KSTKSIZE, KSTACKTOP)
内核态系统栈
[UXSTACKTOP - PGSIZE, UXSTACKTOP )
用户态错误处理栈
[UTEXT, USTACKTOP)
用户态运行栈
Invoking the User Page Fault Handler
用户定义注册了自己的中断处理程序之后,相应的例程运行时的栈,整个过程如下:
- 首先陷入到内核,栈位置从用户运行栈切换到内核栈,进入到trap中,进行中断处理分发,进入到
page_fault_handler()
- 当确认是用户程序触发的page fault的时候(内核触发的直接panic了),为其在用户错误栈里分配一个
UTrapframe
的大小。 - 把栈切换到用户错误栈,运行响应的用户中断处理程序,中断处理程序可能会触发另外一个同类型的中断,这个时候就会产生递归式的处理。处理完成之后,返回到用户运行栈。
page_fault_handler(struct Trapframe *tf)
:
在该函数中应该做如下几件事:
-
判断curenv->env_pgfault_upcall是否设置,如果没有设置也就没办法修复,直接销毁该进程。
-
判断esp位置,切换到用户异常栈。
当正常执行过程中发生了页错误,那么栈的切换是
用户运行栈—>内核栈—>异常栈
而如果在异常处理程序中发生了也错误,那么栈的切换是
异常栈—>内核栈—>异常栈 -
在异常栈上压入一个UTrapframe结构,其结构如下:
<-- UXSTACKTOP trap-time esp trap-time eflags trap-time eip trap-time eax ----| start of struct PushRegs trap-time ecx | trap-time edx | trap-time ebx | trap-time esp | trap-time ebp | trap-time esi | trap-time edi ----| end of struct PushRegs tf_err (error code) fault_va <-- %esp when handler is run
-
将eip设置为curenv->env_pgfault_upcall,然后回到用户态执行curenv->env_pgfault_upcall处的代码。
void page_fault_handler(struct Trapframe *tf)
{
uint32_t fault_va;
// Read processor's CR2 register to find the faulting address
fault_va = rcr2();
// Handle kernel-mode page faults.
// LAB 3: Your code here.
if ((tf->tf_cs & 3) == 0)
panic("page_falut in kernel mode, fault address %d\n", fault_va);
// LAB 4: Your code here.
struct UTrapframe *utf;
if (curenv->env_pgfault_upcall) //判断
{
if (tf->tf_esp >= UXSTACKTOP - PGSIZE && tf->tf_esp < UXSTACKTOP)
{
// on exception user
utf = (struct UTrapframe *)(tf->tf_esp - sizeof(struct UTrapframe) - 4);
}
else
{
// on normal user
utf = (struct UTrapframe *)(UXSTACKTOP - sizeof(struct UTrapframe));
}
//检查异常栈是否溢出
user_mem_assert(curenv, (const void *)utf, sizeof(struct UTrapframe), PTE_P | PTE_W | PTE_U);
utf->utf_fault_va = fault_va;
utf->utf_err = tf->tf_err;
utf->utf_regs = tf->tf_regs;
utf->utf_eflags = tf->tf_eflags;
//保存上下文,方便从异常栈退出时找到之前的位置
utf->utf_eip = tf->tf_eip;
utf->utf_esp = tf->tf_esp;
//切换下一个指令:即运行处理函数
curenv->env_tf.tf_eip = (uint32_t)curenv->env_pgfault_upcall;
//切换到异常栈上
curenv->env_tf.tf_esp = (uint32_t)utf;
env_run(curenv);
}
else
{
// Destroy the environment that caused the fault.
cprintf("[%08x] user fault va %08x ip %08x\n",
curenv->env_id, fault_va, tf->tf_eip);
print_trapframe(tf);
env_destroy(curenv);
}
}
压入异常栈过程:
User-mode Page Fault Entrypoint
用户page fault注册流程:
当从用户定义的处理函数返回之后,如何从用户错误栈直接返回到用户运行栈。
lib/pfentry.S
: :_pgfault_upcall
:
是所有用户页错误处理程序的入口,在这里调用用户自定义的处理程序,并在处理完成后,从错误栈中保存的UTrapframe中恢复相应信息,然后跳回到发生错误之前的指令,恢复原来的进程运行。
_pgfault_handler完成但返回到先前状态之前的内容:
_pgfault_upcall:
// Call the C page fault handler.
pushl %esp // function argument: pointer to UTF
movl _pgfault_handler, %eax
call *%eax
addl $4, %esp // pop function argument
//当_pgfault_handler处理结束后,需要返回到之前的状态
// LAB 4: Your code here.
movl 48(%esp),%eax //取出栈中的trap-time esp,即上文esp
subl $4,%eax // 将其减4,如果是用户栈的esp则自动空一格
//如果是异常栈的esp,因前面保存了一个4字节空间,所以正合适
movl %eax,48(%esp) //再存回trap-time esp中
movl 40(%esp),%ebx //取出上文eip
movl %ebx,(%eax) //放入上文esp指向的空间,即保留的4字节空间
// Restore the trap-time registers. After you do this, you
// can no longer modify any general-purpose registers.
// LAB 4: Your code here.
addl $8,%esp //将esp上移两个4字节,此时指向struct PushRegs end
popal //将struct PushRegs整个pop出来,此时esp指向trap-time eip
// Restore eflags from the stack. After you do this, you can
// no longer use arithmetic operations or anything else that
// modifies eflags.
// LAB 4: Your code here.
addl $4,%esp //上移一个4字节,此时esp指向trap-time eflags
popfl //将efalgs pop出来,此时指向tarp-time esp
// Switch back to the adjusted trap-time stack.
// LAB 4: Your code here.
popl %esp //将trap-time esp(即上文esp) pop出来,由esp寄存器保存
//此时指向的是预留的那4字节,由上面可知,保存了上文eip
// Return to re-execute the instruction that faulted.
// LAB 4: Your code here.
ret //ret会读取esp指向的内容,并保存在eip中,即将上文eip保存在eip寄存器中
lib/pgfault.c
::set_pgfault_handler
:
用户通过调用此函数进行设置pgfault_handler
void set_pgfault_handler(void (*handler)(struct UTrapframe *utf))
{
int r;
if (_pgfault_handler == 0)
{
// First time through!
// LAB 4: Your code here.
if ((r = sys_page_alloc(thisenv->env_id,
(void *)(UXSTACKTOP - PGSIZE),
PTE_P | PTE_W | PTE_U)) < 0)
panic("set_pgfault_handler: %e", r);
sys_env_set_pgfault_upcall(thisenv->env_id, _pgfault_upcall);
// panic("set_pgfault_handler not implemented");
}
// Save handler pointer for assembly to call.
_pgfault_handler = handler;
}
Implementing Copy-on-Write Fork
与dumfork
不同的是,fork 只复制了页映射关系,并且只在尝试对页进行写操作时才进行页内容的 copy。
fork
控制流如下:
- 父进程注册handler
- 父进程call
sys_exofork
创建子进程 - 对每个处于
UTOP
之下的可写或COW页进行复制duppage
。它应该将 copy-on-write 页面映射到子进程的地址空间,然后重新映射 copy-on-write 页面到它自己的地址空间。
此处的顺序十分重要——先将子进程的页标记为COW
,然后将父进程的页标记为COW
:
猜测:如果先将父进程标记为COW,此时,如果有另一个进程抢占CPU,那么此时的子进程此时的状态并没有COW,则如果子进程被改变时,并不会发生COW,以至于修改了与父进程共享的内存
lib/fork.c
::pgfault(struct UTrapframe *utf)
:
pgfault()
检查错误是否为写入FEC_WR(在错误代码中检查),并且页面的 PTE 是否标记为PTE_COW
。如果没有,请panic。pgfault()
在临时位置分配映射的新页面,并将错误页面的内容复制到其中。然后,错误处理程序使用读/写权限将新页面映射到会修改该页的进程中的地址,以代替旧的只读映射。
static void
pgfault(struct UTrapframe *utf)
{
void *addr = (void *)utf->utf_fault_va;
uint32_t err = utf->utf_err;
int r;
// LAB 4: Your code here.
if (!((err & FEC_WR) && (uvpt[PGNUM(addr)] & PTE_COW)))
panic("pgfault:not writabled or a COW page!\n");
// LAB 4: Your code here.
envid_t envid = sys_getenvid();
//为PFTEMP分配一个物理页
if ((r = sys_page_alloc(envid, (void *)PFTEMP, PTE_P | PTE_W | PTE_U)) < 0)
panic("pgfault:page allocation failed: %e", r);
addr = ROUNDDOWN(addr, PGSIZE);
//将addr上的物理页内容拷贝到PFTEMP指向的物理页上
memmove((void *)PFTEMP, (void *)addr, PGSIZE);
//更改addr映射的物理页,改为与PFTEMP指向相同
if ((r = sys_page_map(envid, PFTEMP, envid, addr, PTE_P | PTE_W | PTE_U)) < 0)
panic("pgfault:page map failed: %e", r);
//取消PFTEMP的映射
if ((r = sys_page_unmap(envid, PFTEMP)) < 0)
panic("pgfault: page unmap failed: %e", r);
// panic("pgfault not implemented");
}
lib/fork.c
::duppage(envid_t envid, unsigned pn)
:
进行COW方式的页复制(相当于复制映射,而不是复制页本身),所以将当前进程的第pn页对应的物理页映射到envid的第pn页上去,同时将这一页都标记为COW。
static int
duppage(envid_t envid, unsigned pn)
{
int r;
// LAB 4: Your code here.
void *addr = (void *)(pn * PGSIZE);
if ((uvpt[pn] & PTE_W) || (uvpt[pn] & PTE_COW))
{
if ((r = sys_page_map(0, addr, envid, addr, PTE_P | PTE_U | PTE_COW)) < 0)
return r;
if ((r = sys_page_map(0, addr, 0, addr, PTE_P | PTE_U | PTE_COW)) < 0)
return r;
}
else if ((r = sys_page_map(0, addr, envid, addr, PTE_P | PTE_U)) < 0)
return r;
// panic("duppage not implemented");
return 0;
}
lib/fork.c
::fork(void)
:
设置异常处理函数,创建子进程,映射页面到子进程,为子进程分配用户异常栈并设置 pgfault_upcall
入口,将子进程设置为可运行的
envid_t
fork(void)
{
// LAB 4: Your code here.
set_pgfault_handler(pgfault); //为进程创建异常栈,并设置处理函数
envid_t eid = sys_exofork(); //创建一个空进程,与父进程状态一致
if (eid < 0)
panic("fork failed: sys_exofork faied: %e", eid);
if (eid == 0)
{
// child
thisenv = &envs[ENVX(sys_getenvid())];
return 0;
}
// parent
size_t pn;
int r;
for (pn = PGNUM(UTEXT); pn < PGNUM(USTACKTOP); ++pn)
{
if ((uvpd[pn >> 10] & PTE_P) && (uvpt[pn] & PTE_P))
{
if ((r = duppage(eid, pn)) < 0)
return r;
}
}
// alloc a page and map for child exception stack
//每个进程都必须有自己的异常栈,这样才能进行COW
if ((r = sys_page_alloc(eid, (void *)(UXSTACKTOP - PGSIZE), PTE_U | PTE_P | PTE_W)) < 0)
return r;
extern void _pgfault_upcall(void);
if ((r = sys_env_set_pgfault_upcall(eid, _pgfault_upcall)) < 0)
return r;
if ((r = sys_env_set_status(eid, ENV_RUNNABLE)) < 0)
panic("sys_env_set_status failed: %e", r);
return eid;
// panic("fork not implemented");
}
Part C: Preemptive Multitasking and Inter-Process communication (IPC)
实现抢占非协作式环境,并且实现进程间通信
Clock Interrupts and Preemption
如果一个进程获得CPU后一直死循环而不主动让出CPU的控制权, 整个系统都将 halt。为了允许内核抢占正在运行的环境,强行重获CPU控制权,我们必须扩展JOS内核以支持来自时钟的外部硬件中断。
Interrupt discipline
外部中断(如设备中断)被称为 IRQs。 IRQ号到 IDT 项的映射不是固定的,其会加上一个IRQ_OFFSET的偏移,在picirq.c
的pic_init
中进行了这个映射过程。外部中断的初始化,实际上就是对硬件 8259A的初始化。
修改kern/trapentry.S
和kern/trap.c
来初始化IDT中IRQs0-15的入口和处理函数。然后修改env_alloc
函数来确保进程在用户态运行时中断是打开的:
//kern/trapentry.S
TRAPHANDLER_NOEC(irq0_handler,IRQ_OFFSET+0)
TRAPHANDLER_NOEC(irq1_handler,IRQ_OFFSET+1)
TRAPHANDLER_NOEC(irq2_handler,IRQ_OFFSET+2)
TRAPHANDLER_NOEC(irq3_handler,IRQ_OFFSET+3)
TRAPHANDLER_NOEC(irq4_handler,IRQ_OFFSET+4)
TRAPHANDLER_NOEC(irq5_handler,IRQ_OFFSET+5)
TRAPHANDLER_NOEC(irq6_handler,IRQ_OFFSET+6)
TRAPHANDLER_NOEC(irq7_handler,IRQ_OFFSET+7)
TRAPHANDLER_NOEC(irq8_handler,IRQ_OFFSET+8)
TRAPHANDLER_NOEC(irq9_handler,IRQ_OFFSET+9)
TRAPHANDLER_NOEC(irq10_handler,IRQ_OFFSET+10)
TRAPHANDLER_NOEC(irq11_handler,IRQ_OFFSET+11)
TRAPHANDLER_NOEC(irq12_handler,IRQ_OFFSET+12)
TRAPHANDLER_NOEC(irq13_handler,IRQ_OFFSET+13)
TRAPHANDLER_NOEC(irq14_handler,IRQ_OFFSET+14)
TRAPHANDLER_NOEC(irq15_handler,IRQ_OFFSET+15)
//kern/trap.c
void irq0_handler();
void irq1_handler();
void irq2_handler();
void irq3_handler();
void irq4_handler();
void irq5_handler();
void irq6_handler();
void irq7_handler();
void irq8_handler();
void irq9_handler();
void irq10_handler();
void irq11_handler();
void irq12_handler();
void irq13_handler();
void irq14_handler();
void irq15_handler();
//trap_init()
SETGATE(idt[IRQ_OFFSET + 0], 0, GD_KT, irq0_handler, 0);
SETGATE(idt[IRQ_OFFSET + 1], 0, GD_KT, irq1_handler, 0);
SETGATE(idt[IRQ_OFFSET + 2], 0, GD_KT, irq2_handler, 0);
SETGATE(idt[IRQ_OFFSET + 3], 0, GD_KT, irq3_handler, 0);
SETGATE(idt[IRQ_OFFSET + 4], 0, GD_KT, irq4_handler, 0);
SETGATE(idt[IRQ_OFFSET + 5], 0, GD_KT, irq5_handler, 0);
SETGATE(idt[IRQ_OFFSET + 6], 0, GD_KT, irq6_handler, 0);
SETGATE(idt[IRQ_OFFSET + 7], 0, GD_KT, irq7_handler, 0);
SETGATE(idt[IRQ_OFFSET + 8], 0, GD_KT, irq8_handler, 0);
SETGATE(idt[IRQ_OFFSET + 9], 0, GD_KT, irq9_handler, 0);
SETGATE(idt[IRQ_OFFSET + 10], 0, GD_KT, irq10_handler, 0);
SETGATE(idt[IRQ_OFFSET + 11], 0, GD_KT, irq11_handler, 0);
SETGATE(idt[IRQ_OFFSET + 12], 0, GD_KT, irq12_handler, 0);
SETGATE(idt[IRQ_OFFSET + 13], 0, GD_KT, irq13_handler, 0);
SETGATE(idt[IRQ_OFFSET + 14], 0, GD_KT, irq14_handler, 0);
SETGATE(idt[IRQ_OFFSET + 15], 0, GD_KT, irq15_handler, 0);
//kern/env.c::env_alloc()
// Enable interrupts while in user mode.
// LAB 4: Your code here.
e->env_tf.tf_eflags |= FL_IF;
Handling Clock Interrupts
目前还需要在trap_dispatch()
函数中添加时钟中断处理代码,这样才能重新进入内核,并切换另一个进程:
// Handle clock interrupts. Don't forget to acknowledge the
// interrupt using lapic_eoi() before calling the scheduler!
// LAB 4: Your code here.
if (tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER)
{
lapic_eoi();
sched_yield();
}
Inter-Process communication (IPC)
IPC in JOS
我们将要实现sys_ipc_recv()
和sys_ipc_try_send()
这两个系统调用,来实现进程间通信。并且实现两个包装函数ipc_recv()
和 ipc_send()
。
JOS中进程间通信的“消息”包含两部分:
- 一个32位的值。
- 可选的页映射关系。
Sending and Receiving Messages
sys_ipc_recv()
和sys_ipc_try_send()
是这么协作的:
- 当某个进程调用
sys_ipc_recv()
后,该进程会阻塞(状态被置为ENV_NOT_RUNNABLE
),直到另一个进程向它发送“消息”。当进程调用sys_ipc_recv()
传入dstva参数时,表明当前进程准备接收页映射。 - 进程可以调用
sys_ipc_try_send()
向指定的进程发送“消息”,如果目标进程已经调用了sys_ipc_recv()
,那么就发送数据,然后返回0,否则返回-E_IPC_NOT_RECV
,表示目标进程不希望接受数据。当传入srcva参数时,表明发送进程希望和接收进程共享srcva对应的物理页。如果发送成功了发送进程的srcva和接收进程的dstva将指向相同的物理页。
Transferring Pages
当用户环境使用有效的dstva
参数(低于UTOP)调用sys_ipc_recv
时,环境表示它愿意接收页面映射。如果发送者发送一个页面,那么该页面应该在接收者的地址空间中的dstva
映射。如果接收者已经在dstva
上映射了一个页面,那么之前的页映射被取消。
当用户环境以有效的srcva
(在UTOP下面)以及权限perm为参数调用sys_ipc_try_send
时,这意味着发送者想要将当前映射到srcva
的页面发送给接收者。在成功的IPC之后,发送方在其地址空间中的srcva
保持页面的原始映射,但是接收方在其地址空间最初指定的dstva
处获得了与发送者同一物理页的映射。因此,该页面在发送者和接收者之间共享。
如果发送者或接收者没有指示一个页面应该被传送,那么没有页面被传送。在任何IPC之后,内核将接收者的Env结构中的新字段env_ipc_perm设置为接收到的页面的权限,如果没有接收到页面,则为零。
在Env结构中增加了5个成员来实现IPC:
env_ipc_recving
:当进程使用env_ipc_recv函数等待信息时,会将这个成员设置为1,然后堵塞等待;当一个进程向它发消息解除堵塞后,发送进程将此成员修改为0。
env_ipc_dstva
:如果进程要接受消息并且是传送页,保存页映射的地址,且该地址<=UTOP。
env_ipc_value
:若等待消息的进程接收到消息,发送方将接收方此成员设置为消息值。
env_ipc_from
:发送方负责设置该成员为自己的envid号。
env_ipc_perm
:如果进程要接收消息并且传送页,那么发送方发送页之后将传送的页权限赋给这个成员。
kern/syscall.c
::sys_ipc_recv(void *dstva)
:
当一个进程试图去接收信息的时候,应该将自己标记为正在接收信息,而且为了不浪费CPU资源,应该同时标记自己为ENV_NOT_RUNNABLE,只有当有进程向自己发了信息之后,才会重新恢复可运行。最后将自己标记为不可运行之后,调用调度器运行其他进程。
static int
sys_ipc_recv(void *dstva)
{
// LAB 4: Your code here.
if (dstva < (void *)UTOP && PGOFF(dstva))
return -E_INVAL;
curenv->env_ipc_recving = 1;
curenv->env_ipc_dstva = dstva;
curenv->env_status = ENV_NOT_RUNNABLE;
curenv->env_ipc_from = 0;
sched_yield();
// panic("sys_ipc_recv not implemented");
return 0;
}
kern/syscall.c
::sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
:
权限是否符合要求,要传送的页有没有,能不能将这一页映射到对方页表中去等等。如果srcva是在UTOP之下,那么说明是要共享内存,那就首先要在发送方的页表中找到srcva对应的页表项,然后在接收方给定的虚地址处插入这个页表项。接收完成之后,重新将当前进程设置为可运行,同时把env_ipc_recving设置为0,以防止其他的进程再发送,覆盖掉当前的内容。
static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
// LAB 4: Your code here.
struct Env *dste;
pte_t *pte;
struct PageInfo *pg;
int ret = envid2env(envid, &dste, 0);
if (ret < 0)
return ret;
if (dste->env_ipc_recving == 0 || dste->env_ipc_from != 0)
return -E_IPC_NOT_RECV;
if (srcva < (void *)UTOP)
{
if (PGOFF(srcva) || (perm & PTE_P) == 0 || (perm & PTE_U) == 0 || (perm & ~(PTE_SYSCALL)))
return -E_INVAL;
if ((pg = page_lookup(curenv->env_pgdir, srcva, &pte)) == NULL)
return -E_INVAL;
if ((perm & PTE_W) && !(*pte & PTE_W))
return -E_INVAL;
if (dste->env_ipc_dstva)
{
if ((ret = page_insert(dste->env_pgdir, pg, dste->env_ipc_dstva, perm)) < 0)
return ret;
dste->env_ipc_perm = perm;
}
}
dste->env_ipc_from = curenv->env_id;
dste->env_ipc_recving = 0;
dste->env_ipc_value = value;
dste->env_status = ENV_RUNNABLE;
dste->env_tf.tf_regs.reg_eax = 0;
return 0;
// panic("sys_ipc_try_send not implemented");
}
还需要在syscall函数中添加,才能最终实现系统调用:
case SYS_ipc_try_send:
return sys_ipc_try_send((envid_t)a1, (uint32_t)a2, (void *)a3, (unsigned)a4);
case SYS_ipc_recv:
return sys_ipc_recv((void *)a1);
接下来实现lib/ipc.c
里面的ipc_recv
和ipc_send
:
int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
// LAB 4: Your code here.
if (pg == NULL)
pg = (void *)UTOP;
int ret = sys_ipc_recv(pg);
if (from_env_store)
*from_env_store = ret < 0 ? 0 : thisenv->env_ipc_from;
if (perm_store)
*perm_store = ret < 0 ? 0 : thisenv->env_ipc_perm;
if (ret < 0)
return ret;
return thisenv->env_ipc_value;
// panic("ipc_recv not implemented");
}
void ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
// LAB 4: Your code here.
if (pg == NULL)
pg = (void *)UTOP;
int ret;
while (true)
{
ret = sys_ipc_try_send(to_env, val, pg, perm);
if (ret == 0)
return;
else if (ret == -E_IPC_NOT_RECV)
sys_yield();//通信失败,先主动放弃CPU,轮询运行后继续通信
else
panic("ipc_send failed: %e", ret);
}
// panic("ipc_send not implemented");
}
参考文章:
MIT6.828 Part B: Copy-on-Write Fork_bysui的博客-CSDN博客
MIT-6.828-JOS-lab4:Preemptive Multitasking - gatsby123 - 博客园 (cnblogs.com)
MIT 6.828 (四) Lab 4: Preemptive Multitasking_EW_DUST的博客-CSDN博客_lapic_eoi