47
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

LinuxAdvent Calendar 2017

Day 12

Linuxのドライバの初期化が呼ばれる流れ

Last updated at Posted at 2017-12-11

はしがき

この記事はLinux Advent Calendar 2017の12日目の記事になります。

著者からの独断的なあいさつ

\def\textlarge#1{%
  {\rm\Large #1}
}

2017年も残るところ1ヶ月を切りました。いよいよですね、待ち遠しくなってきました、

$\textlarge{鏡音リン・レン誕生祭!}$

10周年を迎えて記念のコンピアルバムCDもたくさん発表されたりと、ボーカロイド界隈の勢いは衰えません。そこ、ミクさんの存在が大きくてほかは影が薄いとか言わない。ぼくだってこんなにハマるとは思わなかったんです。10年前はマジメに技術的な話だけを追っていたはずなのに、どうしてこうなった。こんな地味なプログラム系記事を書くようなおっさんじゃなくて、リンちゃんの曲を作る超売れっ子ボカロPに生まれたかったです。

・・・失礼しました・・・

本当のはじめに

Linuxのkernelにはじめて触ってみるきっかけになったのはドライバの組み込みや修正だ、という人が多いんじゃないかと思う。現代的なOSではやらなくてはいけないことは非常に多岐にわたるけど、そんな全体像のことはわからずとも、とりあえずドライバをちょっと触ってみる、という程度のことはやりやすいように今のLinuxは整備がされている。

ただ、なんでそう書いたらドライバが組み込まれるのか、などの点を調べてみると、いろいろと手の込んだ仕組みが用意されていたりとする様子が見えてくる。今回は、ドライバの初期化が呼ばれるあたりの箇所を、あまり難しくならない程度に解説できたらと思う。

ちなみに、2017/12/10(Sun)時点で最新リリースバージョンのLinux-4.14あたりを見ています。

典型的なドライバの形

この記事は、簡単なドライバを書いてみるという趣旨の記事ではないので、そういうのは別のところに任せるとして。典型的なドライバは下記のような形でkernel本体側に組み込みを行う事が多い。

testdriver.c
static struct platform_driver testdriver_of_driver = {
    .driver = {
        .name = "testdriver",
        .of_match_table = testdriver_platform_match,
    },
    .probe = testdriver_probe,
    .remove = testdriver_remove,
};
int __init testdriver_init(void)
{
    return platform_driver_register(&testdriver_of_driver);
}
void testdriver_exit(void)
{
    platform_driver_unregister(&testdriver_of_driver);
}
module_init(testdriver_init);
module_exit(testdriver_exit);

なんとなく、起動時(ロード時)にtestmod_init()が呼ばれて、platform_driver_register()で登録して、testdriver_platform_matchで該当ドライバかどうか判定して、testdriver_probe()でデバイスの初期化を行って・・・な雰囲気は漂うけど、これは実際のとこどう動くんだろうというのを考えてみる。

ちなみに、みんな大好きカニさんマークRealtekの有名な8111チップ(PCIe接続のEthernetチップ)は、kernel/drivers/net/ethernet/realtek/r8169.cでこんな感じに書いている。

r8169.c
static struct pci_driver rtl8169_pci_driver = {
	.name		= MODULENAME,
	.id_table	= rtl8169_pci_tbl,
	.probe		= rtl_init_one,
	.remove		= rtl_remove_one,
	.shutdown	= rtl_shutdown,
	.driver.pm	= RTL8169_PM_OPS,
};

module_pci_driver(rtl8169_pci_driver);

詳しくは書かないけど、module_pci_driverのマクロは、何度も展開された結果として、module_initやplatform_driver_register()に行き着く。うん、リビジョンは8111hで止まってくれていたようで良かった。

最近は、module_init()やmodule_exit()を直接書くのではなくて、module_platform_driver()あたりを使うほうが普通なのかな?このあたりは他の類似ドライバを参考に真似たほうがいいのかもしれない。module_platform_driver()を使うと、さっきのtestdriver.cの例のコードがもっと短くなる。

module_initマクロ

module_initマクロはkernel/include/linux/module.hにある。

module.h
/**
 * module_init() - driver initialization entry point
 * @x: function to be run at kernel boot time or module insertion
 *
 * module_init() will either be called during do_initcalls() (if
 * builtin) or at module insertion time (if a module).  There can only
 * be one per module.
 */
#define module_init(x)	__initcall(x);

/**
 * module_exit() - driver exit entry point
 * @x: function to be run when driver is removed
 *
 * module_exit() will wrap the driver clean-up code
 * with cleanup_module() when used with rmmod when
 * the driver is a module.  If the driver is statically
 * compiled into the kernel, module_exit() has no effect.
 * There can only be one per module.
 */
#define module_exit(x)	__exitcall(x);

__initcallはというと、kernel/include/linux/init.hより、

init.h
/*
 * initcalls are now grouped by functionality into separate
 * subsections. Ordering inside the subsections is determined
 * by link order. 
 * For backwards compatibility, initcall() puts the call in 
 * the device init subsection.
 *
 * The `id' arg to __define_initcall() is needed so that multiple initcalls
 * can point at the same handler without causing duplicate-symbol build errors.
 *
 * Initcalls are run by placing pointers in initcall sections that the
 * kernel iterates at runtime. The linker can do dead code / data elimination
 * and remove that completely, so the initcall sections have to be marked
 * as KEEP() in the linker script.
 */

#define __define_initcall(fn, id) \
	static initcall_t __initcall_##fn##id __used \
	__attribute__((__section__(".initcall" #id ".init"))) = fn;

/*
 * Early initcalls run before initializing SMP.
 *
 * Only for built-in code, not modules.
 */
#define early_initcall(fn)		__define_initcall(fn, early)

/*
 * A "pure" initcall has no dependencies on anything else, and purely
 * initializes variables that couldn't be statically initialized.
 *
 * This only exists for built-in code, not for modules.
 * Keep main.c:initcall_level_names[] in sync.
 */
#define pure_initcall(fn)		__define_initcall(fn, 0)

#define core_initcall(fn)		__define_initcall(fn, 1)
#define core_initcall_sync(fn)		__define_initcall(fn, 1s)
#define postcore_initcall(fn)		__define_initcall(fn, 2)
#define postcore_initcall_sync(fn)	__define_initcall(fn, 2s)
#define arch_initcall(fn)		__define_initcall(fn, 3)
#define arch_initcall_sync(fn)		__define_initcall(fn, 3s)
#define subsys_initcall(fn)		__define_initcall(fn, 4)
#define subsys_initcall_sync(fn)	__define_initcall(fn, 4s)
#define fs_initcall(fn)			__define_initcall(fn, 5)
#define fs_initcall_sync(fn)		__define_initcall(fn, 5s)
#define rootfs_initcall(fn)		__define_initcall(fn, rootfs)
#define device_initcall(fn)		__define_initcall(fn, 6)
#define device_initcall_sync(fn)	__define_initcall(fn, 6s)
#define late_initcall(fn)		__define_initcall(fn, 7)
#define late_initcall_sync(fn)		__define_initcall(fn, 7s)

#define __initcall(fn) device_initcall(fn)

#define __exitcall(fn)						\
	static exitcall_t __exitcall_##fn __exit_call = fn

ちょっとわかりにくいけど、__define_initcallのマクロにより、.__initcall6.initという名前のセクションに配置される関数として定義される。わざわざこんなセクションを用意するのには当然わけがあって、kernel/include/asm-generic/vmlinux.lds.hより、

```c`vmlinux.lds.h
#define INIT_CALLS_LEVEL(level)
VMLINUX_SYMBOL(__initcall##level##_start) = .;
KEEP((.initcall##level##.init))
KEEP(
(.initcall##level##s.init)) \

#define INIT_CALLS
VMLINUX_SYMBOL(__initcall_start) = .;
KEEP(*(.initcallearly.init))
INIT_CALLS_LEVEL(0)
INIT_CALLS_LEVEL(1)
INIT_CALLS_LEVEL(2)
INIT_CALLS_LEVEL(3)
INIT_CALLS_LEVEL(4)
INIT_CALLS_LEVEL(5)
INIT_CALLS_LEVEL(rootfs)
INIT_CALLS_LEVEL(6)
INIT_CALLS_LEVEL(7)
VMLINUX_SYMBOL(__initcall_end) = .;


と、リンカスクリプトによりリンク時に__initcall6_start[]という名前の配列に関数ポインタとして入れる。この変数がどう使われるかというと、[kernel/init/main.c](http://elixir.free-electrons.com/linux/v4.14/source/init/main.c#L856)より、

```c:main.c
static initcall_t *initcall_levels[] __initdata = {
	__initcall0_start,
	__initcall1_start,
	__initcall2_start,
	__initcall3_start,
	__initcall4_start,
	__initcall5_start,
	__initcall6_start,
	__initcall7_start,
	__initcall_end,
};

/* Keep these in sync with initcalls in include/linux/init.h */
static char *initcall_level_names[] __initdata = {
	"early",
	"core",
	"postcore",
	"arch",
	"subsys",
	"fs",
	"device",
	"late",
};

static void __init do_initcall_level(int level)
{
	initcall_t *fn;

	strcpy(initcall_command_line, saved_command_line);
	parse_args(initcall_level_names[level],
		   initcall_command_line, __start___param,
		   __stop___param - __start___param,
		   level, level,
		   NULL, &repair_env_string);

	for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
		do_one_initcall(*fn);
}

static void __init do_initcalls(void)
{
	int level;

	for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
		do_initcall_level(level);
}

と、類似の番号が小さい変数から順に配列に入った関数を前から実行していくという仕組みになる。do_initcalls()はkernel起動時の比較的後ろの方(※解釈には個人差があります※)で呼ばれる。このように、起動時に呼ばれる関数を汎用にかつ順序立てて呼ぶ仕組みが用意されている。

ドライバの呼ばれる順番について

先の仕組みにより、「オレの書いたドライバもっと早く初期化関数呼んでほしいんだけど?」て場合は、module_init()の代わりに、rootfs_initcall()とかfs_initcall()とかを使えばいいことがわかる。ただ、名前からわかるように、オレ専用ドライバをこんなところに突っ込んでいいのかどうかという気にもなる。とはいえ、rootfs_initcall()を使ってるところが数えるほどしかないので、まぁクローズドなシステムだとそれでもいいんじゃないかと思う。

同じレベルのもの(例えばmodule_init()同士)の場合はどうかというと、ちょっと自信ないけど、これはリンク時に引数に与える順番になるはず。とはいえ、Linuxのコンパイルではディレクトリ毎にbuilt-in.oという中間ファイルを作るやり方なので、各ディレクトリごとのMakefileを変更していく必要が出てきて、結構めんどそう。

usbやpcieなど、バスの初期化順に依存するようなドライバの場合は、module_pci_driver()やusb_register()といった感じの、汎用ものと類似したバス専用のものが用意されている。このへんはまだ追ったことがないので、今回は省略で。

ドライバの初期化にまつわる雑多な話

initcall_debug

cmdlineにinitcall_debugという引数を加えて起動させると、これまで見てきた__initcall定義した関数ごとにかかった時間をKERN_DEBUGに出力してくれる。kernelの起動時間を分析する時の最初の一歩になる。eLinuxにそれ用のページがあるのでそちらも参考に。

-EPROBE_DEFER

__initcallの話ではないんだけど、attachやprobeにて「-EPROBE_DEFER」を返すとこれは少し変わった意味と解釈される。-EPROBE_DEFERを返したドライバは遅延probeリスト入りする。kernel/drivers/base/dd.cより、

dd.c
	switch (ret) {
	case -EPROBE_DEFER:
		/* Driver requested deferred probing */
		dev_dbg(dev, "Driver %s requests probe deferral\n", drv->name);
		driver_deferred_probe_add(dev);
		/* Did a trigger occur while probing? Need to re-trigger if yes */
		if (local_trigger_count != atomic_read(&deferred_trigger_count))
			driver_deferred_probe_trigger();
		break;

遅延probe入りしたドライバは、すべてのドライバのprobeを呼び終えたあとでもう一度呼ばれる。これを、初期化に成功したドライバの数がゼロになるまで何度も行い続ける。この仕組みを知らないまま、かつ-EPROBE_DEFERを返すドライバを見逃してしまうと、無駄にkernelの起動を費やしてしまうことになる。まずはkernel/drivers/base/dd.cの先頭の方のコメントをちゃんと読もう。

PROBE_PREFER_ASYNCHRONOUS

最近のkernel(Linux-4.2から?)は、struct device_driver の probe_typeにPROBE_PREFER_ASYNCHRONOUSを書くことができる。これをするとprobeを非同期に呼ぶようにしてくれる。通常は1スレッドがすべてのprobeを順に呼ぶ仕組みなので、非同期に呼んでくれると、待ちの長いドライバが多い場合に非常に助かるし、最近のマルチコアCPU当たり前なシステムではもっと助かる。ただ、ドライバの初期化のタイミング乱れは、システムの起動の不安定に直結することもあるので、慎重に使いたいところではある。

deferred_module_init

vanilla kernelには取り込まれてないんだけど、eLinux界隈では有名(?)なdeferred_module_initというパッチがある。これは、ドライバの初期化をもっとアグレッシブに遅延させるためのパッチで、ユーザランドから/proc/deferred_initcallsを触られるまでドライバの初期化を遅らせる仕組みを作る。こうすることで、操作ができるようになるまでの起動を早くしつつ、操作できるようになっても裏ではまだ初期化が続いてる、みたいな仕組みを用意できる。

あとがき

Linuxのコアな箇所が各ドライバのエントリ関数を呼ぶ仕組みについて見てきた。ドライバこそが多種多様なデバイスを扱えるようにするための主役でありつつも、出所不明の怪しい動きをするドライバもやはり世の中にはあり、たくさんの幅広い品質のドライバを束ねて安定して動かすのに苦労してきたであろう箇所の一片にはなっているかと思う。

白状しておくと、この記事は、私が自サイトに書き散らしたものからネタを拾いつつ、読み物になるように構成したものです。ご了承ください。

「あれ?attachやprobeの話は?」って?いや、実はそのへんもあんまり詳しくないもので・・・今回は調査の上そこも入れようと思ってはいたんですが、あまり時間取れなかったんで・・・

謝辞

過去のたくさんのOSS活動に従事してきた人々に感謝しつつ、未来のたくさんのOSS活動に従事するであろう人々にこの記事を捧げる。

参考サイトなど

47
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?