基本的な UNIX の I/O システムモデルは、ランダムアクセスおよび シーケンシャルアクセスの可能なバイト列です。 通常の UNIX ユーザープロセスには、 アクセスメソッド や コントロールブロック は存在しません。
I/O にさまざまなレベルの構造を期待するプログラムは各種ありますが、 カーネルは I/O に構造を課しません。 たとえば、テキストファイルは改行文字 (ASCII LF 文字) で区切られた ASCII 文字の行の集まりですが、 カーネルはそのような構造を関知しません。 ほとんどのプログラムにとって、 このモデルはデータバイトのストリームもしくは I/O ストリーム にすぎません。 このような単一のデータ構造が、UNIX のツールベースのアプローチ (tool-based approach) を可能にしているのですKernighan & Pike, 1984。 あるプログラムの出力ストリームは、他のほとんどのプログラムの入力 ストリームとしてそのまま与える事ができます (このような伝統的な UNIX の I/O ストリームを、Eighth Edition のストリーム I/O システムや、 System V Release 3 の STREAMS と混同すべきではありませんが、 どちらのストリームも伝統的な I/O ストリームと同じようにアクセスすることが可能です)。
UNIX のプロセスは、I/O ストリームを参照するのに 記述子(descriptor)を使用します。 記述子は open または socket システムコールにより取得される符号無しの小さな整数です。 openシステムコールは、 引数にファイル名および許可モードをとり、 それぞれ開くファイルおよび、モード (読み込み、書き込みまたは読み書き) を指定します。 open システムコールは、新しい空のファイルの作成にも使用できます。 readおよび writeシステムコールを記述子に対して使用し、 データの転送を行います。 closeシステムコールは、任意の記述子を開放します。
記述子は、カーネルでサポートされるオブジェクトを表します。 4.4BSD では、ファイル、パイプ、ソケットの 3 つのオブジェクトを 表すことができます。
ファイルは、少なくとも 1 個の名前を持つバイト列です。 ファイルは、すべての名前を明示的に削除し、 その記述子を持つすべてのプロセスが消滅するまで存在します。 プロセスは、open システムコールにより、 指定されたファイル名を持つファイルのファイル記述子を取得します。 I/O デバイスはファイルとしてアクセスされます。
パイプとは、 ファイルと同じくバイト列ですが I/O ストリームとしてのみ使われ、単一方向にのみ使われます。 パイプには名前がないので、open システムコールでは開くことができません。 パイプを開くには、pipe システムコールを使用します。 pipeシステムコールは 2 つの記述子を返します。 ひとつの記述子に入力されたデータは、 もう一方の記述子にそのまま順序を変えずに出力されます。 名前付きパイプ (FIFO) も使用できます。 名前があるのでファイルシステム上に配置され、 open システムコールでアクセスできる以外は、 パイプと同一の機能を持ちます。 FIFO を使用してプロセス間通信を行いたい場合は、 片方のプロセスが FIFO を書き込み用に開き、 もう片方では読み込み用に開きます。
ソケットは、 プロセス間通信のために使用されるオブジェクトで、 ソケットを参照する記述子を持つプロセスが存在する間のみ存在します。 ソケットは socket システムコールで作成します。 socket システムコールは、 作成したソケットの記述子を返します。 さまざまな通信方法を実現するために、 各種のソケットがあります。 たとえば、信頼性の高いデータ転送を目的としたソケット、 メッセージの順番を保持するソケット、 メッセージの境界を保護するソケットなどがあります。
4.2BSD でソケットが導入されるまで、 パイプはファイルシステムを用いて実装されていました。 4.2BSD 以降では、ソケットを使用して実装されています。
カーネルはそれぞれのプロセスの記述子テーブルを保持しており、 記述子の外部表現を内部表現に変換するために用いられます (記述子そのものはこのテーブルへのインデックス値にすぎません)。 記述子テーブルは、親プロセスから子プロセスに継承されます。 そのため、記述子の参照先も同じく継承されます。 記述子を得るためには、オブジェクトを開いたり、 作成したりする以外に、 このような親プロセスからの継承による方法があります。 また IPC ソケットを使用すれば、 同一マシン上で動作している無関係なプロセス間で、 記述子のやりとりが可能です。
すべての有効な記述子は、 オブジェクトの先頭からの位置を ファイルオフセット としてバイト単位で保持しています。 読み込みおよび書き込み動作は、 このオフセット位置から行われ、 データが転送される毎にオフセットの位置は更新されます。 ランダムアクセスを許可しているオブジェクトの場合、 ファイルオフセットは、lseek システムコールを利用して移動することもできます。 通常のファイルやある種のデバイスはランダムアクセス可能です。 パイプ、ソケットはランダムアクセスできません。
プロセスが終了すると、 カーネルはそのプロセスに使用されていたすべての識別子を回収します。 プロセスがオブジェクトへの参照を保持したまま終了した場合は、 オブジェクトマネージャに通知し、ファイルの削除、 ソケットの開放などの必要なクリーンアップを行わせます。
ほとんどの場合、プロセスが起動されると、 3 つの記述子がすでに開かれています。 それらの記述子は、0、1、2 で、それぞれ一般的には、 標準入力、 標準出力、 標準エラー出力 として知られています。 通常これらの識別子は、 ログインプロセスによりユーザの端末に割り当てられています (14.6 節参照)。すなわち、キーボードからの入力を標準入力として受け取り、 標準出力への出力は端末の画面に表示されます。 標準エラー出力もエラー出力用に書き込み用に開かれていますが、 通常の出力には標準出力が利用されます。
これらの記述子を (他の記述子も) 端末以外のオブジェクトに割り当てることも可能です。 このような割り当てを、 I/O リダイレクトと呼びます。 すべての標準シェルでは、ユーザが I/O リダイレクトを行うことができます。 記述子 1 (標準出力) を閉じ、 指定したファイルを記述子 1 として開くことで、 シェルは出力をファイルに送ることができます。 同様に、記述子 0 を閉じ、 指定したファイルを開くことで、 ファイルから標準入力を受け取るようにできます。
パイプは、プログラムの変更をまったく行わず (再リンクも必要ありません)、あるプログラムの出力を 他のプログラムに入力することを可能にします。 出力側のプログラムの記述子 1 (標準出力) は、端末出力の代わりにパイプの入力記述子に割り当てられます。 同様に入力側のプログラムの記述子 0 (標準入力) は、 端末からのキーボード入力ではなくパイプの出力記述子に割り当てられます。
open、 pipe、 socket システムコールは、新しい記述子を生成し、 使用できる最も小さい番号を割り当てます。 パイプを動作させるためには、そのように生成された記述子を 0 や 1 にマップする仕組みが必要になります。 dup システムコールは、 同一のファイルテーブルエントリを指す記述子のコピーを作成します。 新しい記述子も同じく使用可能な最小の番号が使われるため、 dupシステムコールを使用して、 必要なマップを行えます。 ただ、記述子 1 が必要な場合でも、記述子 0 が既に閉じられていると、 記述子 0 が割り当てられてしまいますので注意が必要です。 この問題を避けるため、 dup2 システムコールがあります。 dup に引数が 1 つ追加され、 割り当てたい記述子の番号を指定することができます (もし、指定された番号の記述子が使用中の場合、 dup2 は、まずその記述子を閉じたのち、 再割り当てします)。
ハードウェアデバイスはファイル名を持ち、通常のファイルと 同一のシステムコールでアクセスできます。カーネルは、 デバイス特殊ファイル や 特殊ファイルを区別し、 参照しているデバイスを特定できますが、 ほとんどのプロセスにとって、このような区別は必要ありません。 端末、プリンタ、テープデバイスは、4.4BSD のディスクファイルと同様、 バイト列としてアクセスされます。そのため、デバイス依存部分や特殊部分は、 可能な限りカーネルに隠蔽され、さらにカーネル内でも、 それらの大部分がデバイスドライバ内に分離されています。
ハードウェアデバイスは、 構造を持つデバイスと 構造を持たないデバイスに分けられます。 それぞれ、 ブロックデバイス、 キャラクタデバイスと呼ばれます。 それらのデバイスファイルへのアクセスは、カーネル内の デバイスドライバ と呼ばれるソフトウェアモジュールによって処理されます。 ほとんどのネットワーク通信ハードウェアデバイスは、 ファイルシステム上に特殊ファイルを持たず、 プロセス間通信機能によってのみアクセスできます。 それは、raw-socketの方が特殊ファイルより、 より自然なインタフェースを提供できるためです。
典型的なブロックデバイス (構造を持つデバイス) としては、 ディスク、磁気テープがあげられますが、 ほとんどのランダムアクセスデバイスがそれに該当します。 カーネルは、読み込み-変更-書き込みに対してバッファリングを提供し、 通常ファイルと同様の、 完全なバイトアドレス指定のランダムアクセスを提供します。 ファイルシステムは、ブロックデバイス上に構築されます。
構造を持たないデバイスは、 ブロック構造をサポートしないデバイスで、通信線、ラスタプロッタ、 バッファのない磁気ディスクやテープなどです。 構造を持たないデバイスは通常、 大容量のブロック I/O 転送をサポートします。
構造を持たないファイルはキャラクタデバイスと呼ばれます。 これは最初に実装されたこの種類のデバイスが、 端末デバイスドライバだったからです。 このようなデバイスに対するカーネルのインタフェースは、 他のブロック構造を持たないデバイスに対しても有用であることが証明されました。
デバイス特殊ファイルは、 mknodシステムコールにより作成されます。 ioctlシステムコールは、 特殊ファイルに対応するデバイスのパラメータを操作するのに使われます。 このシステムコールは、他のシステムコールに新たな機能を追加せずに、 デバイスの特殊な機能を操作することを可能にします。 たとえば、ioctlを使用して、 終了マークをテープデバイスに書き込むことができます。 write に変更を加えたり、 特殊なバージョンを用意する必要はありません。
4.2BSD カーネルはソケットを利用して、パイプより柔軟な IPC 機能を導入しました。ソケットは、ファイルやパイプと同様、 記述子により参照される、通信の末端点です。 2 つのプロセスがそれぞれ、ソケットを作成して接続することにより、 信頼性の高いバイトストリームを作成できます。 接続されれば、それぞれのプロセスは、パイプと同じように、 読み込み書き込みをソケットに対して行えます。 ソケットの透明性により、カーネルはプロセスの出力を、 別のマシン上のプロセスの入力に送ることも可能です。 パイプとソケットの大きな違いは、 パイプは共通の親プロセスが設定する必要があるのに対して、 ソケットはまったく無関係の (異なるマシン上で動作する) プロセス間でも使用できる点です。
System V は、FIFO もしくは名前付きパイプと呼ばれる ローカルプロセス間通信の仕組みを備えています。 FIFO はファイルシステム上のオブジェクトとして現われ、 パイプと同様な方法でオープンし、データを送ることができます。 そのため、FIFO は共通の親プロセスによって設定される必要はなく、 プロセス同士が起動し動作開始してから接続することが可能です。 しかしソケットとは異なり、 異なるマシン上で動作するプロセスに対しては使用できません。 4.4BSD で、FIFO が実装されているのは、 POSIX.1 標準に準拠するためのみです。 FIFO の機能は、ソケットの機能の一部になっています。
ソケット機構を実現するには、 伝統的な UNIX の I/O システムコールに名前付けや接続機能を追加する必要がありました。 開発者は、既存のインタフェースへの拡張は既存のシステムコールが変更なしに使用できる範囲にとどめ、 追加機能を扱う新しいインタフェースを設計しました。 バイトストリーム型の接続の読み込み書き込みを行う readと write システムコールに加え、 ネットワークダイアグラムのような宛名付きメッセージを読み込むため、 新たに 6 つのシステムコールが追加されました。 メッセージ書き込み用の send、 sendto、 sendmsg システムコールと、 メッセージの読み込み用の recv、 recvfrom、 recvmsg システムコールです。 考え直して見ると、 それぞれの読み書き用のシステムコールのうち最初の 2 つは次のシステムコールの特殊な場合であるので、 recvfrom と sendto システムコールは、それぞれ recvmsg と sendmsg のライブラリインタフェースとし て追加すべきだったかも知れません。
既存の read および write システムコールに加え、 4.2BSD で scatter/gather I/O 機能が導入されました。 scatter 入力は readv システムコールによって行われ、 複数の異なるバッファに対して単一の読み込みを実行できます。 逆に writev システムコールは、複数の異なるバッファに対してアトミックな書き込みを実行できます。 read や write によって行われるように、 単一のバッファと長さをパラメータとして渡す代わりに、 バッファと長さの配列へのポインタとそのサイズを渡します。
この機能により、 プロセスアドレス空間の異なる場所にあるバッファに対してアトミックな単一の書き込みを行え、 隣接するバッファにコピーする必要もありません。 テープデバイスのように、それぞれの要求に対し、 テープブロックを出力をする必要があるようなレコードベースのデバイスを抽象化した場合、 アトミックな書き込みが必要になります。 また、単一の読み込みリクエストで複数のバッファに読み込めるのは非常に便利です (たとえばレコードヘッダとデータをそれぞれ別のバッファに読み込む場合など)。 もちろん単一の大きなバッファにデータを読み込み、 読み込んだデータを必要な場所に移動することで scatter 動作をシミュレートすることは可能です。 ただし、このようなメモリ間のコピーのコストは、 アプリケーションの動作に必要な時間を 2 倍以上にしてしまうことも良くあります。
send と recv がそれぞれ、 sendto と recvfrom のライブラリインタフェースとして実装可能であったのと同じく、 read と write をそれぞれ、 readv と writev のライブラリインタフェースとして実装も可能であったでしょう。 しかし、 read と write はより頻繁に使われるため、 シミュレートするための追加コストを考えると ライブラリインタフェースとしての実装は割に合わなかったでしょう。
ネットワークコンピューティングの発達により、 ローカルおよびリモートファイルシステムへの対応が望まれるようになりました。 複数のファイルシステムのサポートを簡単にするために、 開発者は vnode インタフェースをカーネルに追加しました。 vnode インタフェースから提供される操作は、 以前にローカルファイルシステムでサポートされていたファイルシステム操作とほぼ同じですが、 幅広いファイルシステムにより使用ができるようになっています。
ローカルのディスクファイルシステム
各種リモートファイルシステムプロトコルによりインポートされたファイル
読み込み専用 CD-ROM ファイルシステム
特殊機能を提供するファイルシステム。 たとえば /proc ファイルシステムなど
4.4BSD 由来の OS の中には FreeBSD のように、 mount でファイルシステムが初めて参照された時にファイルシステムを動的に読み込むことが できるものもあります。 vnode インタフェースについては 6.5 節、 補助サポートルーチンについては 6.6 節、 特殊機能ファイルシステムについては 6.7 節に記載されています。
本文書、および他の文書は ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/ からダウンロードできます。
FreeBSD に関する質問がある場合には、ドキュメント を読んだ上で <questions@FreeBSD.org> まで (英語で)
連絡してください。
本文書に関する質問については、<doc@FreeBSD.org> まで電子メールを (英語で)
送ってください。