遅ればせながら手元の PC を MBR ブートから UEFI ブートに切り替えた。 ハードディスクの最初の 512バイト MBR (Master Boot Record) を読み込んで起動するのが MBR ブート。 Windows だと 2TB 超のディスクは MBR ブートできない (Linux ならブート可能)。 2TB では手狭になってきたのが切り替えを決意した理由だが、 ついでに Linux 専用マシンも PC のファームウェアが対応しているものは UEFI ブートに切り替えた。
UEFI (Unified Extensible Firmware Interface) だと、 普通の FAT32 ボリュームにブートローダのファイルを置いておくだけなので、 わざわざ MBR を書き換えたりするより簡単だし分かりやすい。 PC が起動しなくなった、などのトラブルはよくあるが、 トラブル発生時は気が急くし時間的余裕がないことも多いので、 トラブル時の作業は簡単であればあるほど、 分かりやすければ分かりやすいほど好ましい。
UEFI ブートに切り替えて 1ヶ月ほど経ったある日、 CPU を換装するために落としていた PC の電源を入れたら GRUB のメニュー画面でフリーズしたので私も凍り付いた。 CPU の性能をむやみに上げると、マザーボードとのミスマッチが起りがち。 せっかく買った新しい CPU が問題を起こしたのかと思った。 電源ボタンを長押しして強制的に電源を落とす。
BIOS 設定を確認するためにキーボードをつないで再度電源を入れてみる。 設定に何の問題もない。 続いて GRUB を立ち上げる。問題無く立ち上がる。 Linux を起動する。問題無い。 さっきのフリーズは何だったのだろう?
この時は因果関係に気付かなかったが、 キーボードをつないでいないと GRUB 2.04 (および最新版の 2.05 も) がメニュー画面のカウントダウンでハングする (最初の秒を表示したまま止まる)。 PC を再起動するときは、たいていキーボードをつないでいるので正常に起動し、 この症状は見たことがなかった。
実はこの時も、 なぜキーボードをつないでいなかったかというと明確な理由はない。 CPU 換装後の動作確認なのだからキーボードをつないでおくべきだと思うのだけど、 たまたまつなぐのを忘れていただけかも。 Web で 「headless GRUB hang bug UEFI without keyboard」 などを検索しても似た症例がほとんど見つからないのは、 キーボードをつながずに起動させる人がほとんどいないから?
とはいえ、 何台もあるサーバそれぞれにキーボードをつないでいては邪魔である。 意図して再起動するときはキーボードをつなぐなり KVM スイッチ (Keyboards, Video monitors, Mice Switch) を切り替えてキーボードが効く状態にするのが通常だが、 トラブルや Watchdog タイマーで勝手に再起動したとき、 あるいは遠隔から再起動させたときに、 立ち上がらずにフリーズしてしまったら非常に困る。 不測の再起動に備えて、 急遽テンキーをつないでおくことにした。
GRUB がキー入力を検知する状態であれば、 画面表示の有無自体は関係なくハングする。 逆に言うと、 GRUB がキー入力を検知しなければ、 例えば grub.cfg でキー入力を読む命令が無ければ (menuentry 等が無ければ) ハングしない。 例えば grub.cfg が次のように特定の Linux を起動するだけなら、 正常に起動できる。
insmod all_video insmod part_gpt insmod search_label search --no-floppy --label --set=root / linux /boot/linuz-4.19.69-x86_64 root=LABEL=/ resume=LABEL=swap ro initrd /boot/initz-4.19.69-x86_64 boot
もちろんこれでは GRUB を使う意味がないが、 少なくとも問題が GRUB のキー入力関連にあることが分かった。 同じ PC、同じ GRUB でも、 UEFI ブートではなく MBR ブートなら、 キーボードをつながなくてもハングしない (MBR ブート専用の USB メモリを作って確認した)。 また、UEFI ブートであっても UEFI ファームウェアによってはハングしない場合もあると思われる (未確認)。 が、少なくとも私の PRIMERGY MX130 S2 では確実に再現する。
というわけで、 問題の所在はおおむね絞り込めたので、 GRUB のソースコードを読み始めた。 並行して facebook に、 ことの顛末を書込んでみた。
すると野中尚道さんから grub-core/tem/efi/console.c が怪しそう、 とのヒントを頂いた。 ありがたい! なにぶん EFI のソースコードを読むのは初めてなので、 この時点ではまだ流れを追いきれていなかった。 efi/console.c の grub_console_getkey まわりを重点的に読んでみる。
grub-core/tem/efi/console.cのget_keyで key_exとkey_conを呼び分けている処理が失敗しているような気がします。 key_exの方はオプション機能なので実装有無を確認してるのですが野中尚道さんのコメントから引用
なるほど確かに grub_console_getkey_ex は、 grub_efi_open_protocol の返り値が NULL で無いとき (key_ex の実装が有るとき) に限り呼び出されているが、 grub_console_getkey_con には対応するものがない。 key_con は必ず実装されているから確認不要? でも、キーボードをつないでいない場合はどうなる? key_ex の有無を確認するコード:
text_input_ex_guid = GRUB_EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID; ... text_input = grub_efi_open_protocol(grub_efi_system_table->console_in_handler, &text_input_ex_guid, GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);
をマネして、 key_con の有無を確認するコードをでっち上げてみる。 単に 「_EX_」 の部分を取り除いただけだが、 キーボードをつないでいるか否かを正しく検出しているようだ。
text_input_guid = GRUB_EFI_SIMPLE_TEXT_INPUT_PROTOCOL_GUID; text_input = grub_efi_open_protocol(grub_efi_system_table->console_in_handler, &text_input_guid, GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);
あとはキーボードの有無に応じて grub_efi_console_input_init の返り値を変えるだけ。 grub_efi_console_input_init が 1 (非ゼロ?) を返せば、 GRUB は grub_console_getkey を呼ばなくなるようだ。
以上をまとめると、 次のようなパッチになった:
diff --git a/grub-core/term/efi/console.c b/grub-core/term/efi/console.c index 4840cc5..f2be32f 100644 --- a/grub-core/term/efi/console.c +++ b/grub-core/term/efi/console.c @@ -207,7 +207,12 @@ grub_efi_console_input_init (struct grub_term_input *term) GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL); term->data = (void *)text_input; - return 0; + if (text_input) return 0; + grub_efi_guid_t text_input_guid = GRUB_EFI_SIMPLE_TEXT_INPUT_PROTOCOL_GUID; + text_input = grub_efi_open_protocol(grub_efi_system_table->console_in_handler, + &text_input_guid, + GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL); + return text_input == NULL; } static int
わずか 6行だが、 これで GRUB がハングしなくなった。