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から以下の処理を実行します。
- INIT IPIメッセージをブロードキャストする
- 10 ms待つ
- Startup IPIメッセージをブロードキャストする(割り込みベクタにトランポリンコードの4 KiBページでのインデックスを指定する)
- 200 us待つ
- もう一度Startup IPIメッセージをブロードキャストする
- 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_entry32
は0xc0000000-
の仮想メモリ上を指し示しますが,この時点の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の起動を行いました。次回は,アーキテクチャ依存のコードを整備して,カーネル機能の開発に入れるようにしていこうと思います。