linux systemにおけるinitの最期

2018/12/18import
Linux Advent Calender 2018の18日目です。

元々の予定はボツになった!?ので ちょっと古い話になります。

7日目のsystemd理解のヒント - systemdの概念と歴史と相反するような記事です。
古い情報も必要としている組込もんもいるので、その助けとなりましたら幸いです。

initとは

PID=1として、カーネルの初期化が終わって初めて生成されるプロセスです。
ユーザランドで必要なファイルシステムのマウント・システム設定・サービス(デーモン)やプロセス起動を行います。

kernel parameterに init=でinitコマンドを指定することができます。省略すると /sbin/initなどが参照されます。
init=/etc/preinit
ここでは 組込装置を前提として、busyboxのinitを想定します

設定ファイル

initが起動すると、以下のファイルを参照します。run levelに応じた処理を記述するようになっています。
シリアルコンソールの設定なども、ここに記述することができます。
/etc/inittab
■/etc/inittab
FILE: 06.build/patches/rootfs/etc/inittab
# /etc/inittab
#
# Copyright (C) 2001 Erik Andersen <andersen@codepoet.org>
#
# Note: BusyBox init doesn't support runlevels.  The runlevels field is
# completely ignored by BusyBox init. If you want runlevels, use
# sysvinit.
#
# Format for each entry: <id>:<runlevels>:<action>:<process>
#
# id        == tty to run on, or empty for /dev/console
# runlevels == ignored
# action    == one of sysinit, respawn, askfirst, wait, and once
# process   == program to run

# Startup the system
::sysinit:/bin/mount -t proc proc /proc
::sysinit:/bin/mount -o remount,rw /
::sysinit:/bin/mkdir -p /dev/pts
::sysinit:/bin/mkdir -p /dev/shm
::sysinit:/bin/mount -a
::sysinit:/bin/hostname -F /etc/hostname
# now run any rc scripts
::sysinit:/etc/init.d/rcS

# Put a getty on the serial port
#ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100 # GENERIC_SERIAL

# Stuff to do for the 3-finger salute
#::ctrlaltdel:/sbin/reboot

# Stuff to do before rebooting
::shutdown:/etc/init.d/rcK
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r

システムの終了

reboot、halt、shutdownといったコマンドを実行すれば、システムを再起動・停止・終了することができますね。
このコマンドを実行したとき、何が行われているのでしょうか。

rebootに対象を絞って、かつ、busybox版で確認していきましょう。

v1.19.4
FILE: init/halt.c
//applet:IF_HALT(APPLET(halt, BB_DIR_SBIN, BB_SUID_DROP))
//applet:IF_HALT(APPLET_ODDNAME(poweroff, halt, BB_DIR_SBIN, BB_SUID_DROP, poweroff))
//applet:IF_HALT(APPLET_ODDNAME(reboot, halt, BB_DIR_SBIN, BB_SUID_DROP, reboot))
rebootは haltのaliasになるようですね。
int halt_main(int argc UNUSED_PARAM, char **argv)
|
...
	sleep(0);
	write_wtmp();  // last.logを記録する
	sync();        // デフォルトはsync(2)を実行する.

	kill(1,SIGTERM)	// initに signalを飛ばす. -f付きなら reboot(2)で 0x01234567 を渡す..
コメントに書いたようにsignalを飛ばすだけでした。
initに処理が移ります:

FILE: init/init.c

FUNC: int init_main(int argc UNUSED_PARAM, char **argv)
		bb_signals(0
			+ (1 << SIGUSR1) /* halt */
			+ (1 << SIGTERM) /* reboot */
			+ (1 << SIGUSR2) /* poweroff */
			, halt_reboot_pwoff);
signal handlerを登録します。bb_*は、Busyboxのライブラリ関数です。
第一引数で指定されたsignalを許可し、第二引数の関数をハンドラとして登録します。

ハンドラを見ていきましょう:
static void halt_reboot_pwoff(int sig)
{
	const char *m;
	unsigned rb;

	/* We may call run() and it unmasks signals,
	 * including the one masked inside this signal handler.
	 * Testcase which would start multiple reboot scripts:
	 *  while true; do reboot; done
	 * Preventing it:
	 */
	reset_sighandlers_and_unblock_sigs();
		// ハンドラをデフォルトに戻して, 有効化

	run_shutdown_and_kill_processes();
		// 後述. run_actrions(),全プロセスにSIGTERM, sync(2),sleep(1sec),SIGKILL,sync(2)

	m = "halt";
	rb = RB_HALT_SYSTEM;
	if (sig == SIGTERM) {
		m = "reboot";
		rb = RB_AUTOBOOT;
	} else if (sig == SIGUSR2) {
		m = "poweroff";
		rb = RB_POWER_OFF;
	}
	message(L_CONSOLE, "Requesting system %s", m);
	pause_and_low_level_reboot(rb);
		// 後述. sleep(1sec), vfork(2)した子で reboot(magic:0x1234567), 親(pid=1)はsleep forever.
	/* not reached */
}
いくつか関数を読んでいるので、引用します。コメントに書いたような処理を行っています。
static void run_shutdown_and_kill_processes(void)
{
	/* Run everything to be run at "shutdown".  This is done _prior_
	 * to killing everything, in case people wish to use scripts to
	 * shut things down gracefully... */
	run_actions(SHUTDOWN);

	message(L_CONSOLE | L_LOG, "The system is going down NOW!");

	/* Send signals to every process _except_ pid 1 */
	kill(-1, SIGTERM);
	message(L_CONSOLE | L_LOG, "Sent SIG%s to all processes", "TERM");
	sync();
	sleep(1);

	kill(-1, SIGKILL);
	message(L_CONSOLE, "Sent SIG%s to all processes", "KILL");
	sync();
	/*sleep(1); - callers take care about making a pause */
}
run_actions(SHUTDOWN);では、 inittabで記述した "::shutdown:"のコマンドを実行していきます。
swapoff, unmount -a -rが特徴的ですね。とくに後者は ファイルシステムをアンマウントすることで、
終了処理中に書き込みを行わせないようにしています(アンマウントできないときは
read onlyでリマウントするオプションが設定されています)。
static void pause_and_low_level_reboot(unsigned magic)
{
	pid_t pid;

	/* Allow time for last message to reach serial console, etc */
	sleep(1);

	/* We have to fork here, since the kernel calls do_exit(EXIT_SUCCESS)
	 * in linux/kernel/sys.c, which can cause the machine to panic when
	 * the init process exits... */
	pid = vfork();
	if (pid == 0) { /* child */
		reboot(magic);
		_exit(EXIT_SUCCESS);
	}
	while (1)
		sleep(1);
}
reboot(2)を実行しているようなので、libcを見ていきましょう。

libc

reboot(2)は以下のようにシステムコールを実行するだけでした。
/* Call kernel with additional two arguments the syscall requires.  */
int
reboot (int howto)
{
  return INLINE_SYSCALL (reboot, 3, (int) 0xfee1dead, 672274793, howto);
}
magic1: 0xfee1dead
magic2: 0x28121969

マジックナンバーが入っているけれども、これはカーネルソースで定義されている値ですね。

FILE: include/linux/reboot.h
/*
 * Magic values required to use _reboot() system call.
 */

#define LINUX_REBOOT_MAGIC1     0xfee1dead
#define LINUX_REBOOT_MAGIC2     672274793
#define LINUX_REBOOT_MAGIC2A    85072278
#define LINUX_REBOOT_MAGIC2B    369367448
#define LINUX_REBOOT_MAGIC2C    537993216

/*
 * Commands accepted by the _reboot() system call.
 *
 * RESTART     Restart system using default command and mode.
 * HALT        Stop OS and give system control to ROM monitor, if any.
 * CAD_ON      Ctrl-Alt-Del sequence causes RESTART command.
 * CAD_OFF     Ctrl-Alt-Del sequence sends SIGINT to init task.
 * POWER_OFF   Stop OS and remove all power from system, if possible.
 * RESTART2    Restart system using given command string.
 * SW_SUSPEND  Suspend system using software suspend if compiled in.
 * KEXEC       Restart system using a previously loaded Linux kernel
 */

#define LINUX_REBOOT_CMD_RESTART        0x01234567
#define LINUX_REBOOT_CMD_HALT           0xCDEF0123
#define LINUX_REBOOT_CMD_CAD_ON         0x89ABCDEF
#define LINUX_REBOOT_CMD_CAD_OFF        0x00000000
#define LINUX_REBOOT_CMD_POWER_OFF      0x4321FEDC
#define LINUX_REBOOT_CMD_RESTART2       0xA1B2C3D4
#define LINUX_REBOOT_CMD_SW_SUSPEND     0xD000FCE2
#define LINUX_REBOOT_CMD_KEXEC          0x45584543


kernel実装

(v4.4.120を参照しています)
システムコールから呼ばれる関数は以下ですね:

FILE: kernel/reboot.c
/*
 * Reboot system call: for obvious reasons only root may call it,
 * and even root needs to set up some magic numbers in the registers
 * so that some mistake won't make this reboot the whole machine.
 * You can also set the meaning of the ctrl-alt-del-key here.
 *
 * reboot doesn't sync: do that yourself before calling this.
 */
SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,
		void __user *, arg)
	mutex_lock(&reboot_mutex);
	switch (cmd) {
	case LINUX_REBOOT_CMD_RESTART:
		kernel_restart(NULL);
		break;
..
	mutex_unlock(&reboot_mutex);
	return ret;
}
/**
 *	kernel_restart - reboot the system
 *	@cmd: pointer to buffer containing command to execute for restart
 *		or %NULL
 *
 *	Shutdown everything and perform a clean reboot.
 *	This is not safe to call in interrupt context.
 */
void kernel_restart(char *cmd)
{
	kernel_restart_prepare(cmd);
	migrate_to_reboot_cpu();
	syscore_shutdown();		// syscore_ops_listを走査する
	if (!cmd)
		printk(KERN_EMERG "Restarting system.\n");
	else
		printk(KERN_EMERG "Restarting system with command '%s'.\n", cmd);
	kmsg_dump(KMSG_DUMP_RESTART);
	machine_restart(cmd);
}
EXPORT_SYMBOL_GPL(kernel_restart);
void kernel_restart_prepare(char *cmd)
{
	blocking_notifier_call_chain(&reboot_notifier_list, SYS_RESTART, cmd);
	system_state = SYSTEM_RESTART;
	usermodehelper_disable();		// timeout: 5sec
	device_shutdown();		// timeoutなし, devices_kset->list を走査する.
}
device_shutdown()と syscore_shutdown()の詳細は追い切れていませんが、
登録されているコールバック関数を呼び出して後処理をしているようです。
loadable moduleは removeされるような挙動を見かけたので、ここで解放されていると思われます(要確認)。

アーキテクチャ依存部: machine_restart()

armの場合は以下のとおり。
さらにプラットフォーム依存・ボード依存部で定義された関数呼び出しへと続きます。

FILE: arch/arm/kernel/reboot.c
void machine_restart(char *cmd)
{
	local_irq_disable();
	smp_send_stop();

	if (arm_pm_restart)
		arm_pm_restart(reboot_mode, cmd);
	else
		do_kernel_restart(cmd);

	/* Give a grace period for failure to restart of 1s */
	mdelay(1000);

	/* Whoops - the platform was unable to reboot. Tell the user! */
	printk("Reboot failed -- System halted\n");
	local_irq_disable();
	while (1);
}
このあたりは使用されているターゲットのコードを追いかけましょう。



参考まで、arm64の場合は以下のとおり。firmwareがUEFI対応であれば、それを使うようです。
FILE: arm64/kernel/process.c
/*
 * Restart requires that the secondary CPUs stop performing any activity
 * while the primary CPU resets the system. Systems with multiple CPUs must
 * provide a HW restart implementation, to ensure that all CPUs reset at once.
 * This is required so that any code running after reset on the primary CPU
 * doesn't have to co-ordinate with other CPUs to ensure they aren't still
 * executing pre-reset code, and using RAM that the primary CPU's code wishes
 * to use. Implementing such co-ordination would be essentially impossible.
 */
void machine_restart(char *cmd)
{
	/* Disable interrupts first */
	local_irq_disable();
	smp_send_stop();

	/*
	 * UpdateCapsule() depends on the system being reset via
	 * ResetSystem().
	 */
	if (efi_enabled(EFI_RUNTIME_SERVICES))
		efi_reboot(reboot_mode, NULL);

	/* Now call the architecture specific reboot code. */
	if (arm_pm_restart)
		arm_pm_restart(reboot_mode, cmd);
	else
		do_kernel_restart(cmd);

	/*
	 * Whoops - the architecture was unable to reboot.
	 */
	printk("Reboot failed -- System halted\n");
	while (1);
}

FILE: kernel/reboot.c
/**
 *	do_kernel_restart - Execute kernel restart handler call chain
 *
 *	Calls functions registered with register_restart_handler.
 *
 *	Expected to be called from machine_restart as last step of the restart
 *	sequence.
 *
 *	Restarts the system immediately if a restart handler function has been
 *	registered. Otherwise does nothing.
 */
void do_kernel_restart(char *cmd)
{
	atomic_notifier_call_chain(&restart_handler_list, reboot_mode, cmd);
}

奥が深いですね...(ぇ