0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Linux-psとgrepを使ってパイプに繋がれたプロセスを見る

Posted at

#はじめに
Linux上でコマンドをパイプで繋いでいった時のプロセスの様子を、
psgrepを使って観察してみた結果のメモになります。

調べたのは以下の内容です。
・プロセスの生成順序
・プロセスの実行開始順序

##実行環境
実行環境は以下の通りです。(シェルは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()によって生成されるので、図にしてみると以下のような感じでしょうか。
81C4FBAE-FCAA-4BF4-8060-F9C2827FC438.jpeg

#プロセスの実行開始順序の考察
プロセスの生成は左側から順番であるという推測はつきました。
では、実行についてはどうでしょうか。

##実行も左側から始まるのか
まず、プロセスはfork()で生成された段階では、親プロセスのコピーでしかありません。
つまり、bashから派生した子プロセスもまたbashです。
子プロセスはfork()のあとexec()を実行することで初めてbashから、psgrepなどのコマンドに置き換わります。(先程の図で「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()された時点でまだ以降のgrepexec()実行されていないのであれば、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になっている

この出力結果は、最後のgrepexec()よりもpsのプロセス実行の方が優先されたようにも見えますし、ランダムな順序で実行開始された結果、psの実行時点で最後のgrepexec()がまだ行われていなかっただけのようにも見えます。

実行開始順序に関しては、この考察からは何とも言えないです。。。

##プロセス生成との関係
これまでの結果のみに基づいて言うなら、プロセスの実行開始について次の推測ができなくはないでしょう。

・パイプに繋がれたプロセスがすべて生成されるまで、どのプロセスの実行も開始されない

これは最初の考察にも関わってくるところになりますが、psfork()されたあと、それ以降のgrepfork()されるのを待たずに実行を開始した場合、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のプロセススケジューリングが関与すると、「あるプロセスの実行開始」と「そのプロセスの実行完了」がノンストップで走らないため、パイプに繋がれた全プロセスが生成されるまで他のプロセスが実行開始を待機しているという保証は、これまでの結果からだけでは取れないものです。
実行開始がばらばらでも、実行開始直後に一時停止をされれば、確かにこれまでの出力のようにお互いのプロセスが足並みを揃えることは可能になるからです。

#例外的挙動をするもの
これまでの考察で、およそ以下のことが確認されました。

・プロセスは左側から順番に生成される
・プロセスの実行開始は、左側からでない可能性がある
・全てのプロセスが生成されるまで、どのプロセスも実行開始されない可能性がある

この考察はpsgrepを「順当に」パイプで繋いだ結果によるものです。

しかし、何事にも例外というものは存在するもので、今回もその例には漏れず面白い結果になるパイプの繋ぎ方があったので、以降はそれについて見ていきます。

##データを渡さないパイプを使う
以下のコマンドを見てください。

$ 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つ目のpsexec()よりも早いことが伺えます。

続いて、書き込んだファイルの方を見てみます。(見やすいように少し手を加えています)

$ 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つ目のパイプは特にデータを渡す働きをしておらず、意味のない繋ぎとなっているため、内部ではその前後を別々にグループ化しているのでは?と初めは思いました。
つまり、このような状態です。
C060646D-5A46-431F-87B1-8D244DC271FD_4_5005_c.jpeg
そして、そのそれぞれのグループに対して、最初の「順当に」パイプを繋いだ時に見た考察が当てはまるのかと思いました。

しかし、どうもそういうわけでもないようです。

$ 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は全て同じになっています。
ということは、正しくは以下のようになるわけです。
EFC3D2ED-7FCC-400C-A2B9-4E92D7224E41_4_5005_c.jpeg
また、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つ目のpsexec()より、以降のプロセスのexec()の方が早く開始されているのが伺えます。
「順当に」パイプを繋いだ際も同じように**プロセスはランダムに実行開始され、OSのプロセススケジューリングによって各プロセスの足並みが揃えられていた可能性が高い**と思います。

###「全てのプロセスが生成されるまで、どのプロセスも実行開始されない可能性がある」について
これについては、例外的挙動の実験の際に

結果を見ると、1回目の出力では1つ目のps実行時点で全てのプロセスの生成が完了していたのに対し、2〜4回目では、2つ目のpsコマンド以降のプロセスがまだfork()で生成されていない状態となっていました。

と述べたように、各プロセスは他のプロセスのfork()を待って実行を開始するというわけではないということが分かるため、偽であると思われます。

よって、「全てのプロセスが生成されるまで、どのプロセスも実行開始されていなかった」のではなく、**「OSのプロセススケジューリングによって、全てのプロセスが生成されるまで、他のプロセスの実行開始が待機させられているように見えていた」**という方が正しいのではないかと思えます。

#まとめ
psgrepの出力によって、パイプに繋がれたプロセスの生成や実行について見てみました。
この考察で、パイプに繋がれたプロセス群に関して見えてきたことは、

・プロセスは左側から順番に生成される
・プロセスはランダムに実行開始され、OSのプロセススケジューリングによって各プロセスの足並みが揃えられる
・OSのプロセススケジューリングによって、全てのプロセスが生成されるまで、他のプロセスの実行開始が待機させられているように見える

ということでした。
ただ、これも可能性があるというだけで、本当のところがどうなっているかは、実際のカーネルのソースを読み解いたり、プロセススケジューリングについてもっと深く理解するしかないと考えられます。

Linuxのプロセス周りの話は、本当に奥が深いですね。

0
1
0

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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?