KDOC 63: 『私はどのようにしてLinuxカーネルを学んだか』を読んだ

この文書のステータス

  • 作成
  • レビュー

概要

私はどのようにしてLinuxカーネルを学んだかを読んだメモ。Linuxカーネルについて全体像の解説と、ゼロから学ぶ道筋を示す本。

メモ

  • C languageはわかるのに、デバイスドライバのソースコードが理解できない。プログラムを理解するための前提知識がないから
  • Linuxカーネルに関する情報は十分にあった。しかし当時理解できていなかった。文章を読んでなんとなく言いたいことは理解できるが、腹落ちしない
  • 難しいことを理解するためには、段階を踏んで基本を学んでいく必要がある。下地がない状態でいくら背伸びをしても手は届かない
  • コンピュータアーキテクチャを理解していなかった。コンピュータアーキテクチャを理解せずして、Linuxカーネルを理解できるようにはならない
  • まずはソースコードというよりも、カーネルやOSがわからないと理解できない
  • コードも特有の部分があって、じつは詳しく知らないと難しい
  • Linuxカーネルはあくまでもユーザプロセスを動作させることが目的なので、ユーザプロセスのプログラムの動きを知っておく必要がある。ユーザプロセスとLinuxカーネルの両方の動作を把握することで話がひとつにつながる
  • ユーザプロセスがカーネルの機能を利用することをシステムプログラミングという。システムプログラミングを知らない状態では、いくらカーネルのソースコードを見ても全体像が見えてこない
  • 言語文法は習得済みと思っていたが、実は間違いだった。まだまだ習得できてないところがあり読めないところがあった
  • もっとも重要な概念であるユーザ空間とカーネル空間の違いを理解できていなかった。この違いのために、ユーザプロセスとカーネルとで、プログラムの設計が全く変わってくる。ユーザプロセスにはmain関数があるが、カーネルおよびデバイスドライバにはない
  • カーネルのソースコードを読む前に、ハードウェアの動きを理解しなければならない。Linuxカーネルはさまざまなプラットフォームに対応している。自分がどのプラットフォームに対応するLinuxカーネルを学びたいかを明確にする必要がある
  • unameコマンドでアーキテクチャを確認できる。アーキテクチャの名称はLinuxカーネルをビルドしたときに決定する。 UTS-MACHINE2 という定義でアーキテクチャの文字列が定義される
  • システムプログラミングを知る。システムコールとはユーザプロセスがシステムコールを使って、カーネルの機能を呼び出すこと
    • 厳密には特定のアセンブラ命令を呼び出すこと
  • システムコールとはなにかを理解しなければならない。システムコールはユーザプロセスとカーネルをつなぐためのインターフェースであり、全体像を抑えるうえで必要となるから。システムコールとして使われる関数の意味を知ることがLinuxカーネルを理解するうえで必要になる
  • Linuxカーネルのビルドは慣習的にgccが使われている。gccの拡張機能がソースコードに多く使われている。そのためgccの拡張機能について一通り知っておく必要がある
  • とにかく動かして見る
  • 図を書きながら理解していくとよい
  • 動かして理解していく流れ
    • printk関数で調べる
    • SystemTapというツールもある
    • ビルドする
    • 動かす
  • C言語で使われる整数データ型は大小の関係だけが仕様で規定されており、それぞれのデータ型のサイズはコンパイラ任せになっている
  • カーネルコンフィギュレーションとによってLinuxカーネルを構成するモジュールを自由に選択できる。必要なモジュールのみをカーネルに組み込める。それによってカーネルのサイズを小さくできる
  • Linuxカーネルでは条件コンパイル文を活用してカーネルコンフィギュレーションを実現している
  • マクロが多用されているので、ビルドオプションであらかじめ展開して読むとよい
    • MakefileのEXTRA_CLAGSオプションで、コンパイルの途中結果をファイルに残す。コンパイルの途中結果にプリプロセッサ出力が含まれる
  • マクロの中身を理解する必要はあまりない。必要に迫られたときはプリプロセッサ出力で見るのがおすすめ
  • Linuxカーネルはどれかと聞かれたら、「/boot配下にあるvmlinuzファイルである」で間違いない
  • ブートメッセージを学習の題材とするのもおすすめ
  • initデーモンに何を選択するかはLinuxディストリビューションの開発元が決めること

ソフトウェア割り込みを見る方法。

cat /proc/interrupts | head -n 10
           CPU0       CPU1       CPU2       CPU3       CPU4       CPU5       CPU6       CPU7       CPU8       CPU9       CPU10      CPU11      CPU12      CPU13      CPU14      CPU15
  1:          0          0          0          0          0    1599978          0          0          0          0          0          0          0          0          0          0   IO-APIC    1-edge      i8042
  8:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   IO-APIC    8-edge      rtc0
  9:    3406692          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   IO-APIC    9-fasteoi   acpi
 12:          0          0          0          0     149358          0          0          0          0          0          0          0          0          0          0          0   IO-APIC   12-edge      i8042
 14:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   IO-APIC   14-fasteoi   INTC1055:00
 16:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   IO-APIC   16-fasteoi   i801_smbus
 27:          0          0     392073          0    1739738          0   78235528          0   10926714          0          0          0          0          0        544          0   IO-APIC   27-fasteoi   idma64.0, i2c_designware.0
 56:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   11701805   IO-APIC   56-fasteoi   ELAN067B:00
120:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0   PCI-MSI 98304-edge      PCIe PME

カーネルスレッドを見る方法。initデーモンはユーザプロセスなので、VSZ列とRSS列が0ではなく、COMMAND列には角括弧がついていない。

ps auxw | head -n 10
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0 170996 16260 ?        Ss   Feb11   3:07 /sbin/init splash
root           2  0.0  0.0      0     0 ?        S    Feb11   0:02 [kthreadd]
root           3  0.0  0.0      0     0 ?        I<   Feb11   0:00 [rcu_gp]
root           4  0.0  0.0      0     0 ?        I<   Feb11   0:00 [rcu_par_gp]
root           5  0.0  0.0      0     0 ?        I<   Feb11   0:00 [netns]
root          10  0.0  0.0      0     0 ?        I<   Feb11   0:00 [mm_percpu_wq]
root          11  0.0  0.0      0     0 ?        S    Feb11   0:00 [rcu_tasks_rude_]
root          12  0.0  0.0      0     0 ?        S    Feb11   0:00 [rcu_tasks_trace]
root          13  0.0  0.0      0     0 ?        S    Feb11   0:27 [ksoftirqd/0]
  • CPUのレジスタの状態がユーザプロセスの実行状態を表している
  • ある時点のCPUレジスタの状態を保存しておくと、あとで任意のタイミングでCPUレジスタを復元してユーザプロセスを再開できる
  • CPUレジスタはスナップショットのようなもので、コンテキストという。マルチタスクの実現のために実行中のユーザプロセスを停止し、別のユーザプロセスを実行することをプロセス切り替えやコンテキストスイッチという
  • プロセス切り替えを行うために仮想メモリという概念が必要になる。ユーザプロセスごとにメモリ領域を持っているが、ひとつのプロセスに閉じているため、他のプロセスのメモリ領域とは独立している
  • ページテーブルは仮想メモリを実現するためのしくみ。プロセス切り替えを行うとき、CPUの管理対象となるメモリ領域の切り替えも必要となるから
  • セグメントは16bitCPU時代によく使われていたしくみ。メモリのアドレス空間が1MBであることに対して、CPUは通常16bitであることから、64KBしかアクセスできないセグメントというしくみを使って64KBを超えるアクセスを行っていた
  • 32bit CPUではセグメントという仕組みを使わずともメモリアクセスが行える。
  • 仮想メモリはメモリアドレスを1バイトずつ処理しない。ページサイズの単位で処理する。アドレス変換もページサイズ単位で行う
  • システムコールforkを使うと新しくユーザプロセスを生成できる。別々のプロセスになり、親子関係ができる
  • システムコールcloneを使うと新しくスレッドを生成できる。メモリ領域をユーザプロセスと共有する
  • 定義に飛ぶタグジャンプ、呼び出し箇所を表示するキーワード検索が便利

読み進めるコツ。

  • 上から下まで通しで読む。流れで全体の動きを俯瞰する
  • キャラクタ型デバイスドライバがおすすめ。/dev/nullとか。/drivers/char/mem.cにある
  • プリプロセッサを活用する。マクロが多いのですべて展開して読む
  • わからなくても気にしない。大事なのは全体の流れを把握すること
  • 図に起こす。図を見て全体を俯瞰する