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

#はじめに
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`コマンドのプロセスが生成されていたということになります。
ということは、**<font color="Red">コマンドをパイプで繋いでいった時、プロセスはパイプを介して左側にあるプロセスの実行を待ってから生成されるわけではない</font>**ようです。

##左側のプロセスから生成されるのか、ランダムな順序で生成されるのか
今度は`grep`コマンドを３つ、パイプで繋いでみます。

```
$ 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ももちろん順番どおりです。
これはたまたま今回そうなっただけではなく、何度実行してもこの順序になりました。
どうやら、**<font color="Red">パイプの左側からプロセスは生成されている</font>**みたいです。

プロセスは`fork()`によって生成されるので、図にしてみると以下のような感じでしょうか。
![81C4FBAE-FCAA-4BF4-8060-F9C2827FC438.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1731510/ccc365c4-4627-843f-1e3f-8bf6b0c5196d.jpeg)

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

##実行も左側から始まるのか
まず、プロセスは`fork()`で生成された段階では、親プロセスのコピーでしかありません。
つまり、`bash`から派生した子プロセスもまた`bash`です。
子プロセスは`fork()`のあと`exec()`を実行することで初めて`bash`から、`ps`や`grep`などのコマンドに置き換わります。（先程の図で「ps」ではなく「子プロセス１」などと書いたのもそういう理由です）

ではここで、`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
```

１つ目の`grep`の出力結果をファイルへ書き込み、その後さらにパイプを繋いでいます。
（こんな無駄なことは普通しませんが、今回は実験ということで。。。）

**１つ目の`grep`コマンドとその次の`ps`コマンドは、パイプで繋がれているものの、渡すべきデータがありません。**
そもそも、`ps`コマンドは標準入力から何かデータを取るわけではないので、標準入力からのデータ待ちをする仕様にはなっていないかと思います。

これを実行するとどうなるでしょうか。
実行結果を比較するために、５回連続で実行してみます。

```
//１回目
$ 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

//２回目
$ 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

//３回目
$ 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

//４回目
$ 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

//５回目
$ 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
```

同じコマンドを実行しているのに、かなりでたらめな出力となりましたね。
画面に出ているのは、２つ目の`ps`コマンドの出力を`grep`で絞ったものになります。
つまり、画面に出ているのは２つ目の`ps`コマンドが実行されたときに存在していたプロセスということになるでしょう。

例えば、１回目の出力では先頭２つのプロセスはすでに終了してしまっているのに対し、
５回目の出力ではすべてのプロセスが２つ目の`ps`コマンド実行時点でまだ存在しています。
また、２回目や４回目の出力を見ると、最後の`grep`コマンドのプロセスは未生成となっています。
さらに、５回目の出力を見ると、２つ目の`ps`の実行の方が、１つ目の`ps`の`exec()`よりも早いことが伺えます。

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

```
$ cat test.log
//１回目
$ 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

//２回目
$ 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

//３回目
$ 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

//４回目
$ 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

//５回目
$ 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
```

こちらも出力は一定ではありません。
ファイルに書き込まれた出力は、１つ目の`ps`コマンドの出力を`grep`したものになるので、
要するに１つ目の`ps`コマンド実行時に生成されていたプロセスということになるかと思います。

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

５回目の出力は２〜４回目と同じですが、意味合いが異なります。
５回目の場合、２つ目の`ps`の実行は１つ目の`ps`の実行開始よりも早かったということは先程述べました。
そのこととファイルの出力結果を合わせて考えると、**１つ目の`ps`実行時点で、すでに２つ目の`ps`以降のプロセスは終了していた**ということが導かれます。

##例外的挙動の考察
改めて例外的挙動をしたコマンドを掲載します。

```
$ ps axj | grep -e bash -e axj -e PID >> test.log | ps axj | grep -e bash -e PID -e axj
```

２つ目のパイプは特にデータを渡す働きをしておらず、意味のない繋ぎとなっているため、内部ではその前後を別々にグループ化しているのでは？と初めは思いました。
つまり、このような状態です。
![C060646D-5A46-431F-87B1-8D244DC271FD_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1731510/c02376af-b42b-5db9-20f6-095259551eb5.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欄を見ると、４つのプロセスのグループIDは全て同じになっています。
ということは、正しくは以下のようになるわけです。
![EFC3D2ED-7FCC-400C-A2B9-4E92D7224E41_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1731510/1e825998-9dfd-6edb-0c7a-8a0a8499ad92.jpeg)
また、PIDの順序から見ても、最初の方に見た「プロセスは左から順に生成される」というのが当てはまっています。
このことから、特に意味のないパイプを繋いだとしても、「順当に」パイプを繋いだ時と同じように内部的には動いているということになるかと思います。

もし、「例外的挙動をするもの」で言えることがそのまま「順当に」パイプを繋いだ場合にも言えるものとすれば、以前に見た次の２つの考察について、ある程度の確証が得られそうです。

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

###「プロセスの実行開始は、左側からでない可能性がある」について
この点については、例外的挙動の５回目の実験結果からおよそ真であることが確かめられます。

```
//５回目
$ 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
```

１つ目の`ps`の`exec()`より、以降のプロセスの`exec()`の方が早く開始されているのが伺えます。
「順当に」パイプを繋いだ際も同じように**<font color="Red">プロセスはランダムに実行開始され、OSのプロセススケジューリングによって各プロセスの足並みが揃えられていた可能性が高い</font>**と思います。

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

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

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

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

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

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

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

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