これまでは単一のコマンドが実行されることを想定して説明を行ってきたが、実際にはパイプラインでつながれたコマンドであったり、制御演算子を使って組み合わされたコマンドであったりと、複数のコマンドが1つのまとまりになっていることがある。今回はこうした複雑なコマンドについて説明する。

複雑なコマンド

これまでは説明の中で行データを解析した結果としてシンプルコマンドまでたどり着くケースで説明を行ってきた。しかし、その部分がシンプルなコマンドではなく、コマンドと制御演算子の組み合わせなどになっていることがある。これらは「複雑なコマンド」と言われるもので、次の5つの種類があるとされている。

  • シンプルコマンド
  • パイプライン
  • リストまたは複合リスト
  • 複合コマンド
  • 関数定義


複雑なコマンドは単一のコマンドではなく、実際には複数のコマンドが実行されていることも多い。この場合、その複雑なコマンドの終了ステータスは、最後に実行されたシンプルなコマンドの終了ステータスが使われる。なお、複雑なコマンドがコマンドそのものを含んでいない場合には、終了ステータスは「0」になる。

パイプライン

シェルで最も特徴的で強力な機能のひとつがパイプラインだ。複雑なコマンドの中でも特に使われるものだ。パイプラインはコマンドを「|(パイプ)」で接続したもので、最後のコマンド以外のコマンドについては、標準出力が次のコマンドの標準入力に接続されるという特性を持っている。

command1 | command2 | command3

例えば上記のように記述した場合、command1の標準出力はcommand2の標準入力になり、command2の標準出力はcommand3の標準入力になる。そして、command3の標準出力はコンソールやターミナルに出力される。

パイプラインで接続されるのは標準出力と標準入力だけだ。標準エラー出力はパイプラインでは接続されず、そのままターミナルやコンソールに表示されることになる。ただし、リダイレクト演算子を使って標準エラー出力を標準出力へ割り当てれば、標準エラー出力もパイプラインで流していくことができる。

command1 2>&1 | command2 2>&1 | command3

パイプラインがどのように処理されるかは実装系によって異なる。ashの場合にはサブシェルが生成され、そこでパイプラインが実行されることになる。これは次のようなパイプラインを実行しつつ、ほかのターミナルからプロセスの親子関係を表示させることで確認することができる。

$ sleep 12345 | cat

上記サンプルはパイプラインの使い方としては意味がないのだが、実行されているプロセスをわかりやすくするために使っている。この方法だと「sleep 12345」というプロセスを探せばよいので、どのプロセスが親子関係にあるのかが調べやすい。

この状態でシェルのプロセス関係を調べると、次のように「sh」から「sleep 12345」と「cat」という2つのコマンドが別プロセスで実行されていることがわかる。

$ ps -d
  PID TT  STAT    TIME COMMAND
  ...
42467  2  I    0:00.00 - sh
42468  2  IC+  0:00.00 |-- sleep 12345
42469  2  I+   0:00.00 `-- cat
  ...
$

シェルはパイプラインを実行している間、パイプラインで接続されたすべてのコマンドが終了するまで処理を待つ。次のコマンドを実行するとその様子がわかる。

$ date +%s
1562571760
$ sleep 10 | sleep 1 | sleep 5
$ date +%s
1562571770
$

上記コマンドラインの場合、「sleep 10」の処理が最も長くかかる。「sleep 1」と「sleep 5」はそれぞれ1秒および5秒で終了する。「sleep 1」と「sleep 5」が終了したあとも、シェルは「sleep 10」の終了を待つ。このため、シェルでは10秒経ってから処理が再開されている。

パイプラインでは最後に実行されたコマンドの終了コードがパイプラインの終了コードとなる。次のサンプルを見るとわかるように、パイプラインの中のコマンドの終了コードが0であっても0以外であっても、最後に実行されたコマンドの終了コードがパイプラインの終了コードとして使われている。

■パイプラインの最後のコマンドの終了コードが0 - パイプラインの終了コードは0

$ true | false | true
$ echo $?
0
$

■パイプラインの最後のコマンドの終了コードが1 - パイプラインの終了コードは1

$ true | false | false
$ echo $?
1
$

パイプラインの先頭に「!」を指定すると、終了コードが反転する。終了コード「0」は「1」に、終了コード0以外は「0」になる。

$ ! true | false | true
$ echo $?
1
$ ! true | false | false
$ echo $?
0
$

個々のコマンドの前に「!」を指定した場合、そのコマンドの終了コードが反転する。これはパイプラインと組み合わせることも可能で、次のような結果を得られる。

$ ! true | false | false
$ echo $?
0
$ ! true | false | ! false
$ echo $?
1
$

パイプラインは改行することで区切りとなるほか、同じことは「;」を指定しても実施することができる。例えば、次のサンプルを見れば、改行でも「;」でもパイプラインがその場所で終了していることがわかるだろう。

■改行によるパイプラインの終了

$ date +%s
1562572977
$ sleep 5 | sleep 3
$ sleep 5
$ date +%s
1562572987
$

■;によるパイプラインの終了

$ date +%s
1562573007
$ sleep 5 | sleep 3; sleep 5
$ date +%s
1562573017
$

パイプラインの基本的な機能はこんなところだ。内部的にはfork(2)システムコールによるプロセスの複製、pipe(2)システムコールによる標準出力と標準入力の接続、exec(2)系システムコールによるコマンドの実行、wait(2)系システムコールによるコマンド終了の待機、といった処理がシェルのパイプラインの本質的な処理となっている。シンプルだがカーネルの提供する機能をストレートに使った極めて強力な機能だ。

パイプラインとマルチコア

現在のPCは単一のコアで動作するプロセッサを搭載しているものは少なく、複数のコアを搭載しているものが多い。4、6、8、12、16… と数はさまざまだが、コア1つあたりでこれ以上性能を引き上げるのは現実的ではないため、基本的にはコアの数が増えていくのがこれからのプロセッサということになる。

Linuxカーネルに限らず、最近のUNIX系OSのカーネルは、プロセスを個別のコアに割り当てて動作する。理想的な状態では、コアの数までプロセスが並列に動作するようになる。つまり、コマンドをパイプラインでつないで動作させることは、マルチコアシステムの性能を発揮することとイコールということだ。

Linuxカーネルはこの辺りの開発がよく進んでおり、マルチコアの性能が発揮しやすい状態になっている。なるべくコマンドをパイプラインで接続して処理させることで、マルチコアの性能を使い切りやすくなる。実に簡単で効果的なマルチコアの利用方法だ。

もしこれをプログラミング言語でスレッドなどを使って行おうとすれば、いろいろと面倒なことになる。シェルならコマンドをパイプラインで接続するだけだ。ぜひともマスターしておきたい機能である。

参考資料