在这个lab中,你会在多个同时
active
的用户模式环境中实现
preemptive multitasking
(抢占多任务处理)
在Part A,你会向JOS加入
multiprocessor support
(多处理器支持),实现
round-robin scheduling
(轮转调度),并且添加基础的
environment management system calls
(这个调用能创建和destroy环境,还能分配/映射内存)。
在Part B,你会实现一个像Unix里一样的
fork()
函数,它会允许用户模式环境创建自身的
副本
。
最后在Part C中,你将为
inter-process communication
(IPC 进程间通信)添加支持,允许不同的用户模式环境
显式
地彼此进行交流与同步。你也将为
hardware clock interrupts
(硬件时钟中断)和
preemption
(抢占)添加支持。
从MIT6.828/lab4中fetch下来lab4所需地文件,这次我要好好浏览下这些新增的文件。lab3就是没有预先浏览导致不知道很多有用的函数的存在,走了很多弯路。
kern/cpu.h 多处理器支持的内核私有定义
Cpu状态值、CpuInfo-每个cpu状态结构体、一些函数定义
kern/mpconfig.c 读取多处理器配置(configuration)的代码
mp结构体、mpconf-配置表头结构体、mpproc-处理器表条目结构体
mpsearch1()、mpsearch()、mpconfig()、mp_init()
kern/lapic.c 驱动每个处理器中local APIC (中断控制)单元的内核代码
很多宏定义、lapicw()、lapic_init()、cpunum()、lapic_eoi()承认中断、
microdelay()里面空着、lapic_startap()主要函数、lapic_ipi()
kern/mpentry.S 非引导cpu的入口代码的汇编语言
kern/spinlock.h spin locks (自旋锁)的内核私有定义,包括大内核锁
kern/spinlock.c 实现自旋锁的内核代码
get_caller_pcs()记录caller的eip在pcs[]中、holding()检查thiscpu、
__spin_initlock()、spin_lock()、spin_unlock()
kern/sched.c 要实现的scheduler (调度程序)的代码框架
sched_yield()使用轮换调度选择一个用户环境去运行、sched_halt()中止没事干的CPU
Part A: Multiprocessor Support and Cooperative Multitasking
实验过程点此处
实验要点
1
.symmetric multiprocessing, SMP
中所有cpu都具有对系统资源(如内存和I/O总线)的
等效访问权
。SMP中的cpu分为BSP与Aps。
BSP
负责初始化系统、引导启动操作系统并激活应用程序处理器
APs
2.每个CPU都有一个LAPIC。
LAPIC单元
负责在整个系统中
传输中断
并为其连接的CPU提供
唯一的标识符
。所有 LAPIC 都连接到一个 I/O APIC 上,形成一个一对多的结构,所有的
外部中断
通过
I/O APIC
接收后转发给对应的 LAPIC。但是由于Part A并未涉及I/O APIC编程,所以不太明白其怎么运作
3.使用
大内核锁
的主要原因是在
JOS中每次只能有一个CPU能执行内核代码
。在本部分中,其他申请大内核锁的CPU会简单通过pause指令等待,直到大内核锁被释放
4.Part A部分有三个重点。第一是用
BSP激活所有APs
,找到MP配置表,初始化lapic,找到AP的入口地址以及每个AP的初试栈地址,一个一个启动CPU;第二是实现
轮转调度程序sched_yield()
,在整个用户环境空间中找到一个可执行的程序,如果找不到,就halted当前CPU;第三是实现最简单的
fork的相关系统调用
,允许进程通过这些系统调用创建一个子环境,子环境与父环境有着几乎完全一致的
上下文以及内存空间
(这里是说内容一致,下面Part是说映射一致),不同的只是返回值用以区分,但是这样的
fork花销很大
,因为往往子进程立马调用exec()替换内存空间,之前复制的就浪费掉了。
代码运行流程简述(从i386_init里开始)
- 进入mp_init(),通过mpconfig()找到
与MP,根据MP configuration table了解cpu的总数、它们的APIC IDs和LAPIC单元的MMIO地址等配置信息MP configuration table
- 进入lapic_init(),根据MP配置表找到的lapic的
,完成MMIO地址
操作(感觉这里完成的是BSP的lapic的初始化)lapic的初始化
- BSP申请大内核锁,然后进入
去启动其他CPU。在boot_aps中,找到boot_aps()
,以及AP的入口地址
。AP的初始栈地址
- 进入lapic_startap(),将
(处理器之间中断)以及一个STARTUP IPIs
即AP入口地址发送到相应AP的LAPIC单元初始CS:IP地址
- 进入
完成相应CPU的寄存器初始化,启动分页机制,初始化栈,并调用mp_mainmpentry.S
- 进入
。完成当前CPU的lapic、用户环境、trap的初始化,就算该CPU启动完成。然后想通过mp_main
调度一个进程而sched_yield()
,但此时申请大内核锁
,所以其他CPU都BSP还保持着大内核锁
等待。pause
- BSP启动所有CPU后,
中的代码,开始创建环境,然后执行轮转调度程序sched_yield(),从刚创建的进程中调度一个进程执行,并继续执行i386_init
释放大内核锁
- BSP释放大内核锁后,其他pause的CPU就
到大内核锁,调度一个进程执行,其他接着pause。等该CPU在有一个可以申请
后就又可以有一个CPU申请到大内核锁,就这样一个一个开始执行进程。env_run中释放大内核锁
- 当CPU没有环境可执行时,就会进入
里被halted,当最后那个CPU进入这个函数时,不会被halted,而是开始sched_halted()
。执行monitor
Part B: Copy-on-Write Fork
实验过程点此处
实验要点
一、如果将父进程内存空间完全复制给子进程,而子进程通常很快会调用
exec
得到新进程内存,那么之前的复制就是极大的浪费(花销大)。所以Unix的后续版本利用虚拟内存硬件,允许
父进程和子进程共享映射
到各自地址空间的内存,直到其中一个进程实际修改它。这种技术称为“
copy-on-write
”(写时复制)很秀1。
二、在正常执行期间,JOS中的用户环境将运行在
正常的用户堆栈
上:它的
ESP寄存器
从指向USTACKTOP开始,它推送的堆栈数据驻留在
USTACKTOP- PGSIZE到USTACKTOP-1
的页面上。然而,当页面错误在用户模式下发生时,内核将重新启动用户环境,在另一个堆栈上运行指定的
用户级页面错误处理程序
,即
用户异常堆栈
,其有效字节来自
UXSTACKTOP- PGSIZE到UXSTACKTOP-1
。
三、
用户级页面错误处理程序
的调用流程(其中
栈的切换
已经
eip的处理
很秀2):
- 当向
写入时,会产生page fault陷入COW页面
,内核
- 在trapentry.S–>trap()–>trap_dispatch()–>page_fault_handler()
- 在page_fault_handler()中完成
,将用户进程从普通栈切换到用户异常栈(UTrapframe入用户异常栈
),设置用户进程下一条指令tf->esp指向UXSTACKTOP
。env_run(curenv)回到当前用户进程tf->eip=_pgfault_upcall
- 此时eip指向_pgfault_upcall,esp指向UXSTACKTOP。所以开始进入pfentry.S/_pgfault_upcall–>
。handler里会分配新的物理页,复制COW页面的内容,并映射到对应内存空间handler()
- 回到pfentry.S,UTrapframe出栈,很秀的是将
入原栈trap-time eip
处。这样等trap-time esp-=4
(可能切回用户普通栈,可能是递归页面错误转到用户异常栈靠上方位置。不管怎么说,都在用户空间,所以pop esp实现栈切换
)。这样再SS不用变
就会读到trap-time eip回到发生页面错误处继续往下执行。ret指令
四、既然把复制页面映射成COW,那
权限设置与检查
就非常重要,但这是用户空间下的fork,不能通过page_walk来获得
pde,pte
。所以用了个很秀3的映射技巧
uvpt,uvpd
。由于
“no-op”箭头
被巧妙地插入到页目录表中(
页目录表中条目V指向页目录表自身
),因此我们可以在虚拟地址空间中找到
page directory
和
page tables
的
页面
(通常是不可见的)。
五、
copy-on-write fork
的工作就是
- 调用
。分配一个新环境,除了子进程eax设0外,父子进程exofork()
完全一致。就好像子进程也是从0开始运行到了当前位置。上下文信息tf
- 子进程
。虚拟内存空间初始化
的空间在env_alloc中设好了,所有进程该部分都跟内核的该部分内存空间相同。UTOP之下=UXSTACKTOP(一个PGSIZE大小)+USTACKTOP以下。UTOP以上
会分配新物理页,因为UTrapframe跟handler()都在该页面上运行,UXSTACKTOP
的空间就通过USTACKTOP以下
来复制映射,父子进程内存空间duppage()
物理内存。共享
- 为子进程设置好
。调用sys_env_set_pgfault_upcall()用户级页面错误处理程序
- 设置子进程状态为
。至此子进程完全可以独立运行了。ENV_RUNNABLE
代码运行流程(以forktree为例)
//forktree.c/umain()
forktree("")
进程 "":
-->forkchild(cur, '0');
-->r=fork() 完成后父子进程的下一条指令都是if(r==0)
-->set_pgfault_handler(pgfault);设好用户级页面错误处理程序
-->who = sys_exofork();此时有两个进程,'上下文信息'基本一样,且下一条语句都是if(who==0)
分配一个env,'UTOP以上的内存空间'与'内核该部分空间'一样。(所有进程这部分都一样)
新进程env_tf与父进程完全一样,包括eip即下一条指令也一样,除了reg_eax即返回值不同
新进程状态设为ENV_NOT_RUNNABLE,所以还不能运行,继续父进程
-->for (i: 0~ PGNUM(USTACKTOP) duppage(who, i);
将父进程'内存空间USTACKTOP以下的页面'都'复制映射'给子进程(UTOP以下=USTACKTOP以下+UXSTACKTOP)
如果页面是可写或者COW的,则复制映射给子进程也是COW的,并重新把父进程的也映射成COW
(当要往里面写时再调用'用户级页面错误处理程序'分配一个物理页,并重新映射到该处)
否则就单纯复制映射就行(注意,复制映射不是复制内容)
-->sys_page_alloc(who, (void *)(UXSTACKTOP-PGSIZE), PTE_W|PTE_U);为子进程用户异常栈分配物理页('必须')。
-->sys_env_set_pgfault_upcall(who, _pgfault_upcall);注意此时的'who是子进程的id'
-->sys_env_set_status(who, ENV_RUNNABLE);子进程内存空间、页面错误处理程序都设好了,可以mark它可运行了
-->return who;父进程里返回的是子进程id
-->forkchild(cur, '1');
同上述操作一样
此时整个用户环境空间有三个可运行的环境:父进程"",子进程"0",子进程"1"
具体CPU运行哪一个,按轮转调度程序sched_yield()
子进程"0"(或进程"1"):
-->if(r==0){...}假设此时fork()的返回值是r,r确实为0
-->forktree("0");操作同上面的forktree
-->forkchild('0', '0');
-->forkchild('1', '1');
所以又会fork出两个新进程,进程"00",进程"01" (或者进程"10",进程"11")
父进程"":因为r!=0,所以退出forkchild(),退出forktree(),退出umain()
-->exit() exit gracefully!
当cur长度等于3时,就不会再fork出新子进程了。而完成两次forkchild()的进程都会eixt()
当所有进程都exit()后,CPU就会进入monitor
-->sched_yield()
-->sched_halted()
-->monitor()
Part C: Preemptive Multitasking and Inter-Process communication (IPC)
实验过程点此处
抢占多任务处理
这个其实设置好
时钟中断
(属于设备中断or外部中断)。为每个在CPU上执行的进程分配好时间片。如果时间片用完进程还没主动放弃CPU,那么
时钟中断处理程序
就要调用轮询调度程序
sched_yield()
将CPU给其他进程。要注意
lapic
的管理,以及外部中断入口是
IDT 32-47
进程间通信IPC(以sendpage执行流程为例)
首先说下,
IPC
是Inter-Process Communication。
PIC
是Programmable Interrupt Control。
这里非常秀的是两个系统调用
sys_ipc_try_send()和sys_ipc_recv()
之间的配合,真的是国民老公与傲娇老婆的完美搭配。或者就是一对好基友。
curenv进入接收状态(设好
dstva
,
from=0
证明还没环境发送成功,
recving=1,stats=ENV_NOT_RUNABLE
锁住直到接到"消息"),并让出CPU。要注意的是,除非发生error,否则sys_ipc_recv()是
没有返回值
的。也就是说curenv的%eax将会没有返回值,那怎么办呢?这老婆够傲娇!
不用担心,sys_ipc_try_send为你解决一切烦恼。在对自己进行
详细审查
后才准备发"消息",如果sendenv
发送“消息”成功
了,它会贴心的帮recvenv设置好
env_ipc_*
,并让
recvenv->env_status=ENV_RUNNABLE
,甚至给
recvenv的%eax赋值0
提醒recvenv它收到"消息"了,真是国民好老公啊!
直接从进入sendpage.c/umain()开始说起。怎么进入的请看Lab3:User Environments
//sendpage.c/umain()
//只启动了一个CPU
父进程:who=fork(),产生子进程,两个进程基本一样,
-->下一条语句都是if(who==0){...}
-->父进程:
-->sys_page_alloc(thisenv->env_id, TEMP_ADDR, PTE_P | PTE_W | PTE_U);
-->memcpy(TEMP_ADDR, str1, strlen(str1) + 1);
-->ipc_send(who, 0, TEMP_ADDR, PTE_P | PTE_W | PTE_U);此时who是子进程id
-->r=sys_ipc_try_send(to_env, val, pg, perm);
-->由于子进程不是接收状态,得到r=-E_IPC_NOT_RECV
-->sys_yield()主动放弃CPU,所以子进程得到CPU
-->子进程进入if循环:
-->ipc_recv(&who, TEMP_ADDR_CHILD, 0); 此时who是from_env,是父进程id
-->r=sys_ipc_recv(pg);进入接收状态并让出CPU
设好'dstva','from=0'证明还没环境发送成功,'recving=1,stats=ENV_NOT_RUNABLE'锁住直到接到"消息"
-->父进程此时还在轮询sys_ipc_try_send():
-->r=sys_ipc_try_send(to_env, val, pg, perm);
发现子进程进入接收状态,在对自己进行`详细审查`后才准备发"消息",
发送"消息"成功了,它会贴心的帮recvenv设置好'env_ipc_*',
并让'recvenv->env_status=ENV_RUNNABLE',甚至给`recvenv的%eax赋值0`提醒recvenv它收到"消息"了
-->发送成功,返回0,退出ipc_send()
-->ipc_recv(&who, TEMP_ADDR, 0);
-->r=sys_ipc_recv(pg);进入接收状态并让出CPU
-->子进程还在ipc_recv()里。不过此时已经接收"页面"并映射到TEMP_ADDR_CHILD了,并由父进程的sys_ipc_try_send修复好了状态
-->*from_env_store=父进程id
-->*perm_store=perm
-->cprintf("%x got message: %s\n", who, TEMP_ADDR_CHILD);打印处接收到的信息
-->cprintf("child received correct message\n");验证信息是否正确
-->memcpy(TEMP_ADDR_CHILD, str2, strlen(str2) + 1);向TEMP_ADDR_CHILD写入新内容str2
-->ipc_send(who, 0, TEMP_ADDR_CHILD, PTE_P | PTE_W | PTE_U);
将TEMP_ADDR_CHILD对应页面发给父进程。此时的who由子进程的ipc_recv()赋值了父进程id
并且此时父进程已经由ipc_recv()进入了接收状态,所以子进程可以直接发送成功,不用让出CPU
-->return
-->exit()那子进程到这就exit gracefully了,寿终正寝
-->父进程还在ipc_recv()里:也已经收到子进程的信息了,所以退出ipc_recv(),且此时who又指向了子进程id
-->cprintf("%x got message: %s\n", who, TEMP_ADDR_CHILD);打印处接收到的信息
-->cprintf("child received correct message\n");验证信息是否正确
-->return
-->exit()至此,父进程也完成任务exit gracefully
-->monitor()CPU由于没有进程可执行,在sched_halted里进入monitor