UEFIベアメタルプログラミング - マルチコアを制御する

UEFIベアメタルプログラミング - Hello UEFI!(ベアメタルプログラミングの流れについて) - へにゃぺんて@日々勉強のまとめ
こちらの記事の続きです。

UEFIでマルチコアを扱う方法が分かったので、
この記事ではその方法をまとめてみます。

1. EFI_MP_SERVICES_PROTOCOLについて

前回の記事で、

の旨を説明しました。


マルチコアを扱うプロトコルは「EFI_MP_SERVICES_PROTOCOL」で、
以下のメンバを持っています。

  • GetNumberOfProcessors
  • GetProcessorInfo
  • StartupAllAPs
  • StartupThisAP
  • SwitchBSP
  • EnableDisableAP
  • WhoAmI

2. PCのプロセッサの数を取得する

それでは、実際にプログラムを作ってみます。


まずは、PCのプロセッサの数を取得します。
使う関数は「GetNumberOfProcessors」です。

各種構造体などの定義を省くと、以下のソースコードで実現できます。
なお、ソースコードの全体とMakefileは以下の場所にアップロードしています。

void puts(unsigned short *str, struct EFI_SYSTEM_TABLE *SystemTable)
{
	SystemTable->ConOut->OutputString(SystemTable->ConOut, str);
}

void efi_main(void *ImageHandle __attribute__ ((unused)), struct EFI_SYSTEM_TABLE *SystemTable)
{
	struct EFI_GUID msp_guid = {0x3fdda605, 0xa76e, 0x4f46, {0xad, 0x29, 0x12, 0xf4, 0x53, 0x1b, 0x3d, 0x08}};
	struct EFI_MP_SERVICES_PROTOCOL *msp;
	unsigned long long status;
	unsigned short str[1024];

	status = SystemTable->BootServices->LocateProtocol(&msp_guid, NULL, (void **)&msp);
	if (status) {
		puts(L"error: SystemTable->BootServices->LocateProtocol\r\n", SystemTable);
		while (1);
	}

	unsigned long long nop, noep;
	status = msp->GetNumberOfProcessors(msp, &nop, &noep);
	if (!status) {
		puts(L"nop, noep: ", SystemTable);
		puts(int_to_unicode(nop, 2, str), SystemTable);
		puts(L", ", SystemTable);
		puts(int_to_unicode(noep, 2, str), SystemTable);
		puts(L"\r\n", SystemTable);
	} else {
		puts(L"error: msp->GetNumberOfProcessors: status=0x", SystemTable);
		puts(int_to_unicode_hex(status, 16, str), SystemTable);
		puts(L"\r\n", SystemTable);
		while (1);
	}

	while (1);
}


プロトコルの構造体を取得するためにLocateProtocol()を使います。使い方については前回の記事を見てみてください。ここでは、「EFI_MP_SERVICES_PROTOCOL」構造体の先頭アドレスを変数mspへ格納しています。


そして、PCのプロセッサの数を取得するためにEFI_MP_SERVICES_PROTOCOL構造体が持つGetNumberOfProcessors関数を使います。GetNumberOfProcessors関数では、第2引数のポインタで「システム上の全てのプロセッサの数(NumberOfProcessors)」を、第3引数のポインタで「有効な全てのプロセッサの数(NumberOfEnabledProcessors)」を取得できます。


なお、LocateProtocol()やGetNumberOfProcessors()等、UEFIの各関数は、成功時に0、警告や失敗時に0以外のステータスコードを返します。
そのため、LocateProtocol()とGetNumberOfProcessors()が返すステータスを「0か否か」でチェックしています。


実行すると以下のように画面に表示されます。
(コンパイル手順、実行手順は前回の記事を参照してください。)

私のマシン(Lenovo ThinkPad)の場合、システム上のプロセッサの数・有効なプロセッサの数共に「4」でした。

3. APへ関数を実行させる

GetNumberOfProcessors関数でプロセッサの数が4だと分かりました。この内、1つがBSP(Boot Strap Processor)と呼ばれるもので、起動時から動き続けているメインのプロセッサです。そして残る3つがAP(Application Processor)と呼ばれるもので、BSPに対して追加で存在するプロセッサです。マルチコア設定を行う前の段階ではBSPのみが動き、APは動いていない状態です。


APを動作させる関数は、EFI_MP_SERVICES_PROTOCOLのStartupAllAPs関数とStartupThisAP関数です。StartupAllAPs関数は、すべてのAPへ特定の関数を実行させるもので、StartupThisAP関数は特定のAPへ特定の関数を実行させるものです。


ここではStartupAllAPs関数を試してみます。
StartupAllAPs関数へ与える引数は以下の通りです。

  • 第1引数: StartupAllAPsを含むEFI_MP_SERVICES_PROTOCOLのポインタ
  • 第2引数: APへ実行させる関数(後述)のポインタ
  • 第3引数: シングルスレッドか否か。TRUE(=1)を指定するとプロセッサ番号順にAPが第2引数の関数を実行する(プロセッサ番号1のAPが関数からretunしてからプロセッサ番号2が関数を実行する、となる。)。FALSE(=0)を指定すると全てのAPが一斉に第2引数の関数を実行する。
  • 第4引数: APの実行中、BSPがブロックするか否か。NULLを指定するとAPの実行中、BSPはブロックする。ノンブロッキングにするためにはCreateEvent()で生成したEFI_EVENTを指定する。
  • 第5引数: 第2引数で指定する関数のタイムアウト時間をマイクロ秒で指定する。
  • 第6引数: 第2引数で指定する関数の引数を指定する。
  • 第7引数: 第2引数で指定した関数の実行に失敗、あるいはタイムアウトしたAPのリストを返す。今回は使用しないためNULLを設定している。

ソースコードは以下の通りです。
ソースコード全体とMakefileは以下へアップロードしました。

volatile unsigned char lock_conout = 0;
void puts(unsigned short *str, struct EFI_SYSTEM_TABLE *SystemTable)
{
	while (lock_conout);
	lock_conout = 1;
	SystemTable->ConOut->OutputString(SystemTable->ConOut, str);
	lock_conout = 0;
}

void ap_main(void *_SystemTable)
{
	unsigned short str[1024];
	struct EFI_SYSTEM_TABLE *SystemTable = _SystemTable;

	struct EFI_GUID msp_guid = {0x3fdda605, 0xa76e, 0x4f46, {0xad, 0x29, 0x12, 0xf4, 0x53, 0x1b, 0x3d, 0x08}};
	struct EFI_MP_SERVICES_PROTOCOL *msp;
	unsigned long long status;
	status = SystemTable->BootServices->LocateProtocol(&msp_guid, NULL, (void **)&msp);
	if (status) {
		puts(L"error: SystemTable->BootServices->LocateProtocol\r\n", SystemTable);
		while (1);
	}
	unsigned long long pnum;
	status = msp->WhoAmI(msp, &pnum);
	if (status) {
		puts(L"error: msp->WhoAmI\r\n", SystemTable);
		while (1);
	}
	puts(L"ProcessorNumber: 0x", SystemTable);
	puts(int_to_unicode_hex(pnum, 16, str), SystemTable);
	puts(L"\r\n", SystemTable);

	while (1);
}

void efi_main(void *ImageHandle __attribute__ ((unused)), struct EFI_SYSTEM_TABLE *SystemTable)
{
	struct EFI_GUID msp_guid = {0x3fdda605, 0xa76e, 0x4f46, {0xad, 0x29, 0x12, 0xf4, 0x53, 0x1b, 0x3d, 0x08}};
	struct EFI_MP_SERVICES_PROTOCOL *msp;
	unsigned long long status;

	status = SystemTable->BootServices->LocateProtocol(&msp_guid, NULL, (void **)&msp);
	if (status) {
		puts(L"error: SystemTable->BootServices->LocateProtocol\r\n", SystemTable);
		while (1);
	}

	status = msp->StartupAllAPs(msp, ap_main, 0, NULL, 0, SystemTable, NULL);
	if (status) {
		puts(L"error: msp->StartupAllAPs\r\n", SystemTable);
		while (1);
	}

	while (1);
}

efi_main()では、LocateProtocol()でEFI_MP_SERVICES_PROTOCOLプロトコルの構造体の先頭アドレスを取得し、StartupAllAPs()ですべてのAPに対して関数を実行させています。

APが実行する関数がap_main()です。ap_main()でも同じく、EFI_MP_SERVICES_PROTOCOLプロトコルの構造体の先頭アドレスを取得した後、WhoAmI()で自身のプロセッサ番号を取得しています。

実機で実行した様子は以下のとおりです。

全てのAPが並列で動いているので、コンソール出力を取り合っているのが分かります。
プロセッサ番号1が全てのメッセージを出力した後、プロセッサ番号3が「0x」までを出力した所で、プロセッサ番号2にコンソール出力を取られている様子です。

4. おわりに(5/19誤記修正&少し追記)

今回はEFI_MP_SERVICES_PROTOCOLプロトコルが持つStartupAllAPs()を使用してみました。


記事で触れた073_mp_start_ap_multithreadのサンプルでは第3引数を0(FALSE)にし、
複数のコアを「同時に」動かしていましたが、
第2引数で指定する関数を各コアに「順に」実行させたい場合は、第3引数を1(TRUE)にします。(*3)

(*3)https://github.com/cupnes/bare_metal_uefi/tree/master/072_mp_start_ap_singlethread


また、StartupThisAP()を使用すると、特定のAPへ関数を実行させることもできます。


このように、UEFIファームウェアが持つ機能を使用すると、比較的簡単にマルチコアを制御できます。
PCでのベアメタルプログラミングの際にはぜひ使ってみるとよいかと思います。

073_mp_start_ap_multithreadのlock_conoutの意図(5/19追記)

サンプルコード内の「lock_conout」というロック処理が何を解決しているのかを
説明できていませんでしたので、少し補足します。


073_mp_start_ap_multithreadサンプルのlock_conoutが無かった場合、下図の様になります。


複数のコアでput_str関数の処理が同時に実行される事によってコンソール出力の取り合いが発生し、
画面に文字が表示されていないと思われます。


lock_conoutは、put_str関数内の処理が同時に複数のコアで実行されないように、
コンソール出力をロックしています。