Top

第2章 カーネル側でAPを制御する

前章では、UEFIの機能を使用してAPを制御する方法を紹介しました。この章では、UEFIの世界を抜けてカーネルへジャンプしてきた後でAPを制御する方法を紹介します。

2.1 ジャンプしてきたAPを認識する

前章の最後で、APをカーネルへジャンプさせました。これで、BSP同様に、カーネルへ来ることはできているのですが、カーネル側のエントリー関数である現状のstart_kernel()には、「BSPであるかAPであるか」の条件分岐が無いです。そのため、BSP/APに関わらずカーネルの初期化処理が実施されます。デバドラの初期化など、カーネルの初期化処理はたいてい、BSP側で一度実施しておけば良いので、start_kernel()冒頭にBSP/APの条件分岐を入れます。

BSPかAPかの判別にプロセッサ番号を見たいところですが、UEFIの世界は既に抜けているので、WhoAmI()は使えません。実は、プロセッサ番号は「Local APIC」と呼ばれるICが持っています。ここでは、Local APICの情報からAP自身のプロセッサ番号を確認してみます。

この項のサンプルディレクトリは「021_lapic_id」です。

2.1.1 Local APICとは

「APIC」は「Advanced Programmable Interrupt Controller」の略で、旧来の割り込みコントローラ(PIC)の進化版です。各プロセッサに一つずつ付いているため「Local APIC」と呼ばれています。(LocalではないAPICが居たりする訳ではないです。)APIC周りの構成例をIntel SDM*1から引用します(図2.1)。

[*1] Intelが提供している開発者向けのマニュアル(Intel Software Developer's Manual)です。配布場所などは本書末尾の「参考情報」を参照してください。

APIC周りの構成例

図2.1: APIC周りの構成例

前章でWhoAmI()で取得したプロセッサ番号の実体は「Local APIC ID」と呼ばれるもので、プロセッサ毎のLocal APICのレジスタに格納されています。

2.1.2 Local APIC IDを取得してみる

それでは、Local APICのレジスタにアクセスし、Local APIC IDを取得してみます。

まず、Local APICのレジスタへはメモリへマップされたアドレスでアクセスします。

Local APICの領域の先頭アドレスは0xfee00000です。これは物理アドレスなのですが、このシリーズの自作OSはページテーブルを、UEFIが設定しておいてくれるまま、物理アドレス=仮想アドレスで使用しているので、そのまま0xfee00000の領域にアクセスすれば良いです。

そして、Local APIC IDが格納されているレジスタは0xfee00020の「Local APIC ID Register」です。4バイトのレジスタで、上位8ビットに実際のLocal APIC IDが格納されています(その他は予約ビットです)。

これを踏まえて、プロセッサ番号(=Local APIC ID)を取得する関数「get_pnum」を「apic.c」というファイルを作って追加してみます(リスト2.1)。

リスト2.1: 021_lapic_id/apic.c

/* 追加(ここから) */
#define LAPIC_REG_BASE  0xfee00000
#define LAPIC_ID_REG    (*(volatile unsigned int *)(LAPIC_REG_BASE + 0x20))

unsigned char get_pnum(void)
{
        return LAPIC_ID_REG >> 24;
}
/* 追加(ここまで) */

0xfee00020からunsigned int(4バイト)でレジスタ値を取得し、24ビット右シフトすることで上位8バイトを得ています。

また、併せてincludeディレクトリに「apic.h」を追加し、get_pnum()のプロトタイプ宣言を追加しておきます(コードは割愛)。

加えて、"acpi.o"をMakefileの"OBJS"へ追加しておいてください(こちらもコードは割愛)。

2.1.3 APの場合の条件分岐を追加する

get_pnum()を実装したことで、カーネル側でもBSP/AP自身がプロセッサ番号を取得できるようになりました。

これを利用して、start_kernel()にAPの場合の条件分岐を追加します。BSP/APそれぞれで、自身のプロセッサ番号を表示させるようにしてみました(リスト2.2)。

リスト2.2: 021_lapic_id/main.c

/* ・・・ 省略 ・・・ */
#include <pic.h>
#include <apic.h> /* 追加 */
/* ・・・ 省略 ・・・ */

#define INIT_APP        "test"

/* 追加(ここから) */
/* コンソールへアクセス可能なCPUを管理(同時アクセスを簡易的に防ぐ) */
unsigned char con_access_perm = 0;
/* 追加(ここまで) */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        /* 追加(ここから) */
        unsigned char pnum = get_pnum();

        /* APの場合、初期化処理をスキップ */
        if (pnum) {
                /* 自分の番まで待つ */
                while (con_access_perm != pnum);

                /* AP自身のプロセッサ番号を表示 */
                puth(pnum, 1);

                /* 次の番へ回す */
                con_access_perm++;

                /* haltして待つ */
                while (1)
                        cpu_halt();
        }
        /* 追加(ここまで) */

        /* フレームバッファ周りの初期化 */
        fb_init(&pi->fb);
        set_fg(255, 255, 255);
        set_bg(0, 70, 250);
        clear_screen();

        /* 追加(ここから) */
        /* BSP自身のプロセッサ番号を表示 */
        puth(pnum, 1);

        /* 次の番へ回す */
        con_access_perm++;

        /* haltして待つ */
        while (1)
                cpu_halt();
        /* 追加(ここまで) */

        /* ACPIの初期化 */
        acpi_init(pi->rsdp);

        /* ・・・ 省略 ・・・ */

start_kernel()冒頭に、プロセッサ番号を確認して0(BSP)でなければAP用の処理を実施する処理を追加しました。BSP/AP共に、実験として、自身のプロセッサ番号を表示した後、無限ループで止めています。BSPの場合のみ、コンソールの初期化をしてからプロセッサ番号表示を行います。

なお、自作カーネルにはまだロック機構が無いため、ここでは、BSPと各APが文字表示を順番に行うように「con_access_perm」変数で、「今コンソールへアクセスして良いプロセッサ番号」を管理しています。プロセッサ番号は、BSPが0、APは1以降となりますので、con_access_permを0から始め、各プロセッサで文字表示し次第、con_access_permをインクリメントすることで、各プロセッサが順番にコンソールへ出力するようにしています。なお、これはあくまでも仮の実装です。ロックについては次項以降で改めて説明、実装します。

2.1.4 動作確認

実行すると、プロセッサの数だけ0から順に番号が表示されます(図2.2)。

021_lapic_idの実行結果

図2.2: 021_lapic_idの実行結果

筆者の環境の場合、プロセッサは4つなので「0123」と表示されています。

2.2 初期化前のコンソールへのアクセスを防ぐ

前項では、BSP/APでのコンソールへの同時アクセスを防ぐため、順番にアクセスする仕組みを入れていました。

汎用的なロックを実装する前に、そもそもAP側で何らかのリソースを使うとき、自身で初期化しないなら誰か(主にBSP)が初期化するのを待つ必要があります。この項では、APがコンソール初期化を待つ処理を追加してみます。

この項のサンプルディレクトリは「022_wait_con_init」です。

2.2.1 APはコンソールの初期化を待つようにする

コンソールの初期化が完了したことを示すフラグ変数「is_con_inited」を追加し、APはこのフラグがセットされるのを待つようにします(リスト2.3)。

リスト2.3: 022_wait_con_init/main.c

/* ・・・ 省略 ・・・ */
#define INIT_APP        "test"

/* 変更(ここから) */
/* コンソールの初期化が完了したか否か */
unsigned char is_con_inited = 0;
/* 変更(ここまで) */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        unsigned char pnum = get_pnum();

        /* APの場合、初期化処理をスキップ */
        if (pnum) {
                /* 変更(ここから) */
                /* コンソールの初期化が完了するまで待つ */
                while (!is_con_inited);

                /* AP自身のプロセッサ番号を表示 */
                puth(pnum, 1);
                /* 変更(ここまで) */

                /* haltして待つ */
                while (1)
                        cpu_halt();
        }

        /* フレームバッファ周りの初期化 */
        fb_init(&pi->fb);
        set_fg(255, 255, 255);
        set_bg(0, 70, 250);
        clear_screen();
        is_con_inited = 1;      /* 追加 */

        /* BSP自身のプロセッサ番号を表示 */
        puth(pnum, 1);

        /* haltして待つ */
        while (1)
                cpu_halt();

        /* ・・・ 省略 ・・・ */

2.2.2 動作確認

実行すると図2.3のように表示されました。

022_wait_con_initの実行結果

図2.3: 022_wait_con_initの実行結果

APがコンソールの初期化を待つようにはなったのですが、put系の関数を同時に呼び出すため、文字と文字の出力が被って表示が壊れてしまっています。

2.3 spin lockを追加する

それでは、コンソールへのアクセスを保護するロック機構を実装します。

この項のサンプルディレクトリは「023_spin_lock」です。

なお、ここで保護する対象は、「putc()による1文字の出力」とします。それにより、前項で確認した「文字出力同士がかぶり、文字が壊れる事態」を防ぎます。文字を出力する順序に対しては特になにもしないので、同時にputc()が呼ばれたときの文字の表示順は任意となります。

2.3.1 ロックとスピンロック

ソフトウェアの処理において、「ここの処理は、別のスレッド等から同時実行されると困る」場合があります。今回のputc()が正にそれで、putc()である1文字を描画中にputc()が呼ばれると、同じ位置に文字を書こうとして文字の表示が壊れます。そのような場合に特定の処理の実行を順番待ちさせる仕組みが「ロック」です。

ロックにはいくつか種類があるのですが、ここでは比較的シンプルでIntel SDMにも実装例がある「スピンロック(Spin Lock)」を実装してみます。スピンロックは、ある区間を誰かがロックを取って実行中の時、その区間を実行しようとした2つ目以降のプロセッサをロックが解放されるまでビジーループで待たせる方式です。シンプルなものなので詳しくは実装で説明します。

2.3.2 スピンロックを実装してみる

さっそくコードを紹介します(リスト2.4)。

リスト2.4: 023_spin_lock/x86.c

/* ・・・ 省略 ・・・ */
/* 追加(ここから) */
/*
Spin_Lock:
        CMP lockvar, 0          ;Check if lock is free
        JE Get_Lock
        PAUSE                   ;Short delay
        JMP Spin_Lock
Get_Lock:
        MOV EAX, 1
        XCHG EAX, lockvar       ;Try to get lock
        CMP EAX, 0              ;Test if successful
        JNE Spin_Lock
Critical_Section:
        <critical section code>
        MOV lockvar, 0
        ...
Continue:

# ref:
# 8.10.6.1 Use the PAUSE Instruction in Spin-Wait Loops
# - Intel(R) 64 and IA-32 Architectures Software Developer's Manual
#   Volume 3 System Programming Guide
*/

void spin_lock(unsigned int *lockvar)
{
        unsigned char got_lock = 0;
        do {
                while (*lockvar)
                        CPU_PAUSE();

                unsigned int lock = 1;
                asm volatile ("xchg %[lock], %[lockvar]"
                              : [lock]"+r"(lock), [lockvar]"+m"(*lockvar)
                              :: "memory", "cc");

                if (!lock)
                        got_lock = 1;
        } while (!got_lock);
}

void spin_unlock(volatile unsigned int *lockvar __attribute__((unused)))
{
        *lockvar = 0;
}
/* 追加(ここまで) */

spin_lock()がロックを取得する関数で、spin_unlock()で解放します。ロック状態は変数で管理し、これらの関数へポインタで渡します(lockvar変数)。

冒頭のコメントが、Intel SDM記載のサンプルで、spin_lock()とspin_unlock()は、C言語で書き下したものです。

spin_lock()/spin_unlock()共にロック状態を保持する変数のポインタを渡すようになっています。

spin_lock()で行っていることは以下の3つです。

  1. ロック状態(lockvarポインタの指す先)が解放済み(0)になるのを待つ。
  2. atomic命令でロック状態を変更(ロックを取得(1))する。
  3. ロックの取得に失敗した場合、再度1.から実施する。成功した場合は関数をreturnする。

1.について、「CPU_PAUSE()」は今回追加したマクロで、x86.hへリスト2.5の様にマクロ定義を追加しました。

リスト2.5: 023_spin_lock/include/x86.h

/* ・・・ 省略 ・・・ */

#define MAX_INTR_NO     256

#define CPU_PAUSE()     asm volatile ("pause")        /* 追加 */

struct __attribute__((packed)) interrupt_descriptor {
        /* ・・・ 省略 ・・・ */

「pause」という命令を呼び出すだけのマクロです。pause命令は、CPUへビジーループがあることを伝えるヒントとして機能する命令です。コレを入れておくとCPUがよしなにウェイト時間を設けて、省電力化へ寄与します。これまで知らなかったので使っていませんでしたが、ビジーループで待機させている箇所にはpause命令を入れておくと良さそうです。

そして、2.について、「atomic命令」は、他の一般的な命令では複数の命令になってしまうものを1命令で行ってくれるものです。1命令(不可分、atomic)なのでその間に割り込まれる事が無いと保証できます。

今回使用しているatomic命令は「xchg(Exchange Register/Memory with Register)」です。これはレジスタあるいはメモリの内容を別のレジスタと入れ換える命令です。インラインアセンブラの書き方についてここでは説明しませんが、この様に書くことでlockvar変数とlock変数の内容の入れ替えを1命令で行うことができます*2。lock変数には予め、ロックを示す値(1)を入れているので、入れ替えによってlockvarへ1がセットされます。

[*2] xchg命令は「メモリ領域とレジスタ」あるいは「レジスタとレジスタ」の入れ替えしかできません。ここでは「lockvar(メモリ領域)とlockの内容を格納したレジスタ」の入れ替えとなるようにインラインアセンブラを書いています。asm volatile ()のコロンで区切られた2つ目に、「lock」に対しては「+r(レジスタ)」を、「lockvar」に対しては「+m(メモリ)」を指定しているのがそうです。

なぜこのような方法でlockvarの書き換えを行っているのかというと、これによってロックを取得できたのか否かを確実に知る事ができるからです。確実にロックを取得する上で問題となるのが1.でロックが解放済み(0)であることを確認した後xchg命令を実行するまでの間で、他のプロセッサ等が先にxchg命令を実行してロックを取ってしまうことです。そんな場合でもlockvar自体は1になるので、ロックが取れたと思って目的の処理を実施すると先にロックを取ったプロセッサ側と処理が競合します。

ロックが取れなかった時は取れなかったと知るためにlock変数側を使います。別のプロセッサ等によりロックが取られてしまっている時は、xchg命令実行時、既にlockvarは1になっています。そのためxchgの実行後、lockの内容は1になります。他方、ロックを正常に取得できた場合、xchg命令実行時、lockvarは0なので、lockの内容は0になります。

そのため、最後に3.ではlockの内容を確認し、その内容が1(ロックを取得できなかった)ならば、もう一度1.から繰り返すようにしています。そうではなく、lockが0ならばロックを取得できたとしてgot_lockに1を代入してwhileを抜け、関数からもreturnします。

spin_unlock()はもっと単純で、単にlockvarの指す先に0を代入するだけです。spin_unlock()を実行する時、既にロックは取っているので、lockvarを書き換える何かが競合してくることはありません。心置きなくロックを解放するだけです。

あとは、x86.cへ追加したspin_lock()/spin_unlock()のプロトタイプ宣言をx86.hへ追加しておきます(コードを引用しての紹介は割愛)。

2.3.3 スピンロックを使ってみる

それでは、spin_lock()/spin_unlock()を使ってみます。

文字出力系の関数で最終的に1文字を描画するputc()にロック処理を追加します(リスト2.6)。

リスト2.6: 023_spin_lock/fbcon.c

#include <x86.h>  /* 追加 */
#include <fbcon.h>
#include <fb.h>
#include <font.h>

/* 64bit unsignedの最大値0xffffffffffffffffは
 * 10進で18446744073709551615(20桁)なので'\0'含め21文字分のバッファで足りる */
#define MAX_STR_BUF     21

unsigned int cursor_x = 0, cursor_y = 0;
unsigned int putc_lock = 0;     /* 追加 */

void putc(char c)
{
        spin_lock(&putc_lock);      /* 追加 */

        unsigned int x, y;

        /* ・・・ 省略 ・・・ */

        }

        spin_unlock(&putc_lock);    /* 追加 */
}

/* ・・・ 省略 ・・・ */

putc()のロック状態を管理する変数としてputc_lockをグローバル変数として用意し、putc()の冒頭にspin_lock()を、末尾にspin_unlock()を追加しました。これで、putc()を複数のプロセッサから同時に呼び出された場合も、先にspin_lock()を取得した側でのみputc()の中身が実行され、取得できなかった方はspin_lock()内のビジーループで待ち続ける様になります。

2.3.4 動作確認

実行すると、今度は文字が壊れること無くBSPと各APのプロセッサ番号が表示されるようになりました(図2.4)。

023_spin_lockの実行結果

図2.4: 023_spin_lockの実行結果

2.4 APに外部アプリを実行させる

ここまでで、AP側をカーネルへ遷移させ、カーネル内のコードを実行させることができました。

次に、APへ外部アプリを実行させてみます。

この項のサンプルディレクトリは「024_exec」です。

2.4.1 APの初期化処理(ap_init())を追加する

外部アプリは、何らかの処理を行うためにシステムコールを発行します。システムコールはソフトウェア割り込みなので、外部アプリを実行するにはAP側でも割り込みの設定を行う必要があります。

そろそろAPに関する処理は別ファイルへ切り出そうと思います。ap.cを作成しそこに初期化処理(ap_init())を実装します(リスト2.7)。

リスト2.7: 024_exec/ap.c

/* 追加(ここから) */
#include <x86.h>
#include <intr.h>
#include <syscall.h>

void ap_init(void)
{
        /* CPU周りの初期化 */
        gdt_init();
        intr_init();

        /* システムコールの初期化 */
        syscall_init();
}
/* 追加(ここまで) */

BSP同様にGDT/IDTの初期化と、システムコールの初期化を行いました。syscall_init()の中でソフトウェア割り込みの割り込みハンドラの設定を行っています。

2.4.2 APの外部アプリ実行(ap_run())を追加する

次にAPが外部アプリを実行する処理を「ap_run」という関数で追加します(リスト2.8)。

リスト2.8: 024_exec/ap.c

#include <x86.h>
#include <intr.h>
#include <proc.h> /* 追加 */
#include <syscall.h>
#include <fs.h>   /* 追加 */
#include <common.h>       /* 追加 */

#define MAX_APS         16      /* 追加 */

struct file *ap_task[MAX_APS] = { NULL };       /* 追加 */

void ap_init(void)
{
        /* ・・・ 省略 ・・・ */
}

/* 追加(ここから) */
void ap_run(unsigned char pnum)
{
        while (1) {
                /* 自分用のタスクが登録されるのを待つ */
                while (!ap_task[pnum - 1])
                        CPU_PAUSE();

                /* 実行 */
                exec(ap_task[pnum - 1]);

                /* 空に戻す */
                ap_task[pnum - 1] = NULL;
        }
}
/* 追加(ここまで) */

APに実行させる外部アプリ(タスク)を登録するための変数「ap_task」を用意し、ap_run()では、自分用のタスクが登録され次第、exec()で実行します。exec()は同期型で外部アプリを実行するので、外部アプリの実行が終わり、戻ってきたらap_taskを空(NULL)に戻します。

また、タスク登録用の関数「ap_enq_task」も追加しておきます(リスト2.9)。

リスト2.9: 024_exec/ap.c

/* ・・・ 省略 ・・・ */

struct file *ap_task[MAX_APS] = { NULL };
unsigned int ap_task_lock[MAX_APS] = { 0 };     /* 追加 */

/* ・・・ 省略 ・・・ */

void ap_run(unsigned char pnum)
{
        /* ・・・ 省略 ・・・ */
}

/* 追加(ここから) */
int ap_enq_task(struct file *f, unsigned char pnum)
{
        int result = -1;

        spin_lock(&ap_task_lock[pnum - 1]);

        /* 空いていればタスクを登録 */
        if (!ap_task[pnum - 1]) {
                ap_task[pnum - 1] = f;
                result = 0;
        }

        spin_unlock(&ap_task_lock[pnum - 1]);

        return result;
}
/* 追加(ここまで) */

空きがあれば登録し、無ければ何もせずエラーを返すだけです。

この関数も複数のコンテキストから呼ばれる可能性があるので、スピンロックでロックをとるようにしています。

以上でap.cへの関数追加は完了です。併せてinclude/ap.hを作成してこれらの関数のプロトタイプ宣言と、Makefileの"OBJS="への"ap.o"の追加を行っておいてください(共にコードは割愛)。

2.4.3 APへ外部アプリを実行させる

作成した関数を使用してAPへ外部アプリを実行させてみます(リスト2.10)。

リスト2.10: 024_exec/main.c

#include <x86.h>
#include <ap.h>   /* 追加 */
/* ・・・ 省略 ・・・ */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        unsigned char pnum = get_pnum();

        /* 変更(ここから) */
        /* 専用の初期化処理を行い、実行を開始する */
        if (pnum) {
                ap_init();
                ap_run(pnum);
        }
        /* 変更(ここまで) */

        /* フレームバッファ周りの初期化 */
        fb_init(&pi->fb);
        set_fg(255, 255, 255);
        set_bg(0, 70, 250);
        clear_screen();
        is_con_inited = 1;

        /* プロセッサ番号表示処理は削除 */

        /* ACPIの初期化 */
        acpi_init(pi->rsdp);

        /* ・・・ 省略 ・・・ */

        /* ファイルシステムの初期化 */
        fs_init(_fs_start);

        /* 追加(ここから) */
        /* AP1で外部アプリ実行 */
        struct file *app = open("puta");
        ap_enq_task(app, 1);    /* 1回目 */
        while (ap_enq_task(app, 1) != 0) /* 2回目を試みる */
                CPU_PAUSE();

        /* haltして待つ */
        while (1)
                cpu_halt();
        /* 追加(ここまで) */

        /* スケジューラの初期化 */
        sched_init();

        /* ・・・ 省略 ・・・ */

start_kernel()冒頭のAP用の条件分岐で、APの初期化(ap_init())と実行(ap_run())を行っています。

その後、APはap_run()の中でタスク登録待ちになりますので、BSP側はファイルシステム初期化(fs_init())後、プロセッサ番号1のAPに外部アプリを実行させます。繰り返し実行できることの確認で2度実行しています。

最後にMakefileのOBJSへap.oを追加すれば完了です(コードの紹介は割愛)。

2.4.4 動作確認

動作確認用の外部アプリには、本シリーズのパート3「システムコールの薄い本」で作成した「02_putc」というアプリを使用します。putcシステムコールの実験アプリで、「A」という1文字を画面表示するだけのアプリです。このアプリのソースコードも「024_exec」内に置いています。

このディレクトリへ移動した後、「make」コマンドでアプリケーションバイナリができあがります。

$ make
gcc -Wall -Wextra -nostdinc -nostdlib -fno-builtin -fno-common -Iinclude -fPIE \
-c -o app.o app.c
gcc -Wall -Wextra -nostdinc -nostdlib -fno-builtin -fno-common -Iinclude -fPIE \
-c -o lib.o lib.c
ld -Map app.map -s -x -T ../app.ld -pie -o test app.o lib.o

「test」というバイナリ名で生成されますので、「puta」へリネームしてください。

$ ls test
test
$ mv test puta
$ ls puta
puta

そして、ファイルシステムイメージを生成するスクリプトも「024_exec」に含めています。以下のようにファイルシステムイメージ「fs.img」を生成します。

$ ../../tools/create_fs.sh puta
$ ls fs.img
fs.img

このfs.imgをカーネル(kernel.bin)と同じ階層に配置してください。ファイルシステムイメージについて詳しくは、当シリーズのパート1である「フルスクラッチで作る!x86_64自作OS」をご覧ください。

実行すると、図2.5の様に「A」が2回表示される事が確認できました。putaを2回実行できているので良さそうです。

024_execの実行結果

図2.5: 024_execの実行結果

カーネルとアプリバイナリのグローバル変数に注意

今回まででカーネルと外部アプリをAPへ実行させることができるようになりました。

AP側でカーネルやアプリを実行する際も、BSP側と同様に、それらが配置されているアドレスへジャンプする方針で進めてきました。ただ、ここまで作ってきた自作OSは、実行コンテキスト毎にメモリ空間を分けていたりもしないので*3、グローバル変数やスタティック変数等のように、実行バイナリ内にその変数の実体がある変数は注意が必要です。BSP/AP間で共に同じアドレスへアクセスすることになるので変数の内容がプロセッサ間で共有されます。スタックに実体がある関数内のローカル変数等では、当然、そのようなことは起きません。

[*3] URFIが用意してくれた「物理アドレス=仮想アドレス」のページテーブルをそのまま使っているので、プロセス毎に別々のアドレス空間を用意していたりはしません。

本書では、その事をわかった上で、BSP/AP間で同時に実行されるカーネルやアプリでは、プロセッサ間で共有してほしくない変数はスタックから確保されるように実装しています*4

[*4] カーネルは、世の中のものも、BSPとAPでアドレス空間を分けていたりはしない(同じアドレスで動作しカーネルバイナリ内にBSP/APの分岐がある)のかと思いますが、アプリは流石にBSP/APを意識していちいち実装するのは面倒です。なので、やはりプロセス毎のメモリ空間の分離は実現しておいた方が便利です。が、今回に関して言えば、複数のプロセッサで同時に実行する際は、そのアプリをファイルごとコピーして物理的に別々のアドレスにする、という方法でも実現可能です。

2.5 APでの外部アプリ実行をシステムコール化する

前項で追加したAPでの外部アプリ実行の仕組みをシステムコール化します。

この項のサンプルディレクトリは「025_syscall」です。

2.5.1 システムコールにエントリを追加する

「SYSCALL_EXEC_AP」というシステムコールのエントリをsyscall.cに追加します(リスト2.11)。

リスト2.11: 025_syscall/syscall.c

#include <ap.h>   /* 追加 */
#include <intr.h>
/* ・・・ 省略 ・・・ */

enum SYSCCALL_NO {
        SYSCALL_PUTC,
        SYSCALL_OPEN,
        SYSCALL_EXEC,
        SYSCALL_ENQ_TASK,
        SYSCALL_RCV_FRAME,
        SYSCALL_SND_FRAME,
        SYSCALL_EXEC_AP,        /* 追加 */
        MAX_SYSCALL_NUM
};

unsigned long long do_syscall_interrupt(
        unsigned long long syscall_id, unsigned long long arg1,
        unsigned long long arg2 __attribute__((unused)),
        unsigned long long arg3 __attribute__((unused)))
{
        unsigned long long ret_val = 0;

        switch (syscall_id) {
        /* ・・・ 省略 ・・・ */

        /* 追加(ここから) */
        case SYSCALL_EXEC_AP:
                ret_val = ap_enq_task((struct file *)arg1, arg2);
                break;
        /* 追加(ここまで) */
        }

        /* ・・・ 省略 ・・・ */

前項で追加したap_enq_task()をシステムコール経由で呼び出せるようにしました。システムコールの第1・第2引数をap_enq_task()の第1・第2引数に渡し、ap_enq_task()の戻り値をシステムコールの戻り値として返すようにしています。

併せて、main.cのstart_kernel()でプロセッサ番号1のAPに外部アプリを実行させて止めていた処理を削除し、通常通りスケジューラまで実行されるようにします(リスト2.15)。

リスト2.15: 025_syscall/main.c

/* ・・・ 省略 ・・・ */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        /* ・・・ 省略 ・・・ */

        /* ファイルシステムの初期化 */
        fs_init(_fs_start);

        /* AP1への外部アプリ実行と、
         * その後の処理を止めていたwhile()を削除 */

        /* スケジューラの初期化 */
        sched_init();

        /* ・・・ 省略 ・・・ */

2.5.2 システムコールを使ってみる

それでは、追加したSYSCALL_EXEC_APシステムコールを使ってみます。

システムコールを呼び出すアプリケーションはサンプルディレクトリ内の以下の場所に作成することにします。

ここでは、前著「ぼくらのイーサネットフレーム」で最後に作成した「06_beef_server」からの差分のみ紹介します。(06_beef_serverもappsディレクトリに入っています。)

まず、アプリ側のライブラリ関数を定義しているlib.cへ、SYSCALL_EXEC_APを関数化した「exec_ap()」を追加します(リスト2.13)。

リスト2.13: 025_syscall/apps/07_exec_ap/lib.c

/* ・・・ 省略 ・・・ */

enum SYSCCALL_NO {
        SYSCALL_PUTC,
        SYSCALL_OPEN,
        SYSCALL_EXEC,
        SYSCALL_ENQ_TASK,
        SYSCALL_RCV_FRAME,
        SYSCALL_SND_FRAME,
        SYSCALL_EXEC_AP,        /* 追加 */
        MAX_SYSCALL_NUM
};

/* ・・・ 省略 ・・・ */

void exec(struct file *file)
{
        syscall(SYSCALL_EXEC, (unsigned long long)file, 0, 0);
}

/* 追加(ここから) */
int exec_ap(struct file *file, unsigned char pnum)
{
        return (int)syscall(SYSCALL_EXEC_AP, (unsigned long long)file, pnum, 0);
}
/* 追加(ここまで) */

/* ・・・ 省略 ・・・ */

定数SYSCALL_EXEC_APと、exec_ap()を追加し、exec_ap()ではsyscall()を使ってシステムコールを呼び出しています*5

[*5] システムコールの仕組みについて詳しくは、当シリーズのパート3である「システムコールの薄い本」をご覧下さい。

また、この関数追加に併せて、include/lib.hへexec_ap()のプロトタイプ宣言を追加しておいてください(コードは割愛)。

そして、アプリ本体であるapp.cはリスト2.14のように作成します。

リスト2.14: 025_syscall/apps/07_exec_app/app.c

#include <lib.h>

#define MAX_EXEC_COUNT  2

int main(void)
{
        unsigned char i;
        for (i = 0; i < MAX_EXEC_COUNT;) {
                if (exec_ap(open("puta"), 1) == 0)
                        i++;
        }

        return 0;
}

exec_ap()が2回成功するまで繰り返しています。実行する外部アプリは前項と同じくputaです。

このアプリはBSP側に実行させます。このアプリからreturnしてくると、haltして待機し続けます(リスト2.15)。

リスト2.15: 025_syscall/main.c

/* ・・・ 省略 ・・・ */

#define INIT_APP        "test"

/* ・・・ 省略 ・・・ */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        /* ・・・ 省略 ・・・ */

        /* initアプリ起動 */
        exec(open(INIT_APP));

        /* haltして待つ */
        while (1)
                cpu_halt();
}

07_exec_appのビルドも02_putcと同様です。BSPで実行するinitアプリ名(INIT_APP定数)は「test」なので、生成されるtestバイナリをリネームする必要はありません。

ただし、fs.imgにはtestとputaの両方を含めておく必要があるので、testとputaを同じディレクトリに配置し、以下のようにcreate_fs.shを実行します。(create_fs.shへの相対パスは適宜変更してください。)

$ ls test puta
test puta
$ ../../tools/create_fs.sh test puta
$ ls fs.img
fs.img

2.5.3 動作確認

実行すると、前項と同様にAが2回表示されます。(前項と同じなので実行結果の画像は割愛)


Top