電源投入からPID 1まで:Linuxカーネルの起動シーケンス
前提知識
- ›第1回:アーキテクチャとディレクトリ構成
- ›Cの関数ポインタとリンカセクションの基本的な理解
電源投入からPID 1まで:Linuxカーネルの起動シーケンス
起動直後のCPUで最初の命令が実行される瞬間から、login: プロンプトが表示されるまでの間に、カーネルは複雑な初期化シーケンスをこなしています。厳密な順序で呼ばれる約80の関数呼び出し、8段階の優先レベルを持つ自己登録型ドライバの仕組み、そしてシステムで最も重要な2つのプロセスの誕生。この起動パスを理解することで、Linuxがどのように起動するかだけでなく、中央レジストリなしにどうして何千ものドライバに対応できるのかが見えてきます。
第1回で見たように、カーネルのソースはアーキテクチャ固有層とポータブル層に分かれています。起動シーケンスはまさにこの2つが交わる場所です。アーキテクチャ固有のアセンブリがハードウェアを初期化し、その後ポータブルなCコードに制御を渡して各サブシステムを順に立ち上げていきます。
アーキテクチャエントリから start_kernel() へ
x86-64システムが起動すると、ブートローダ(GRUB、systemd-bootなど)が圧縮されたカーネルイメージをメモリに読み込み、エントリポイントへジャンプします。カーネルは自己解凍し、暫定的なページテーブルを構築したあと、C実行環境を整えるアーキテクチャ固有のスタートアップコードへと処理を進めます。
この流れの核心が start_kernel() へのジャンプです。init/main.c にあるこの関数がCコードのエントリポイントとなります。この呼び出しの前に、アセンブリコードは以下の処理を済ませています。
- 有効なカーネルスタックのセットアップ
- 基本的なページングの有効化
- BSSセクションのゼロ初期化
- GDT(Global Descriptor Table)のセットアップ
flowchart LR
A["Bootloader<br/>(GRUB)"] --> B["Decompress<br/>kernel"]
B --> C["Arch-specific<br/>assembly setup"]
C --> D["start_kernel()<br/>in init/main.c"]
D --> E["rest_init()<br/>creates PID 1 & 2"]
start_kernel() の関数シグネチャにも注目してください。複数の属性が付与されています。
asmlinkage __visible __init __no_sanitize_address __noreturn __no_stack_protector
void start_kernel(void)
特に重要なのが __init アノテーションです。これによってこの関数は .init.text セクションに配置され、起動完了後に解放されます。asmlinkage はアセンブリからスタック経由で引数が渡されることをコンパイラに伝え、__noreturn はその名の通り、この関数が決して返らないことを示しています。
start_kernel() の内部を追う
start_kernel() の本体は、約80の初期化呼び出しが並んだ線形のシーケンスです。その順序は決して恣意的ではなく、各呼び出しはそれより前に初期化済みのサブシステムに依存しています。
論理的なグループ分けは次の通りです。
sequenceDiagram
participant SK as start_kernel()
participant HW as Hardware Setup
participant MM as Memory Mgmt
participant SCHED as Scheduler
participant IRQ as Interrupts/Timers
participant VFS as VFS & Processes
SK->>HW: set_task_stack_end_magic()
SK->>HW: setup_arch(&command_line)
SK->>MM: mm_core_init()
SK->>SCHED: sched_init()
SK->>IRQ: init_IRQ(), timers_init()
SK->>IRQ: hrtimers_init(), softirq_init()
SK->>VFS: vfs_caches_init()
SK->>VFS: fork_init(), signals_init()
SK->>SK: rest_init()
フェーズ1:初期ハードウェア — setup_arch() がアーキテクチャ固有の大きな処理を担います。x86では、CPUの識別、BIOS/UEFIからのメモリマップの解析、初期メモリレイアウトの構築をここで行います。setup_arch() より前の処理は、最小限のアーキテクチャ非依存コードで動作します。
フェーズ2:メモリ — mm_core_init() がページアロケータ、スラブアロケータ、vmallocを初期化します。ここ以降、カーネルは kmalloc() を使えるようになります。
フェーズ3:スケジューラ — sched_init() がCPUごとの実行キューを初期化し、アイドルタスクを生成します。この時点でスケジューラは機能しますが、SMPはまだ有効ではありません。
フェーズ4:割り込みとタイマー — init_IRQ()、tick_init()、timers_init()、hrtimers_init()、softirq_init() が割り込みとタイマーのサブシステムを立ち上げます。1138行目の local_irq_enable() 以降、割り込みが動作し始めます。
フェーズ5:コアサブシステム — vfs_caches_init() がデントリキャッシュとinodeキャッシュを作成します。fork_init() がプロセス生成の仕組みをセットアップし、signals_init()、proc_root_init() など数十の関数が続きます。
最後の呼び出しが rest_init() です。その控えめな名前に反して、最も重要なことがここで起きます。
ヒント: 起動中にハングが発生した場合は、カーネルのコマンドラインに
initcall_debugを追加してみましょう。各初期化呼び出しにタイムスタンプが付くので、どこで止まっているかがすぐにわかります。
Initcallメカニズム
rest_init() が最初のプロセスを生成する前に、プロセスがドライバを初期化するために使う仕組み——initcallシステムを理解しておく必要があります。
問題はこうです。カーネルには初期化が必要なドライバ、ファイルシステム、サブシステムが何千も存在します。中央集権的なリストで管理しようとすれば、すぐに手に負えなくなります。そこで各サブシステムは独自のinit関数を宣言し、リンカセクションに自己登録する方式を採っています。
include/linux/init.h#L293-L309
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define late_initcall(fn) __define_initcall(fn, 7)
各レベルには _sync バリアント(例:core_initcall_sync)があり、バリアとして機能します。前のレベルのinitcallがすべて完了してから、syncの呼び出しが実行されます。
| レベル | 名称 | 主な利用者 |
|---|---|---|
| 0 | pure_initcall |
静的変数の初期化のみ |
| 1 | core_initcall |
カーネルコアインフラ(IRQ、DMAなど) |
| 2 | postcore_initcall |
バス種別(PCI、USBバスの登録) |
| 3 | arch_initcall |
アーキテクチャ固有のセットアップ |
| 4 | subsys_initcall |
サブシステム初期化(ネットワーク、ブロック層) |
| 5 | fs_initcall |
ファイルシステムの登録 |
| rootfs | rootfs_initcall |
ルートファイルシステムのセットアップ |
| 6 | device_initcall |
大多数のデバイスドライバ(デフォルトの module_init) |
| 7 | late_initcall |
他のすべてに依存する処理 |
__define_initcall マクロは関数ポインタを .initcall1.init のような名前のリンカセクションに配置します。リンカスクリプトがこれらのセクションを数値順に並べることで、優先度順にソートされた関数ポインタの配列が作られます。どのドライバも他のドライバを知る必要はありません。
PID 1とPID 2の誕生
rest_init() はカーネルの最初の2つのプロセスを生成します。
static noinline void __ref __noreturn rest_init(void)
{
struct task_struct *tsk;
int pid;
rcu_scheduler_starting();
/*
* We need to spawn init first so that it obtains pid 1, however
* the init task will end up wanting to create kthreads, which, if
* we schedule it before we create kthreadd, will OOPS.
*/
pid = user_mode_thread(kernel_init, NULL, CLONE_FS);
...
pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES);
...
}
PID 1(kernel_init)はすべてのユーザ空間プロセスの祖先です。PID 2(kthreadd)はカーネルスレッドデーモンであり、システム上のすべてのカーネルスレッドは最終的にkthreaddが生成します。
コメントが示す通り、順序には微妙な制約があります。PID 1はpid 1を得るために先に生成されますが、kthreadd(PID 2)が存在するまでスケジュールしてはなりません。kernel_initがその処理の中でカーネルスレッドを生成しようとするためです。
flowchart TD
rest_init["rest_init()"] --> pid1["PID 1: kernel_init<br/>(user_mode_thread)"]
rest_init --> pid2["PID 2: kthreadd<br/>(kernel_thread)"]
pid1 --> kif["kernel_init_freeable()"]
kif --> dbs["do_basic_setup()"]
dbs --> di["driver_init()"]
dbs --> dic["do_initcalls()"]
dic --> lvl0["Level 0: pure"]
dic --> lvl1["Level 1: core"]
dic --> lvl6["..."]
dic --> lvl7["Level 7: late"]
kif --> sinit["Search for /sbin/init"]
pid2 --> kt["Manages all<br/>kernel threads"]
kernel_init 関数は kernel_init_freeable() を呼び出し、さらに do_basic_setup() へと進みます。
static void __init do_basic_setup(void)
{
cpuset_init_smp();
driver_init();
init_irq_proc();
do_ctors();
do_initcalls();
}
そして do_initcalls() が8つの優先レベルをすべて順に処理します。
static void __init do_initcalls(void)
{
int level;
...
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) {
strcpy(command_line, saved_command_line);
do_initcall_level(level, command_line);
}
...
}
ここが、カーネルに組み込まれたすべてのドライバ、ファイルシステム、サブシステムが初期化される瞬間です。明示的な登録呼び出しではなく、リンカセクションに配置された関数ポインタをイテレートするだけで実現しています。
/sbin/init の探索
すべてのinitcallが完了すると、kernel_init はinitプログラムを exec() することでカーネル空間からユーザ空間へと移行します。
探索の順序は次の通りです。
ramdisk_execute_command— initramfsの/init(通常はここ)execute_command— カーネルコマンドラインでinit=として渡されたものCONFIG_DEFAULT_INIT— コンパイル時のデフォルト/sbin/init、/etc/init、/bin/init、/bin/sh— フォールバック探索
いずれも見つからない場合は panic("No working init found.") となります。
これがカーネル初期化とユーザ空間の境界線です。kernel_execve() が成功した瞬間から、PID 1はユーザ空間のプロセス(systemd、OpenRC、あるいは使用しているinitシステム)になります。カーネルの役割はここからシステムコールの処理、メモリ管理、プロセスのスケジューリングへと移ります。
__init とメモリの回収
これまで見てきた start_kernel()、initcall関数群、init探索ロジックはすべて __init でアノテートされています。
#define __init __section(".init.text") __cold __latent_entropy __no_kstack_erase
#define __initdata __section(".init.data")
#define __initconst __section(".init.rodata")
__init マクロは関数を .init.text セクションに、__initdata はデータを .init.data セクションに配置します。起動が完了すると、free_initmem() がこれらのページをアロケータに返却します。一般的なシステムでは、起動時にしか必要のなかった数メガバイトのRAMがここで解放されます。
void __weak free_initmem(void)
{
free_initmem_default(POISON_FREE_INITMEM);
}
起動時に Freeing unused kernel memory: 2048K というメッセージが表示されるのを見たことがあるでしょう。まさにこの free_initmem() が動いている証拠です。__init パターンはカーネル開発において非常に重要で、ビルドシステムは「セクションミスマッチ」を積極的にチェックします。非initコードがinitコードを参照していると、解放後の使用(use-after-free)バグになるためです。
ヒント: カーネルモジュールで初期化時にしか使わないコードがある場合は
__initでマークしましょう。ただし注意が必要です。ローダブルモジュールはいつでもロード・アンロードできるため、モジュール内の__initはシステム起動後ではなくmodule_init()が実行された後に解放されます。
次回予告
アセンブリのエントリポイントからPID 1の誕生まで、カーネルの起動シーケンスを一通り追ってきました。次回は、start_kernel() で呼ばれていた sched_init() の中身——スケジューラに踏み込みます。sched_class のvtableパターンが6種類のスケジューリングポリシーをどのように実現しているか、__schedule() が次のタスクをどう選ぶか、そしてBPFでスケジューラポリシーを記述できる新しいsched_extフレームワークについて詳しく見ていきましょう。