🐧

tty・i-node・ファイルディスクリプタによる入出力の流れを "echo World > hello.txt" で理解する

2022/06/19に公開

はじめに

ここ最近、 UNIX Linux における標準入出力・ファイル管理の仕組みをすっかり忘れてしまったなと感じることがありました。
そのため、これら入出力で使われる tty i-node ファイルディスクリプタなどを週末にお勉強しました。

知識の定着のために、以下のサンプル入出力がどのように行われるかを辿りながらまとめることで、できるだけ分かりやすくまとめようと思います。

echo World > hello.txt

対象読者

  • 「プロセス」がわかる
  • 「パイプ」がわかる

注意点

間違いが含まれている可能性が高いです。
ぜひ詳しいかたにコメント欄でお教えいただければと思います。

tty


われわれエンジニアはターミナルを利用してコマンドを入力し、出力されたコマンドの結果を文字として得ています。
このとき、各ターミナルには 擬似端末 が紐付けられています。

具体的には、 Linux ではターミナルに /dev/ttys004 のような擬似端末が紐付けられています。
この擬似端末 /dev/ttys004 はスペシャルファイル(別名デバイスファイル)です。
このスペシャルファイルに入出力することで、擬似端末に紐付けられているターミナルに文字を出力できます。
擬似端末も「ファイル」として表現されているため、通常のファイル(hello.txt など)と同様に扱えます。

https://ja.wikipedia.org/wiki/Tty

具体例

ここからは具体例で tty の挙動を説明します。

zenn-ganyariya ディレクトリを表示しているターミナルの擬似端末は /dev/ttys004 です。
擬似端末・スペシャルファイルのパスは tty コマンドで確認できます。

また、同時に他のターミナルも開いてみます。
画像の例では、develop/competitive/go を表示しているターミナルには /dev/ttys0013 が紐付けられています。
このとき、 /dev/ttys0013 のターミナルで echo "Hello, World" > /dev/ttys004 と打ち込みました。
すると、 "Hello, World" が /dev/ttys004 スペシャルファイルにリダイレクトされ、 /dev/ttys004 側に表示されていることが確認できます。
このように、擬似端末を入出力に指定することで、メッセージをやり取りできます。

上記の画像における下側のサンプルは、ファイルディスクリプタに触れたあとで説明します。

ボリューム

i-node を説明するために、ボリュームにも触れたほうがよいため簡単にまとめます。

HDD SDD USB などを Linux で統一的に扱うために、これらの記憶媒体をボリュームとして管理します。
このボリュームは物理ボリュームと論理ボリュームに分けられます。
Linux では論理ボリュームとして、論理的に分割されたブロックに、データ領域や i-node リストなどを保存しています。

hello.txt の実体、つまりテキストバイナリの内容などはデータ領域に保存されます。
このデータ領域を指すために i-node が必要です。

i-node

echo World > hello.txt

上記を実行するためには、 hello.txt の実体、そして管理情報が必要です。
実体はさきほどのボリュームのデータ領域にあります。
この実体を指すために i-node というデータ構造が必要です。

具体的には、 i-node (index-node) は以下のような情報を保持する C 言語の構造体です。

  • i-node 番号
  • ファイルサイズ
  • ユーザ ID / グループ ID
  • データ領域におけるブロック位置
  • ...

i-node は論理ボリュームにおけるデータ領域ブロックデバイスのブロックの位置を保持しています。
これによって、データファイルの実体の位置を探すインデックス(index)として機能し、実体を見つけられます。
データファイルの実体と管理情報が分離されているため、なにかと便利そうですね。

上記の画像では、 i-node 番号 40 の i-node 構造体が hello.txt の実体を指しています。

ls で i オプションを利用することで、 ファイルに紐付いている i-node 番号を調べられます。
上記の例では、README.md ファイルの i-node 番号は 22845133 です。

ちなみに、 i-node ではファイルの名前を管理していません。
上記の README.md の例では、22845133 の ID の i-node に README.md という名前は管理されていません。

名前自体は、ファイルが所属するディレクトリエントリに紐付けられて保存されています。
というのも Linux ではディレクトリ自体もファイルとして表現されているためです。
zenn-ganyariya ディレクトリエントリに、所属するファイルの i-node 番号の列とファイル名の列が保存されています。

ファイルディスクリプタ

i-node がファイルの実体を指す役割を持っていることがわかりました。
あとは、 i-node を用いて hello.txt ファイルの位置を特定し、実際に開けばよさそうです。

echo World > hello.txt

プロセスがファイルを操作するためには、はじめにファイルをオープンする必要があります。
C 言語では、 open 関数を呼び出すと指定されたファイルをオープンできます。
このとき、オープンされたファイルを操作するための識別子として、ファイルディスクリプタが利用されます。

以下の疑似プログラムでは、hello.txt を開くと、そのファイルを指すファイルディスクリプタ 3 が返されています。

// 操作イメージ
int main(void) {
    int fd = open("hello.txt");
    // fd = 3
    printf("%d", fd);
}

ここで、ファイルディスクリプタはプロセスごとに独立しています。
プロセス ID のように OS 全体で共通した通し番号を使うわけではありません。
プロセスごとに 0 1 2 ... と、小さな値から利用されていきます。
ここで、 0 1 2 は特別なファイルディスクリプタであり、標準入力・標準出力・エラー出力です。(ファイルを追加で開くと 3 から振られていきます。)

具体例

ファイルディスクリプタの話が出たため、 tty の話で途中になっていたサンプルについて説明します。

zenn-ganyariya を指すターミナルに紐付いている shell プロセスの ID は 4028 です。
この 4028 プロセスが利用しているファイルの情報は、 Linux において /proc/4028/fd で管理されています。
/proc/4028/fd には、ファイルディスクリプタの整数を名前としてもつファイルが保存されています。
実際に ls proc/4028/fd (fd はファイルディスクリプタの略) を実行すると、 0, 1, 2 というファイルがあります。
これらのファイルはシンボリックリンクであり、 /dev/ttys004 を指しています。
よって 4028 プロセスが標準出力しようとすると、ファイルディスクリプタ 1 が指している /dev/ttys004 に標準出力し、結果としてターミナルに文字が出力されます。

このように、プロセスがファイルを開いたときに、ファイルディスクリプタが返されます。
そして、このファイルディスクリプタを用いて、ファイルへ入出力しています。

ファイルを開く

この説は自分の推測が大きいため間違っている可能性が高いです。
ぜひご指摘いただければと思います。

C 言語で open を実行して得られたファイルディスクリプタで read write するのは非常に工数がかかります。
これは、プログラマ側で I/O 効率が悪くならないようにバッファを用意するなどの工夫が多く必要になるためです。

そこで、 C 言語でファイルを抽象化した FILE 構造体を利用すればよいです。
open のかわりに fopen することで、ファイル構造体へのポインタを得ることができます。
ファイル構造体を利用することで、内部的にバッファを利用でき、効率的な I/O が可能です。

ファイル構造体には以下のような情報が含まれています。
これによって、次のファイルの読み出し位置を理解しています。

  • ファイルディスクリプタ番号
  • アクセスモード
  • 次にファイルを読み出す位置
  • バッファ
  • ...

プロセスごとにオープンしたファイルの情報を保存するテーブルが用意されます。
このテーブルにおいて、ファイルディスクリプタ番号と FILE 構造体のポインタが紐付けられます。

これによって、あるファイルディスクリプタに入出力しようとすると、 FILE 構造体にアクセスし、ファイル構造体が指す次に読むファイル実体の位置を取得できます。

自分の完全なる推測ですが open fopen するときに、内部的に i-node を利用してファイルを開いていると考えています。

まとめ

Linux における tty i-node ファイルディスクリプタの用語と流れを個人的にまとめました。
書いてみるとまだまだ理解が浅いなぁと感じました。
より理解を深めるには自作 OS を作ったり、カーネルのコードを読むのが良いのかなと感じました。

参考にさせていただいたもの

https://milestone-of-se.nesuke.com/sv-basic/linux-basic/fd-stdinout-pipe-redirect/

https://qiita.com/toshihirock/items/22de12f99b5c40365369

https://qiita.com/katsuo5/items/fc57eaa9330d318ee342

http://www.coins.tsukuba.ac.jp/~yas/coins/os2-2011/2012-02-28/

http://web.sfc.keio.ac.jp/~hagino/sa17/02.pdf

https://qiita.com/tajima_taso/items/7e696944cb521958e63d

https://uc2.h2np.net/index.php?title=デバイススペシャルファイル

https://ja.wikipedia.org/wiki/デバイスファイル

https://www.amazon.co.jp/オペレーティングシステム-情報処理入門コース-2-清水-謙多郎/dp/4000078526

GitHubで編集を提案

Discussion