在上文(Xv6学习小记(一)——编译与运行)中,我们介绍了Linux下编译运行Xv6系统的方式。
本文将介绍Xv6是如何多核启动的,涉及到的内容有:Xv6多核启动的大致步骤、Xv6检测CPU个数的方法和Xv6发送中断的方法等。
1 多核启动步骤说明
Xv6启动时先将系统放入BSP(Bootstrap
processor
,启动CPU)中启动,BSP进入main()
方法后首先进行了一系列初始化,其中包括mpinit()
,此方法目的是检测CPU个数并将检测到的CPU存入一个全局的数组中,之后进入startothers()
方法通过向AP(non-boot
CPU
,非启动CPU)发送中断的方式来启动AP,最后执行mpmain()
方法。
main()
方法代码如下:
1 | int |
AP被BSP在startothers()
方法里启动,启动后会进入mpenter()
方法,mpenter()
方法的代码如下:
1 | // Other CPUs jump here from entryother.S. |
可以看出,AP在执行完一些初始化后最后也是执行了mpmain()
方法。
即每个CPU启动后都会执行mpmain()
方法,mpmain()
方法代码如下:
1 | static void |
在mpmain()
中,会打印输出当前正在启动的CPU的ID及“starting
”,然后初始化IDT,将CPU的已启动标志置1,最后开始进程调度。至此,多核启动完成。
BSP和AP启动时执行的函数对比:
1.BSP和AP都需要执行的几个函数是:
seginit() //段初始化
lapicinit() //本地APIC初始化
mpmain()2.BSP需要执行而AP不需要执行的主要函数有:(按执行顺序)
kinit1(end, P2V(4*1024*1024)); // phys page allocator
kvmalloc(); // kernel page table
mpinit(); // collect info about this machine
picinit(); // interrupt controller
ioapicinit(); // another interrupt controller
consoleinit(); // I/O devices & their interrupts
uartinit(); // serial port
pinit(); // process table
tvinit(); // trap vectors
binit(); // buffer cache
fileinit(); // file table
ideinit(); // disk
if(!ismp)
timerinit(); // uniprocessor timer
startothers(); // start other processors
kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // must come after startothers()
userinit(); // first user process3.AP需要执行而BSP不需要执行的函数有:
switchkvm();
2 检测CPU个数的方法
2.1 系统首先进行查找MP浮点结构:
①.如果BIOS扩展资料区域(EBDA)已经定义,则在其中的第一K字节中进行查找,否则到②;
②.若EBDA未被定义,则在系统基本内存的最后一K字节中寻找;
③.在BIOS ROM里的0xF0000到0xFFFFF的地址空间中寻找。
注:关于如何判断是否定义EBDA、EBDA的地址、系统基本内存的地址参见附录1(Xv6启动中有关BDA的相关说明)。
在实模式下运行以下代码:
1 | static struct mp *mpsearch(void) |
2.2 mpsearch1()方法
在如上代码中,被多次调用的mpsearch1()
方法即为查找MP浮点结构的具体方法,mpsearch1()
的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13static struct mp*
mpsearch1(uint a, intlen)
// 从内存地址a开始,长度为len的区域中搜索,返回指向MP浮点结构的指针
{
uchar *e, *p, *addr;
addr = p2v(a);
e = addr+len;
for(p = addr; p < e; p += sizeof(struct mp))
if(memcmp(p, "_MP_", 4) == 0 && sum(p, sizeof(struct mp)) == 0)
return (struct mp*)p;
return 0;
}
可以看出,此方法将“_MP_”字符串作为了MP浮点结构的标识,匹配到此字符串即找到了MP浮点结构,本函数返回指向该MP浮点结构的指针。
2.3 mp浮点结构的结构体:
1 | struct mp { // floating pointer |
如上,紧跟在”_MP_”此标识后面的就是指向MP配置表头物理地址的指针。mp.c文件中的mpconfig()方法返回了MP配置表头的虚拟地址,代码如下:
1 | static struct mpconf* |
MP配置表头的结构体如下:
1 | struct mpconf { // configuration table header |
2.4 MP配置表
MP浮点结构中包含指向MP配置表头的物理地址的指针, MP配置表由MP配置表头(基本部分)和扩展部分组成,基本部分就是MP配置基表即MP配置表头,扩展部分紧跟表头后面,扩展部分由5种不同类型的入口组成,分别为:
1 | // Table entry types |
2.5 mpinit()方法
程序在mpinit()
方法中遍历MP扩展部分通过判断入口类型来进行相应操作,如判断入口类型为MPPROC
时则将ncpu加1,部分代码如下:
1 | bcpu = &cpus[0]; |
以上代码中,mpproc
和mpioapic
分别是CPU入口结构和I/OAPIC入口结构,他们的结构体定义如下:
1 | struct mpproc { // processor table entry |
1 | struct mpioapic { // I/O APIC table entry |
2.6 系统执行完mpinit()方法后即将CPU个数存入了全局变量ncpu中。
3 startothers()方法
xv6通过一个结构体将每个CPU的信息保存起来,具体的cpu结构体如下:(在proc.h中)
1 | // Per-CPU state |
xv6使用一个数组来保存这样的结构体,并用一个全局变量表示CPU数量:1
2extern struct cpu cpus[NCPU];
extern int ncpu;
xv6调用mpinit()
方法初始化了cpus结构体数组,并确定了lapic地址
、ioapicid
,得到了每个CPU的id和CPU数量,接下来在main()
函数中调用startothers()
函数来启动其他CPU。
在startothers()
函数中,首先把entryother.S
的代码拷贝到以0x7000
起始的这块内存(因为这段内存未被使用)里。然后在0x7000-4
、0x7000-8
两个内存单元记录下entryother.S
中将要进行跳转的内核栈位置以及mpmain
的入口地址(mpenter
)。
这样当CPU运行完entryother.S
中的代码之后将进入mpmain
过程。在mpmain
中,每个CPU将进行中断表和段表的初始化,然后打开中断进入scheduler()
过程。
有关entryother
这段启动代码的说明:
根据Makefile的102到106行:1
2
3
4
5102:entryother: entryother.S
103: $(CC) $(CFLAGS) -fno-pic -nostdinc -I. -c entryother.S
104: $(LD) $(LDFLAGS) -N -e start -Ttext 0x7000 -o bootblockother.o entryother.o
105: $(OBJCOPY) -S -O binary -j .text bootblockother.o entryother
106: $(OBJDUMP) -S bootblockother.o > entryother.asm
可以了解到:Makefile的103行是通过gcc把entryother.S
编译成目标文件entryother.o
。104行是通过LD把entryother.o
进行地址重定位,设定其起始入口点为start
,起始地址位0x7000
,并生成文件bootblockother.o
。105行是通过objcopy
把bootblockother.o
转变成二进制代码entryother
。106行是通过objdump
把bootblockother.o
反汇编成entryother.asm
。
将entryothers
移动到物理地址0x7000
处使其能正常运行。因为这是其他CPU最初运行的内核代码,所以没有开启保护模式和分页机制,entryothers
将页表设置为entrypgdir
,在设置页表前,虚拟地址等于物理地址。
startothers()
代码说明如下:
1 | static void |
由上述代码可知,BSP启动AP时经过了以下步骤:
1.复制启动代码到0x7000处,这部分代码相当于boot CPU的启动扇区代码
2.为每个AP分配stack(每个CPU都一个自己的stack)
3.告诉每个AP,kernel入口在哪里(mpenter函数)
4.告诉每个AP,页目录在哪里(entrypgdir)
4 Xv6中断
4.1 Xv6系统中断说明:
Xv6系统在单核处理器上使用8259A中断控制器来处理中断(代码在picirq.c
,此处不表),在多核处理器上采用了APIC(Advanced Programmable Interrupt Controller)
来处理中断。
APIC机制中,每一颗 CPU 都需要一个中断控制器来处理发送给它的中断,而且也得有一个方法来分发中断。 这一方式包括两个部分:一个部分是在 I/O系统中的(IO APIC
,ioapic.c
),另一部分是关联在每一个处理器上的(本地APIC,lapic.c
),本小节主要讲解lapic
。
4.2 地址:
lapic
的物理地址为0xFEE00000
(参考Intel官方手册)。
在xv6系统中,系统通过调用mpinit()
方法中,读取MP配置表头获取到了lapic
的物理地址。
4.3 lapic.c中的主要函数:
lapicw()
: 写Local APIC
寄存器,此函数有两个参数,第一个参数为lapic
的偏移地址,第二个参数为要写入的值;cpunum()
:返回正在运行的CPU的ID;lapiceoi()
:响应中断,即向EOI寄存器发送0;lapicinit()
:初始化本CPU的Local APIC
;lapicstartap()
:通过写ICR寄存器的方式启动AP,此函数有两个参数,第一个参数为要启动的AP的ID,第二个参数为启动代码的物理地址。具体讲解见下文。
4.4 lapicstartap()函数说明:
BSP通过向AP逐个发送中断来启动AP,首先发送INIT中断来初始化AP,然后发送SIPI中断来启动AP,发送中断使用的是写ICR寄存器的方式,代码说明如下:
1 | // 发送INIT中断以重置AP |
4.5 ICR寄存器说明:
中断命令寄存器(ICR)是一个 64 位本地 APIC寄存器,允许运行在处理器上的软件指定和发送处理器间中断(IPI)给系统中的其它处理器。发送IPI时,必须设置ICR 以指明将要发送的 IPI消息的类型和目的处理器或处理器组。一般情况下,ICR寄存器的物理地址为0xFEE00300,其结构图如下:
如图,一般在传送模式域中写各种传送类型,本例中用到了101INIT和110Start Up两种类型。Destination Mode域是0时表示Destination Field域中为一个CPU的ID,是1时表示Destination Field域中为一组CPU。
SIPI是一个特殊的IPI。典型情况下,在发送SIPI时,ICR的向量域中指向一个启动例程,本例中即将entryother的代码地址写入了ICR的向量域,以启动AP。
附录1 Xv6启动中有关BDA的相关说明
当计算机通电时,BIOS数据区(BIOS Data Area)将在000400h处创建。它长度为256字节(000400h - 0004FFh),包含有关系统环境的信息。该信息可以被任何程序访问和更改。计算机的大部分操作由此数据控制。此数据在启动过程中由POST(BIOS开机自检)加载。
如果EBDA(Extended BIOS Data Area,扩展BIOS数据区)不存在,BDA[0x0E]和BDA[0x0F]的值为0;如果EBDA存在,其段地址被保存在BDA[0x0E]和BDA[0x0F]中,其中BDA[0x0E]保存EBDA段地址的低8位,BDA[0x0F]保存EDBA段地址的高8位,所以(BDA[0x0F]\<\<8) | BDA[0x0E]就表示了EDBA的段地址,将段地址左移4位即为EBDA的物理地址,如下图,BDA[0x0F]=0x9F,BDA[0x0E]=0xC0,所以xv6中EBDA存在且段地址为0x9FC0,物理地址为0x9FC00。
BDA[0x13]和BDA[0x14]分别存放着系统基本内存的大小的低8位和高8位,如上图,BDA[0x14]=0x2,BDA[0x13]=0x7F,所以系统基本内存的大小为0x27F个KB,再乘1024即将单位转化为了B。因为系统基本内存的地址是从0开始的,所以将指针p指向其内存大小,就获得了其末尾边界的地址。