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
}