Linuxの基礎の復習

Linuxについては何度か本を読んで知識は得ているものの、実際にソースを書いたり読んだりしているわけではないので、プロセスやらストリームのことを聞かれると、とっさに言葉が出てきません(で、「そんなことも知らんのか」と怒られる)。

ということで、ちょっとLinuxの復習メモ。

昔、職場ですごい技術力を持った先輩にお勧めされた本です。以下、自分が忘れがちのところをピックアップしてメモります。

  • Linuxを構成する3つの概念
    • ファイルシステム
    • プロセス
    • ストリーム
  • カーネルのプログラム本体は、例えば以下のような名前になっている
    • vmlinuz
    • vmlinux
    • vmlinux-X.X.X
  • 特定のデバイスを操作するためのソフトウェア部品のことをデバイスドライバという。デバイスが異なれば操作方法も異なるので、カーネル側ではデバイス毎のコードが必要になる。しかし、それ以外の部分は共通なので、デバイスドライバの部分だけは独立して交換可能になっている。(←インタフェースが揃っている?)
  • ハードウェアと直接やり取りできるのは、カーネル(の中のデバイスドライバ)のみに限定されるので、普通のプログラムがハードウェアを操作したときには、カーネルに仕事を頼んで間接的に操作するしかない。この仕事を頼むために使うのが、システムコール。例えば以下がシステムコール。
    • open
    • read
    • fork
    • exec
  • ファイルには以下の種類がある。
    • 普通のファイル
    • ディレクトリ
    • シンボリックリンク
    • デバイスファイル …  デバイス(ハードウェア)をファイルとして表現したもの。
    • 名前付きパイプ
    • UNIXドメインソケット … プロセス間通信で使うファイル。現在ではTCPソケットで代替可能。
  • 「プロセス」とは、簡単に言えば、動作中のプログラムのこと。1つプログラムがあれば、プロセスはいくつでも作れる。
  • 「ストリーム」とは、(この本では)バイトストリームのこと。ファイルディスクリプタで表現され、read()またはwrite()を呼べるもののこと。バイト列が流れる通り道。
  • 例えばプロセスがファイルの内容にアクセスしたいときは、ファイルに繋がるストリームを作ってもらえるよう、カーネルに(システムコールを使って)頼む。バイト列の流れとして考えられるものであれば、どんなものでもストリームに繋ぐことができる。デバイスファイルは、ストリームを得るためのとっかかりとして存在する。
  • 「パイプ」とは、ストリームの両端がプロセスになっているもの。「ネットワーク通信」は、ストリームが別のコンピュータ(の、ファイルやプロセス)にまで延びているもの
  • パイプやネットワーク通信のように、プロセス同士がストリームを通じてデータをやりとりしたり意思の疎通をはかることを、一般にプロセス間通信という。
  • Linuxでは、ハードウェア(端末)をデバイスファイルとして表現することにより、表現されているハードに接続するためのストリームを得ることができる。この仕組みによって、端末はストリームとして抽象化されるので、プロセスからは端末をただのストリームとして扱うことができる。すなわち、「Linuxはファイルシステムとプロセスとストリームでできている」と言える。
  • IPはパケット(=細切れのバイト列)なので、このままではLinuxのストリームとしては扱えない。パケットをまとめて、ストリームとして扱えるようにしてくれているプロトコルが、TCP。
  • シェルからプロセスを起動した場合、以下の3つのプロセスがデフォルト用意されている
    • 標準入力
    • 標準出力
    • 標準エラー出力
  • 標準出力も標準エラー出力も、シェルのプロセスではどちらも端末(モニタへの出力)に繋がっている。標準エラー出力が用意されている理由は、標準出力の方は、パイプによって次のプログラムの標準入力に繋がってしまっていることがよくあるから。端末に繋がっている可能性が高いストリーム(=標準エラー出力)を余分に用意して、プログラムに渡したい出力は標準出力に、人間に読ませた出力は標準エラー出力に出すようにする。
  • FILE型は、stdio(標準入出力ライブラリ)でストリームを指すためのもの。ファイルディスクリプタを使った生のストリーム(システムコールレベル)にバッファ機能を追加する層(ラッパー)。
  • /procディレクトリには、プロセスファイルシステム(procfs)がマウントされている。プロセスファイルシステムとは、プロセスをファイルシステム上に表現する仕掛け。catコマンドで中身を表示したり、シンボリックリンクをたどることで、様々な情報がリアルタイムに得られる。
  • プログラムの起動を実現しているのは、以下の3つのシステムコール。
    • fork
    • exec
    • wait
  • forkを呼び出すと、カーネルはそのプロセスを複製し、2つのプロセスに分裂させる。この時点で、「複製前のプロセス(親プロセス)」と「複製後のプロセス(子プロセス)」はどちらもforkを呼び出した状態になっており、その後両方のプロセスでfork以後のコードが実行される。
  • 子プロセスでのforkの戻り値は0、親プロセスでのforkの戻り値は子プロセスのプロセスIDになる。forkが失敗した場合には、子プロセスは作成されず、親にのみ-1が返る。これを利用して、forkするコードは、forkの後に親プロセス用と子プロセス用の両方の処理を条件分岐で書く。
  • execを呼び出すと、その時点で現在実行しているプログラムが消滅し、自プロセス上に新しいプログラムをロードして実行する。つまり、forkして即座にexecすれば、新しいプログラムを実行したことになる。execは成功すると呼び出しが戻らないので、呼び出しが戻った場合は常に失敗で、-1が返る。
  • wait()は子プロセスのうちどれか1 つが終了するのを待つ。waitpid()は、pidで指定したプロセスが終了するのを待つ。
  • パイプは、プロセスからプロセスに繋がったストリームのこと。ファイルに繋がったストリームと同じように、パイプもファイルディスクリプタを使って表現される。パイプのファイルディスクリプタは、1つのディスクリプタに対しては読むか書くかのどちらかしかできない。
  • pipe()は、両端とも自プロセスに繋がったストリームを作成し、その両端のファイルディスクリプタ2つを返す(fds[0]:読み込み専用、fds[1]:書き込み専用)。単独で実行してもほとんど意味はなく、fork()と組み合わせることで意味が出てくる。forkでプロセスを複製するときには、ストリームもすべて複製される。ここで、親プロセスが読み込み側をcloseし、子プロセスが書き込み側をcloseすると、親から子へのパイプが出来上がる。
  • さらに、パイプの接続元、接続先を特定のファイルディスクリプタにするためには、dup()、もしくはdup2()を使う。例えば、3番に繋がっているパイプを0番に移したいときは、以下のようにする。
    1. close(0)
    2. dup2(3,0)
    3. close(3)
  • initはブート時にカーネルが直接起動するプログラム。すべてのプログラムは、このinitからforkかそれに類するAPIによって生成される。