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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章