Xv6学习小记(二)——多核启动

本文首发于我的个人博客QIMING.INFO,转载请带上链接及署名。(注:本文代码中的注释很重要,如看不清,可移步我的个人博客中查看)

在上文(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()方法代码如下:

int
main(void)
{
  kinit1(end, P2V(4*1024*1024)); // phys page allocator
  kvmalloc();      // kernel page table
  mpinit();        // collect info about this machine
  lapicinit();
  seginit();       // set up segments
  cprintf("\ncpu%d: starting xv6\n\n", cpu->id);
  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 process
  // Finish setting up this processor in mpmain.
  mpmain();
}

AP被BSP在startothers()方法里启动,启动后会进入mpenter()方法,mpenter()方法的代码如下:

// Other CPUs jump here from entryother.S.
static void
mpenter(void)
{
  switchkvm(); 
  seginit();
  lapicinit();
  mpmain();
}

可以看出,AP在执行完一些初始化后最后也是执行了mpmain()方法。

即每个CPU启动后都会执行mpmain()方法,mpmain()方法代码如下:

static void
mpmain(void)
{
  cprintf("cpu%d: starting\n", cpu->id);
  idtinit();              // load idt register
  xchg(&cpu->started, 1); // tell startothers() we're up
  scheduler();            // start running processes
}

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 process

3.AP需要执行而BSP不需要执行的函数有:
switchkvm();

2 检测CPU个数的方法

2.1 系统首先进行查找MP浮点结构:

①.如果BIOS扩展资料区域(EBDA)已经定义,则在其中的第一K字节中进行查找,否则到②;

②.若EBDA未被定义,则在系统基本内存的最后一K字节中寻找;

③.在BIOS ROM里的0xF0000到0xFFFFF的地址空间中寻找。

注:关于如何判断是否定义EBDA、EBDA的地址、系统基本内存的地址参见附录1(Xv6启动中有关BDA的相关说明)。

在实模式下运行以下代码:

static struct mp *mpsearch(void)
{
uchar *bda;
uint p;
struct mp *mp;
bda = (uchar *) P2V(0x400); //将0x400转换成虚拟地址,0x400为BIOS存放检测到的数据(即BDA)的物理地址       
if((p = ((bda[0x0F]<<8)| bda[0x0E]) << 4)){ //判断ebda是否存在,如果存在,将ebda的起始指针赋给p
if((mp = mpsearch1(p, 1024)))        //在EDBA的前1024个字节中查找
return mp;                           //找到后返回指向mp浮点结构的指针
  } else {                           //如果EDBA未被定义
p = ((bda[0x14]<<8)|bda[0x13])*1024; //得到系统基本内存的末尾边界地址
if((mp = mpsearch1(p-1024, 1024)))   //在系统基本内存的最后1K中查找
return mp;
  }
return mpsearch1(0xF0000, 0x10000);
}

2.2 mpsearch1()方法

在如上代码中,被多次调用的mpsearch1()方法即为查找MP浮点结构的具体方法,mpsearch1()的代码如下:

static 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浮点结构的结构体:

struct mp {             // floating pointer
  uchar signature[4];   // 标志,为"_MP_"时表示此为MP浮点结构
  void *physaddr;       // MP配置表头的物理地址
  uchar length;         // 此MP浮点结构的长度
  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配置表头物理地址的指针。mp.c文件中的mpconfig()方法返回了MP配置表头的虚拟地址,代码如下:

static struct mpconf*
mpconfig(struct mp **pmp)
{
  struct mpconf *conf;
  struct mp *mp;

  if((mp = mpsearch()) == 0 || mp->physaddr == 0)
                           //判断MP浮点结构或者MP配置表头是否存在
    return 0;              //两者中有一个不存在即返回0
  conf = (struct mpconf*) p2v((uint) mp->physaddr);
			              //将MP配置表头的物理地址转换成虚拟地址并将值赋给conf
  //以下代码为对此地址及结构进行一些合法性判定
  if(memcmp(conf, "PCMP", 4) != 0)
    return 0;
  if(conf->version != 1 && conf->version != 4)
    return 0;
  if(sum((uchar*)conf, conf->length) != 0)
    return 0;
  *pmp = mp;
  return conf;             //返回MP配置表头的虚拟地址
}

MP配置表头的结构体如下:

struct mpconf {         // configuration table header
  uchar signature[4];           // 标志为"PCMP"
  ushort length;                // MP配置表的长度
  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;                 // 入口数
  uint *lapicaddr;              // local APIC的地址
  ushort xlength;               // extended table length
  uchar xchecksum;              // extended table checksum
  uchar reserved;
};

2.4 MP配置表

MP浮点结构中包含指向MP配置表头的物理地址的指针, MP配置表由MP配置表头(基本部分)和扩展部分组成,基本部分就是MP配置基表即MP配置表头,扩展部分紧跟表头后面,扩展部分由5种不同类型的入口组成,分别为:

// Table entry types
#define MPPROC    0x00  //入口类型为处理器
#define MPBUS     0x01  //入口类型为总线
#define MPIOAPIC  0x02  //入口类型为I/O APIC
#define MPIOINTR  0x03  //入口类型为I/O 中断分配
#define MPLINTR   0x04  //入口类型为逻辑中断分配

2.5 mpinit()方法

程序在mpinit()方法中遍历MP扩展部分通过判断入口类型来进行相应操作,如判断入口类型为MPPROC时则将ncpu加1,部分代码如下:

bcpu = &cpus[0];
if((conf = mpconfig(&mp)) == 0)          //调用mpconfig()方法以获取MP配置表头,将值赋给conf并判断其是否为0,若为0,说明MP配置表头不存在,返回
    return;
ismp = 1;
lapic = (uint*)conf->lapicaddr;          //初始化lapic为表头中的lapic地址
for(p=(uchar*)(conf+1), e=(uchar*)conf+conf->length; p<e; ){
    switch(*p){
        case MPPROC:                     //入口类型为处理器
            proc = (struct mpproc*)p;
            if(ncpu != proc->apicid){
                cprintf("mpinit: ncpu=%d apicid=%d\n", ncpu, proc->apicid);
                ismp = 0;
            }
            if(proc->flags & MPBOOT)     //判断此CPU是否为主引导CPU(BSP)
                bcpu = &cpus[ncpu];      //若是BSP,将此CPU设为第0个CPU
            cpus[ncpu].id = ncpu;        //给每个CPU设置ID并存入cpus数组中
            ncpu++;                      //CPU个数+1
            p += sizeof(struct mpproc);  //给p加上此种类型的长度
            continue;                    //继续循环
        case MPIOAPIC:                   //入口类型为I/O APIC
            ioapic = (struct mpioapic*)p;      
            ioapicid = ioapic->apicno;   //给全局变量ioapicid赋值
            p += sizeof(struct mpioapic);//给p加上此种类型的长度
            continue;                    //继续循环
        case MPBUS:                      //入口类型为总线
        case MPIOINTR:                   //入口类型为I/O 中断分配
        case MPLINTR:                    //入口类型为逻辑中断分配 
            p += 8;                      //给p加上此种类型的长度:8
            continue;                    //继续循环
        default:
            cprintf("mpinit: unknown config type %x\n", *p);
            ismp = 0;
    }
}

以上代码中,
mpprocmpioapic分别是CPU入口结构和I/OAPIC入口结构,他们的结构体定义如下:

struct mpproc {         // processor table entry
  uchar type;                 // 入口类型(0)
  uchar apicid;               // local APIC id
  uchar version;              // local APIC verison
  uchar flags;                // CPU flags(CPU启动的标志)
    #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
};

2.6 系统执行完mpinit()方法后即将CPU个数存入了全局变量ncpu中。

3 startothers()方法

xv6通过一个结构体将每个CPU的信息保存起来,具体的cpu结构体如下:(在proc.h中)

// Per-CPU state
struct cpu {
uchar id;                    // Local APIC ID; index into cpus[] below
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.
};

xv6使用一个数组来保存这样的结构体,并用一个全局变量表示CPU数量:

extern struct cpu cpus[NCPU];
extern int ncpu;

xv6调用mpinit()方法初始化了cpus结构体数组,并确定了lapic地址ioapicid,得到了每个CPU的id和CPU数量,接下来在main()函数中调用startothers()函数来启动其他CPU。

startothers()函数中,首先把entryother.S的代码拷贝到以0x7000起始的这块内存(因为这段内存未被使用)里。然后在0x7000-40x7000-8两个内存单元记录下entryother.S中将要进行跳转的内核栈位置以及mpmain的入口地址(mpenter)。
这样当CPU运行完entryother.S中的代码之后将进入mpmain过程。在mpmain中,每个CPU将进行中断表和段表的初始化,然后打开中断进入scheduler()过程。

有关entryother这段启动代码的说明:
根据Makefile的102到106行:

102: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行是通过objcopybootblockother.o转变成二进制代码entryother。106行是通过objdumpbootblockother.o反汇编成entryother.asm

entryothers移动到物理地址0x7000处使其能正常运行。因为这是其他CPU最初运行的内核代码,所以没有开启保护模式和分页机制,entryothers将页表设置为entrypgdir,在设置页表前,虚拟地址等于物理地址。

startothers()代码说明如下:

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);  // 将启动代码复制到0x7000处
    memmove(code, _binary_entryother_start, (uint)_binary_entryother_size);

    for(c = cpus; c <cpus+ncpu; c++){
        //逐个开启每个CPU让每个CPU从entryothers中start标号开始运行
        if(c == cpus+cpunum())  // cpunum()返回当前CPU的ID,所以此处即判断c是否指向BSP,若是,跳过后续部分继续循环
            continue;

        // 告诉entryother.S堆栈地址、mpenter方法的地址和页表的地址。
        stack = kalloc();
        *(void**)(code-4) = stack + KSTACKSIZE;
        *(void**)(code-8) = mpenter;  
        *(int**)(code-12) = (void *) v2p(entrypgdir);

        lapicstartap(c->id, v2p(code));//向这个CPU发中断,让此CPU执行boot程序,此方法将在下文中详细介绍。

        // wait for cpu to finish mpmain()
        while(c->started == 0)        //cpu启动后会将started变为1 ,所以若started为0,则继续循环
            ;
    }
}

由上述代码可知,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 APICioapic.c),另一部分是关联在每一个处理器上的(本地APIC,lapic.c),本小节主要讲解lapic

4.2 地址:

lapic的物理地址为0xFEE00000(参考《IA-32-3中文版》)。

在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寄存器的方式,代码说明如下:

// 发送INIT中断以重置AP
lapicw(ICRHI, apicid<<24);             //将目标CPU的ID写入ICR寄存器的目的地址域中
lapicw(ICRLO, INIT | LEVEL | ASSERT);  //在ASSERT的情况下将INIT中断写入ICR寄存器
microdelay(200);                       //等待200ms
lapicw(ICRLO, INIT | LEVEL);           //在非ASSERT的情况下将INIT中断写入ICR寄存器
microdelay(100); // 等待100ms (INTEL官方手册规定的是10ms,但是由于Bochs运行较慢,此处改为100ms)

//INTEL官方规定发送两次startup IPI中断
for(i = 0; i < 2; i++){
    lapicw(ICRHI, apicid<<24);          //将目标CPU的ID写入ICR寄存器的目的地址域中
    lapicw(ICRLO, STARTUP | (addr>>12));//将SIPI中断写入ICR寄存器的传送模式域中,将启动代码写入向量域中
    microdelay(200);                    //等待200ms
}

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指向其内存大小,就获得了其末尾边界的地址。

你可能感兴趣的:(操作系统,XV6,多核,lapic,中断)