panda's tech note

Advent Calendar 2018: advos

Day 14: マルチコア化に向けた準備(ACPI)

これまではCPUの1つのコアしか使ってきませんでした。近年はマルチコアが当たり前の環境で,シングルコアでしか動かないOSを作っても楽しくないと思うので,マルチコアを取り扱います。また,以前説明した通り,最近のCPUはマルチソケットではNUMAが当たり前の環境になってきています。

これまで処理を実行してきたコアをBootStrap Processor (BSP)と呼びます。一方,BSPから起動するコアをApplication Processorと呼びます。

QEMUのマルチコア・NUMAサポート

これまでは,実行環境としてQEMUで1ソケット1コアのプロセッサをエミュレーションしていましたが,今回からはマルチコアとNUMAのエミュレーションを有効にします。そのために,Dockerfileを以下のように書き換えました。2ソケット8コア,2 NUMAドメインをエミュレートします。

## Run the OS with qemu
CMD ["qemu-system-x86_64", "-m", "1024", \
        "-smp", "cores=4,threads=1,sockets=2", \
        "-numa", "node,nodeid=0,cpus=0-3", \
        "-numa", "node,nodeid=1,cpus=4-7", \
        "-drive", "id=disk,format=raw,file=advos.img,if=none", \
        "-device", "ahci,id=ahci", \
        "-device", "ide-drive,drive=disk,bus=ahci.0", \
        "-boot", "a", "-display", "curses"]

panic関数の用意

今回から所々にassertionを入れていきます。例えば構造体のメモリサイズを一定以下であることを保証するためにif文を追加します。このとき,バグなどで予定外の値が来たときにエラーにするようにします。このエラーはエラーハンドリング不可能なので,CPUの実行も止めるようにします。Windowsのブルースクリーンの超簡易版です。

ブルースクリーンは少し嫌なので,今回はグリーンスクリーンになるpanic()関数を作りました。

void
panic(const char *s)
{
    uint16_t *video;
    uint16_t val;
    int i;
    int col;
    int ln;

    /* Video RAM */
    video = (uint16_t *)0xb8000;

    /* Fill out with green */
    for ( i = 0; i < 80 * 25; i++ ) {
        video[i] = 0x2f00;
    }

    col = 0;
    ln = 0;
    for ( i = 0; *s; s++  ) {
        switch ( *s ) {
        case '\r':
            video -= col;
            i -= col;
            col = 0;
            break;
        case '\n':
            video += 80;
            i += 80;
            ln++;
            break;
        default:
            *video = 0x2f00 | *s;
            video++;
            i++;
            col++;
        }
    }

    /* Move the cursor */
    val = ((i & 0xff) << 8) | 0x0f;
    outw(0x3d4, val);   /* Low */
    val = (((i >> 8) & 0xff) << 8) | 0x0e;
    outw(0x3d4, val);   /* High */

    /* Stop forever */
    while ( 1 ) {
        hlt();
    }
}

ACPI

Advanced Configuration and Power Interface (ACPI)を扱います。ACPIは本当にややこしいので,できれば扱いたくなかったのですが,NUMAドメインの設定情報を見るために必須なので扱います。また,折角ACPIを取り扱うなら,マルチコアを起動するためにマイクロ秒オーダーで待つ必要があるのですが,そこにACPI PMクロックを使うことにします。

ACPIは,ハードウェア構成のテーブルとAMLコードおよびシステム管理モードのコードで構成されたランタイムで構成されます。今回は前半のハードウェア構成のテーブル情報のみを取り扱います。このテーブル情報はツリー構造になっているため,以下ではルートから取り扱います。

Root System Descriptor Pointer (RSDP)

RSDPはACPIのハードウェア構成テーブルのうち,システム情報を表すテーブルです。このテーブルはExtended BIOS Data Area (EBDA)の最初の1 KiB領域,または1 MiB以下のMain BIOS Area (0x000e0000-0x000fffff)にあります。EBDAのセグメント情報は通常BDAのうち0x040eに入っています。

RSDPの構造は以下の36バイトになっています。signatureにはRSD PTRが入ります。前半の20バイトがACPI 1.0のデータ構造で,checksumはこの20バイトのチェックサムです。36バイトの構造体はACPI 2.0以降のデータ構造で,チェックサムはextended_checksumに入ります。revision0の場合はACPI 1.xで,1以上の場合はACPI 2.0以降です。

struct acpi_rsdp {
    char signature[8];
    uint8_t checksum;
    char oemid[6];
    uint8_t revision;
    uint32_t rsdt_addr;
    /* the following values are introduced since 2.0 */
    uint32_t length;
    uint64_t xsdt_addr;
    uint8_t extended_checksum;
    char reserved[3];
} __attribute__ ((packed));

以下のacpi_parse()関数から始まるコードのように,これらの領域から,チェックサムが正しく,シグネチャがRSD PTRとなる領域を探します。見つかれば,そこからパースを開始します。

static int
_validate_checksum(const uint8_t *ptr, int len)
{
    uint8_t sum = 0;
    int i;

    for ( i = 0; i < len; i++ ) {
        sum += ptr[i];
    }

    return sum;
}

static int
_rsdp_search_range(acpi_t *acpi, uintptr_t start, uintptr_t end)
{
    uintptr_t addr;
    struct acpi_rsdp *rsdp;

    for ( addr = start; addr < end; addr += 0x10 ) {
        /* Check the checksum of the RSDP */
        if ( 0 == _validate_checksum((uint8_t *)addr, 20) ) {
            /* Checksum is correct, then check the signature. */
            rsdp = (struct acpi_rsdp *)addr;
            if ( 0 == kmemcmp((uint8_t *)rsdp->signature, "RSD PTR ", 8) ) {
                /* This seems to be a valid RSDP, then parse RSDT. */
                return _parse_rsdt(acpi, rsdp);
            }
        }
    }

    return 0;
}

int
acpi_parse(acpi_t *acpi)
{
    uint16_t ebda;
    uintptr_t ebda_addr;

    /* Reset the data structure */
    kmemset(acpi, 0, sizeof(acpi_t));

    /* Check 1KB of EBDA, first */
    ebda = *(uint16_t *)BDA_EDBA;
    if ( ebda ) {
        ebda_addr = (uintptr_t)ebda << 4;
        if ( _rsdp_search_range(acpi, ebda_addr, ebda_addr + 0x0400) ) {
            return 1;
        }
    }

    /* If not found in the EDBA, check main BIOS area */
    return _rsdp_search_range(acpi, 0xe0000, 0x100000);
}

Root System Descriptor Table (RSDT)

RSDTはRSDPのrsdt_addr(ACPI 1.x)またはxsdt_addr(ACPI 2.0以降)からポイントされるテーブルです。このテーブル内に,各種System Descriptor Tableへのポインタが入っています。以下の構造体はRSDTのヘッダ(各種System Descriptor Tableのヘッダも共通)です。lengthはこのヘッダとヘッダの後に続くポインタ配列を合わせたサイズです。

struct acpi_sdt_hdr {
    char signature[4];
    uint32_t length;
    uint8_t revision;
    uint8_t checksum;
    char oemid[6];
    char oemtableid[8];
    uint32_t oemrevision;
    uint32_t creatorid;
    uint32_t creatorrevision;
} __attribute__ ((packed));

ポインタのサイズはACPI 1.xとACPI 2.0以降で異なるので,以下のコードのようにACPIのバージョンでサイズを決定して,エントリ数を数えます。

    /* Check the ACPI version */
    if ( rsdp->revision >= 1 ) {
        /* ACPI 2.0 or later */
        sz = 8;
        rsdt = (struct acpi_sdt_hdr *)rsdp->xsdt_addr;
        if ( 0 != kmemcmp((uint8_t *)rsdt->signature, "XSDT", 4) ) {
            return 0;
        }
    } else {
        /* Parse RSDT (ACPI 1.x) */
        sz = 4;
        rsdt = (struct acpi_sdt_hdr *)(uintptr_t)rsdp->rsdt_addr;
        if ( 0 != kmemcmp((uint8_t *)rsdt->signature, "RSDT", 4) ) {
            return 0;
        }
    }

    /* Compute the number of SDTs */
    nr = (rsdt->length - sizeof(struct acpi_sdt_hdr)) / sz;

このポインタ先もstruct acpi_sdt_hdrをヘッダとして持つので,signatureを確認してパースをします。以下に今回扱うシグネチャとそのテーブルの説明をまとめます。

  • APIC: Multiple APIC Description Table (MADT)
  • FACP: Fixed ACPI Description Table (FADT)
  • SRAT: System/Static Resource Affinity Table (SRAT)

Multiple APIC Description Table (MADT)

struct acpi_sdt_hdrのあとに

struct acpi_sdt_apic {
    uint32_t local_controller_addr;
    uint32_t flags;
} __attribute__ ((packed));

が続きます。local_controller_addrはLocal APICのアドレスで,flagsは1であれば8259 PICが2つ搭載されていることを意味します。

この下に可変長のエントリが複数入ります。各エントリには以下のヘッダ(タイプと長さ)が入ります。

struct acpi_sdt_apic_hdr {
    uint8_t type; /* 0 = local APIC, 1 = I/O APIC */
    uint8_t length;
} __attribute__ ((packed));

タイプが0の場合はLocal APICの情報が入ります。以下はその構造体です。

struct acpi_sdt_apic_lapic {
    struct acpi_sdt_apic_hdr hdr;
    uint8_t cpu_id;
    uint8_t apic_id;
    uint32_t flags;
} __attribute__ ((packed));

タイプが1の場合はI/O APICの情報が入ります。以下はその構造体です。

struct acpi_sdt_apic_ioapic {
    struct acpi_sdt_apic_hdr hdr;
    uint8_t id;
    uint8_t reserved;
    uint32_t addr;
    uint32_t global_int_base;
} __attribute__ ((packed));

Fixed ACPI Description Table (FADT)

FADTはACPI関係の情報がまとまっています。大量にありますが,ACPIタイマのポートpm_timer_blockまたはx_pm_timer_block.addrとタイマが24ビット数であることを表すflagsのBit 8を後で使うのでacpi_t構造体に保存します。また,pm1a_ctrl_blockまたはx_pm1a_ctrl_block.addrpm1b_ctrl_blockまたはx_pm1b_ctrl_block.addrは電源を切るために後で使うかもしれませんのでacpi_t構造体に保存しておきます。他にもいくつか値を保存していますが,今回は説明しません。

struct acpi_sdt_fadt {
    /* acpi_sdt_hdr */
    uint32_t firmware_ctrl;
    uint32_t dsdt;

    uint8_t reserved;

    uint8_t preferred_pm_profile;
    uint16_t sci_interrupt;
    uint32_t smi_cmd_port;
    uint8_t acpi_enable;
    uint8_t acpi_disable;
    uint8_t s4bios_req;
    uint8_t pstate_ctrl;
    uint32_t pm1a_event_block;
    uint32_t pm1b_event_block;
    uint32_t pm1a_ctrl_block;
    uint32_t pm1b_ctrl_block;
    uint32_t pm2_ctrl_block;
    uint32_t pm_timer_block;
    uint32_t gpe0_block;
    uint32_t gpe1_block;
    uint8_t pm1_event_length;
    uint8_t pm1_ctrl_length;
    uint8_t pm2_ctrl_length;
    uint8_t pm_timer_length;
    uint8_t gpe0_length;
    uint8_t gpe1_length;
    uint8_t gpe1_base;
    uint8_t cstate_ctrl;
    uint16_t worst_c2_latency;
    uint16_t worst_c3_latency;
    uint16_t flush_size;
    uint16_t flush_stride;
    uint8_t duty_offset;
    uint8_t duty_width;
    uint8_t day_alarm;
    uint8_t month_alarm;
    uint8_t century;

    uint16_t boot_arch_flags;

    uint8_t reserved2;
    uint32_t flags;

    struct acpi_generic_addr_struct reset_reg;
    uint8_t reset_value;

    uint8_t reserved3[3];

    uint64_t x_firmware_ctrl;
    uint64_t x_dsdt;

    struct acpi_generic_addr_struct x_pm1a_event_block;
    struct acpi_generic_addr_struct x_pm1b_event_block;
    struct acpi_generic_addr_struct x_pm1a_ctrl_block;
    struct acpi_generic_addr_struct x_pm1b_ctrl_block;
    struct acpi_generic_addr_struct x_pm2_ctrl_block;
    struct acpi_generic_addr_struct x_pm_timer_block;
    struct acpi_generic_addr_struct x_gpe0_block;
    struct acpi_generic_addr_struct x_gpe1_block;

} __attribute__ ((packed));

System/Static Resource Affinity Table (SRAT)

SRATはプロセッサ(Local APIC)とメモリのNUMAドメインを解決するのに使います。

SRATの各エントリは,タイプ,長さ,エントリのコンテンツから構成されます。タイプが0のときはLocal APIC,2のときはx2APICのAffinity情報エントリで,以下の構造体で表されます。このapic_idがLocal APICのIDです。proximity_domainはNUMAドメインを表します。

struct acpi_sdt_srat_lapic {
    uint8_t type;                    /* 0: Local APIC */
    uint8_t length;                  /* 16 */
    uint8_t proximity_domain;        /* Bit 7-0 */
    uint8_t apic_id;
    uint32_t flags;
    uint8_t local_sapic_eid;
    uint8_t proximity_domain2[3];    /* Bit 31-8 */
    uint32_t clock_domain;
} __attribute__ ((packed));

また,タイプが1のときはメモリのAffinity情報エントリです。base_addr_lowおよびbase_addr_highlength_lowおよびlength_highがメモリの領域を表し,proximity_domainがNUMAドメインを表します。

struct acpi_sdt_srat_memory {
    uint8_t type;                    /* 1: Memory */
    uint8_t length;                  /* 40 */
    uint32_t proximity_domain;
    uint8_t reserved1[2];
    uint32_t base_addr_low;
    uint32_t base_addr_high;
    uint32_t length_low;
    uint32_t length_high;
    uint32_t reserved2;
    uint32_t flags;
    uint8_t reserved3[8];
} __attribute__ ((packed));

ACPI情報の読み込み

acpi_load()関数でCPUやメモリのNUMA情報やACPI PMタイマのポート情報などをacpi_tに読み込みます。

ACPI読み込みの一連の処理は,src/kernel/arch/x86_64/acpi.cの実装を参照してください。

ACPI PMクロック

この後,APを起動するのにマイクロ秒オーダーで処理を待つ必要があるため,何らかのクロックソースで時間計測を行う必要があります。クロックソースとしてCPUはクロックに合わせてカウントをするTSCというものがあります。クロックソースとしてはTSC,HPETやACPI PMがありますが,今回はACPI PMを使います。TSCが一番簡単なのですが,QEMUなどではInvariant TSCと呼ばれる機能がサポートされておらず,TSCのクロック周波数を取得できないため,ACPI PMを使います。

上述のacpi_load()で読み込んだacpi_tの構造体にはACPI PMのI/Oポート情報が入っています。この値により,以下の関数のようにACPI PMタイマが使えるかを確認できます。

int
acpi_timer_available(acpi_t *acpi)
{
    if ( 0 == acpi->pm_tmr_port ) {
        return -1;              /* Not available */
    } else {
        return 0;               /* Available */
    }
}

また,タイマの値は以下のようにI/Oから読み込めます。

uint32_t
acpi_get_timer(acpi_t *acpi)
{
    return inl(acpi->pm_tmr_port);
}

最後に,usecマイクロ秒だけビジーウェイトする関数を実装します。ACPI PMタイマのカウンタのビット幅はバージョンによって異なります。そのため,以下のようにacpi->pm_tmr_extの情報を見て,ビット幅を決定する必要があります。

#define ACPI_TMR_HZ 3579545
uint64_t
acpi_get_timer_period(acpi_t *acpi)
{
    if ( acpi->pm_tmr_ext ) {
        /* 32-bit counter */
        return ((uint64_t)1ULL << 32);
    } else {
        /* 24-bit counter */
        return (1 << 24);
    }
}

ACPI PMのタイマは約3579545 Hzで動作するため,以下のコードで第二引数のusecマイクロ秒ビジーウェイトします。

#define ACPI_TMR_HZ 3579545
void
acpi_busy_usleep(acpi_t *acpi, uint64_t usec)
{
    uint64_t clk;
    volatile uint64_t acc;
    volatile uint64_t cur;
    volatile uint64_t prev;

    /* usec to count */
    clk = (ACPI_TMR_HZ * usec) / 1000000;

    prev = acpi_get_timer(acpi);
    acc = 0;
    while ( acc < clk ) {
        cur = acpi_get_timer(acpi);
        if ( cur < prev ) {
            /* Overflow */
            acc += acpi_get_timer_period(acpi) + cur - prev;
        } else {
            acc += cur - prev;
        }
        prev = cur;
        pause();
    }
}

動作テスト

動作テストとして,src/kernel/arch/x86_64/arch.cにACPIで読んだメモリのNUMA情報を表示するプログラムを書きました。src/kernel/kernel.cに書いたシステムメモリマップ表示もこちらに移動しました。このシステムメモリマップ表示前に5秒のビジーウェイトを入れています。

Welcome to advos (64-bit)!             
# of CPU cores = 0x00000008
Base             Length           Domain
0000000000000000 00000000000a0000 0000000000000000                        
0000000000100000 000000001ff00000 0000000000000000
0000000020000000 0000000020000000 0000000000000001
0000000000000000 0000000000000000 0000000000000000

と表示された5秒後にシステムメモリマップが表示されると思います。

まとめと明日の予定

今日はマルチコアを使うためにACPIを扱いました。明日は本格的にマルチコア対応に入ろうと思います。