本实验的主要内容:
- 多处理器支持,循环调度。
- 实现一个类 Unix 的
fork()
。 - 支持对进程间通信 (IPC) 的支持。
1、多处理器支持
此部分是对多处理器的初始化。
i386_init
// 多处理器初始化函数
--> mp_init() // 在启动 APs 之前,BSP 首先收集有关多处理器系统的信息,例如 CPU 的总数、其 APIC ID
// 和 LAPIC 单元的 MMIO 地址。根据找到的配置信息,设置 MultiProcessor Specification
--> mpconfig() // 寻找 MP 配置表,检查签名、校验和和和版本是否正确。
--> mpsearch() // 从 BIOS 的 0xE0000-0xFFFFF 处找到一个 MP 结构。
--> mpsearch1(a, len) // 在物理地址 a 的 len 字节中查找 MP 结构。
// LAPIC 单元负责在整个系统中提供中断。LAPIC 还为其连接的 CPU 提供唯一标识符。
--> lapic_init()
--> mmio_map_region()
--> boot_map_region()
--> lapicw()
// 多任务初始化函数
// Initialize the 8259A interrupt controllers.
--> pic_init()
--> irq_setmask_8259A(irq_mask_8259A)
// BSP 获取大内核锁
--> lock_kernel()
--> spin_lock() //请求锁,一直循环直到获得锁
// 由 BSP 启动应用处理器 KADDR(MPENTRY_PADDR) is : f0007000
--> boot_aps()
// 一次引导一个 AP
// Tell mpentry.S what stack to use
// Start the CPU at mpentry_start
--> lapic_startap() // Start additional processor running entry code at addr.
// Wait for the CPU to finish some basic setup in mp_main().
// Send startup IPI(处理器之间中断) (twice!) to enter code.
// entry point for APs
--> mpentry.S // 设置寄存器、开启分页、初始化堆栈、调用 mp_main()
--> mp_main() // 初始化 lapic、env、trap,启动完成。获取内核锁,运行程序。
// 所有 CPU 启动完成,开始运行进程。首先创建进程,然后运行 sched_yield() 进行进程调度。
--> sched_yield()
// 寻找所有可执行的进程执行。如果没有可以运行的进程,使用 sched_halt(),使 CPU halted。在最后一个
// CPU 时,进入 monitor。
--> sched_halt()
--> monitor()
2、实现系统调用以支持用户进程创建进程
以 kern/dumbfork.c
为例。
kern/dumbfork.c
// 从用户态下 umain() 开始、
--> umain(argc, argv) // 执行程序代码
--> who = dumbfork() // fork a child process, return envid
// 系统调用,进入内核态
--> envid = sys_exofork() // Allocate a new child environment
// 调用完成,返回用户态
// Eagerly copy our entire address space into the child.
--> duppage() // Also copy the stack we are currently running on.
// Start the child environment.
--> sys_env_set_status(envid, ENV_RUNNABLE)
// running
--> sys_yield() // 父子进程循环调度执行
3、Copy-on-Write Fork
本实验最重要的内容。
实现用户级别的 lib/fork()
。
fork()
的基本控制流程:
父级使用上面实现的
set_pgfault_handler()
函数将pgfault()
安装为 C 级页面错误处理程序。父级调用
sys_exofork()
来创建子环境。-
对于 UTOP 下其地址空间中的每个可写页或写时复制页,父级调用
duppage
,它应将 写时复制页 映射到子级的地址空间,然后在自己的地址空间中重新映射 “写时复制” 页。[注:此处的顺序(即在子页面中标记为 COW,然后在父页面中标记该页面)实际上很重要!你知道为什么吗?试着想一个具体的例子,在这种情况下,颠倒顺序可能会引起麻烦]duppage
设置两个 PTE,使页面不可写,并在 “avail” 字段中包含PTE_COW
,以区分写时复制页和真正的只读页。但是,异常堆栈不会以这种方式重新映射。相反,您需要为异常堆栈在子级中分配一个新页。由于页错误处理程序将执行实际的复制操作,而页错误处理程序在异常堆栈上运行,因此无法在写入时复制异常堆栈:谁会复制它?
fork()
还需要处理存在但不可写或写时复制的页。 父级为子级设置用户页错误入口点,使其看起来像自己的。
子进程现在可以运行了,因此父进程将其标记为可运行的。
以 forktree
为例;
// user/forktree.c/umain()
--> forktree("")
// fork 子进程 0
--> forkchild(cur, '0')
--> fork()
--> set_pgfault_handler(pgfault) // Set up our page fault handler.
--> sys_exofork() // Allocate a new environment.
// Copy our address space and page fault handler setup to the child.
// Map our virtual page pn (address pn*PGSIZE) into the target envid
// at the same virtual address.
--> duppage()
--> sys_page_map()
// alloc a page and map child exception stack.
--> sys_page_alloc()
// 将父进程的 pgfault 函数注册给子进程。
--> sys_env_set_pgfault_upcall()
// 标记子进程为可运行。
--> sys_env_set_status()
// fork 子进程 1
--> forkchild(cur, '1')
4、进程间通信 (IPC)
也是很重要的一个内容。
主要实现两个系统调用:
sys_ipc_try_send()
sys_ipc_recv()
进程间通过这两个系统调用实现通信。
以 sendpage
为例:
user/sendpage/umain()
// parent process
--> who = fork() // fork 一个子进程,返回子进程的 id。父与子进程内容基本相同。
// kern/syscall
--> sys_exofork() // 分配一个新进程,并返回进程 id。
// 子进程的 env_parent_id 字段被设置为 parent_id。
// 父进程没有进入 if ((who = fork()) == 0) 分支,顺序执行。
--> sys_page_alloc() // 在指定位置分配一页内存。
--> memcpy() // 将要发送的内容复制到分配的内存页地址处。
--> ipc_send() // 父进程发送内存的页地址给子进程。
// kern/syscall
// 不断轮询发送直到发送成功。
--> r = sys_ipc_try_send(to_env, val, pg, perm)
// 若子进程没有收到,则放弃 CPU,进行调度。
// 系统调用,进入内核态。
--> sys_yield()
// 调度,运行子进程。
--> sched_yield()
// child process
// 子进程进入 if ((who = fork()) == 0)。
--> ipc_rev(&who, TEMP_ADDR_CHILD, 0) // 子进程接收信息。who 是父进程 id。
// kern/syscall
--> r = sys_ipc_recv() // 从指定位置接收信息。
// 设置子进程为接收状态,调度,执行父进程。
--> sched_yield()
// parent process
// 继续轮询,发现子进程已经进入接收状态,发送消息:
// 即设置子进程的 env_ipc_* 等属性。
--> r = sys_ipc_try_send(to_env, val, pg, perm)
--> 发送完毕。
--> ipc_recv(&who, TEMP_ADDR, 0) // 父进程接收信息。who 是子进程id。
--> r = sys_ipc_recv(pg) // 进入接收状态,让出 CPU。
--> sched_yield()
// child process
// 子进程执行 ipc_rev()。
--> ipc_recv()
--> ipc_send() // 子进程发送信息。
// kern/syscall
// 发送信息,成功。
--> r = sys_ipc_try_send(to_env, val, pg, perm)
--> return
--> exit()
// parent process
--> ipc_recv() // 从指定位置接收信息。
-->sys_ipc_recv(dstva)
--> return
--> exit()