『ふつうの Linux プログラミング』で 3 本の柱をものにする

『ふつうの Linux プログラミング』 は Linux での C 言語プログラミング入門書です.
3 つのコンセプトを軸に据えた説明が特徴で, すんなり読めて自然に Linux の勘所が分かるような構成です.

3 つのコンセプトとは

  • ストリーム
  • ファイルシステム
  • プロセス

のことです. これらを中心に据えて Linux の仕組みを学びます.

タイトルにある「ふつうの」は標準的な方法を学ぶという意図を表しています. Linux の中心的な仕組みを知ることができるだけでなく, catgrep など身近なコマンドを実装するなどの実例も豊富なので, 「API は分かったけど結局どう使うのか分からない」といったことがありません. 実践しながら Linux の仕組みを学びたい方におすすめです.

本書を読んでいる間に書いたソースコードは momorin256/note-standard-linux-programing にあります. 良ければご参照ください.

3 つのコンセプト

まずは 3 つのコンセプトについての説明です.

  • データを保存する場所である「ファイルシステム」
  • コンピュータ上での活動の主体である「プロセス」
  • プロセスがファイルや他のプロセスとデータをやり取りするための「ストリーム」

ストリームは本書独自の用語のようですが, バイト列の流れのことです.

この 3 つのコンセプトが念頭にあれば Linux の様々なものを整理して理解することができます. 以下のような説明してみます.

  • リダイレクト: プロセスの標準入出力ストリームの先をファイルに変える機能
  • パイプ: プロセスとプロセスを繋ぐストリーム
  • シグナル: カーネルからプロセスへのストリーム (のようなもの)
  • パーミッション: プロセスからファイルへのアクセスを制限する仕組み

確かに Linux についての多くのものが整然と理解できる気がするので, この 3 つのコンセプトに着目して Linux を学ぶのは優れたアプローチだと思いました.

ストリーム

ストリームについての主なシステムコール (=カーネルに実行してもらう関数) は 4 つだけです.

  • open
  • close
  • read
  • write

ストリームを開いて読み書きして閉じるという一連の流れがシンプルに表現されていて美しいです.
とはいえ, やはり標準から外れるものもあります. プリンタや DVD ドライイブなどのデバイス操作や, ファイルのロック, 読み書きモードの操作などは ioctlfcntl にまとめられています. 抽象化の枠に収まらない部分はどうしても出てくるよなぁと思って設計者に勝手に感情移入してしまいました. 例外的なものは潔く「その他」としてまとめてしまうのもありなのかもしれません.

かつてはその他の操作が ioctl にまとめられていたところ, せめてファイル関連の操作だけでも分離しようとして fcntl が生まれたそうです. 個人的にこういう歴史小話は興味を惹かれることが多いです. 現在の状況について調べるのは比較的簡単ですが, なぜ現在の形になったのかという疑問は調べるのが難しいと思っています.

主なシステムコール 4 つを挙げましたが, ファイルの入出力でこれらのシステムコールを直接扱うことは少ないと思います. write ではなく printf を使いますよね.
C 言語にはストリーム操作の API をラップした stdio というライブラリが用意されています.
私はこれまで stdio がどういう役割を持ったライブラリなのかきちんと理解していませんでした. printf などのようにフォーマットするためのものなのかなくらいに思っていましたが, 重要な役割はバッファリングです.
一般的にシステムコールは遅いので, 例えば 10byte ずつ 100 回 read するより 1000byte まとめて 1 回で read するほうが効率的です. 読み書きのデータをバッファにためておいて, ある程度の量まとめてシステムコールを呼ぶテクニックをファイルバッファリングといいます.
stdio はバッファリングを提供するライブラリなので, fflush などのバッファを操作するための関数があります. stdio はシステムコールをラップするライブラリなので, 生のファイルディスクリプタと stdio で扱う FILE とを相互変換する関数があります. というように, 役割を知ればこれらの関数が用意されているのは当たり前ですし, 知らなくても用意されているだろうと予想が付くかもしれません.

バッファリングに関連して標準エラー出力の意味も知りました.
Linux のプログラムは基本的に入力を受け取り, 加工して出力するものが一般的ですが, デフォルトの入出力先のことを標準入力・標準出力といいます. これとは別に標準エラー出力というのもあります. 名前の通り, エラーのように人間に読ませたいメッセージの出力先として利用されます.
なぜ標準エラー出力が存在するのか理解していませんでしたが, 以下のような役割があるということで標準出力との違いが把握できました.

  • パイプではなく端末につながっている可能性が高い出力先を用意しておく
  • バッファリングしない. 標準出力はバッファリングされて, 実際に端末に表示されるまで時間差があることがある.

ファイルシステム

ディレクトリ読み取りは opendir / closedir / readdir で, ストリーム操作の API と似ています. その他にディレクトリの作成/削除/移動/リネーム/メタデータ取得など, 使用頻度の高そうなものは一通り紹介されています.

ファイルシステムについての説明ではっとする思いがしたのはリンクについてです.
普段ファイルだと思っているものは「名前」「データの実態」「名前とデータの結びつき=リンク」の 3 つに分解できるという話です. 今までハードリンクとシンボリックリンクの違いについて曖昧な認識しか持っていなかったのですがすっきりと理解できました.

  • ハードリンク: データ実態に対して新たに名前を付ける
  • シンボリックリンク: 名前に対する名前を付ける

以下のようなイメージです.

ハードリンク: 一つのデータを A または B という 2 つの名前で参照できる.

A ---> データ実態 <--- B

シンボリックリンク: 名前を指し示す名前を付ける.

データ実態 <--- A <--- B <--- C

ファイルの作成/削除のシステムコールが link/unlink という名前なのをこれまで奇妙に思っていたのですが, 「ファイルを作成する」のではなく「リンクを作成する」のだと考えると納得です. ファイル作成/削除はあくまでもリンク作成/削除の例外的なケースであり, リンクを作成してもし実態がまだ存在しないなら実態を新たに作成し, リンクを削除してもしリンク数が 0 になったら実態も削除するということですね.

プロセス

主なシステムコールは 4 つです.

  • fork
  • exec
  • wait
  • exit

その他, プロセス間通信に使う pipe, ファイルディスクリプタの操作に使う dup/dup2 も紹介されています.
練習としてシェルを作るのが良い勉強になりました. 以前にもこれらのシステムコールを使って簡単なシェルを作ったことがある のですが, その時はパイプの処理などに甘い部分があったのでリベンジの気持ちで取り組みました. 例として以下のようなコマンドを実行できるようなものを作りました.

$> cat < sample/in.txt | head -n 4 | tail -n 2 > sample/out.txt
$> cd /var/tmp

cdexit など which で場所を調べると cd: shell built-in command のように表示されるものがあることは認識していたのですが, シェル組み込みのコマンドあるということが何を意味するのかいまいち理解していませんでした. しかし本書を読んで, カレントディレクトリはプロセスが持つ情報なのでシェル自身のコマンドとして実装しなければならないのだと腑に落ちました.
シェルというと特別なもののように感じていましたが, 他のプログラムと同じく普通の Linux の機能を用いて実装されているのだと実感できて満足です.

ネットワークプログラミング

最後の章では HTTP サーバを作ります. 本書の内容を一通り実践できるので締めに丁度よい題材だと思います.
個人的にソケットや TCP には少し触れたことがあったのでそれほど新鮮味はありませんでしたが, 単純にブラウザや curl からのリクエストを処理できると楽しいですね.

特に勉強になったのは子プロセスのゾンビ問題への対処法です.
子プロセスが親プロセスから wait されるまで, カーネルは子プロセスのステータスコードを保持し続ける必要があります. 親プロセスから wait されず, カーネルがいつまで経ってもステータスコードを破棄できない状態になったプロセスのことをゾンビプロセスと呼びます. プロセス数が増えるとカーネルの負担が増えるためゾンビプロセスが生まれないようにしたい訳ですが, 以下のような対処法があります.

  1. fork したら忘れずに wait する
  2. ダブル fork: 途中で余計に fork を挟むことでプロセスの親がいない状態にするテクニック
  3. sigaction で子プロセスを wait しないことをカーネルに伝える

HTTP サーバの場合, 1 つのリクエストを処理している間にも並行して他の処理を行うためにマルチプロセス化 (またはマルチスレッド化) すると効率的です. fork を使うにしても, 親プロセスで wait しないといけないならせっかく並列化した意味がなく, どうすればよいのだろうと思っていました.
シグナルを使って子プロセスを wait しないことをカーネルに伝えると, カーネルがプロセスをゾンビにせずに始末してくれます. または子プロセスが終了したシグナルを受け取ってコールバックで処理をするのも良いですね.

結語

3 つのコンセプトを中心に Linux を概観するというアプローチは非常に理にかなっていると思いました.
説明の軸がぶれずに一貫していますし, 話の流れがスムーズで理解しやすかったです. 詳細には立ち入らなかったり, 難しい部分はなんとなくの理解で良いと注釈をつけたり, すんなりと読み進められるテンポの良さは筆者の説明のバランス感覚の賜物だと思いました. 「概念の説明」→「具体的な API の紹介」→「実践」という流れも私の好みでした.

今後さらに学びを深めるとしたら以下のような勉強ができるかなと思っています.

  • 並行プログラミング (マルチスレッド, マルチプロセス, IO 多重化などのトピック)
  • Linux カーネルの実装について
  • chroot や名前空間などの機能 (Docker のようなコンテナがこういった機能を用いて実現されているらしい)
  • シェルプログラミング (シェルスクリプトや sed/awk などのコマンド)

即物的かもしれませんが, シェルスクリプトや awk は一度しっかり勉強したいと思っているので Linux 関連だと次はそういった方面に取り組もうかなと思います.