DeviceTreeの基礎

2019/03/23

device.tree.org

devicetreeの実装サイドからの仕様を公開していると思われます。

そもそもdevicetreeとはコンピュータシステムの構成要素を記述するために用いられています。

冒頭で 関連している仕様として、以下が挙げられています:

  • IEEE 1275 Open Firmware standard―IEEE Standard for Boot (Initialization Configuration) Firmware: Core Requirements and Practices [IEEE1275].
  • EPAPR

この仕様書は、2016年からlinaro社, ARM社によって生成されている模様です。

コンピュータシステムの初期化を行う際に、OS起動前に初期化処理の必要なハードウェア類があります。ブートローダ、ハイパーバイザ等が初期化等を行って、最終的にOSへと処理を移します。このプログラム間で情報のやりとりを行うための規定にも使えるもの、とあります。

仕様の詳細は、Devicetree Specificationを参照しましょう。


なぜ必要か

PCでは ACPIがハードウェア情報を提供してくれるため、必要な設定・ドライバのロードが可能でした。

そこで Open Firmwareの頃から powerpcアーキテクチャで devicetreeの利用が始まっていました。その後 Linux kernelソースコードの汚い肥大化をLinusに指摘されたことにより、ARMアーキテクチャにも適用することとなったものです。したがって、poewrpcやARMアーキテクチャのカーネルドライバ・u-bootに関わる方には必須の事項となります。

ブートプログラムがシステム内のハードウェアの初期化処理を行うために必要な情報を得るために必要です。これがなければ、ブートプログラムはシステム内のハードウェア構成にあわせて個々に用意する必要があります。これではブートプログラムの実装効率も悪く、同じようなコードが散乱することになりかねません。そこで、ドライバソフトウェアを個々のハードウェアに対応したものとし、そのリソースを含めて定義できるようにすることでソフトウェアを共通化することができます。

その昔、PC/AT互換機が出てから `Plug and Play` が登場するまでは、ボードのリソース設定等をディップスイッチで行って、インストール作業者がリソース競合を回避するといった作業を行っていました。

PCIデバイスでは、ターゲット側がconfiguration spaceを用意することで、デバイスの素性や要求リソースを検出する仕組みが用意されています。したがって、Devitetreeはこれらのターゲットデバイスの定義を含めないこととしています(例外として、ソフト的にproveできないPCI host bridge deviceを挙げています)。


Devicetree format(概要編)

Devicetree Specificationに詳細な記述仕様があります。ざっと要点を抜粋していきます。

tree状に定義する様式となっており、'/'をルートノードとして、ファイルシステムのディレクトリ階層のように記述していきます。簡単な記述例を以下に示します。実用的な記述ではありませんが、コンパイルは通ります。

/dts-v1/;
/ {
        #address-cells = <1>;
        #size-cells = <1>;

        soc {
                compatible = "simple-bus";
                #address-cells = <1>;
                #size-cells = <1>;
                intc: intc@100 {
                        reg = <1000 100>;
                };
                spi: spi@0 {
                        reg = <0 0>;
                        interrupts = <&intc 1>;
                };
    };
};

ここで socintc@100 はノードといいます。中括弧で括った要素(プロパティ)をまとめる器です。これはバスを表現したり、特定の機能をもつIPコアを表現したり、Flash memoryやセンサICを表現したりします。

また、ノードは何段でも階層化した記述ができ、CPUから見えるシステムバスの多段表現や、その先にあるIPコアのレジスタインタフェースを表現することができます。SoC内部に限らず、SPI・I2C・非同期バスなどを介して接続するデバイスを表現します。

それぞれのノードに、プロパティを定義します。プロパティは 項目名と値とからなる組み合わせで、値がないものも存在します。値はDTspecで規定されているものなら自由に設定できますが、それを参照するのは対応するドライバです。ドライバが判るように設定してあげる必要があります。

kernel versionが新しくなるにつれ、ドキュメントも整備されていきました。kernel/Documentation/devicetree/bindings/ 配下に、ある程度まとめられています。善意の協力のもとで作られていますので、必ずしも欲しい情報が存在する/正しいとは限りません。

自分が商用向けで触ったりする場合には 可能な限りソースコードも参照することをおすすめします。

linux systemにおけるinitの最期

2018/12/18
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);
}

奥が深いですね...(ぇ
OK キャンセル 確認 その他