ファイルディスクリプタ(file descriptor, fd)は、C言語でファイルや標準入出力を扱う際に非常に重要な役割を果たします。fdの基本から始め、リダイレクトやプロセス間通信といった応用操作の初歩までを解説していきます。
ファイルディスクリプタ(fd)とは?
ファイルディスクリプタの基本概念
「ファイルディスクリプタはファイルやデバイス(標準入出力など)を操作するための整数値です」というだけではわからないですよね。
以下の書籍で学ぶことをおすすめします。C言語やLinuxに触り始めて数ヶ月の自分でもわかりやすかったです。
標準入力・標準出力・標準エラー(0, 1, 2)の意味
システムは、プログラムが起動する際に自動的に3つのファイルディスクリプタを開きます。
標準入力(fd = 0)
標準出力(fd = 1)
標準エラー(fd = 2)
これらは、プログラムの入出力操作に必須で、特に標準入力と標準出力はリダイレクト処理に頻繁に使用されます。
後述のbashコマンドでpipeを繋いだときにはこの標準入出力が見事にコントロールされています。
fdがOSによって管理される仕組み
ファイルディスクリプタは、OSが管理しており、プログラムがファイルを開くとOSが利用可能なfdを割り当てます。OSはこのfdを通じて、プログラムがどのファイルやデバイスにアクセスしているかを追跡しています。
fdを一つのプログラムでどれだけ使用できるのかは環境に依存します。
以下の記事を参考に、是非調べてみてください。
ファイルディスクリプタの基本操作
open() の使い方
C言語でファイルを開くには、open()システムコールを使用します。以下は、その基本的な使い方です。
#include <fcntl.h>
#include <unistd.h>
int fd = open("example.txt", O_RDWR | O_CREAT, 0644);
if (fd == -1) {
perror("open");
return 1;
}
O_RDWR: 読み書きモード
O_CREAT: ファイルが存在しない場合は新しく作成
0644: ファイルパーミッション
エラーハンドリングはmanを叩いてReturn valueのあたりを読むと良いと思います。
bash$ man 2 open
read() と write() でファイル操作
ファイルディスクリプタを使ってファイルを読み書きするには、read() と write() を使用します。
char buffer[128];
ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
if (bytesRead == -1) {
perror("read");
}
write()も同様に、指定したバイト数をファイルに書き込みます。
close() でファイルディスクリプタを解放
使い終わったファイルディスクリプタは必ずclose()で解放します。これを怠るとfdが枯渇し、他のファイル操作ができなくなります。
close(fd);
ファイルディスクリプタを使ったリダイレクト
標準出力・標準エラーのリダイレクトとは?
リダイレクトとは、プログラムの出力をファイルや別の出力先に変更することです。通常、標準出力(fd 1)はターミナルに表示されますが、これをファイルに書き込むようにリダイレクトできます。
bashの例はこんな感じ。
bash$ echo hello
hello
bash$ echo hello > hello.txt //ターミナルには何も出ず、ファイルの中身にhelloが記載されている
リダイレクト演算子は4種類あります。是非調べて使ってみてください。
# include <unistd.h>
int fd_out = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd_out == -1) {
perror("open");
}
// 標準出力(値は1だが、STDOUT_FILENOというマクロ)をファイルにリダイレクト
dup2(fd_out, STDOUT_FILENO);
これで、printf()などの標準出力がすべてoutput.txtに書き込まれるようになります。
本章はプロセス間通信を学んでいないと有用性が分かりづらいです。
ここまでお読みになり、よくわからなかったり、プロセス間通信をご存知でない方は以下を参考にすると良いと思います。
dup系システムコールでファイルディスクリプタを操作する
dup() と dup2() の使い方と違い
dup()とdup2()は、ファイルディスクリプタを複製します。dup()は使用可能なfdのうち、最も小さい正の整数を返し、dup2()は指定したfdに複製します。これも詳しくはmanを叩いてみてください。
#include <unistd.h>
int new_fd = dup(fd); // fdの複製
dup2(fd_out, STDOUT_FILENO); // fd_outをfd 1 = STDOUT_FILENOに複製(標準出力をリダイレクト)
fcntl() を用いたファイルディスクリプタの制御
fcntl()は、ファイルディスクリプタに対して様々な操作を行うためのシステムコールです。例えば、F_DUPFDを使用してfdを指定範囲で複製することができます。
int new_fd = fcntl(fd, F_DUPFD, 3); // fdを3以上のfdに複製
4.3 pipe() を使ったプロセス間通信の基本
pipe()は、2つのプロセス間でデータをやり取りするための仕組みです。これにより、1つのプロセスが書き込み、もう1つのプロセスが読み取れます。
プロセス間通信は本記事では深堀りしませんので、気になった方は章の頭のリンクや以下のyoutubeプレイリスト(英語)を閲覧してみてください。
※英語ですがプロセス間通信を簡単にコードベースで解説してくださっています。
https://youtube.com/playlist?list=PLK4FY1IoDcHG-jUt93Cl7n7XLQDZ0q7Tv&si=wDalLy_0IJbvuJyJ
int pipefd[2];
pipe(pipefd); // pipefd[0]で読み取り、pipefd[1]で書き込み
エラーハンドリングとファイルディスクリプタの管理
基本的なエラーハンドリング
システムコールは失敗する可能性があるため、常にエラーチェックが必要です。各関数のmanを叩き、errorの場合に何を返すのか理解して使用しましょう。
リソースリークを防ぐ
ファイルディスクリプタは適切に管理しなければリソースリークを引き起こします。プログラムの途中でエラーが発生しても、開いたfdは忘れずにclose()するよう心がけましょう。
まとめ
C言語でのファイルディスクリプタ操作は、シンプルな操作から高度なプロセス間通信まで幅広い応用が可能です。まずは基本操作に慣れ、dup()やpipe()のような応用テクニックを活用して、リダイレクトやプロセス間通信に挑戦してみてください。
ここまでお読み頂きありがとうございました。