Hatena::ブログ(Diary)

へにゃぺんて@日々勉強のまとめ

2017-05-03

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か否か」でチェックしています。


実行すると以下のように画面に表示されます。

(コンパイル手順、実行手順は前回の記事を参照してください。)

f:id:cupnes:20170503135513j:image

私のマシン(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関数へ与える引数は以下の通りです。

ソースコードは以下の通りです。

ソースコード全体と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()で自身のプロセッサ番号を取得しています。

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

f:id:cupnes:20170503135512j:image

全ての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が無かった場合、下図の様になります。

f:id:cupnes:20170518235227j:image


複数のコアでput_str関数の処理が同時に実行される事によってコンソール出力の取り合いが発生し、

画面に文字が表示されていないと思われます。


lock_conoutは、put_str関数内の処理が同時に複数のコアで実行されないように、

コンソール出力をロックしています。

uchan_nosuchan_nos 2017/05/15 08:11 とても手軽にマルチコアを有効にできることが分かって大変面白い記事ですね!

2つ質問があります。
・lock_conoutで競合回避をしてるようですが、これでは目的を達成できてないと思います(実際に競合も起きてるみたいですし)。なぜ、その行を残しているのですか?
・UEFIを終了させてOSに処理を引き継いだあと、マルチコア制御はどうなるんでしょう。本来はOS側でマルチコアを有効にすべきなのでしょうか?

cupnescupnes 2017/05/19 00:10 コメントありがとうございます!

> ・lock_conoutで競合回避をしてるようですが、これでは目的を達成できてないと思います(実際に競合も起きてるみたいですし)。なぜ、その行を残しているのですか?

サンプルコードの意図が分かり難く、すみません。

追記しましたが、lock_conoutは「コンソール出力の取り合いで画面に文字が出ない」事態への対処として用意しました。
lock_conoutは、put_str関数内のコンソール出力をロックしています。

put_str関数が複数のコアで何度も呼ばれた時に、画面へ出力する文字列の順序は制御していません。
このサンプル(073_mp_start_ap_multithread)としては、
StartupAllAPs関数の第3引数へ"FALSE(=0)"を設定した場合に、複数のコアが「同時に」動く様子を、
「あるコアのput_str関数とput_str関数の間に、別のコアのput_str関数が割り込む」挙動で見てもらおうと思い、
このようなサンプルを作ってみました。

> ・UEFIを終了させてOSに処理を引き継いだあと、マルチコア制御はどうなるんでしょう。本来はOS側でマルチコアを有効にすべきなのでしょうか?

すみません、分かりません。。
現状、「OSに処理を引き継ぐ前段階で、UEFIファームウェアでいろいろと遊んでみる」という所で、
そもそも「UEFIからどのようにOSを立ち上げるのか」も、まだ分かっていません。。。

ただし、UEFIのファームウェアの実装はメーカーに依存するので、
ファームウェアを信用しないのであれば、OS側でマルチコアを有効化すべきかと思います。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/cupnes/20170503/1493787477