2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

dup2が分からないような少し分かったような気になったので記事にしてみた

2
Last updated at Posted at 2026-01-28

きっかけ

コンピューターサイエンスの初学者です。現在通っている学校で、C言語でbashのパイプ"|"を再現してみるという課題があったのですが、いくら解説サイトを読んでも、見本のソースを見ても、なぜこのようなコードになるのかいまいちピンと来ませんでした。しかし1週間ほどソースとにらめっこしていたところ、分からないような少し分かったような気になってきたので記事にまとめてみたいと思います。間違っている記載がありましたらご教示いただけますと幸いです。

前提

今回再現したいのは、配列としてコマンドが送られてきたとき、それらが"|"でつながっているものとして実行し、その結果を標準出力に書き出す、というものです。
以下のようなbashのコマンドがあるとします。
コマンド: echo "abcabc" | cat | sed "s/a/x/g"
実行結果: xbcxbc
これを今回作成するソースで実行する際、このように3つの要素を持つリストとして送られて来るイメージです。

  • 1コマンド目 → "echo" "abcabc"
  • 2コマンド目 → "cat"
  • 3コマンド目 → "sed" "s/a/x/g"

今回使用する関数は以下の"pipe"、"fork"、"dup2"、"execvp"です。

pipeとはなんぞや?

int pipe(int pipefd[2]);
パイプとは、プロセス間でのデータのやり取りをできるようにするための仕組みです。Linuxでは、通常のファイルの入出力と同じように操作できるよう、これを「無名の特殊ファイル」として実装しています。 pipe関数は引数に2つの要素を持つint型の配列が必要です。実行すると、新しくパイプを作成してその配列に以下のファイルディスクリプタを設定してくれます。

  • fd[0] → 読み取り口のファイルディスクリプタ
  • fd[1] → 書き込み口のファイルディスクリプタ

成功時は戻り値として0を、失敗時は-1を返します。

forkとはなんぞや?

pid_t fork(void);
呼び出し元のプロセスを複製して新しいプロセスを生成する関数です。生成された子プロセスは、fork()を実行した瞬間から親プロセスより分裂して実行されます。戻り値のpid_tは、親プロセスの場合には子プロセスのプロセスIDが、子プロセスの場合は0が、fork()に失敗した場合-1が返ってきます。
子プロセスは親プロセスの持つ変数とは別の領域に変数を確保します。しかし、ファイルディスクリプタの対応関係はそのままコピーされます。そのため、親と子は「同じパイプ」「同じファイル」を指すファイルディスクリプタを持つことになります。そのためどちらかのプロセスで読み書きすると、その結果はもう一方からも観測できます。ただし、ファイルディスクリプタの番号自体はコピーされているだけなので、親と子で独立して close() する必要があります。

dupとはなんぞや?

int dup(int oldfd);
int dup2(int oldfd, int newfd);
dup()は引数として受け取ったファイルディスクリプタoldfdのコピーを作成して、最も小さい番号の未使用のファイルディスクリプタを返す関数です。成功した場合、古いファイルディスクリプタと新しいファイルディスクリプタは同じファイルを読み書きする関係になります。どちらかで読み書きすると、もう一方にもその影響が反映されます。
dup2()はdup()とほぼ同じですが、コピーとして新たに使用するファイルディスクリプタを第2引数で指定することができます。ファイルディスクリプタ newfd が使用されていた場合には、そのファイルディスクリプタをクローズしてから再利用します。
補足として、ファイルディスクリプタはプロセスが持つ単なる番号であり、実際にどのパイプやファイルにつながっているかという情報は、OS側で管理されているそうです。dup() や dup2() が複製するファイルディスクリプタはその同じOS内部の管理情報を参照する関係になります。

execvpとはなんぞや?

int execvp(const char *file, char *const argv[]);
第1引数fileで指定された文字列をコマンドとして解釈して実行します。指定されたファイル名にスラッシュ "/" が含まれていない場合、実行可能ファイルを環境変数PATHから検索して実行します。その際のパラメタとしてargvを使用します。
execvpが呼ばれた時、プロセスは新しいプログラムに置き換わり、execvp より後ろに書いたprintfなどの処理はエラーがない限り実行されません。そのためfork()で子プロセスに分裂させてから実行する必要が出てきます。

1コマンド目

初期状態

各プロセスはファイルディスクリプタ 0 (STDIN)と 1 (STDOUT)、 2 (STDERR)を持っています。分かりづらいので以降はSTDERRと親の標準入出力は省略します。
image.png

PIPE実行後

pipe()実行後にできたプロセスIDを仮に

  • fd[0] (読み取り口) = 3
  • fd[1] (書き込み口) = 4

とします。今回できたパイプを仮にパイプAとします。
image.png

FORK実行

fork()すると子プロセスは親プロセスと同じファイルディスクリプタテーブルのコピーを持つことになります。もちろん、 0 (STDIN)と 1 (STDOUT)も持っています。今回できた子プロセスを子Aとします。
image.png

子処理

パイプは書き込み口が1つでも開いたままだと、読み取り側は EOF を受け取れずブロックし続けるため、不要な書き込み口はすべてclose()する必要があります。これからexecvpで呼び出す処理のためにも余分なファイルディスクリプタを開放してあげたいと思います。子プロセスAで使用しないパイプAの読み取り口 3 はclose()してしまいます。
dup2(4, STDOUT);
上記のようにdup2を行うとファイルディスクリプタ 4 が複製され、ファイルディスクリプタ 1 (STDOUT、すなわち番号1)と同じ参照先を持つようになり、このプロセスの結果の書き込み先がパイプAになります。
パイプAの書き込み口はファイルディスクリプタ 1 として保持するようになったので、ファイルディスクリプタ 4 はclose()してしまいます。
image.png

親処理

親処理側ではパイプAの書き込み口 4 は使用しないのでclose()し、wait()やwaitpid()で子プロセスの終了を待ちます。
子Aのexecvp()でコマンド呼び出しを行った結果の文字列(今回はabcabc)がパイプAのバッファに書き込まれ、それを読み取るための口がファイルディスクリプタ 3 として親プロセスに残ります。パイプAの中の実行結果は読み取りが行われるまで保持されます。
※ここでは処理の流れを分かりやすくするため、各コマンドごとに親プロセスで wait() を行っています
image.png

2コマンド目以降

初期状態

前回のコマンド実行後の結果がファイルディスクリプタ 3 で読み取れる状態になっています。
image.png

PIPE実行

前回のものとは別に、新たにパイプBを作成します。
pipe()実行後にできたプロセスIDを仮に

  • fd[0] (読み取り口) = 5
  • fd[1] (書き込み口) = 6

とします。
image.png

FORK実行

子プロセスは親プロセスと同じファイルディスクリプタテーブルのコピーを持つので、新しく作られたパイプBの読み取り口と書き込み口に加えて、前回のパイプAとつながるファイルディスクリプタ 3 も同様にコピーされます。今回できた子プロセスを子Bとします。
image.png

子処理Ⅰ

今回は最初のコマンドとは異なり、データの読み取り口を前回のコマンドの実行結果にしてあげる必要があります。
dup2(3, STDIN);
上記の処理を実行するとファイルディスクリプタ 3 が複製され、ファイルディスクリプタ 0 (STDIN)と紐づき、このプロセスのデータの読み取り口が前回のパイプAになります。
パイプAの読み取り口はファイルディスクリプタ 0 を使用するようになったので、ファイルディスクリプタ 3 はclose()してしまいます。
image.png

子処理Ⅱ

こちらは最初のコマンドの子処理と同様です。
子プロセスB側で使用しないファイルディスクリプタ 5 はclose()してしまいます。
dup2(6, STDOUT);
上記のようにdup2を行うとこのプロセスBの結果の書き込み先がパイプBになります。ファイルディスクリプタ 6 は複製済みのためclose()してしまいます。
image.png

親処理

親処理側ではパイプAの読み込み口 3 やパイプBの書き込み口 6 は使用しないのでclose()します。
子Bの処理でexecvp()を行った結果がパイプBの中に入り、ファイルディスクリプタ 5 として保持されています。パイプBの中の実行結果は読み取りが行われるまで保持されます。
image.png

並び替えると...

1つ目のパイプAから2つめの子プロセスBを通じて2つ目のパイプBにつながる処理の繋がりができました。2コマンド目以降の処理を繰り返せばより長いコマンドの繋がりを作ることもできます。最後のコマンドでは、子処理の出力先を新しいパイプに接続せず、そのまま標準出力に書き出すことで、結果が画面に表示されます。処理の最後でパイプの閉じ忘れの無いように気をつけてください。
image.png

参考

ふつうのLinuxプログラミング 第2版
Operating Systems. Exercise 02
Linux Programmer's Manual (2) DUP
manページ — EXEC

2
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?