entry.S 的任务:开启大页模式、设置栈寄存器 %esp,然后通过设置 %eip 进入了 main() 函数。
所以我们这节将开始调试 main 函数,将追踪 XV6 是如何初始化各种资源的。像前面几节一样进入调试,然后将断点打开 main 入口。
(gdb) b main
Breakpoint 1 at 0x80102eb0: file main.c, line 19.
(gdb) c
Continuing.
The target architecture is assumed to be i386
=> 0x80102eb0 <main>: lea 0x4(%esp),%ecx
Thread 1 hit Breakpoint 1, main () at main.c:19
19 {
(gdb) l
14 // Bootstrap processor starts running C code here.
15 // Allocate a real stack and switch to it, first
16 // doing some setup required for memory allocator to work.
17 int
18 main(void)
19 {
20 kinit1(end, P2V(4*1024*1024)); // phys page allocator
21 kvmalloc(); // kernel page table
22 mpinit(); // detect other processors
23 lapicinit(); // interrupt controller
我们可以看到,main 的逻辑地址是 0x80102eb0,系统运行在 i386 架构上,main 函数位于 main.c 的第 19 行。
main 函数一开始就调用了 kinit1 函数,收集空闲物理页帧,用于之后的页表分配。进入函数查看一下:
(gdb) s
=> 0x80102ebf <main+15>: sub $0x8,%esp
main () at main.c:20
20 kinit1(end, P2V(4*1024*1024)); // phys page allocator
(gdb) s
=> 0x80102405 <kinit1+5>: mov 0xc(%ebp),%esi
kinit1 (vstart=0x801154a8, vend=0x80400000) at kalloc.c:33
33 {
kinit1 收集的是 0x1154a8~0x400000 的空闲页帧,此时虽然是大页模式,但是收集页帧的单位已经是 4 K 了,这里的 end 是 kernel 的结束位置,也就是说物理内存中 0x100000 ~ end 已经被内核使用,而 end ~ 0x400000 依旧是空闲内存。
这里的 end 是由链接器定义的,从 ELF 中加载进来的,所以这里使用的是声明 extern。接着往下看
(gdb) n
=> 0x80102408 <kinit1+8>: sub $0x8,%esp
34 initlock(&kmem.lock, "kmem");
(gdb) n
=> 0x8010241a <kinit1+26>: mov 0x8(%ebp),%eax
36 freerange(vstart, vend);
为了管理内存,这里使用到了 kmem 结构体,由于内存是共享资源(临界资源),所以提供了自旋锁实现互斥,一开始 kmem.use_lock = 0(这里没有显示,是因为编译的时候会打乱一些指令的顺序,这涉及到流水线的知识),因为处理器上只有进程也就是内核本身,所以不需要互斥锁。
紧接着就调用 freerange 函数回收 [vstart, vend) 的内存。
freerange 函数其实就是对每一页帧调用 kfree 回收罢了。因此 main 的第一个函数分析完毕。
在调用 kinit1 函数的时候,内核还是使用大页模式,用的是 entry.S 中定义的简易的页表 entrypgdir[],此时查看 %cr3 的值即可找到 entrypgdir[] 的物理地址。在 QEMU 调试器下查看 CR3=00109000,说明页表 entrypgdir[] 就在该物理地址处。
我们看 kvmalloc 函数是如何设置内核页表的。打断点进入该函数:
(gdb) b kvmalloc
Breakpoint 2 at 0x80106ca0: file vm.c, line 142.
(gdb) c
Continuing.
=> 0x80106ca0 <kvmalloc>: push %ebp
Thread 1 hit Breakpoint 2, kvmalloc () at vm.c:142
142 {
(gdb) s
=> 0x80106ca6 <kvmalloc+6>: call 0x80106c20 <setupkvm>
143 kpgdir = setupkvm();
函数使用了一个指针 kpgdir 指向 setupkvm 函数的返回值, setupkvm 函数从 kmem 链表中摘取一页物理页帧作为页表,首先将该页帧初始化为全 0。然后根据 kmap 的布局对页表进行设置。
kmap 结构体的定义如下:
// This table defines the kernel's mappings, which are present in
// every process's page table.
static struct kmap {
void *virt;
uint phys_start;
uint phys_end;
int perm;
} kmap[] = {
{ (void*)KERNBASE, 0, EXTMEM, PTE_W}, // I/O space
{ (void*)KERNLINK, V2P(KERNLINK), V2P(data), 0}, // kern text+rodata
{ (void*)data, V2P(data), PHYSTOP, PTE_W}, // kern data+memory
{ (void*)DEVSPACE, DEVSPACE, 0, PTE_W}, // more devices
};
布局很简单,共有四个主要区,分别是 IO 区、kernel 的代码和只读数据、kernel 的变量和空闲内存、设备区。区域划分如下表,其中 data 变量是由 kernel.ld 定义的,在 GDB 中用 p/x data 查看发现 data=0x80108000
| 虚拟起始地址 | 物理起始地址 | 物理结束地址 | 读写权限 | |
|---|---|---|---|---|
| IO | 0x80000000 | 0 | 0x100000 | 可写 |
| 代码和只读数据 | 0x80100000 | 0x100000 | 0x108000 | 0 |
| 变量和内存 | 0x80108000 | 0x108000 | 0xE000000 | 可写 |
| 设备区 | 0xFE000000 | 0xFE000000 | 0xFFFFFFFF | 可写 |
当内核页表设置好,我们就可以调用 switchkvm 函数来切换并使用新的内核页表了。我们查看调用 switchkvm() 前后 %cr3 的值。调用前 CR3=00109000,调用后 CR3=003ff000。
至此,内核页表初始化完毕。
XV6 是支持多处理器的,接下来 main 调用 mpinit 函数来检测其他处理器,并根据收集的信息设置 cpus[] 数组和 ismp 多核标志。首先我们看看 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?
struct proc *proc; // The process running on this cpu or null
};
mpinit 只是定义了 cpus[] 数组并设置了 cpus->apicid
接下来开始调试:
(gdb) b mpinit
Breakpoint 1 at 0x80103050: file mp.c, line 93.
(gdb) c
Continuing.
The target architecture is assumed to be i386
=> 0x80103050 <mpinit>: push %ebp
Thread 1 hit Breakpoint 1, mpinit () at mp.c:93
93 {
这里有个关键字 thread 1,说明还有其他的线程,我们查看一下:
(gdb) info threads
Id Target Id Frame
* 1 Thread 1 (CPU#0 [running]) mpinit () at mp.c:93
2 Thread 2 (CPU#1 [halted ]) 0x000fd412 in ?? ()
果然,已经有两个 CPU 了,不过 CPU#1 还处于 halted 状态。往下继续,mpinit 首先调用 mpconfig 函数,mpconfig 又调用 mpsearch 函数,这里涉及到一个 mp 结构体,如下:
struct mp { // floating pointer
uchar signature[4]; // "_MP_"
void *physaddr; // phys addr of MP config table
uchar length; // 1
uchar specrev; // [14]
uchar checksum; // all bytes must add up to 0
uchar type; // MP system config type
uchar imcrp;
uchar reserved[3];
};
其实就是为了找到内存上存储 MP 结构体信息,然后再根据 MP->physaddr 找到 mpconf 结构体,内容如下:
struct mpconf { // configuration table header
uchar signature[4]; // "PCMP"
ushort length; // total table length
uchar version; // [14]
uchar checksum; // all bytes must add up to 0
uchar product[20]; // product id
uint *oemtable; // OEM table pointer
ushort oemlength; // OEM table length
ushort entry; // entry count
uint *lapicaddr; // address of local APIC
ushort xlength; // extended table length
uchar xchecksum; // extended table checksum
uchar reserved;
};
如果没找到 moconf 就说明是单核,将执行在一个 SMP (Symmetrical Multi-Processing)上。
接下来将根据 mpconf 表头信息配置 lapic(Local APIC)。配置信息存储在两个结构体当中,分别是 mpproc 和 mpioapic。
struct mpproc { // processor table entry
uchar type; // entry type (0)
uchar apicid; // local APIC id
uchar version; // local APIC verison
uchar flags; // CPU flags
#define MPBOOT 0x02 // This proc is the bootstrap processor.
uchar signature[4]; // CPU signature
uint feature; // feature flags from CPUID instruction
uchar reserved[8];
};
struct mpioapic { // I/O APIC table entry
uchar type; // entry type (2)
uchar apicno; // I/O APIC id
uchar version; // I/O APIC version
uchar flags; // I/O APIC flags
uint *addr; // I/O APIC address
};
APIC 全称 Advanced Programmable Interrupt Controller, APIC 是为了多核平台而设计的。它由两个部分组成 IOAPIC 和 LAPIC。其中 IOAPIC 用于处理设备所产生的各种中断,LAPIC 则是每个 CPU 都会有一个。
所以检测其他 CPU 核其实就是配置这些相关信息,为多核 CPU 启动作准备。
接下来开始初始化中断控制器 LAPIC,每个 CPU 都有一个。首先开启 LAPIC:
// Enable local APIC; set spurious interrupt vector.
lapicw(SVR, ENABLE | (T_IRQ0 + IRQ_SPURIOUS));
然后开启时钟中断:
// The timer repeatedly counts down at bus frequency
// from lapic[TICR] and then issues an interrupt.
// If xv6 cared more about precise timekeeping,
// TICR would be calibrated using an external time source.
lapicw(TDCR, X1);
lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));
lapicw(TICR, 10000000);
然后关闭逻辑中断线:
// Disable logical interrupt lines.
lapicw(LINT0, MASKED);
lapicw(LINT1, MASKED);
禁止计数器溢出中断:
// Disable performance counter overflow interrupts
// on machines that provide that interrupt entry.
if(((lapic[VER]>>16) & 0xFF) >= 4)
lapicw(PCINT, MASKED);
错误中断映射:
// Map error interrupt to IRQ_ERROR.
lapicw(ERROR, T_IRQ0 + IRQ_ERROR);
清除错误状态寄存器,置 0
// Clear error status register (requires back-to-back writes).
lapicw(ESR, 0);
lapicw(ESR, 0);
同步总裁机制:
// Send an Init Level De-Assert to synchronise arbitration ID's.
lapicw(ICRHI, 0);
lapicw(ICRLO, BCAST | INIT | LEVEL);
while(lapic[ICRLO] & DELIVS)
;
启用 APIC 上的中断(但处理器还没开启)
// Enable interrupts on the APIC (but not on the processor).
lapicw(TPR, 0);
之前 bootmain 已经对分段机制做了一些处理,现在我们继续完善。首先我们看看原来在 bootasm.S 中设置的 GDT 如下
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg
gdtdesc:
.word (gdtdesc - gdt - 1) # sizeof(gdt) - 1
.long gdt # address gdt
相应的段寄存器如下:
ES =0010 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
CS =0008 00000000 ffffffff 00cf9a00 DPL=0 CS32 [-R-]
SS =0010 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
DS =0010 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
FS =0000 00000000 00000000 00000000
GS =0000 00000000 00000000 00000000
相应的段表地址如下:
GDT= 00007c60 00000017
IDT= 00000000 000003ff
接下来我们看看执行完 seginit 函数后这些都有什么变化。首先看看 seginit 函数如下:
// 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));
}
首先获取当前 cpu 的私有变量,接着设置 GDT,此时的 GDT 比 bootblock 时期多了用户态段,说明此时开启了段保护机制,我们制作一个表格来直观感受:
| 权限 | 基址 | 段长 | 权限 | |
|---|---|---|---|---|
| SEG_KNODE | 可读可执行 | 0 | 4 G | 0 |
| SEG_KDATA | 可写 | 0 | 4 G | 0 |
| SEG_UCODE | 可读可执行 | 0 | 4 G | 3 |
| SEG_UDATA | 可写 | 0 | 4 G | 3 |
最后使用 lgdt 更新 GDTR 寄存器。更新后 GDT= 801127f0 0000002f 。由于相应的偏移没有变,所以段寄存器的值也不需要变化。
main 接着采用 picinit 函数禁用 8259A 中断控制器(单核),XV6 是使用 SMP 硬件(多核)。
// Don't use the 8259A interrupt controllers. Xv6 assumes SMP hardware.
void
picinit(void)
{
// mask all interrupts
outb(IO_PIC1+1, 0xFF);
outb(IO_PIC2+1, 0xFF);
}
XV6 是运行在多核 SMP 系统上的,因此中断控制器是使用 APIC。
void
ioapicinit(void)
{
int i, id, maxintr;
ioapic = (volatile struct ioapic*)IOAPIC;
maxintr = (ioapicread(REG_VER) >> 16) & 0xFF;
id = ioapicread(REG_ID) >> 24;
if(id != ioapicid)
cprintf("ioapicinit: id isn't equal to ioapicid; not a MP\n");
// Mark all interrupts edge-triggered, active high, disabled,
// and not routed to any CPUs.
for(i = 0; i <= maxintr; i++){
ioapicwrite(REG_TABLE+2*i, INT_DISABLED | (T_IRQ0 + i));
ioapicwrite(REG_TABLE+2*i+1, 0);
}
}
使用 consoleinit 初始化终端设备,主要是将读端和写端配置好。
void
consoleinit(void)
{
initlock(&cons.lock, "console");
devsw[CONSOLE].write = consolewrite;
devsw[CONSOLE].read = consoleread;
cons.locking = 1;
ioapicenable(IRQ_KBD, 0);
}
紧接着调用 uartinit 函数初始化串口。关掉 FIFO,设置接收中断,然后向其输出 xv6... 字符,说明初始化完成。
void
uartinit(void)
{
char *p;
// Turn off the FIFO
outb(COM1+2, 0);
// 9600 baud, 8 data bits, 1 stop bit, parity off.
outb(COM1+3, 0x80); // Unlock divisor
outb(COM1+0, 115200/9600);
outb(COM1+1, 0);
outb(COM1+3, 0x03); // Lock divisor, 8 data bits.
outb(COM1+4, 0);
outb(COM1+1, 0x01); // Enable receive interrupts.
// If status is 0xFF, no serial port.
if(inb(COM1+5) == 0xFF)
return;
uart = 1;
// Acknowledge pre-existing interrupt conditions;
// enable interrupts.
inb(COM1+2);
inb(COM1+0);
ioapicenable(IRQ_COM1, 0);
// Announce that we're here.
for(p="xv6...\n"; *p; p++)
uartputc(*p);
}
其实就是对 ptable 锁初始化,使其处于开锁状态。
void
pinit(void)
{
initlock(&ptable.lock, "ptable");
}
对中断向量初始化,总共 256 个。
void
tvinit(void)
{
int i;
for(i = 0; i < 256; i++)
SETGATE(idt[i], 0, SEG_KCODE<<3, vectors[i], 0);
SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER);
initlock(&tickslock, "time");
}
对内核的读写其实是对 bcache 结构体的操作,我们先看看 bcache 结构体:
struct {
struct spinlock lock;
struct buf buf[NBUF];
// Linked list of all buffers, through prev/next.
// head.next is most recently used.
struct buf head;
} bcache;
其中有一个自旋锁,还有 NBUF 个缓存存,我们看看盘块的结构体 buf 如下:
struct buf {
int flags;
uint dev;
uint blockno;
struct sleeplock lock;
uint refcnt;
struct buf *prev; // LRU cache list
struct buf *next;
struct buf *qnext; // disk queue
uchar data[BSIZE];
};
其中包括设备号 dev,数据 data。bcache 是磁盘的代表,初始化主要让这些盘块串成一个双链表,为之后的调度做准备。
void
binit(void)
{
struct buf *b;
initlock(&bcache.lock, "bcache");
//PAGEBREAK!
// Create linked list of buffers
bcache.head.prev = &bcache.head;
bcache.head.next = &bcache.head;
for(b = bcache.buf; b < bcache.buf+NBUF; b++){
b->next = bcache.head.next;
b->prev = &bcache.head;
initsleeplock(&b->lock, "buffer");
bcache.head.next->prev = b;
bcache.head.next = b;
}
}
文件表 ftable 记录所有打开的文件,可类比程序表 ptable。
void
fileinit(void)
{
initlock(&ftable.lock, "ftable");
}
IDE 磁盘驱动器的初始化,IDE 属于临界资源,故也需要自旋锁保护。
void
ideinit(void)
{
int i;
initlock(&idelock, "ide");
ioapicenable(IRQ_IDE, ncpu - 1);
idewait(0);
// Check if disk 1 is present
outb(0x1f6, 0xe0 | (1<<4));
for(i=0; i<1000; i++){
if(inb(0x1f7) != 0){
havedisk1 = 1;
break;
}
}
// Switch back to disk 0.
outb(0x1f6, 0xe0 | (0<<4));
}
此时开始初始化其他 CPU 核,调用 startothers 函数。AP 的启动过程和 BP(boot processer)基本一致,首先将 entryother.S 拷贝到无用的物理地址 0x7000(之前是 bootblock 的内存),然后设置好栈(通过 kalloc 分配和设置 %esp)、入口地址(设置 %eip)、页表(使用 entrypgdir)等。然后 BP 发出中断,进入死循环等待 AP 开启完毕。
// 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)
;
}
}
设置好后会调用 entryother.S 中的代码,entryother.S 相当于 bootasm.S 和 entry.S 的结合体,AP 设置完毕后调用 mpenter 函数,并设置页表等,然后在进入 scheduler 之前让 BP 继续完成接下来的工作。
AP 启动完成后,会调用 mpmain 进入调度程序,进入前会告诉 BP 自己已经启动,于是 BP 从 startothers 函数返回,开始将所有的空闲物理内存收集起来,可用物理地址为 [0, 240 MB],其中 [0,4 MB] 以及由 kernel 占用或被 kinit1 收集,剩下的 [4 MB, 240 MB] 交给 kinit2 函数负责。
void
kinit2(void *vstart, void *vend)
{
freerange(vstart, vend);
kmem.use_lock = 1;
}
之后就开始使用 kmem.lock 了。
通过调用 userinit 来启动第一个用户进程,首先调用 allocproc 函数分配 PCB,我们看看 PCB 结构体是如何的:
// Per-process state
struct proc {
uint sz; // Size of process memory (bytes)
pde_t* pgdir; // Page table
char *kstack; // Bottom of kernel stack for this process
enum procstate state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
struct trapframe *tf; // Trap frame for current syscall
struct context *context; // swtch() here to run process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
};
上面的注释已经很清楚了,allocproc 函数首先找一个空闲的 PCB,然后将 PCB 状态置为 EMBRYO,pid 由全局自增变量赋值,接着分配内核栈,每个进程都有自己的内核栈,然后将栈首地址赋给 sp 指针。
由于栈的地址是从高到低的,所以栈地址应该记录栈顶,也就是加上 KSTACKSIZE。
进程的内核栈初始化后的结构如下:
/*
进程的kernel stack初始化状态,
/ +---------------+ <-- stack base(= p->kstack + KSTACKSIZE)
| | ss |
| +---------------+
| | esp |
| +---------------+
| | eflags |
| +---------------+
| | cs |
| +---------------+
| | eip | <-- 从此往上部分,在iret时自动弹出到相关寄存器中,只需把%esp指到这里即可
| +---------------+
| | err |
| +---------------+
| | trapno |
| +---------------+
| | ds |
| +---------------+
| | es |
| +---------------+
| | fs |
struct trapframe | +---------------+
| | gs |
| +---------------+
| | eax |
| +---------------+
| | ecx |
| +---------------+
| | edx |
| +---------------+
| | ebx |
| +---------------+
| | oesp |
| +---------------+
| | ebp |
| +---------------+
| | esi |
| +---------------+
| | edi |
\ +---------------+ <-- p->tf
| trapret |
/ +---------------+ <-- forkret will return to
| | eip(=forkret) | <-- return addr
| +---------------+
| | ebp |
| +---------------+
struct context | | ebx |
| +---------------+
| | esi |
| +---------------+
| | edi |
\ +-------+-------+ <-- p->context
| | |
| v |
| empty |
+---------------+ <-- p->kstack
*/
紧接着全局指针 initproc 标记第一个进程。然后配置进程空间的内核区,紧接着配置第一个进程空间的用户区,如下:
// Load the initcode into address 0 of pgdir.
// sz must be less than a page.
void
inituvm(pde_t *pgdir, char *init, uint sz)
{
char *mem;
if(sz >= PGSIZE)
panic("inituvm: more than a page");
mem = kalloc();
memset(mem, 0, PGSIZE);
mappages(pgdir, 0, PGSIZE, V2P(mem), PTE_W|PTE_U);
memmove(mem, init, sz);
}
配置完继续完善 PCB,包括文件大小、段寄存器、EFLAGS 标志寄存器、栈寄存器、指令寄存器、名字、目录。最后设置状态为 RUNNABLE。
//PAGEBREAK: 32
// Set up first user process.
void
userinit(void)
{
struct proc *p;
extern char _binary_initcode_start[], _binary_initcode_size[];
p = allocproc();
initproc = p;
if((p->pgdir = setupkvm()) == 0)
panic("userinit: out of memory?");
inituvm(p->pgdir, _binary_initcode_start, (int)_binary_initcode_size);
p->sz = PGSIZE;
memset(p->tf, 0, sizeof(*p->tf));
p->tf->cs = (SEG_UCODE << 3) | DPL_USER;
p->tf->ds = (SEG_UDATA << 3) | DPL_USER;
p->tf->es = p->tf->ds;
p->tf->ss = p->tf->ds;
p->tf->eflags = FL_IF;
p->tf->esp = PGSIZE;
p->tf->eip = 0; // beginning of initcode.S
safestrcpy(p->name, "initcode", sizeof(p->name));
p->cwd = namei("/");
// this assignment to p->state lets other cores
// run this process. the acquire forces the above
// writes to be visible, and the lock is also needed
// because the assignment might not be atomic.
acquire(&ptable.lock);
p->state = RUNNABLE;
release(&ptable.lock);
}
最后 BP 也调用 mpmain 函数进入调度函数 scheduler(),所以我们可以发现,其实 BP 是最后进入调度函数的,每个 CPU 进入调度函数之前都会向终端输出启动信息,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
}