Yangbo's Blog

MIT 6.828 Lab 4 - Preemptive Multitasking

Q1: Compare kern/mpentry.S side by side with boot/boot.S. Bearing in mind that kern/mpentry.S is compiled and linked to run above KERNBASE just like everything else in the kernel, what is the purpose of macro MPBOOTPHYS? Why is it necessary in kern/mpentry.S but not in boot/boot.S? In other words, what could go wrong if it were omitted in kern/mpentry.S?
A1: Macro MPBOOTPHYS maps a virtual address (above KERNBASE) to be its physical address. It is needed because mpentry.S is linked at high addresses but gets loaded by boot_aps() at the low address MPENTRY_ADDR. The bootloader doesn’t need a macro like this because it is linked and loaded at the same low address (0x00007c00). You can verify this by running objdump -h obj/boot/boot.out and comparing the VMA and LMA columns.

Q2: It seems that using the big kernel lock guarantees that only one CPU can run the kernel code at a time. Why do we still need separate kernel stacks for each CPU? Describe a scenario in which using a shared kernel stack will go wrong, even with the protection of the big kernel lock.
A2: We still need separate stacks for each CPU because during a trap/interrupt, the trapframe is pushed onto the stack without holding the kernel lock (see trapentry.S). For example, when CPU 1 enters the kernel on a system call and while it is in the kernel, CPU 2 attempts to enter the kernel on a timer interrupt. CPU 2 can’t enter the kernel, it will be spinning at the lock we just added in trap(). However, it will have pushed its trap frame on top of the trap frame already pushed by CPU 1. This of course means that when CPU 1 returns to user mode, it will pop off CPU 2’s frame and return in that environment instead of its own.

After the implementation of Exercise 6, the code run into a triple fault when running make qemu-nox. It took me lots of time to finally figure this out. It was due to the problem when I was implementing Exercise 4 the line below

1
2
gdt[GD_TSS0 >> 3] = SEG16(STS_T32A, (uint32_t) (&ts),
sizeof(struct Taskstate) - 1, 0);

was incorrectly modified to below

1
2
gdt[(GD_TSS0 >> 3) + i] = SEG16(STS_T32A, (uint32_t) (thiscpu), // should be "&thiscpu->ts"
sizeof(struct Taskstate) - 1, 0);

Q3: In your implementation of env_run() you should have called lcr3(). Before and after the call to lcr3(), your code makes references (at least it should) to the variable e, the argument to env_run. Upon loading the %cr3 register, the addressing context used by the MMU is instantly changed. But a virtual address (namely e) has meaning relative to a given address context–the address context specifies the physical address to which the virtual address maps. Why can the pointer e be dereferenced both before and after the addressing switch?
A3: The Env array envs is mapped to virtual address UENVS under page directories kern_pgdir in the function call i386_init -> mem_init; The mappings are then copied to all new page directories e->env_pgdir through call chain i386_init -> ENV_CREATE -> env_create -> env_alloc -> env_setup_vm. The pointer e therefore can be dereferenced after the addressing switch although the addressing context has been changed.

Q4: Whenever the kernel switches from one environment to another, it must ensure the old environment’s registers are saved so they can be restored properly later. Why? Where does this happen?
A4: The registers are saved on the user environment’s stack as part of the trapframe constructed by the int instruction and the code in alltraps. To restore the state of a new process, JOS uses the env_pop_tf() function, which switches first to the new process’ stack and the pops all the registers in place.