2008/08/12からのアクセス回数 36655
1968年、UNIXの産みの親であるKen ThompsonとDennis RitchieがAT&Tのベル研究所でMulticsと呼ばれる大型のオペレーティングシステムを開発していた頃、「インタラクティブで便利なコンピュータサービス」が欲しいと言って作ったのがUNIXです。
彼らは、「ベル研の文書処理システムを作る」と言って予算を引き出し、PDP-11(システムメモリ16Kバイト、ユーザメモリ8Kバイト、ハードディスク512Kバイト)という現在のPDA以下のハードウェアを購入し、その上に現在のUNIXシステムのコマンド群とroffと呼ばれる文書処理システムを構築したのでした。
今では、Linuxの普及により誰でもUNIXの環境を持つことができるようになりました。
今回は、「パイプでつなく」をテーマにUNIXの偉大な大発明の中から、
について例題を交えながら説明していきます。
パイプを使った処理の例として、「tr」コマンドのmanページの単語の種類をカウントします。
$ man tr | tr -cs 'A-Za-z' '\n' | sort -u | wc -l 492
このようにUNIXのコマンドをパイプでつなくことによって簡単に必要な処理をこなすことができます。
「単語処理の例」は、別にパイプを使わなくてもファイルを使って逐次的に処理できます。 パイプがファイルと決定的に異なる点は、リアルタイムの処理です。
図は、マウスイベントを別Windowにプロットする例です。
このデモでは、xevコマンドとEvent2Plot, Graphのシェルスクリプトをパイプでつないで、xplotの画面にイベントをリアルタイムでプロットします。
$ xev | Event2Plot | Graph
pipeシステムコールは、本当に不思議な関数です。自分が出力したものを自分へ送るストリームの輪(パイプ)がpipeシステムによって生成されます。 マニュアルの説明を読んだだけでpipeシステムコールの使い方を理解できる人は少ないでしょう。
パイプでつなぐときに必要なシステムコール
について、おさらいも含めて説明します。
forkシステムコールの仕様を簡単に書くと、
呼び出し形式 #include <unistd.h> pid_t fork(void); 機能 呼び出し元のプロセスをコピーして、新しいプロセスを生成する。 戻り値 成功すると、子プロセスには0が返され、 親プロセスには、子プロセスのプロセスIDが返されます。forkに失敗したら、-1を返します。
です。
forkでは、
ます。
簡単なプログラムで、上記の仕様を確認してみましょう。
#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); } }
以下のようにコンパイルして、実行すると
$ 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)をリダイレクトでファイルに保存します。
$ 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システムコールの仕様を簡単に書くと、
呼び出し形式 #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にエラー番号をセットする。
パイプは、
のように機能します。
簡単なプログラムで、上記の仕様を確認します。
// 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"); } }
プログラムを実行すると
$ ex2 fd[0]=3, fd[1]=4 parent finishied len=13, buf=hello world child finishied. len=0
と出力され、
ます。*1
同様にdupシステムコールの仕様を簡単に書くと、
呼び出し形式 #include <unistd.h> int dup(int fildes); 機能 既存のファイル記述子を複製し、新しい生成されたファイル記述子を返す。 プロセスには、getdtablesize()で返される大きさのファイル記述子テーブルを持ち、 新しいファイル記述子には、未使用のもっとも小さい値が返される。 戻り値 正常終了の場合、0以上の値が返され、そうでない場合には-1を返します。
となります。
UNIXのファイル記述子の割り当てルールは、
ので、特定の値のファイル記述子を割り当てたい時には、
ことで実現できます。 リダイレクトやパイプは、この簡単なルールを使って実現されています。
dupの動作を確認するために、以下のサンプルプログラムを作成します。
// 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); } }
プログラムを実行すると、
$ ex3 out=1 parent: exec echo 'hello world' in=0 child : exec wc 1 2 12
と出力されます。
wcの出力結果は、
$ echo 'hello world' | wc 1 2 12
と同じであり、親プロセスと子プロセスの間でパイプが正常に結ばれたことが確認できました。
パイプのつなぎ方が分かったところで、最初に紹介したパイプのサンプルを使ってパイプを使うことのメリットについて説明します。
パイプの特徴として、以下のことが挙げられます。
いくつかの処理を確認しながら、パイプを増やしていくのがパイプを使ったプログラムの特徴です。 最初のサンプルで、man trの結果から単語を切り出す部分をみてみましょう。
trの処理は、
この結果、英字以外の文字列は、改行(\n)1個に置き換わり、1行に1個の単語が抽出されます。
実際にその処理をみてみましょう。
$ man tr | tr -cs 'A-Za-z' '\n' TR BSD General Commands Manual TR 途中省略 Std POSIX standard BSD July BSD
と出力されます。
単語が抽出できたことを確認し、同じ単語を1つにまとめます。
sortコマンドの
を追加して重複を取り除きます。
$ 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ページが、
のように文書整形コマンドによって、強調文字、アンダーラインが付加付加されています。 これをターミナルに出力するために、制御コードが付加されているためです。
$ 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 以下省略
のように強調文字では、
アンダラインでは、
しているのです。
正しい結果がでるようにするには、バックスペースとその前の文字を削除しなければなりません。
そこで、sedコマンドを追加します。(以下のコマンドで^Hは、Ctrl-vの後にCtrl-Hを入力)
$ 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に変えると
$ man tr | sed -e 's/.^H//g' | tr -cs 'A-Za-z' '\n' | sort -u | wc -l 436
正しい単語の数を得ることができました。
このように一つずつ確認しながらパイプをつなぐことによって短時間に正しい結果を得ることができます。
例えば、WebのアクセスログからGoogleの検索でブログにアクセスしたログを抽出したいとします。
この条件を使って
$ grep http://www.google.co.jp/search /var/log/apache2/access.log | grep index.php 218.228.8.55 - - [09/Aug/2009:06:43:41 +0900] "GET /~take/TakeWiki/index.php?avr %2F%E6%9C%80%E5%88%9D%E3%81%AE%E4%B8%80%E6%AD%A9 HTTP/1.1" 200 33207 "http://www .google.co.jp/search?q=avr+isp&hl=ja&lr=lang_ja&client=firefox-a&rls=org.mozilla :ja:official&start=50&sa=N" "Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9 .1.2) Gecko/20090729 Firefox/3.5.2" 以下省略
のように簡単に出力できます。
もちろん、grepの正規表現を使って1つのgrepで検索することもできますが、少しの間違いで検索できなくなります。AかつBのような時、
$ grep A | grep B
のようにパイプでつなぐのが簡単かつ確実な方法です。特に急いでいるときには難しい表現はさけ、簡単な処理をパイプでつないでいく方が堅実です。
2つ目のサンプル、
xev | Event2Plot | Graph
では、Event2PlotとGraphというシェルスクリプトを使いました。
引数やオプションが多い場合には、それをシェルスクリプトに書いて一つにまとめると便利です。 Graphは、graphコマンド*2のオプションを以下のようにまとめたものです。
#! /bin/sh graph -T X -x 0 100 -y 0 100 -m 0 -S 5
エディタでGraphを入力した後に、シェルで実行できるように実行権限をセットします。
$ chmod +x Graph
Graphを起動して、X,Yの座標をブランク区切りで入力すると、リアルタイムに座標が画面にプロットされます。
$ Graph 10 30 50 50 70 20
これでGraphの完成です。*3
次にxevコマンドを実行し、太い線の正方形の中で、マウスを動かしてみてください。
MotionNotify event, serial 23, synthetic NO, window 0xa00001, root 0x1fd, subw 0x0, time 236035801, (68,77), root:(298,252), state 0x0, is_hint 0, same_screen YES MotionNotify event, serial 23, synthetic NO, window 0xa00001, root 0x1fd, subw 0x0, time 236035818, (66,106), root:(296,281), state 0x0, is_hint 0, same_screen YES
のようにMotionNotify eventの次の行に(68,77)のようにマウスの矩形内の座標(左上が原点)が含まれています。
これをawkを使ってX Y座標に変換しているのが、Event2Plotです。 先ほどと同様に
#! /bin/sh awk -F, ' /MotionNotify/ { getline; gsub(/\(/, "", $4); gsub(/\)/, "", $5); printf("%d %d\n", $4, 100 - $5); }'
をEvent2Plotに入力して、
chmod +x Event2Plot
を実行してください。
xevとEvent2Plotをぱいぷでつないで、
$ xev | Event2Plot 6 59 27 85 37 82 42 81 43 80 42 80 41 79 40 79 39 79 38 79 以下省略
最初、ある程度マウスを動かすと画面に座標が表示されます。*4
ここまで、確認できたらGraphと連結してみましょう。
$ xev | Event2Plot | Graph
無事、最初の図と同じように出力されたでしょうか。
グラフにプロットされるデータがどのようになっているか知りたいことはありませんか。 このような場合には、teeコマンドで途中のデータをファイルに出力し、tailコマンドで表示します。
$ xev | Event2Plot | tee /tmp/1 | Graph & $ tail -f /tmp/1 58 50 65 54 73 65 72 71 62 80 57 81 途中省略 終了するには、Ctrl-Cを入力してください
リアルタイムとまではいきませんが、途中結果が表示されます。
初期のUNIXでは、ファイルシステムを跨ぐファイルのコピーには、cpコマンドが使えませんでした。 そこで、tarコマンドとパイプを使って以下のようにコピーしました。
$ tar cf - . | (cd /mnt/bak; tar -xf -)
これまでは、()で括ってパイプを使ったことはありませんでしたが、これはグルーピングと呼ばれる 処理で、()で括ったコマンドは入出力が共通になります。*5
例えば、変数NOWに今の時刻をセットしたい場合に、
$ NOW=`date` $ echo $NOW Wed Aug 12 20:15:11 JST 2009
とすると、変数NOWにdateコマンドの実行結果がセットされ、echo $NOWでセットした時刻が表示されます。
また、あるコマンドの実行結果を別のコマンドの引数にしたい場合にも便利です。 例)変数iの値に1を足した値を表示する場合
$ i=1 $ echo `expr $i + 1` 2
これを使えば、変数の値を変える場合には、
$ i=`expr $i + 1` $ echo $i 2
とすればよいことが分かります。
皆様のご意見、ご希望をお待ちしております。