MIT6.828LAB4 (1)

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的数据和属性。

  • 28
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值