dupによるリダイレクトの実装
dup2関数は、ファイルディスクリプタのコピーを行います。
int dup2(int fd1, int fd2)
dup2()に指定するfd1をfd2にコピーします。fd2は、現在開いているファイルからfd1が開いているファイルを示すように置き換えられます。
例えば、あるファイルをopen()で書き込みで開いたときに返されたファイルディスクリプタが「8」だった場合、
dup2(8, 1)
を行うと、8で開いたファイルは1でも同時に開いている状態になります。1は「標準出力」を示すディスクリプタで、デフォルトでは端末画面に文字を表示するものですが、その向かう先が8のファイルへ置き換えられたようになります。これがシェルのコマンドラインで行うリダイレクトの働きとなります。
dup2()はコピーされる側(fd2)が現在開いているファイルを一旦閉じてからfd1に置き換えます。
dup2()の元であるdup()は、この「一旦閉じる」という処理を行わないためdup()とclose()を組み合わせて実装する必要があります。そのため、別のファイルが割り込んで開いた場合などの面倒な考慮が必要なのでほとんど使われていません。
dup2( )による標準出力のファイルへのリダイレクト書き込み
次の例は、端末画面に表示する標準出力を、書き込みで開いたファイル「test.txt」へリダイレクトで書き込みます。この実行結果は、printf()のところで画面に「ABCDEFG」が表示されずに、test.txtに書き込まれます。
dup_stdout.c
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h> // dup2()
#include <stdio.h>
int main(int argc, char *argv[])
{
int fd;
fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, 1); // 1(標準出力)はfdと同じファイルを開くことに
close(fd); // fdを閉じても1がtest.txtを開いていることになる
printf("ABCDEFG"); // 画面表示ではなくtest.txtに書かれる
return 0;
}
$ ./dup_stdout
$ cat test.txt
ABCDEFG
dup2(fd,1)により、端末表示する標準出力「1」が一旦閉じて、fdが示すtest.txtへの書き込みで開き直します。printf()はいつもどおりファイルディスクリプタ「1」の標準出力へ文字列を出力しますが、文字列は端末画面へは向かわずディスク上のtest.txtへ向かいます。
test.txtへの書き込みは標準出力の「1」になったので、fdは不要となりdup2()の後にclose()で閉じています。
echoコマンドをファイルにリダイレクト
次の例は、exec()関数を使って標準出力へ出力するコマンドをファイルtest.txtにリダイレクトしています。これは、
$ echo ABCDEFG > test.txt
と同じことを行っています。プロセスはexeclp()により途中からechoコマンドの実行に置き換わります。echoに置き換わってもプロセスはそのままファイルディスクリプタの状態を引き継いでいるので、echoコマンドの標準出力はtest.txtへの書き込みになります。
dup_echo.c
int main(int argc, char *argv[])
{
int fd;
fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, 1);
close(fd);
execlp("echo", "echo", "ABCDEFG", NULL);
}
$ ./dup_echo
$ cat text.txt
ABCDEFG
標準入力をファイルからの入力にリダイレクト
次の例は、Googleについての説明が書かれたテキストファイル「google.txt」を読み出しで開き、そのfdを標準入力「0」にコピーすることで、標準入力をキーボード入力からファイルの読み出しに置き換えます。execlp()によりプロセスは途中からgrepコマンドの実行に入れ替わり、標準入力元となったgoogle.txtからテキストを読み出し文字列“Google”の検索を処理します。
dup_grep.c
int main(int argc, char *argv[])
{
int fd;
fd = open("google.txt", O_RDONLY);
dup2(fd, 0);
close(fd);
execlp("grep", "grep", "Google", NULL);
}
google.txt
Google was founded on September 4, 1998,
by Larry Page and Sergey Brin while they were PhD students
at Stanford University in California.
Together they own about 14% of its publicly listed shares
and control 56% of the stockholder voting power through super-voting stock.
Google was reorganized as a wholly owned subsidiary of Alphabet Inc.
$ ./dup_grep
Google was founded on September 4, 1998,
Google was reorganized as a wholly owned subsidiary of Alphabet Inc.
標準エラー出力のリダイレクト
標準エラー出力は、通常は標準出力と同じ端末画面への表示となります。次の例は、catコマンドでzzz.txtというファイルの内容をdup2によりtest.txtへリダイレクト出力し、またzzz.txtファイルが存在しないなどのエラーの場合のエラーメッセージもtest.txtへ出力します。標準出力0とともに標準エラー出力「2」もdup2()でfdに置き換えることで、標準エラー出力もtest.txtへ書き込まれます。
dup_stderr.c
int main(int argc, char *argv[])
{
int fd;
fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
execlp("cat", "cat", "zzz.txt", NULL);
return 0;
}
$ ./dup_stderr
$ cat test.txt
cat: zzz.txt: そのようなファイルやディレクトリはありません
パイプ・名前付きパイプによるプロセス間通信(IPC)
パイプ(匿名パイプ)
pipe()関数はプロセス間通信の「パイプ」を生成し、読み出しと書き込みの2つのファイルディスクリプタを配列引数に返します。
int pipe(int fd[2])
fd[0] = 読み出し側が返る
fd[1] = 書き込み側が返る
pipe()が生成するパイプは、書き込み口と読み出し口のある一方通行の筒のようなバッファで、その両端で別々のプロセス間でファイルとしてデータの受け渡しができます。バッファサイズはOSにより決定されます(Linuxのは64KBバイト)。
pipe()が生成するパイプが存在するのはカーネル内でプロセスの実行中のみで「匿名パイプ」とも呼ばれます。それに対し後述のmkfifo()で生成する「名前付きパイプ(FIFO)」は、ファイルシステム上に恒久的に存在し続けます。
パイプにより得られる出入り口のファイルディスクリプタは、そのプロセスからfork()で生成した子プロセスにも引き継がれるので、それらのプロセス間でパイプを通してデータ通信ができます。パイプはカーネル内で作られるので、シェルから実行する別々のコマンド同士で使うことはできません。
次の例は、パイプを生成し、書き込み側のファイルディスクリプタ(fd[1])からデータを書き込んで、読み出し側(fd[0])から読み出しています。意味のない例ですが、このようにパイプの一方から入れたデータを他方から引き出せるだけの単純なものだということがわかります。
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int fd[2];
char buf[32];
pipe(fd);
write(fd[1], "ABCDEFG", 7);
read(fd[0], buf, sizeof(buf));
puts(buf);
close(fd[0]);
close(fd[1]);
return 0;
}
ABCDEFG
forkによる子プロセスとのパイプ通信
fork()により生成した子プロセスは、親プロセスのファイルディスクリプタを引き継ぐので、pipe()が生成するファイルディスクリプタも親プロセスと子プロセスの両方が持つことになります。分離したプロセス間でパイプの両端のディスクリプタを通してデータの受け渡しができます。
次の例は、パイプを通して親プロセスからfork()で分離した子プロセスへデータを送っています。
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int fd[2];
int i, n;
char buf[3];
pipe(fd);
if (fork() == 0) {
/* child proc */
close(fd[1]);
while ((n = read(fd[0], buf, sizeof(buf))) > 0) {
buf[n] = 0;
puts(buf);
}
close(fd[0]);
} else {
close(fd[0]);
for (i = 0; i < 3; i++)
write(fd[1], "ABCDEFG", 7);
close(fd[1]);
wait(NULL);
}
return 0;
}
子プロセス側はデータを送られる側なので、fork()後に使わない書き込み側のfd[1]を閉じます。同様に親プロセス側も使わない読み出し側を閉じています。
この例は、送信側が一度に書き込む文字数よりも、受信側が一度に読み出せるバッファサイズをわざと小さくしています。その結果は下のようになります。書き込み側がパイプのバッファに置いたものを少しづつ読み出す様子がわかります。
ABC
DEF
GAB
CDE
FGA
BCD
EFG
パイプとdup2を使った子プロセスのリダイレクト
fork()でプロセスを生成しexec()でコマンド等を実行するとき、子プロセスが引き継ぐパイプのファイルディスクリプタをdup2()を使って標準出力や標準入力へ置き換えると、子プロセスとして実行させるコマンドのリダイレクトが可能になります。
次の例は、fork()でプロセスを生成し、exec()でwcコマンドを実行します。このとき子プロセスwcが使うパイプの読み出し側をdup2()で標準入力にコピーします。それにより親プロセスからパイプで送るデータが、wcコマンドの標準入力にリダイレクトされます。
pipe_wc
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int fd[2];
int i, n;
char buf[128];
pipe(fd);
if (fork() == 0) {
/* child proc */
close(fd[1]);
dup2(fd[0], 0);
execlp("wc", "wc", NULL);
close(fd[0]);
} else {
close(fd[0]);
write(fd[1], argv[1], sizeof(buf));
close(fd[1]);
wait(NULL);
}
return 0;
}
$ ./pipe_wc "AB 12 CD 34"
0 4 128
popenによるコマンドのリダイレクト
popen()は、上の例のようなfork()、exec()、dup()を組み合わせてコマンドの標準入出力を親プロセスがリダイレクトで送る、または受け取る処理を一括で行うことができます。
次の例は、catコマンドを実行した結果の標準出力をファイルストリーム(FILE)にリダイレクトして読み出しています。簡単に言えば、catの画面表示結果を文字列に取り込むことができるのです。
#include <stdio.h>
int main(int argc, char *argv[])
{
FILE *fp;
char buf[128];
if ((fp = popen("cat /etc/hosts", "r")) != NULL) {
fgets(buf, sizeof(buf), fp);
printf("/etc/hosts: %s", buf);
pclose(fp);
}
return 0;
}
/etc/hosts: 127.0.0.1 localhost
popen()はfopen()に似ています。popen()にはファイルのパスではなく起動するコマンドラインを指定し、読み出しオープン「r」ならばコマンドの標準出力を取り出すことができ、「w」ならばコマンドの標準入力へデータを送ることができます。fclose()相当するpclose()でファイルストリームを閉じます。
次の例では、fpからwcコマンドへ標準入力へ書き込んでいます。wcコマンドの結果は端末画面に表示されます。
#include <stdio.h>
int main(int argc, char *argv[])
{
FILE *fp;
if ((fp = popen("wc", "w")) != NULL) {
fprintf(fp, "12 ab cd 34");
pclose(fp);
}
return 0;
}
0 4 11
名前付きパイプ(FIFO)
mkfifo()は「名前付きパイプ」を生成します。名前付きパイプを「FIFO」と呼ぶこともあります。pipe()が生成するパイプはプロセスの実行中のみ有効ですが、名前付きパイプはファイルシステム上にパイプファイルとして永続的に存続します。それにより、fork()から生成した子プロセスに限らないパイプファイルにアクセスできるプロセス間の通信が可能になります。
mkfifo()はパイプを作成するパス名と、8進数のアクセスモード(0644のような)を指定します。
int mkfifo(const char *path, mode_t mode)
名前付きパイプのファイルは、通常のファイルと同様に開いて読み書きできます。通常のファイルでもプロセス間で共有して書き込んだものを読み出してデータの受け渡しはできますが、名前付きパイプのファイルには通常のファイルのようにディスク上にデータが保存されるのではなく、OSの中にバッファが確保されます。保持できるバッファサイズはpipe()と同様に有限(OSで定められる)です。
名前付きパイプは、通常は読み出し側と書き込み側の両方で開いて使います。そしてFIFOと呼ばれるように、最初に書き込んだデータから順に読み出されます。
次の例は、fifo_rとfifo_wのプロセスがfifo_aというパイプファイルを通してプロセス間通信を行います。最初にfifo_rプロセスがパイプfifo_aをカレントディレクトリに作成します。
fifo_r
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
char buf[PIPE_BUF];
int fd;
mkfifo("fifo_a", 0644);
if ((fd = open("fifo_a", O_RDONLY)) > 0) {
while (read(fd, buf, sizeof(buf)) > 0) {
fputs(buf, stdout);
}
close(fd);
}
}
パイプファイルは、lsコマンドで実際に存在が確認できます(pというパイプを示すフラグが立っています)。
prw-r--r-- 1 tenkai tenkai 0 11月 20 06:59 fifo_a
fifo_w
int main(int argc, char *argv[])
{
int fd;
if ((fd = open("fifo_a", O_WRONLY)) > 0) {
write(fd, "MESSAGE ABCDEFG\n", 16);
close(fd);
}
}
バックグラウンドとフォアグラウンドのコマンド間のデータ受け渡しが確認できます。
$ ./fifo_r &
[1] 138528
$ ./fifo_w
MESSAGE ABCDEFG
名前付きパイプを開くとき、パイプの読み出しと書き込みの両端でファイルが開かれるまで互いのopen()関数はブロックします。先にopen()書き込みで開こうとするプロセスは、読み出し側が開かれるまで待機します。
ただし、open()にO_NONBLOCKを指定した場合はブロックせずに即座に返ります。
「O_RDONLY|O_NONBLOCK」では、書き込み側が開いていない場合ブロックせずに直ちにファイルディスクリプタを返します。
「O_WRONLY|O_NONBLOCK」では、読み出し側が開いていなければ待たずに直ちに-1を返します。
mkfifo("fifo_a", 0644);
if ((fd = open("fifo_a", O_RDONLY | O_NONBLOCK)) > 0) {
while ((n = read(fd, buf, sizeof(buf))) > 0) {
fputs(buf, stdout);
}
close(fd);
}
mkfifo("fifo_a", 0644);
if ((fd = open("fifo_a", O_WRONLY | O_NONBLOCK)) > 0) {
write(fd, "MESSAGE ABCDEFG\n", 16);
close(fd);
}