Linuxカーネルのソースコードを読む方法(その2)

Linux カーネルソースコードを読んだのでメモを公開します.Linuxカーネルを読む際の参考になれば幸いです.

今回のお題: kernel.panic

今回はカーネル内部のエラー処理(カーネルパニック)の部分です.ソースコードのバージョンは linux-5.0.1 です.

カーネルパニック

ユーザ空間のプログラムは,NULLポインタアクセスなどで例外をおこすと,例外ハンドラが起動します.例外ハンドラは,プログラムをコアダンプさせたり,強制終了させたりします.

同じようにカーネル空間で例外が発生すると,例外ハンドラが起動します.復旧不可能な致命的なエラーの場合は,カーネルはデバック情報をコンソールに出力して,動作を停止させます.これがカーネルパニックです.

カーネルパニックを意図的に発生させる

デバッグなどの目的で意図的にカーネルパニックを発生させることもできます.sysreqの機能を使います.つまりroot権限で,コマンドライン

$ echo c > /proc/sysrq-trigger

を実行します

キーボードで sysreq + c を押しても同じことができます.つまり[Alt]+[Print Screen]+[c]を同時に押します

カーネルパニック後に自動再起動させる方法

Webサーバーなどで不運にもカーネルパニックが発生した場合は,とりあえずサーバーを自動再起動させるという運用方法が考えられます.
(運用ポリシーとして,障害時はサーバーを止めたままにするか,自動再起動させるか,どちらが良いかは意見が分かれるところです)

この設定はカーネルの起動オプションや,sysctl で設定できます

以下の内容で /etc/sysctl.d/99-panic.conf みたいなファイルを作成するだけです

kernel.panic = 90

この場合カーネルパニックから90秒後にシステムは自動再起動します

デバイスドライバ開発時は,kernel.panic = 10ぐらいにしておいて,再起動した場合は /var/log 以下のログを眺める,という使い方も出来ます

ソースコード

カーネルパニックの処理は,ソースコードとしては kernel/panic.c あたりで実装されています

kernel.panic などのオプション設定の定義は kernel/panic.c の687行目あたりにあります

687:
core_param(panic, panic_timeout, int, 0644);
core_param(panic_print, panic_print, ulong, 0644);
core_param(pause_on_oops, pause_on_oops, int, 0644);
core_param(panic_on_warn, panic_on_warn, int, 0644);

kernel.panic = 90 と設定した場合は panic_timeout というint型の変数に90という値が格納されます.

panic_timeout 変数の定義は46行目

int panic_timeout = CONFIG_PANIC_TIMEOUT;
EXPORT_SYMBOL_GPL(panic_timeout);

です.このように 初期値は CONFIG_PANIC_TIMEOUT つまり カーネルのconfigで設定しています

念の為 CONFIG_PANIC_TIMEOUT の値を確認します. 設定ファイルは .config なので grep するだけです

$ cd path/to/カーネルのソースコードのディレクトリ
$ grep CONFIG_PANIC_TIMEOUT .config
CONFIG_PANIC_TIMEOUT=60

私のカーネルは60秒でビルドされていました

パニック時の処理

panic(const char *fmt, ...) という関数で実装されています

161:
void panic(const char *fmt, ...) {
	static char buf[1024];
	va_list args;
	long i, i_next = 0, len;
	int state = 0;
	int old_cpu, this_cpu;
	bool _crash_kexec_post_notifiers = crash_kexec_post_notifiers;

	/*
	 * Disable local interrupts. This will prevent panic_smp_self_stop
	 * from deadlocking the first cpu that invokes the panic, since
	 * there is nothing to prevent an interrupt handler (that runs
	 * after setting panic_cpu) from invoking panic() again.
	 */
	local_irq_disable();

コメントに書いてある通りで,panic 時には割り込みを禁止状態にして,他の処理は完全は中断されるようになっています.

244:
	/*
	 * Run any panic handlers, including those that might need to
	 * add information to the kmsg dump output.
	 */
	atomic_notifier_call_chain(&panic_notifier_list, 0, buf);

	/* Call flush even twice. It tries harder with a single online CPU */
	printk_safe_flush_on_panic();
	kmsg_dump(KMSG_DUMP_PANIC);

その後 ksmg dumpのフック関数をコールして,最後に kmsg_dump をコールしてデバッグ情報をコンソールに出力しています.

287:
	if (panic_timeout > 0) {
		/*
		 * Delay timeout seconds before rebooting the machine.
		 * We can't use the "normal" timers since we just panicked.
		 */
		pr_emerg("Rebooting in %d seconds..\n", panic_timeout);

		for (i = 0; i < panic_timeout * 1000; i += PANIC_TIMER_STEP) {
			touch_nmi_watchdog();
			if (i >= i_next) {
				i += panic_blink(state ^= 1);
				i_next = i + 3600 / PANIC_BLINK_SPD;
			}
			mdelay(PANIC_TIMER_STEP);
		}
	}
	if (panic_timeout != 0) {
		/*
		 * This will not be a clean reboot, with everything
		 * shutting down.  But if there is a chance of
		 * rebooting the system it will be rebooted.
		 */
		emergency_restart();
	}

panic_timeout > 0 の場合は,PANIC_TIMER_STEP ミリ秒刻みでウエイトを入れながら panic_blinck()を繰り返しコールしています.おそらく画面の点滅表示処理です

#define PANIC_TIMER_STEP 100

PANIC_TIMER_STEPは100ミリ秒でした

そして panic_timeout秒経過したら emergency_restart() をコールして強制終了をおこないます

panic()はどこから呼ばれているか?

カーネルソースコードを "panic(" で検索すると直ぐに判りますが,至るところで panic() をコールするようになっています.

例えばカーネル起動時の処理 init/main.c では3箇所 panic() を呼び出す処理が書かれています

662:
	/*
	 * HACK ALERT! This is early. We're enabling the console before
	 * we've done PCI setups etc, and console_init() must be aware of
	 * this. But we do want output early, in case something goes wrong.
	 */
	console_init();
	if (panic_later)
		panic("Too many boot %s vars at `%s'", panic_later,
		      panic_param);

これはカーネルコマンドライン引数(起動時のオプション)を解析した後の処理です.コマンドライン引数が多すぎると"something goes wrong"ということでパニックを起こします

1079:
	/*
	 * We try each of these until one succeeds.
	 *
	 * The Bourne shell can be used instead of init if we are
	 * trying to recover a really broken machine.
	 */
	if (execute_command) {
		ret = run_init_process(execute_command);
		if (!ret)
			return 0;
		panic("Requested init %s failed (error %d).",
		      execute_command, ret);
	}
	if (!try_to_run_init_process("/sbin/init") ||
	    !try_to_run_init_process("/etc/init") ||
	    !try_to_run_init_process("/bin/init") ||
	    !try_to_run_init_process("/bin/sh"))
		return 0;

	panic("No working init found.  Try passing init= option to kernel. "
	      "See Linux Documentation/admin-guide/init.rst for guidance.");

これはカーネルの初期化が終了して,システムのinit プロセス(PIDが0のプロセス)を起動する部分です.

initプロセスの実行ファイル(/bin/systemd とか /bin/sh)が指定されていない場合,またはそれが存在しない場合は,どうしようも無いのでパニックを起こします