MIT6.828学习之Lab4: Preemptive Multitasking

在这个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里开始)

  1. 进入mp_init(),通过mpconfig()找到MP configuration table与MP,根据MP configuration table了解cpu的总数、它们的APIC IDs和LAPIC单元的MMIO地址等配置信息

  2. 进入lapic_init(),根据MP配置表找到的lapic的MMIO地址,完成lapic的初始化操作(感觉这里完成的是BSP的lapic的初始化)

  3. BSP申请大内核锁,然后进入boot_aps()去启动其他CPU。在boot_aps中,找到AP的入口地址,以及AP的初始栈地址

  4. 进入lapic_startap(),将STARTUP IPIs(处理器之间中断)以及一个初始CS:IP地址即AP入口地址发送到相应AP的LAPIC单元

  5. 进入mpentry.S 完成相应CPU的寄存器初始化,启动分页机制,初始化栈,并调用mp_main

  6. 进入mp_main。完成当前CPU的lapic、用户环境、trap的初始化,就算该CPU启动完成。然后想通过sched_yield()调度一个进程而申请大内核锁,但此时BSP还保持着大内核锁,所以其他CPU都pause等待。

  7. BSP启动所有CPU后,继续执行i386_init中的代码,开始创建环境,然后执行轮转调度程序sched_yield(),从刚创建的进程中调度一个进程执行,并释放大内核锁

  8. BSP释放大内核锁后,其他pause的CPU就有一个可以申请到大内核锁,调度一个进程执行,其他接着pause。等该CPU在env_run中释放大内核锁后就又可以有一个CPU申请到大内核锁,就这样一个一个开始执行进程。

  9. 当CPU没有环境可执行时,就会进入sched_halted()里被halted,当最后那个CPU进入这个函数时,不会被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):

  1. 当向COW页面写入时,会产生page fault陷入内核
  2. 在trapentry.S–>trap()–>trap_dispatch()–>page_fault_handler()
  3. 在page_fault_handler()中完成UTrapframe入用户异常栈,将用户进程从普通栈切换到用户异常栈(tf->esp指向UXSTACKTOP),设置用户进程下一条指令tf->eip=_pgfault_upcall。env_run(curenv)回到当前用户进程
  4. 此时eip指向_pgfault_upcall,esp指向UXSTACKTOP。所以开始进入pfentry.S/_pgfault_upcall–>handler()。handler里会分配新的物理页,复制COW页面的内容,并映射到对应内存空间
  5. 回到pfentry.S,UTrapframe出栈,很秀的是trap-time eip入原栈trap-time esp-=4处。这样等pop esp实现栈切换(可能切回用户普通栈,可能是递归页面错误转到用户异常栈靠上方位置。不管怎么说,都在用户空间,所以SS不用变)。这样再ret指令就会读到trap-time eip回到发生页面错误处继续往下执行。

四、既然把复制页面映射成COW,那权限设置与检查就非常重要,但这是用户空间下的fork,不能通过page_walk来获得pde,pte。所以用了个很秀3的映射技巧uvpt,uvpd。由于“no-op”箭头被巧妙地插入到页目录表中(页目录表中条目V指向页目录表自身),因此我们可以在虚拟地址空间中找到page directorypage tables页面(通常是不可见的)。

五、copy-on-write fork的工作就是

  1. 调用exofork()。分配一个新环境,除了子进程eax设0外,父子进程上下文信息tf完全一致。就好像子进程也是从0开始运行到了当前位置。
  2. 子进程虚拟内存空间初始化UTOP以上的空间在env_alloc中设好了,所有进程该部分都跟内核的该部分内存空间相同。UTOP之下=UXSTACKTOP(一个PGSIZE大小)+USTACKTOP以下。UXSTACKTOP会分配新物理页,因为UTrapframe跟handler()都在该页面上运行,USTACKTOP以下的空间就通过duppage()来复制映射,父子进程内存空间共享物理内存。
  3. 为子进程设置好用户级页面错误处理程序。调用sys_env_set_pgfault_upcall()
  4. 设置子进程状态为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进入接收状态(设好dstvafrom=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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值