Read OSS

Linux カーネルアーキテクチャ:ソースコード探索者のための地図

中級

前提知識

  • C 言語の基礎知識
  • オペレーティングシステムの概要についての理解

Linux カーネルアーキテクチャ:ソースコード探索者のための地図

Linux カーネルは約 3000 万行のコードから成り、数十年にわたって何千人もの開発者が貢献してきた巨大なプロジェクトです。リポジトリを初めて開いたとき、その規模に圧倒されるのは無理のないことです。個々のファイルが難解なわけではなく、明確な入口の不在こそが原因です。main() とリクエストハンドラーがあるような Web フレームワークとは異なり、カーネルは条件付きコンパイルによって構成される巨大なシステムであり、ツリー上のコードの半分が特定のビルドに含まれないこともあります。この記事は、そんなカーネルを読み解くための地図です。

トップレベルのディレクトリ構成、ハードウェア固有のコードとポータブルなロジックを分離する階層型アーキテクチャ、カーネル内部とユーザースペース向けヘッダーの重要な境界、そして何をコンパイルするかを決定する Kconfig/Kbuild システムについて順に見ていきます。読み終えるころには、どのカーネルサブシステムを探索するときでも、目的のコードがどこにあるかがわかるようになっているはずです。

3000 万行という挑戦

まず理解しておくべきことがあります。カーネルを読むとき、カーネル全体を読むことは決してありません。典型的な x86-64 サーバー向けビルドがコンパイルするのは、ソースツリーの 10〜15% 程度です。残りは他のアーキテクチャ向けのコード、無効化されたフィーチャー、手元にないハードウェア用のドライバーです。ビルドシステムは、まさにこの選択を管理するために存在しています。

MAINTAINERS ファイルは 29,000 行以上あり、カーネルを読み解く際のロゼッタストーンとも言える存在です。すべてのサブシステム、ドライバー、ファイルパターンに対して、担当のメンテナー、メーリングリスト、ステータスが記録されています。見慣れないディレクトリに出会ったら、MAINTAINERS を確認すれば、誰がオーナーで議論がどこで行われているかがわかります。

ヒント: scripts/get_maintainer.pl を任意のファイルに対して実行すると、担当者とメーリングリストをすぐに特定できます。MAINTAINERS を自動でパースしてくれます:./scripts/get_maintainer.pl fs/ext4/super.c

トップレベルのディレクトリ構成

トップレベルのレイアウトは、カーネルをサブシステムに明確に分解した設計思想を反映しています。各ディレクトリの役割は以下のとおりです。

ディレクトリ 役割
init/ 起動時の初期化処理(start_kernel() を含む)
kernel/ カーネルコア:スケジューリング、シグナル、ロック、タイマー、トレーシング
mm/ メモリ管理:ページアロケーター、スラブ、仮想メモリ
fs/ Virtual Filesystem Switch(VFS)および全ファイルシステム実装
net/ ネットワークスタック:TCP/IP、ソケット、netfilter
drivers/ デバイスドライバー(ツリー全体の約 60% を占める最大のディレクトリ)
arch/ アーキテクチャ固有のコード:x86、arm64、riscv など
include/ ヘッダーファイル:カーネル内部向けと UAPI の両方
block/ ファイルシステムとディスクドライバーの間に位置するブロック I/O レイヤー
io_uring/ 非同期 I/O サブシステム(2022 年にトップレベルへ昇格)
security/ LSM フレームワーク:SELinux、AppArmor など
crypto/ 暗号化アルゴリズムと API
rust/ Rust 言語サポートと kernel クレート
virt/ 仮想化サポート(KVM は arch/ 配下だが、共有コンポーネントはここに置かれる)
sound/ ALSA オーディオサブシステム
scripts/ ビルドスクリプト、コード検査ツール、メンテナー向けヘルパー
tools/ ユーザースペースツール:perf、bpftool、セルフテスト
lib/ カーネル内部ライブラリ:ソート、展開、文字列操作など
ipc/ System V IPC:共有メモリ、セマフォ、メッセージキュー
certs/ モジュール検証用の署名証明書
usr/ initramfs の生成
graph TD
    subgraph "Core Kernel"
        init["init/"]
        kernel["kernel/"]
        mm["mm/"]
        lib["lib/"]
        ipc["ipc/"]
    end
    subgraph "Subsystems"
        fs["fs/"]
        net["net/"]
        block["block/"]
        io_uring["io_uring/"]
        security["security/"]
        crypto["crypto/"]
        sound["sound/"]
        virt["virt/"]
    end
    subgraph "Hardware Abstraction"
        arch["arch/"]
        drivers["drivers/"]
    end
    subgraph "Newer Additions"
        rust["rust/"]
    end
    init --> kernel
    kernel --> mm
    kernel --> arch
    fs --> block
    block --> drivers
    io_uring --> fs

ビルド順序はトップレベルの Kbuild ファイルで明示的に定義されています。アルファベット順ではなく、意図を持った順序になっています。

Kbuild#L90-L111

obj-y           += init/
obj-y           += usr/
obj-y           += arch/$(SRCARCH)/
...
obj-y           += kernel/
obj-y           += mm/
obj-y           += fs/
...
obj-$(CONFIG_BLOCK)    += block/
obj-$(CONFIG_IO_URING) += io_uring/
obj-$(CONFIG_RUST)     += rust/
...
obj-y           += drivers/

パターンに注目してください。obj-y は「常にコンパイルする」を意味し、obj-$(CONFIG_*) は「このコンフィグシンボルが設定されている場合のみ」を意味します。ブロックレイヤー、io_uring、Rust サポート、ネットワーク、サンプルコードはすべて条件付きです。これが Kconfig システムの動作原理であり、後ほど詳しく説明します。

3 層アーキテクチャ

カーネルのコードは 3 層のアーキテクチャに従って構成されていますが、コンパイラーによる強制ではなく、慣習として維持されています。

flowchart TB
    subgraph Layer1["Layer 1: Architecture-Specific"]
        direction LR
        x86["arch/x86/"]
        arm64["arch/arm64/"]
        riscv["arch/riscv/"]
    end
    subgraph Layer2["Layer 2: Core Kernel"]
        direction LR
        sched["kernel/sched/"]
        mm2["mm/"]
        vfs["fs/ (VFS)"]
        netcore["net/core/"]
    end
    subgraph Layer3["Layer 3: Drivers & Filesystems"]
        direction LR
        dri["drivers/gpu/"]
        ext4["fs/ext4/"]
        tcp["net/ipv4/"]
        nvme["drivers/nvme/"]
    end
    Layer1 -->|"defined interfaces<br/>(asm/ headers)"| Layer2
    Layer2 -->|"operations structs<br/>(vtables)"| Layer3

レイヤー 1(アーキテクチャ固有) は、CPU アーキテクチャごとに異なる最下層のコードです。起動時のアセンブリ、割り込み処理、ページテーブルの形式、システムコールのエントリーポイントなどが含まれます。各アーキテクチャは arch/<name>/ 以下に置かれ、arch/<name>/include/asm/ ヘッダーを通じて明確に定義されたインターフェースを公開します。

レイヤー 2(カーネルコア) は、ポータブルなサブシステムを担います。kernel/sched/ のスケジューラー、mm/ のメモリ管理、fs/ の VFS、net/ のネットワーキングがここに相当します。このコードは、アーキテクチャ固有の関数プロトタイプを通じてレイヤー 1 を呼び出し、C 構造体ベースの「vtable」パターンを通じてレイヤー 3 への拡張ポイントを提供します。このパターンは第 3・4 回の記事で詳しく取り上げます。

レイヤー 3(ドライバーとファイルシステム実装) は、コードの大半が集中する層です。ext4 のようなファイルシステムは struct file_operationsstruct inode_operationsstruct super_operations を実装して VFS に組み込まれます。デバイスドライバーは、対応するバス(PCI、USB、プラットフォーム)固有の操作構造体を実装します。この層はアーキテクチャ固有のコードに直接触れることはなく、常にレイヤー 2 の抽象化を通じてアクセスします。

ヒント: 見慣れないドライバーを読み始めるときは、まず操作構造体を探しましょう。そのドライバーがどのレイヤー 2 インターフェースを実装しているかが一目でわかります。

カーネルとユーザースペースの API ヘッダー

include/ ディレクトリには、初心者が見落としがちな重要な区分があります。

  • include/linux/ — カーネル内部ヘッダー。リリース間で予告なく変更される可能性があり、カーネルコードのみがインクルードします。
  • include/uapi/linux/ — ユーザースペース向けの API ヘッダー。カーネルとユーザースペースアプリケーションの間の安定した ABI を定義します。変更には後方互換性の維持が求められる、非常に慎重な判断が必要な領域です。

この分離は、2012 年ごろの「UAPI 分離」作業によって導入されました。それ以前は、カーネル内部とユーザースペース向けの定義が同じヘッダーに混在しており、#ifdef __KERNEL__ ガードで内部ビットを隠す方式が取られていました。現在のレイアウトは、その境界をディレクトリ構造として明示しています。

たとえば include/uapi/linux/io_uring.h には、ユーザースペースプログラムが書き込む submission queue エントリー(struct io_uring_sqe)が定義されています。liburing のようなユーザースペースライブラリからインクルードできるよう、デュアルライセンス(GPL OR MIT)が付与されています。一方、include/linux/io_uring_types.h にはカーネル内部の struct io_ring_ctx が定義されており、ユーザースペースからは見えません。

ルールはシンプルです。uapi/ 以下にあるものはユーザースペースプログラムへの約束です。それ以外はすべて実装の詳細にすぎません。

MAINTAINERS によるオーナーシップモデル

カーネルは、単一チームが管理するモノリスではありません。それぞれ担当メンテナーを持つサブシステムの連合体です。MAINTAINERS ファイルにはその構造が次のような形式でエンコードされています。

FILESYSTEMS (VFS and infrastructure)
M:	Alexander Viro <viro@zeniv.linux.org.uk>
M:	Christian Brauner <brauner@kernel.org>
L:	linux-fsdevel@vger.kernel.org
S:	Maintained
F:	fs/*
F:	include/linux/fs.h

各エントリーは、ファイルパターン(F:)をメンテナー(M:)、レビュアー(R:)、メーリングリスト(L:)、ステータス(S:)に対応付けています。get_maintainer.pl スクリプトはこれらのパターンを利用して、特定のパッチのレビュアーを特定します。

このオーナーシップモデルの結果として、コード品質の基準、命名規則、レビューの厳格さはサブシステムによって異なります。たとえばネットワークスタックは、特に厳格なレビュープロセスで知られています。サブシステムのオーナーを把握することは、そのコードの文化を理解することにもつながります。

Kconfig と Kbuild:大規模な条件付きコンパイル

カーネルのビルドシステムは 2 つの柱で成り立っています。Kconfig は設定できる項目を定義し、Kbuild は選択された項目をコンパイルします。

Kconfig:設定言語

ルートの Kconfig ファイルは、サブシステムの設定ファイルを定義された順序でインクルードします。

mainmenu "Linux/$(ARCH) $(KERNELVERSION) Kernel Configuration"
source "scripts/Kconfig.include"
source "init/Kconfig"
source "kernel/Kconfig.freezer"
source "fs/Kconfig.binfmt"
source "mm/Kconfig"
source "net/Kconfig"
source "drivers/Kconfig"
...
source "io_uring/Kconfig"

各ファイルでは config エントリーが定義され、型(booltristateintstring)、デフォルト値、依存関係、ヘルプテキストが指定されます。tristate オプションは y(組み込み)、m(モジュール)、n(無効)の 3 値を取れます。CONFIG_* シンボルの総数は 20,000 を超えます。

flowchart LR
    A["make menuconfig"] --> B[".config file<br/>(CONFIG_* = y/m/n)"]
    B --> C["include/config/auto.conf<br/>(processed config)"]
    C --> D["Kbuild Makefiles<br/>obj-$(CONFIG_*) rules"]
    D --> E["vmlinux binary"]

Kbuild:条件付きオブジェクトのインクルード

カーネルの各ディレクトリには Makefile(または Kbuild ファイル)があり、obj-yobj-$(CONFIG_*) を使ってコンパイル対象を制御します。そのパターンはシンプルかつ明快です。

obj-$(CONFIG_EXT4_FS)   += ext4/
obj-$(CONFIG_XFS_FS)    += xfs/
obj-$(CONFIG_BTRFS_FS)  += btrfs/

CONFIG_EXT4_FS=y のとき、式は obj-y += ext4/ に展開され、ext4 がカーネルに組み込まれます。CONFIG_EXT4_FS=m のときは obj-m += ext4/ となり、ローダブルモジュールとしてビルドされます。CONFIG_EXT4_FS=n(または未設定)のときは、ext4 ディレクトリ全体がスキップされます。

トップレベルの Makefile は最終リンクを統括し、すべてのビルド済みオブジェクトを vmlinux.a にまとめて vmlinux バイナリにリンクします。

targets += vmlinux.a
vmlinux.a: $(KBUILD_VMLINUX_OBJS) scripts/head-object-list.txt FORCE
	$(call if_changed,ar_vmlinux.a)
flowchart TD
    subgraph "Build Phases"
        K["Kconfig<br/>Configuration"] --> P["Preprocessing<br/>bounds.h, offsets.h"]
        P --> C["Compilation<br/>Per-directory Makefiles"]
        C --> A["Archive<br/>vmlinux.a"]
        A --> L["Link<br/>vmlinux"]
        L --> Z["Compress<br/>bzImage/zImage"]
    end

このシステムが意味するのは、カーネルが単一のプログラムではなく、特定のハードウェアと機能要件に合わせた何千もの異なるプログラムを生成するフレームワークだということです。組み込み IoT 向けのカーネルは 2,000 ファイル程度をコンパイルし、ディストリビューション向けカーネルは 10,000 ファイルに及ぶこともあります。ソースツリーはそのすべてに対応しています。

次のステップ

この地図を手に入れた今、何がどこにあり、なぜそこにあるのかが見えてきたはずです。次回の記事では、電源投入直後の最初の命令から start_kernel() の初期化シーケンスまでを追いながら、各サブシステムが綿密に振り付けられたブートシーケンスの中で立ち上がる様子を見ていきます。