panda's tech note

Advent Calendar 2018: advos

Day 15: マルチコア対応

今日はBSPからAPを起動して複数のCPUコアでプログラムを実行できるようにします。

APIC

BSPとAPのシグナリングはプロセッサ間割り込みで行います。

x86-64では,割り込みコントローラとしてAdvanced Programmable Interrupt Controller (APIC)を使います。APICにはCPUコアごとの割り込みコントローラであるLocal APICとI/Oなど外部機器用の割り込みコントローラであるI/O APICがあります。

Local APIC ID

各CPUコアにはLocal APICがあるため,Local APIC IDによりCPUコアを識別できます。

Local APIC IDは,APIC_BASEアドレスの0x20のBit 31–24読むことで取得できます。APIC_BASEアドレスはIA32_APIC_BASE (0x1b)のMachine Specific Register (MSR)の下位24ビットをクリアすることで取得できます。アセンブリで書くと以下のようになります。%eaxにLocal APIC IDが得られます。

    movl    $0x1b,%ecx
    rdmsr
    andl    $0xfffffffffffff000,%eax
    movl    0x20(%eax),%eax
    shrl    $24,%eax

APIC_BASEアドレスは初期値だと0xfee00000に置かれています。10日目に設定したページテーブルは,この領域が無効化されているので,0xfee00000-0xffffffffをリニアマッピングするようにページテーブルを以下のように若干変更します。

    /* 3-4 GiB (first 2 MiB) */
    e = (uint64_t *)(base + 0x3000);
    *(e + 0) = (0x00000000ULL) | 0x83;
    for ( i = 503; i < 512; i++ ) {
        *(e + i) = (0xc0000000ULL + 0x200000ULL * i) | 0x83;
    }

InterProcessor Interrupt (IPI)

Local APICは,プロセッサ(コア)間の割り込みInterprocessor Interrupt (IPI)を発行・処理する機能を持っています。これを使うことで,BSPからAPの起動を行います。それ以外にもコア間で処理を同期するときなどにIPIを使います。

IPIはLocal APICのInterrupt Command Register (ICR)に書き込むことで発行できます。具体的な話は実装で説明しようと思います。

Application Processorの初期化

APを起動するにはBSPから以下の処理を実行します。

  1. INIT IPIメッセージをブロードキャストする
  2. 10 ms待つ
  3. Startup IPIメッセージをブロードキャストする(割り込みベクタにトランポリンコードの4 KiBページでのインデックスを指定する)
  4. 200 us待つ
  5. もう一度Startup IPIメッセージをブロードキャストする
  6. 200 us待つ

これにより,APが起動し,トランポリンコードを実行します。

INIT IPI

#define APIC_ICR_INIT                   0x00000500
#define APIC_ICR_DEST_ALL_EX_SELF       0x000c0000
/*
 * lapic_send_init_ipi -- send INIT IPI
 */
void
lapic_send_init_ipi(void)
{
    uint32_t icrl;
    uint32_t icrh;
    uint64_t apic_base;

    apic_base = lapic_base_addr();

    icrl = mfrd32(apic_base + APIC_ICR_LOW);
    icrh = mfrd32(apic_base + APIC_ICR_HIGH);

    icrl = (icrl & ~0x000cdfff) | APIC_ICR_INIT | APIC_ICR_DEST_ALL_EX_SELF;
    icrh = (icrh & 0x000fffff);

    mfwr32(apic_base + APIC_ICR_HIGH, icrh);
    mfwr32(apic_base + APIC_ICR_LOW, icrl);
}

SIPI

#define APIC_ICR_STARTUP                0x00000600
#define APIC_ICR_DEST_ALL_EX_SELF       0x000c0000
/*
 * lapic_send_startup_ipi -- send start up IPI
 */
void
lapic_send_startup_ipi(uint8_t vector)
{
    uint32_t icrl;
    uint32_t icrh;
    uint64_t apic_base;

    apic_base = lapic_base_addr();

    do {
        icrl = mfrd32(apic_base + APIC_ICR_LOW);
        icrh = mfrd32(apic_base + APIC_ICR_HIGH);
        /* Wait until it's idle */
    } while ( icrl & (APIC_ICR_SEND_PENDING) );

    icrl = (icrl & ~0x000cdfff) | APIC_ICR_STARTUP | APIC_ICR_DEST_ALL_EX_SELF
        | vector;
    icrh = (icrh & 0x000fffff);

    mfwr32(apic_base + APIC_ICR_HIGH, icrh);
    mfwr32(apic_base + APIC_ICR_LOW, icrl);
}

トランポリンコード

APを起動したときに実行されるコードを トランポリン と呼びます。この名前の由来は,BSPがブートし,64ビットモードに切り替わった後に,APが16ビットリアルモードで起動し,64ビットモードに切り替えるコードであり,飛び跳ねた状態からもう一度同じ位置に跳ね上がってくる様子を例えたものです。

以下の領域の0x00070000-0x00073fffの16 KiBをトランポリンコード用に使います。トランポリンコードはカーネルとは別のバイナリとしてイメージファイルのどこかに保存し,カーネルから読み込んでもよいですが,今回はカーネルと一緒のバイナリに組み込みます。また,カーネルで扱うグローバル変数領域も確保します。

Start End Description
00000500 00007bff ブートローダのスタック領域
00007c00 00007dff MBR
00007e00 00007fff 未使用
00008000 00008fff ブート情報(ドライブ番号など)
00009000 0000cfff ブートモニタ (16 KiB)
0000d000 0000ffff 未使用 (ブートモニタ拡張用に予約)
00010000 0002ffff カーネル (最大128 KiB)
00030000 00067fff 予約
00060000 0006ffff グローバル変数領域
00068000 00068fff コアゾーン管理用領域
00069000 0006ffff カーネルのベースページテーブル
00070000 00073fff trampoline (16 KiB)
00074000 00075fff 予約:GDT (8 KiB)
00076000 00077fff 予約:IDT (8 KiB)
00078000 00078fff 予約
00079000 0007ffff ロングモード用ページテーブル (24 KiB = 6 * 4 KiB以上必要)

トランポリンコードは以下のようにします。IDTやGDTの設定はブートローダで行ったこととほとんど同じです。また,CR0のBit 0をセットしてProtected Modeを有効にして,32ビットコードのエントリポイントへジャンプをします。ただし,ap_entry320xc0000000-の仮想メモリ上を指し示しますが,この時点のAPはまだページテーブルの設定をしていないので,物理メモリアドレスでアクセスする必要があります。物理メモリアドレスに変換するために,0xc0000000を引いています。

#include "const.h"

/* Note that .data section cannot be used in the trampoline file because we
   need to determine the contiguous space of the trampoline code within a
   section from two labels; between _trampoline and _trampoline_end. */

    .globl	_trampoline
    .globl	_trampoline_end

    .text
    .code16

/*
 * Trampoline code starts.  Note that the trampoline code is loaded into a
 * 4 KiB (aligned) page in the lower 1 MiB of memory.  The %cs is automatically
 * set after the SIPI.  %ip is expected to be zero but not sure.  So, we first
 * calculate the offsets of idtr and gdtr.
 */
_trampoline:
    cli

    /* Calculate the base address */
    xorl	%eax,%eax
    movw	$(TRAMPOLINE_VEC << 8),%ax
    movw	%ax,%ds

    /* Setup GDT and IDT */
    lidt	%ds:(idtr - _trampoline)
    lgdt	%ds:(gdtr - _trampoline)

    /* Turn on protected mode */
    movl	%cr0,%eax
    orl	$0x1,%eax
    movl	%eax,%cr0
    ljmpl	$AP_GDT_CODE32_SEL,$(ap_entry32 - KERNEL_RELOCBASE)

    /* Data section but trampoline code cannot be in separated sections */
    .align	16

/* Pseudo interrupt descriptor table */
idtr:
    .word	0x0		/* Limit */
    .long	0x0		/* Base address */
/* Temporary global descriptor table */
gdt:
    .word	0x0,0x0,0x0,0x0		/* Null descriptor */
    .word	0xffff,0x0,0x9a00,0xaf	/* Code 64 */
    .word	0xffff,0x0,0x9200,0xaf	/* Data 64 */
    .word	0xffff,0x0,0x9a00,0xcf	/* Code 32 */
    .word	0xffff,0x0,0x9200,0xcf	/* Data 32 */
    .word	0xffff,0x0,0x9a00,0x8f	/* Code 16 */
    .word	0xffff,0x0,0x9200,0x8f	/* Data 16 */
gdt.1:
gdtr:
    .word	gdt.1 - gdt - 1	/* Limit */
    .long	gdt		/* Base address */

_trampoline_end:

APの起動と確認

トランポリンコードを0x70000にコピーし,上述のIPIにより,割り込みベクタ0x70へのStartup IPIを発行しAPの起動を行えば,トランポリンコードが実行されます。

起動したことを確認するために,80x25のテキスト画面に対して,Local APIC ID行目の右端に!を表示する処理をap_entry32内で実行しています。

#include "const.h"

#define MSR_APIC_BASE           0x1b
#define APIC_LAPIC_ID           0x020

    .globl	ap_entry32

    .text
    .code32

/*
 * Entry point to the 32-bit protected mode for application processors
 */
ap_entry32:
    cli

    /* %cs is automatically set after the long jump operation */
    /* Setup other segment registers */
    movl	$AP_GDT_DATA64_SEL,%eax
    movl	%eax,%ss
    movl	%eax,%ds
    movl	%eax,%es
    movl	%eax,%fs
    movl	%eax,%gs

    /* Get the local APIC ID */
    movl    $MSR_APIC_BASE,%ecx
    rdmsr
    andl	$0xfffffffffffff000,%eax        /* APIC Base */
    movl	APIC_LAPIC_ID(%eax),%eax
    shrl	$24,%eax

    movl	$0xb8000,%ebx
    movl	$160,%ecx
    mull	%ecx
    subl	$2,%eax
    movl	$0x0721,%edx
    addl	%eax,%ebx
    movl	%edx,(%ebx)

1:
    hlt
    jmp	1b

APの起動をするコードはsrc/kernel/arch/x86_64/arch.cに以下のように書いています。

    /* Load trampoline code */
    sz = (uint64_t)trampoline_end - (uint64_t)trampoline;
    if ( sz > TRAMPOLINE_MAX_SIZE ) {
        panic("Trampoline code is too large to load.");
    }
    kmemcpy((void *)(TRAMPOLINE_VEC << 12), trampoline, sz);

    /* Send INIT IPI */
    lapic_send_init_ipi();

    /* Wait 10 ms */
    acpi_busy_usleep(acpi, 10000);

    /* Send a Start Up IPI */
    lapic_send_startup_ipi(TRAMPOLINE_VEC & 0xff);

    /* Wait 200 us */
    acpi_busy_usleep(acpi, 200);

    /* Send another Start Up IPI */
    lapic_send_startup_ipi(TRAMPOLINE_VEC & 0xff);

    /* Wait 200 us */
    acpi_busy_usleep(acpi, 200);

これを実行すると以下のように表示されるはずです。

Welcome to advos (64-bit)!                                                     !
00000000fee00000                                                               !
# of CPU cores = 0x00000008                                                    !
Base             Length           Domain                                       !
0000000000000000 00000000000a0000 0000000000000000                             !
0000000000100000 000000001ff00000 0000000000000000                             !
0000000020000000 0000000020000000 0000000000000001                             !
0000000000000000 0000000000000000 0000000000000000
----------
System memory map; # of entries = 0x0006
Base             Length           Type     Attribute
0000000000000000 000000000009fc00 00000001 00000001
000000000009fc00 0000000000000400 00000002 00000001
00000000000f0000 0000000000010000 00000002 00000001
0000000000100000 000000003fedf000 00000001 00000001
000000003ffdf000 0000000000021000 00000002 00000001
00000000fffc0000 0000000000040000 00000002 00000001

まとめと明日の予定

今日はAPの起動を行いました。次回は,アーキテクチャ依存のコードを整備して,カーネル機能の開発に入れるようにしていこうと思います。