xv6-多核启动
内核初始化和多核启动
- 查看对应的文件使用
runoff.list
文件
内核初始化
bootloader 将内核载入物理地址 0x100000,通过跳转命令正式将控制权交给内核
# Entering xv6 on boot processor, with paging off.
.globl entry
entry:这里的 entry 便是内核最开始运行的代码,前面说过,此时虽然已经开始了保护模式但是分页机制并没有开启,这个时候线性地址等于物理地址,但是内核中所有的符号地址都是位于高内存处的虚拟地址,所以最开始先设置页表并开启分页机制
# Turn on page size extension for 4Mbyte pages
movl %cr4, %eax
orl $(CR4_PSE), %eax
movl %eax, %cr4
# Set page directory
movl $(V2P_WO(entrypgdir)), %eax
movl %eax, %cr3
# Turn on paging.
movl %cr0, %eax
orl $(CR0_PG|CR0_WP), %eax
movl %eax, %cr0在这里可以看到内核将 entrypgdir 的物理地址载入 cr3 寄存器并开启分页,由于内核中
虚拟地址(线性地址)-KERNBASE(0x80000000) = 物理地址
所以即便未开启分页机制的情况下仍然能够正确寻址到 entrypgdir,entrypgdir 内容为:/*
main.c
*/
// The boot page table used in entry.S and entryother.S.
// Page directories (and page tables) must start on page boundaries,
// hence the __aligned__ attribute.
// PTE_PS in a page directory entry enables 4Mbyte pages.
__attribute__((__aligned__(PGSIZE)))
pde_t entrypgdir[NPDENTRIES] = {
// Map VA's [0, 4MB) to PA's [0, 4MB)
[0] = (0) | PTE_P | PTE_W | PTE_PS,
// Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
[KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,
};页表只是暂时将内核虚拟地址的 4Mb 内存映射到物理地址的低内存,但是需要注意的一个问题是,eip 依然指向的物理地址的低地址处,因为虽然分页机制开启了,但是由于未发生跳转,所以 eip 仍然在低地址处增加指令计数,此时的 eip 是虚拟地址,需要经过页表转换,所以必须在页表中将虚拟地址的低地址处映射到物理地址的低地址处,这里直接将虚拟地址前 4Mb 映射到物理地址前 4Mb。
接下来便调用 c 函数 main,但是现在的栈是 bootloader 设置的不处在内核中,所以首先得把栈设为内核栈,然后通过跳转使 eip 指向高地址处# Set up the stack pointer.
movl $(stack + KSTACKSIZE), %esp
# Jump to main(), and switch to executing at
# high addresses. The indirect call is needed because
# the assembler produces a PC-relative instruction
# for a direct jump.
mov $main, %eax
jmp *%eax
.comm stack, KSTACKSIZE进入 main 函数后,便进行一系列的初始化
// Bootstrap processor starts running C code here.
// Allocate a real stack and switch to it, first
// doing some setup required for memory allocator to work.
int
main(void)
{
kinit1(end, P2V(4*1024*1024)); // phys page allocator
kvmalloc(); // kernel page table
mpinit(); // detect other processors
lapicinit(); // interrupt controller
seginit(); // segment descriptors
picinit(); // disable pic
ioapicinit(); // another interrupt controller
consoleinit(); // console hardware
uartinit(); // serial port
pinit(); // process table
tvinit(); // trap vectors
binit(); // buffer cache
fileinit(); // file table
ideinit(); // disk
startothers(); // start other processors
kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // must come after startothers()
userinit(); // first user process
mpmain(); // finish this processor's setup
}
多核启动
多核启动主要是按照 Intel 的手册进行各种配置,具体配置由在 main 函数中调用的 mpinit 函数负责
Intel 系列多核 CPU 在启动时首先会有一个 CPU 先运行内核代码,然后通过具体的配置使其他 CPU 开始运行,xv6 通过一个结构体将每个 CPU 的信息保存起来,具体的 cpu 结构体如下:// Per-CPU state
struct cpu {
uchar apicid; // Local APIC ID
struct context *scheduler; // swtch() here to enter scheduler
struct taskstate ts; // Used by x86 to find stack for interrupt
struct segdesc gdt[NSEGS]; // x86 global descriptor table
volatile uint started; // Has the CPU started?
int ncli; // Depth of pushcli nesting.
int intena; // Were interrupts enabled before pushcli?
// Cpu-local storage variables; see below
struct cpu *cpu;
struct proc *proc; // The currently-running process.
};可以看出,每个 CPU 都有独立的 scheduler 内核线程句柄,ts 任务栈,gdt 描述符表和唯一的标识 apicid,Intel 的每个 CPU 都有独立的 lapic,每个 lapic 有一个 ID,apicid 是区分 CPU 的重要标识。
xv6 使用一个数组来保存这样的结构体,并用一个全局变量表示 CPU 数量:
extern struct cpu cpus[NCPU];
extern int ncpu;
extern struct cpu *cpu asm("%gs:0"); // &cpus[cpunum()]
extern struct proc *proc asm("%gs:4"); // cpus[cpunum()].proc通过指针 cpu 和 proc,能够准确引用当前所在 CPU 的 cpu 结构体(每个 cpu 有一个这样的结构体,当然每个运行代码的 CPU 都应该引用自己的 cpu 结构,通过这种做法可以直接使用 cpu->,proc-> 的方式自动引用独立的 cpu 结构体),这里的做法是在访问这两个变量的时候通过段寄存器 gs 访问,得到的线性地址便是:
- gs 选择子对应的
描述符基址 + 0或4 == 线性地址
- 因为每个 CPU 都有独立的 gdt,所以在设置 gdt 的时候可以这样设置:
//vm.c
// Set up CPU's kernel segment descriptors.
// Run once on entry on each CPU.
void
seginit(void)
{
struct cpu *c;
// Map "logical" addresses to virtual addresses using identity map.
// Cannot share a CODE descriptor for both kernel and user
// because it would have to have DPL_USR, but the CPU forbids
// an interrupt from CPL=0 to DPL=3.
c = &cpus[cpuid()];
c->gdt[SEG_KCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, 0);
c->gdt[SEG_KDATA] = SEG(STA_W, 0, 0xffffffff, 0);
c->gdt[SEG_UCODE] = SEG(STA_X|STA_R, 0, 0xffffffff, DPL_USER);
c->gdt[SEG_UDATA] = SEG(STA_W, 0, 0xffffffff, DPL_USER);
lgdt(c->gdt, sizeof(c->gdt));
}- gs 选择子对应的
cpu 结构体在内核中是重要的数据结构,也是 xv6 中区分不同 CPU 的唯一地方,它的初始化是由 mpinit 函数负责的,前面说过,mpinit 由 main 函数调用
mpinit 初始化了 cpus 结构体数组,并确定了 lapic 地址,ioapicid
通过在地址空间中找到 mp 的数据结构后,得到每个 CPU 的 apicid 和 CPU 数量。
通过 mpinit 函数,有关的 cpus 结构体也初始完成了,接下来在 main 函数中调用 startothers 函数启动其他 CPU。
void
mpinit(void)
{
uchar *p, *e;
int ismp;
struct mp *mp;
struct mpconf *conf;
struct mpproc *proc;
struct mpioapic *ioapic;
if((conf = mpconfig(&mp)) == 0)
panic("Expect to run on an SMP");
ismp = 1;
lapic = (uint*)conf->lapicaddr;
for(p=(uchar*)(conf+1), e=(uchar*)conf+conf->length; p<e; ){
switch(*p){
case MPPROC:
proc = (struct mpproc*)p;
if(ncpu < NCPU) {
cpus[ncpu].apicid = proc->apicid; // apicid may differ from ncpu
ncpu++;
}
p += sizeof(struct mpproc);
continue;
case MPIOAPIC:
ioapic = (struct mpioapic*)p;
ioapicid = ioapic->apicno;
p += sizeof(struct mpioapic);
continue;
case MPBUS:
case MPIOINTR:
case MPLINTR:
p += 8;
continue;
default:
ismp = 0;
break;
}
}
if(!ismp)
panic("Didn't find a suitable machine");
if(mp->imcrp){
// Bochs doesn't support IMCR, so this doesn't run on Bochs.
// But it would on real hardware.
outb(0x22, 0x70); // Select IMCR
outb(0x23, inb(0x23) | 1); // Mask external interrupts.
}
}startothers 让其他 CPU 执行名为 entryother.S 对应的代码
entryother.S 是作为独立的二进制文件与内核二进制文件一起组成整体的 ELF 文件,通过在 LD 链接器中-b 参数来整合一个独立的二进制文件,在内核中通过_binary_entryother_start
和_binary_entryother_size
来引用,具体的 makefile 如下:$(LD) $(LDFLAGS) -T kernel.ld -o kernelmemfs entry.o $(MEMFSOBJS) -b binary initcode entryother fs.img
entryothers 在生成二进制文件时指定入口点为 start 以及加载地址和链接地址都为 0x7000
entryother: entryother.S
$(CC) $(CFLAGS) -fno-pic -nostdinc -I. -c entryother.S
$(LD) $(LDFLAGS) -N -e start -Ttext 0x7000 -o bootblockother.o entryother.o
$(OBJCOPY) -S -O binary -j .text bootblockother.o entryother
$(OBJDUMP) -S bootblockother.o > entryother.asm在 startothers 中,首先将 entryothers 移动到物理地址 0x7000 处使其能正常运行,在这里需要注意的是此时 entryothers 相当于 CPU 刚上电的情形,因为这是其他 CPU 最初运行的内核代码,所以没有开启保护模式和分页机制,entryothers 将页表设置为 entrypgdir,在设置页表前,虚拟地址等于物理地址
然后循环逐个开启每个 CPU 让每个 CPU 从 entryothers 中 start 标号开始运行:
// Start the non-boot (AP) processors.
static void
startothers(void)
{
extern uchar _binary_entryother_start[], _binary_entryother_size[];
uchar *code;
struct cpu *c;
char *stack;
// Write entry code to unused memory at 0x7000.
// The linker has placed the image of entryother.S in
// _binary_entryother_start.
code = P2V(0x7000);
memmove(code, _binary_entryother_start, (uint)_binary_entryother_size);
for(c = cpus; c < cpus+ncpu; c++){
if(c == mycpu()) // We've started already.
continue;
// Tell entryother.S what stack to use, where to enter, and what
// pgdir to use. We cannot use kpgdir yet, because the AP processor
// is running in low memory, so we use entrypgdir for the APs too.
stack = kalloc();
*(void**)(code-4) = stack + KSTACKSIZE;
*(void(**)(void))(code-8) = mpenter;
*(int**)(code-12) = (void *) V2P(entrypgdir);
lapicstartap(c->apicid, V2P(code));
// wait for cpu to finish mpmain()
while(c->started == 0)
;
}
}mpter 设置新的内核页表和进行段初始化,最后调用 mpmain 开始调度进程
// Other CPUs jump here from entryother.S.
static void
mpenter(void)
{
switchkvm();
seginit();
lapicinit();
mpmain();
}
// Common CPU setup code.
static void
mpmain(void)
{
cprintf("cpu%d: starting %d\n", cpuid(), cpuid());
idtinit(); // load idt register
xchg(&(mycpu()->started), 1); // tell startothers() we're up
scheduler(); // start running processes
}