#はじめに
Linux上でコマンドをパイプで繋いでいった時のプロセスの様子を、
ps
とgrep
を使って観察してみた結果のメモになります。
調べたのは以下の内容です。
・プロセスの生成順序
・プロセスの実行開始順序
##実行環境
実行環境は以下の通りです。(シェルはbash)
$ uname -rs
Linux 3.10.0-1160.31.1.el7.x86_64
$cat /etc/centos-release
CentOS Linux release 7.9.2009(Core)
#プロセスの生成順序の考察
プロセスの生成順序について考察していきます。
##左側のプロセスの実行が早いか、右側のプロセスの生成が早いか
パイプに繋いだコマンドのプロセスが、左側から順に生成&実行されていると仮定します。
まず、以下のコマンドを実行してみたいと思います。
$ ps axj | grep -e bash -e PID -e axj
grep
のオプションについては、出力行を抑制したいだけなのであまり気にしないでください。(以下同様)
もしps
コマンドが実行されるのを待ってからgrep
コマンドのプロセスが生成されているとすれば、ps
にはgrep
コマンドのプロセスは出力されないはずです。
以下、実行結果を見てみます。
$ ps axj | grep -e bash -e PID -e axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5435 Ss 1001 0:01 -bash
1499 5436 5435 1499 tty1 5435 R+ 1001 0:00 ps axj
1499 5437 5435 1499 tty1 5435 R+ 1001 0:00 grep -e bash -e PID -e axj //grepが出力されている
ps
コマンドの出力にgrep
コマンドのプロセスがありますね。
つまり、ps
コマンド実行時にはすでにgrep
コマンドのプロセスが生成されていたということになります。
ということは、**コマンドをパイプで繋いでいった時、プロセスはパイプを介して左側にあるプロセスの実行を待ってから生成されるわけではない**ようです。
##左側のプロセスから生成されるのか、ランダムな順序で生成されるのか
今度はgrep
コマンドを3つ、パイプで繋いでみます。
$ ps axj | grep -e bash -e PID -e axj | grep -e bash -e PID -e axj | grep -e bash -e PID -e axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5896 Ss 1001 0:01 -bash
1499 5897 5896 1499 tty1 5896 R+ 1001 0:00 ps axj
1499 5898 5896 1499 tty1 5896 R+ 1001 0:00 grep -e bash -e PID -e axj
1499 5899 5896 1499 tty1 5896 S+ 1001 0:00 grep -e bash -e PID -e axj
1499 5900 5896 1499 tty1 5896 S+ 1001 0:00 grep -e bash -e PID -e axj
先程のように、ps
コマンドの出力にgrep
コマンドのプロセスが含まれています。
上の出力からだと、プロセスの生成(実行ではない)が左から順に行われているのか、それともランダムに行われているのかわからないです。
そこで、grep
のオプションをそれぞれ変えて判別できるようにしてみます。
例えばこんな感じ。
$ ps axj | grep -e bash -e -PID -e axj | grep -e bash -e S -e R | grep -e bash -e R -e S
上記を実行して、もしコマンドがパイプの左側から順に生成されているとすれば、ps
の出力もその順番で上から並ぶはずです。
また、プロセスが生成されるとプロセスIDが若い番号から順番に割り当てられるので、PID欄を見ても生成順序がわかります。
では、実行してみます。
$ ps axj | grep -e bash -e -PID -e axj | grep -e bash -e S -e R | grep -e bash -e R -e S
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 5903 5902 1499 tty1 5902 R+ 1001 0:00 ps axj
1499 5904 5902 1499 tty1 5902 R+ 1001 0:00 grep -e bash -e PID -e axj
1499 5905 5902 1499 tty1 5902 S+ 1001 0:00 grep -e bash -e S -e R
1499 5906 5902 1499 tty1 5902 S+ 1001 0:00 grep -e bash -e R -e S
コマンドの並び順と同じ順序でps
コマンドでも出力されています。PIDももちろん順番どおりです。
これはたまたま今回そうなっただけではなく、何度実行してもこの順序になりました。
どうやら、**パイプの左側からプロセスは生成されている**みたいです。
プロセスはfork()
によって生成されるので、図にしてみると以下のような感じでしょうか。
#プロセスの実行開始順序の考察
プロセスの生成は左側から順番であるという推測はつきました。
では、実行についてはどうでしょうか。
##実行も左側から始まるのか
まず、プロセスはfork()
で生成された段階では、親プロセスのコピーでしかありません。
つまり、bash
から派生した子プロセスもまたbash
です。
子プロセスはfork()
のあとexec()
を実行することで初めてbash
から、ps
やgrep
などのコマンドに置き換わります。(先程の図で「ps」ではなく「子プロセス1」などと書いたのもそういう理由です)
ではここで、exec()
が実行された時点をプロセスの実行開始とみなしたいと思います(厳密には違うと思いますが)。
もし実行開始順序も生成と同様に左側からだとすれば、ps
コマンドの出力はこのようになっていてもおかしくはないでしょう。
$ ps axj | grep -e bash -e -PID -e axj | grep -e bash -e S -e R | grep -e bash -e R -e S
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 5903 5902 1499 tty1 5902 R+ 1001 0:00 ps axj
1499 5904 5902 1499 tty1 5902 R+ 1001 0:00 -bash //ここがexec()未実行
1499 5905 5902 1499 tty1 5902 S+ 1001 0:00 -bash //ここがexec()未実行
1499 5906 5902 1499 tty1 5902 S+ 1001 0:00 -bash //ここがexec()未実行
psコマンドのプロセスがexec()
された時点でまだ以降のgrep
がexec()
実行されていないのであれば、grep
のプロセスはfork()
された直後の姿、すなわち親プロセスのコピーを映し出しているはずです。
しかし、実際には上記のような出力ではなく、
$ ps axj | grep -e bash -e -PID -e axj | grep -e bash -e S -e R | grep -e bash -e R -e S
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 5903 5902 1499 tty1 5902 R+ 1001 0:00 ps axj
1499 5904 5902 1499 tty1 5902 R+ 1001 0:00 grep -e bash -e PID -e axj
1499 5905 5902 1499 tty1 5902 S+ 1001 0:00 grep -e bash -e S -e R
1499 5906 5902 1499 tty1 5902 S+ 1001 0:00 grep -e bash -e R -e S
このようになっています。
つまり、ps
コマンド実行開始段階ですでに、以降のgrep
の実行開始処理が走っている可能性があります。
「可能性があります」と書いたのは、プロセスの実行についてはOSのプロセススケジューリングが大いに関わってくるため、一概にはそのように言えないからです。
例えば、上記のps
実行直後(出力結果表示前)にps
がプロセススケジューリングによって一時実行停止され、次に実行再開されるよりも前にgrep
の実行が開始されていれば、ps
実行開始段階で必ずしもgrep
の実行が開始されている必要はないと言えます。
実際、同じコマンドを何度も何度も実行していると、稀に以下のようになることがありました。
$ ps axj | grep -e bash -e -PID -e axj | grep -e bash -e S -e R | grep -e bash -e R -e S
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 5903 5902 1499 tty1 5902 R+ 1001 0:00 ps axj
1499 5904 5902 1499 tty1 5902 S+ 1001 0:00 grep -e bash -e PID -e axj
1499 5905 5902 1499 tty1 5902 S+ 1001 0:00 grep -e bash -e S -e R
1499 5906 5902 1499 tty1 5902 R+ 1001 0:00 -bash //bashになっている
この出力結果は、最後のgrep
のexec()
よりもps
のプロセス実行の方が優先されたようにも見えますし、ランダムな順序で実行開始された結果、ps
の実行時点で最後のgrep
のexec()
がまだ行われていなかっただけのようにも見えます。
実行開始順序に関しては、この考察からは何とも言えないです。。。
##プロセス生成との関係
これまでの結果のみに基づいて言うなら、プロセスの実行開始について次の推測ができなくはないでしょう。
・パイプに繋がれたプロセスがすべて生成されるまで、どのプロセスの実行も開始されない
これは最初の考察にも関わってくるところになりますが、ps
がfork()
されたあと、それ以降のgrep
がfork()
されるのを待たずに実行を開始した場合、ps
の出力結果にいずれかのgrep
が欠けていてもおかしくはありません。
つまり、
$ ps axj | grep -e bash -e -PID -e axj | grep -e bash -e S -e R | grep -e bash -e R -e S
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 5903 5902 1499 tty1 5902 R+ 1001 0:00 ps axj
1499 5904 5902 1499 tty1 5902 S+ 1001 0:00 grep -e bash -e PID -e axj
このようになってもおかしくはないでしょう。
しかしこのようにはならず、毎回すべてのgrep
が出力されるということは、あながち先程の推測も間違ってはいないかもしれません。
ですがこの推測も微妙と言えば微妙です。
やはりOSのプロセススケジューリングが関与すると、「あるプロセスの実行開始」と「そのプロセスの実行完了」がノンストップで走らないため、パイプに繋がれた全プロセスが生成されるまで他のプロセスが実行開始を待機しているという保証は、これまでの結果からだけでは取れないものです。
実行開始がばらばらでも、実行開始直後に一時停止をされれば、確かにこれまでの出力のようにお互いのプロセスが足並みを揃えることは可能になるからです。
#例外的挙動をするもの
これまでの考察で、およそ以下のことが確認されました。
・プロセスは左側から順番に生成される
・プロセスの実行開始は、左側からでない可能性がある
・全てのプロセスが生成されるまで、どのプロセスも実行開始されない可能性がある
この考察はps
とgrep
を「順当に」パイプで繋いだ結果によるものです。
しかし、何事にも例外というものは存在するもので、今回もその例には漏れず面白い結果になるパイプの繋ぎ方があったので、以降はそれについて見ていきます。
##データを渡さないパイプを使う
以下のコマンドを見てください。
$ ps axj | grep -e bash -e axj -e PID >> test.log | ps axj | grep -e bash -e PID -e axj
1つ目のgrep
の出力結果をファイルへ書き込み、その後さらにパイプを繋いでいます。
(こんな無駄なことは普通しませんが、今回は実験ということで。。。)
1つ目のgrep
コマンドとその次のps
コマンドは、パイプで繋がれているものの、渡すべきデータがありません。
そもそも、ps
コマンドは標準入力から何かデータを取るわけではないので、標準入力からのデータ待ちをする仕様にはなっていないかと思います。
これを実行するとどうなるでしょうか。
実行結果を比較するために、5回連続で実行してみます。
//1回目
$ ps axj | grep -e bash -e axj -e PID >> test.log | ps axj | grep -e bash -e PID -e axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 8473 8471 1499 tty1 8471 R+ 1001 0:00 ps axj
1499 8474 8471 1499 tty1 8471 R+ 1001 0:00 grep -e bash -e PID -e axj
//2回目
$ ps axj | grep -e axj -e PID >> test.log | ps axj | grep -e PID -e axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 8476 8475 1499 tty1 8475 S+ 1001 0:00 grep -e bash -e axj -e PID
1499 8477 8475 1499 tty1 8475 R+ 1001 0:00 ps axj
//3回目
$ ps axj | grep -e axj -e PID >> test.log | ps axj | grep -e PID -e axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 8509 8507 1499 tty1 8507 R+ 1001 0:00 ps axj
//4回目
$ ps axj | grep -e axj -e PID >> test.log | ps axj | grep -e PID -e axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 8514 8513 1499 tty1 8513 S+ 1001 0:00 grep -e bash -e axj -e PID
1499 8515 8513 1499 tty1 8513 R+ 1001 0:00 ps axj
//5回目
$ ps axj | grep -e axj -e PID >> test.log | ps axj | grep -e PID -e axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 8520 8519 1499 tty1 8519 R+ 1001 0:00 -bash
1499 8521 8519 1499 tty1 8519 S+ 1001 0:00 grep -e bash -e axj -e PID
1499 8522 8519 1499 tty1 8519 R+ 1001 0:00 ps axj
1499 8523 8519 1499 tty1 8519 R+ 1001 0:00 -bash
同じコマンドを実行しているのに、かなりでたらめな出力となりましたね。
画面に出ているのは、2つ目のps
コマンドの出力をgrep
で絞ったものになります。
つまり、画面に出ているのは2つ目のps
コマンドが実行されたときに存在していたプロセスということになるでしょう。
例えば、1回目の出力では先頭2つのプロセスはすでに終了してしまっているのに対し、
5回目の出力ではすべてのプロセスが2つ目のps
コマンド実行時点でまだ存在しています。
また、2回目や4回目の出力を見ると、最後のgrep
コマンドのプロセスは未生成となっています。
さらに、5回目の出力を見ると、2つ目のps
の実行の方が、1つ目のps
のexec()
よりも早いことが伺えます。
続いて、書き込んだファイルの方を見てみます。(見やすいように少し手を加えています)
$ cat test.log
//1回目
$ ps axj | grep -e bash -e axj -e PID >> test.log | ps axj | grep -e bash -e PID -e axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 8471 8471 1499 tty1 8471 R+ 1001 0:00 ps axj
1499 8472 8471 1499 tty1 8471 R+ 1001 0:00 grep -e bash -e axj -e PID
1499 8473 8471 1499 tty1 8471 R+ 1001 0:00 ps axj
1499 8474 8471 1499 tty1 8471 S+ 1001 0:00 grep -e bash -e PID -e axj
//2回目
$ ps axj | grep -e bash -e axj -e PID >> test.log | ps axj | grep -e bash -e PID -e axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 8475 8475 1499 tty1 8475 R+ 1001 0:00 ps axj
1499 8476 8475 1499 tty1 8475 R+ 1001 0:00 grep -e bash -e axj -e PID
//3回目
$ ps axj | grep -e bash -e axj -e PID >> test.log | ps axj | grep -e bash -e PID -e axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 8507 8507 1499 tty1 8507 R+ 1001 0:00 ps axj
1499 8508 8507 1499 tty1 8507 R+ 1001 0:00 grep -e bash -e axj -e PID
//4回目
$ ps axj | grep -e bash -e axj -e PID >> test.log | ps axj | grep -e bash -e PID -e axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 8513 8513 1499 tty1 8513 R+ 1001 0:00 ps axj
1499 8514 8513 1499 tty1 8513 R+ 1001 0:00 grep -e bash -e axj -e PID
//5回目
$ ps axj | grep -e bash -e axj -e PID >> test.log | ps axj | grep -e bash -e PID -e axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 8520 8519 1499 tty1 8519 R+ 1001 0:00 ps axj
1499 8521 8519 1499 tty1 8519 R+ 1001 0:00 grep -e bash -e axj -e PID
こちらも出力は一定ではありません。
ファイルに書き込まれた出力は、1つ目のps
コマンドの出力をgrep
したものになるので、
要するに1つ目のps
コマンド実行時に生成されていたプロセスということになるかと思います。
結果を見ると、1回目の出力では1つ目のps
実行時点で全てのプロセスの生成が完了していたのに対し、2〜4回目では、2つ目のps
コマンド以降のプロセスがまだfork()
で生成されていない状態となっていました。
5回目の出力は2〜4回目と同じですが、意味合いが異なります。
5回目の場合、2つ目のps
の実行は1つ目のps
の実行開始よりも早かったということは先程述べました。
そのこととファイルの出力結果を合わせて考えると、1つ目のps
実行時点で、すでに2つ目のps
以降のプロセスは終了していたということが導かれます。
##例外的挙動の考察
改めて例外的挙動をしたコマンドを掲載します。
$ ps axj | grep -e bash -e axj -e PID >> test.log | ps axj | grep -e bash -e PID -e axj
2つ目のパイプは特にデータを渡す働きをしておらず、意味のない繋ぎとなっているため、内部ではその前後を別々にグループ化しているのでは?と初めは思いました。
つまり、このような状態です。
そして、そのそれぞれのグループに対して、最初の「順当に」パイプを繋いだ時に見た考察が当てはまるのかと思いました。
しかし、どうもそういうわけでもないようです。
$ ps axj | grep -e bash -e axj -e PID >> test.log | ps axj | grep -e bash -e PID -e axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 8471 8471 1499 tty1 8471 R+ 1001 0:00 ps axj
1499 8472 8471 1499 tty1 8471 R+ 1001 0:00 grep -e bash -e axj -e PID
1499 8473 8471 1499 tty1 8471 R+ 1001 0:00 ps axj
1499 8474 8471 1499 tty1 8471 S+ 1001 0:00 grep -e bash -e PID -e axj
この出力のPGID欄を見ると、4つのプロセスのグループIDは全て同じになっています。
ということは、正しくは以下のようになるわけです。
また、PIDの順序から見ても、最初の方に見た「プロセスは左から順に生成される」というのが当てはまっています。
このことから、特に意味のないパイプを繋いだとしても、「順当に」パイプを繋いだ時と同じように内部的には動いているということになるかと思います。
もし、「例外的挙動をするもの」で言えることがそのまま「順当に」パイプを繋いだ場合にも言えるものとすれば、以前に見た次の2つの考察について、ある程度の確証が得られそうです。
・プロセスの実行開始は、左側からでない可能性がある
・全てのプロセスが生成されるまで、どのプロセスも実行開始されない可能性がある
###「プロセスの実行開始は、左側からでない可能性がある」について
この点については、例外的挙動の5回目の実験結果からおよそ真であることが確かめられます。
//5回目
$ ps axj | grep -e axj -e PID >> test.log | ps axj | grep -e PID -e axj
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
689 1499 1499 1499 tty1 5902 Ss 1001 0:01 -bash
1499 8520 8519 1499 tty1 8519 R+ 1001 0:00 -bash
1499 8521 8519 1499 tty1 8519 S+ 1001 0:00 grep -e bash -e axj -e PID
1499 8522 8519 1499 tty1 8519 R+ 1001 0:00 ps axj
1499 8523 8519 1499 tty1 8519 R+ 1001 0:00 -bash
1つ目のps
のexec()
より、以降のプロセスのexec()
の方が早く開始されているのが伺えます。
「順当に」パイプを繋いだ際も同じように**プロセスはランダムに実行開始され、OSのプロセススケジューリングによって各プロセスの足並みが揃えられていた可能性が高い**と思います。
###「全てのプロセスが生成されるまで、どのプロセスも実行開始されない可能性がある」について
これについては、例外的挙動の実験の際に
結果を見ると、1回目の出力では1つ目の
ps
実行時点で全てのプロセスの生成が完了していたのに対し、2〜4回目では、2つ目のps
コマンド以降のプロセスがまだfork()
で生成されていない状態となっていました。
と述べたように、各プロセスは他のプロセスのfork()
を待って実行を開始するというわけではないということが分かるため、偽であると思われます。
よって、「全てのプロセスが生成されるまで、どのプロセスも実行開始されていなかった」のではなく、**「OSのプロセススケジューリングによって、全てのプロセスが生成されるまで、他のプロセスの実行開始が待機させられているように見えていた」**という方が正しいのではないかと思えます。
#まとめ
ps
とgrep
の出力によって、パイプに繋がれたプロセスの生成や実行について見てみました。
この考察で、パイプに繋がれたプロセス群に関して見えてきたことは、
・プロセスは左側から順番に生成される
・プロセスはランダムに実行開始され、OSのプロセススケジューリングによって各プロセスの足並みが揃えられる
・OSのプロセススケジューリングによって、全てのプロセスが生成されるまで、他のプロセスの実行開始が待機させられているように見える
ということでした。
ただ、これも可能性があるというだけで、本当のところがどうなっているかは、実際のカーネルのソースを読み解いたり、プロセススケジューリングについてもっと深く理解するしかないと考えられます。
Linuxのプロセス周りの話は、本当に奥が深いですね。