LAB3_Part A Multiprocessor Support and Cooperative Multitasking
前言
记录一下自己的学习过程
实验内容翻译:
https://gitee.com/cherrydance/mit6.828
该翻译仅供参考
练习1
练习 1. 在 kern/pmap.c 中实现 mmio_map_region 函数。要了解它的使用方式,请查看 kern/lapic.c 中 lapic_init 函数的开头部分。在运行 mmio_map_region 的测试之前,你还需要完成下一个练习。
查看lapic_init的开头部分。看到如下片段
// lapicaddr is the physical address of the LAPIC's 4K MMIO
// region. Map it in to virtual memory so we can access it.
lapic = mmio_map_region(lapicaddr, 4096);
意思是将LAPIC 的 4K MMIO 区域映射到虚拟地址。进入mmio_map_region函数,从注释中提取出我们要做的东西。
函数的目的是在MMIO中预留size大小的区域并把[pa,pa+size]映射到该区域,返回值为该区域的基址。
static uintptr_t base = MMIOBASE;
size = ROUNDUP(size, PGSIZE);//size大小不一定是PGSIZE的倍数,所以先执行对齐
if(base + size > MMIOLIM){//预留的区域大小不能超过MMIOLIM
painc("mmio_map_region: base + size overflow MMIOLTM\n");
}
int perm = PTE_W | PTE_PCD | PTE_PWT;//需要告诉 CPU 不要对此内存进行缓存访问
boot_map_region(kern_pgdir, base, size, pa, perm);//映射
uintptr_t temp = base;
base += size;//更新地址
return (void *)temp;//返回基址
练习2
阅读 kern/init.c 中的 boot_aps() 和 mp_main(),以及 kern/mpentry.S 中的汇编代码。确保你理解 AP 引导过程中的控制流转移。然后修改 kern/pmap.c 中 page_init() 的实现,以避免将位于 MPENTRY_PADDR 的页面添加到空闲列表中,这样我们就可以安全地复制和运行 AP 引导代码到该物理地址上。你的代码应该通过更新后的 check_page_free_list() 测试(但可能会未通过更新后的 check_kern_pgdir() 测试,我们很快会修复这个问题)。
阅读boot_aps()函数,该函数的作用是启动未启动的AP。
extern unsigned char mpentry_start[], mpentry_end[];//获取boot代码的起点和终点
void *code;
struct CpuInfo *c;
// Write entry code to unused memory at MPENTRY_PADDR
code = KADDR(MPENTRY_PADDR);//将物理地址0x7000转化为虚拟地址
memmove(code, mpentry_start, mpentry_end - mpentry_start);
// Boot each AP one at a time
for (c = cpus; c < cpus + ncpu; c++) {
if (c == cpus + cpunum()) // We've started already.
continue;
// Tell mpentry.S what stack to use
mpentry_kstack = percpu_kstacks[c - cpus] + KSTKSIZE;
// Start the CPU at mpentry_start
lapic_startap(c->cpu_id, PADDR(code));//从这里进入boot启动对应的cpu
// Wait for the CPU to finish some basic setup in mp_main()
while(c->cpu_status != CPU_STARTED)
;
}
在mpentry.S中调用了mp_main
# Call mp_main(). (Exercise for the reader: why the indirect call?)
# 间接调用时运行时才确定函数的位置,直接调用则是在编译阶段。
# 但这里为什么要采用间接调用呢?有种说法是因为对每个cpu地址不一样,但我不清楚
movl $mp_main, %eax
call *%eax
练习2最终的目的是修改 kern/pmap.c 中 page_init() 的实现,以避免将位于 MPENTRY_PADDR 的页面添加到空闲列表中。代码如下:
if(i == MPENTRY_PADDR / PGSIZE){
pages[i].pp_ref = 1;
continue;
}
运行结果如下,成功通过了check_page_free_list
问题1:
将 kern/mpentry.S 与 boot/boot.S 进行对比。请记住,kern/mpentry.S 被编译和链接以在 KERNBASE 之上运行,就像内核中的其他所有内容一样。宏 MPBOOTPHYS 的目的是什么?为什么在 kern/mpentry.S 中需要它,而在 boot/boot.S 中不需要?换句话说,如果在 kern/mpentry.S 中省略了它,可能会出现什么问题?
宏 MPBOOTPHYS 的目的是计算物理地址,因为在boot.S中还没有分页机制,此时虚拟地址和物理地址是相等的。在mpentry.S中已经实现了分页,省略这个宏会导致地址错误,因为mpentry.S是运行在虚拟地址中。
练习3
修改 mem_init_mp() 函数(位于 kern/pmap.c)以在 KSTACKTOP 处映射每个 CPU 的栈,如 inc/memlayout.h 所示。每个栈的大小为 KSTKSIZE 字节,加上未映射的保护页面 KSTKGAP 字节。你的代码应该通过 check_kern_pgdir() 中的新检查。
在memlayout.h中为我们清晰的展示了每个cpu的栈是什么样的。
实现代码如下,提示告诉我们每个cpu使用 ‘percpu_kstacks[i]’ 所引用的物理内存作为它的内核栈。从虚拟地址kstacktop_i = KSTACKTOP - i * (KSTKSIZE + KSTKGAP) 开始向下增长并分为两个部分。
size_t i;
uintptr_t kstacktop_i;
for(i = 0; i < NCPU; ++i){
kstacktop_i = KSTACKTOP - i * (KSTKSIZE + KSTKGAP);
boot_map_region(kern_pgdir, kstacktop_i - KSTKSIZE, KSTKSIZE, PADDR(percpu_kstacks[i]), PTE_W);
}
}
结果如下:
练习4
trap_init_percpu() 函数(kern/trap.c)中的代码为 BSP 初始化了 TSS 和 TSS 描述符。在 Lab 3 中,这段代码可以正常工作,但在其他 CPU 上运行时是不正确的。修改代码,使其在所有 CPU 上都能正常工作。(注意:你的新代码不应再使用全局变量 ts。)
1,通过thiscpu来获取当前运行的cpu
2,通过thiscpu->cpu_id来得到cpu的id
3,使用 "thiscpu->cpu_ts" 作为当前 CPU 的 TSS,而不是全局的 "ts" 变量
4,对于 CPU i 的 TSS 描述符,使用 gdt[(GD_TSS0 >> 3) + i]
根据提示我们写出代码:
thiscpu->cpu_ts.ts_esp0 = KSTACKTOP - thiscpu->cpu_id *(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) + thiscpu->cpu_id] = SEG16(STS_T32A, (uint32_t) (&thiscpu->cpu_ts),
sizeof(struct Taskstate) - 1, 0);
gdt[(GD_TSS0 >> 3) + thiscpu->cpu_id].sd_s = 0;
// Load the TSS selector (like other segment selectors, the
// bottom three bits are special; we leave them 0)
ltr(GD_TSS0 + (thiscpu->cpu_id << 3));
// Load the IDT
lidt(&idt_pd);
运行结果如图:
练习5
练习5. 按照上述描述,通过在适当的位置调用 lock_kernel() 和 unlock_kernel() 来应用大内核锁。
要求我们在四个位置加锁
在 i386_init() 中,在 BSP 唤醒其他 CPU 之前获取锁。
在 mp_main() 中,在初始化 AP 后获取锁,并调用 sched_yield() 来开始在该 AP 上运行环境。
在 trap() 中,当从用户模式陷入时获取锁。要确定陷入是发生在用户模式还是内核模式中,请检查 tf_cs 的低位。
在 env_run() 中,在切换到用户模式之前释放锁。不要过早或过晚释放锁,否则会出现竞态条件或死锁。
按照要求在前三个位置加锁,在最后一个位置解锁就好了。
问题2:
使用大内核锁似乎可以确保只有一个 CPU 可以同时运行内核代码。为什么我们仍然需要为每个 CPU 使用单独的内核栈?描述一个场景,在这个场景中,即使有大内核锁的保护,使用共享的内核栈也会出现问题。
为了确保每个cpu的状态保存和恢复是独立的。有时会需要保存进入内核前的数据。
总结
完成了lab4 partA的前半部分,这部分主要是对AP的初始化,从boot_aps,加载每个AP的boot,然后从boot进入mp_main初始化每个cpu的数据和属性。