[[FrontPage]] #contents * パイプでつなぐ [#g6c751b8] 1968年、UNIXの産みの親であるKen ThompsonとDennis RitchieがAT&Tのベル研究所でMulticsと呼ばれる大型のオペレーティングシステムを開発していた頃、「インタラクティブで便利なコンピュータサービス」が欲しいと言って作ったのがUNIXです。 彼らは、「ベル研の文書処理システムを作る」と言って予算を引き出し、PDP-11(システムメモリ16Kバイト、ユーザメモリ8Kバイト、ハードディスク512Kバイト)という現在のPDA以下のハードウェアを購入し、その上に現在のUNIXシステムのコマンド群とroffと呼ばれる文書処理システムを構築したのでした。 今では、Linuxの普及により誰でもUNIXの環境を持つことができるようになりました。 ** UNIXの3大発明 [#i61b3a24] 今回は、「パイプでつなく」をテーマにUNIXの偉大な大発明の中から、 - fork : 子プロセスの生成 - pipe : パイプ - リダイレクト について例題を交えながら説明していきます。 *** パイプを使った処理の例 [#a948992b] - 単語処理の例 パイプを使った処理の例として、「tr」コマンドのmanページの単語の種類をカウントします。 #pre{{ $ man tr | tr -cs 'A-Za-z' '\n' | sort -u | wc -l 492 }} このようにUNIXのコマンドをパイプでつなくことによって簡単に必要な処理をこなすことができます。 - リアルタイムの例 「単語処理の例」は、別にパイプを使わなくてもファイルを使って逐次的に処理できます。 パイプがファイルと決定的に異なる点は、リアルタイムの処理です。 図は、マウスイベントを別Windowにプロットする例です。 &ref(demo_pipeline.jpg); このデモでは、xevコマンドとEvent2Plot, Graphのシェルスクリプトをパイプでつないで、xplotの画面にイベントをリアルタイムでプロットします。 #pre{{ $ xev | Event2Plot | Graph }} ** パイプのつなぎ方 [#pfaae892] pipeシステムコールは、本当に不思議な関数です。自分が出力したものを自分へ送るストリームの輪(パイプ)がpipeシステムによって生成されます。 マニュアルの説明を読んだだけでpipeシステムコールの使い方を理解できる人は少ないでしょう。 - 最初にパイプを生成します。 &ref(pipe.jpg); - 次にforkシステムコールを使って、子プロセスを生成します &ref(fork.jpg); - 親のin、子のoutをクローズします &ref(close.jpg); - 親のstdoutをクローズし、dupします、子のstdinをクローズしてdupします &ref(dup.jpg); ** パイプを実現する3つのシステムコール [#wece4043] パイプでつなぐときに必要なシステムコール - fork - pipe - dup について、おさらいも含めて説明します。 *** fork システムコールのおさらい [#mcf73ea6] forkシステムコールの仕様を簡単に書くと、 #pre{{ 呼び出し形式 #include <unistd.h> pid_t fork(void); 機能 呼び出し元のプロセスをコピーして、新しいプロセスを生成する。 戻り値 成功すると、子プロセスには0が返され、 親プロセスには、子プロセスのプロセスIDが返されます。forkに失敗したら、-1を返します。 }} です。 forkでは、 - 子プロセスは、変数、ファイルなどのリソースを親システムと共有 - 親と子プロセスは、forkのリターン値で別の処理に切り分けられる ます。 簡単なプログラムで、上記の仕様を確認してみましょう。 #pre{{ #include <stdio.h> #include <unistd.h> main() { int pid; if ((pid = fork()) == -1) { fprintf(stderr, "can't fork\n"); } else if (pid == 0) { // child fprintf(stdout, "this is a child process\n"); fprintf(stderr, "pid(child)=%d\n", pid); } else { // parent fprintf(stdout, "this is a parent process\n"); fprintf(stderr, "pid(parent)=%d\n", pid); } } }} 以下のようにコンパイルして、実行すると #pre{{ $ cc -o ex1 ex1.c $ ex1 this is a parent process pid(parent)=6556 this is a child process pid(child)=0 }} と出力され、分岐とpidの値が正しくセットされていることが分かります。 次に、標準出力(1)とエラー出力(2)をリダイレクトでファイルに保存します。 #pre{{ $ ex1 1>1.out 2>2.out $ more *.out << 1.outの内容 >> this is a parent process this is a child process << 2.outの内容 >> pid(parent)=6570 pid(child)=0 }} と、親と子プロセスが同じファイルに書き込み、ファイルの共有がされていることが確認できます。 *** pipe システムコールの使い方 [#ka7a7042] pipeシステムコールの仕様を簡単に書くと、 #pre{{ 呼び出し形式 #include <unistd.h> int pipe(int fildes[2]); 機能 パイプを生成し、ペアのファイル記述子を割り当てる。1個目のファイル記述子が読み込み、 2個目が書き込み用となる。 fildes[1]に書き込まれたデータは、fildes[0]から読み込まれる。これにより、 あるプログラムの出力をから他のプログラムの入力にすることができる。 読み込みまたは書き込みのファイル記述子のいずれかがクローズするとwindowed(未亡人)となる。 windowedとなったパイプに書き込むと書き込みプロセスはSIGPIPEシグナルを受け取る。 windowedにすることで、読み込みプロセスにEnd-Of-Fileを送ることができる。 読み手がパイプのデータをすべて読み込んだ後やwindowedになった後のパイプから読み込もうとすると 0が返される。 戻り値 成功すると0を返し、そうでない場合には-1を返し、変数errnoにエラー番号をセットする。 }} パイプは、 - 単一方向のデータフローを持つバッファです - バッファが満杯になった場合には、書き込みプロセスはスリープ状態になり、十分な空きができるまで待ちます - 読み込みプロセスは、バッファに書き込まれるまでスリープ状態で待ちます - 書き込み側がクローズするとパイプからの読み込みでは0(End-Of-File)が返されます - 読み手側でクローズされたパイプに書き込もうとするとSIGPIPEシグナルが発生します のように機能します。 簡単なプログラムで、上記の仕様を確認します。 #pre{{ // ex2.c : pipe example #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUFSIZE (128) main() { int pid; int fd[2]; // create a pipe. if (pipe(fd) == -1) { fprintf(stderr, "Can't create pipe\n"); exit(1); } printf("fd[0]=%d, fd[1]=%d\n", fd[0], fd[1]); if ((pid = fork()) == 0) { // child char buf[BUFSIZE]; int len = 0; // close pipe out. close(fd[1]); // read message. len = read(fd[0], buf, BUFSIZE); printf("len=%d, buf=%s", len, buf); len = read(fd[0], buf, BUFSIZE); close(fd[0]); fprintf(stderr, "child finishied. len=%d\n", len); } else { // parent char buf[] = "hello world\n"; // close pipe in. close(fd[0]); // write message. write(fd[1], buf, sizeof(buf)); close(fd[1]); fprintf(stderr, "parent finishied\n"); } } }} プログラムを実行すると #pre{{ $ ex2 fd[0]=3, fd[1]=4 parent finishied len=13, buf=hello world child finishied. len=0 }} と出力され、 - pipeシステムコールによってfdに3, 4のファイル記述子が割り当てられ - 親プロセスがhello world\nを書き込み、書き込み用ファイル記述子をクローズする - 子プロセスが、hello world\nを読み込み、次の読み込みをすると0が返され ます。((最初の読み込みで、len=13となっているのは、文字列の終わりの0が含まれているためです)) *** dup システムコールの使い方 [#ic4ba39d] 同様にdupシステムコールの仕様を簡単に書くと、 #pre{{ 呼び出し形式 #include <unistd.h> int dup(int fildes); 機能 既存のファイル記述子を複製し、新しい生成されたファイル記述子を返す。 プロセスには、getdtablesize()で返される大きさのファイル記述子テーブルを持ち、 新しいファイル記述子には、未使用のもっとも小さい値が返される。 戻り値 正常終了の場合、0以上の値が返され、そうでない場合には-1を返します。 }} となります。 UNIXのファイル記述子の割り当てルールは、 - 新しいファイル記述子には、未使用のもっとも小さい値が返される ので、特定の値のファイル記述子を割り当てたい時には、 - dupの直前にそのファイル記述子をクローズする ことで実現できます。 リダイレクトやパイプは、この簡単なルールを使って実現されています。 dupの動作を確認するために、以下のサンプルプログラムを作成します。 #pre{{ // ex3.c : dup example #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUFSIZE (128) main() { int pid; int fd[2]; // create a pipe. if (pipe(fd) == -1) { fprintf(stderr, "can't create pipe\n"); exit(1); } if ((pid = fork()) == 0) { // child int in = -1; // close pipe out. close(fd[1]); // close stdin. close(0); in = dup(fd[0]); close(fd[0]); fprintf(stderr, "in=%d\n", in); fprintf(stderr, "child : exec wc\n"); execl("/usr/bin/wc", "wc", 0); } else { // parent int out = -1; // close pipe in. close(fd[0]); // close stdout. close(1); out = dup(fd[1]); close(fd[1]); fprintf(stderr, "out=%d\n", out); fprintf(stderr, "parent: exec echo 'hello world'\n"); execl("/bin/echo", "echo", "hello world", 0); } } }} プログラムを実行すると、 #pre{{ $ ex3 out=1 parent: exec echo 'hello world' in=0 child : exec wc 1 2 12 }} と出力されます。 - 親プロセスのパイプの出力用ファイル記述子は、1(標準出力)に割り当てられ - echo 'hello world'を実行します - 子プロセスのパイプの入力用ファイル記述子は、0(標準入力)に割り当てられ - echoの結果'hello world'を標準入力から読み込みwcを実行します wcの出力結果は、 #pre{{ $ echo 'hello world' | wc 1 2 12 }} と同じであり、親プロセスと子プロセスの間でパイプが正常に結ばれたことが確認できました。 ** パイプをつなぐサンプル [#hbf674b0] パイプのつなぎ方が分かったところで、最初に紹介したパイプのサンプルを使ってパイプを使うことのメリットについて説明します。 パイプの特徴として、以下のことが挙げられます。 - 確認しながら、パイプをつなぐ - 複雑な処理を、単純な処理をパイプでつなぐ - 途中経過を確認しながら、パイプでつなぐ *** 確認しながら、パイプをつなぐ [#mb5f3fb6] いくつかの処理を確認しながら、パイプを増やしていくのがパイプを使ったプログラムの特徴です。 最初のサンプルで、man trの結果から単語を切り出す部分をみてみましょう。 trの処理は、 - -cオプションは、指定されたパターン以外のも文字列を置換の対象ととし、 - -sオプションは、同じ文字に置換された場合、それを1個にまとめるようにします この結果、英字以外の文字列は、改行(\n)1個に置き換わり、1行に1個の単語が抽出されます。 実際にその処理をみてみましょう。 #pre{{ $ man tr | tr -cs 'A-Za-z' '\n' TR BSD General Commands Manual TR 途中省略 Std POSIX standard BSD July BSD }} と出力されます。 単語が抽出できたことを確認し、同じ単語を1つにまとめます。 sortコマンドの - -uオプションは、入力をソートするとき重複する行を1つにまとめます を追加して重複を取り除きます。 #pre{{ $ man tr | tr -cs 'A-Za-z' '\n' | sort -u A ALL AM AN AR ASCII AT Additionally Any As B BI BSD 以下省略 }} と出力され、単語とは思えない1または2文字が含まれていることが分かります。 これは、manページは、 &ref(man.jpg); のように文書整形コマンドによって、強調文字、アンダーラインが付加付加されています。 これをターミナルに出力するために、制御コードが付加されているためです。 #pre{{ $ man tr | od -c | more 0000000 T R ( 1 ) 0000020 B S D G e 0000040 n e r a l C o m m a n d s M 0000060 a n u a l 0000100 T R ( 1 ) \n \n 0000120 N \b N A \b A M \b M E \b E \n 0000140 t \b t r \b r - - t r a n 0000160 s l a t e c h a r a c t e r s 0000200 \n \n S \b S Y \b Y N \b N O \b O P \b 0000220 P S \b S I \b I S \b S \n 0000240 t \b t r \b r [ - \b - C \b C c \b 0000260 c s \b s u \b u ] _ \b s _ \b t _ 0000300 \b r _ \b i _ \b n _ \b g _ \b 1 _ 0000320 \b s _ \b t _ \b r _ \b i _ \b n _ \b 0000340 g _ \b 2 \n t \b t r \b r 以下省略 }} のように強調文字では、 - N \b Nのように2度表示 アンダラインでは、 - _ \b s のように先に_を表示し、バックスペースでもどし、文字を出力 しているのです。 正しい結果がでるようにするには、バックスペースとその前の文字を削除しなければなりません。 そこで、sedコマンドを追加します。(以下のコマンドで^Hは、Ctrl-vの後にCtrl-Hを入力) #pre{{ $ man tr | sed -e 's/.^H//g' | tr -cs 'A-Za-z' '\n' | sort -u | more A ALL ASCII Additionally Any As BSD C COLLATE COMPATIBILITY CTYPE }} とただし単語が抽出できました。 確認が終わった後で、moreをwc -lに変えると #pre{{ $ man tr | sed -e 's/.^H//g' | tr -cs 'A-Za-z' '\n' | sort -u | wc -l 436 }} 正しい単語の数を得ることができました。 このように一つずつ確認しながらパイプをつなぐことによって短時間に正しい結果を得ることができます。 ** sh でのパイプの使われ方 [#j46d5541] *** パイプ(|) [#r0f82b15] *** バックアクセント(`) [#p3d27cbb] *** ヒアドキュメント(<<EOF) [#u89a4f77] ** 簡単シェルスクリプト [#vf5f7efd] *** おさらい [#b051ca79] - リダイレクト - ファイル展開 - シェル変数と環境変数 - 特殊変数 - クォーティング *** おきまりのパターン [#a20dec8e] - 引数チェック - 各ファイルに対する繰り返し - 制御コマンド -- if文 -- while文 -- testコマンド ** シェルスクリプトTIP集 [#x2bd3c3c] - パイプをまとめる括弧 - バックグラウンド処理 - 縦をよこにする - 一意なファイル名の生成 - 出力の結合 - 特殊ファイル 皆様のご意見、ご希望をお待ちしております。 #comment