FrontPage

パイプでつなぐ

1968年、UNIXの産みの親であるKen ThompsonとDennis RitchieがAT&Tのベル研究所でMulticsと呼ばれる大型のオペレーティングシステムを開発していた頃、「インタラクティブで便利なコンピュータサービス」が欲しいと言って作ったのがUNIXです。

彼らは、「ベル研の文書処理システムを作る」と言って予算を引き出し、PDP-11(システムメモリ16Kバイト、ユーザメモリ8Kバイト、ハードディスク512Kバイト)という現在のPDA以下のハードウェアを購入し、その上に現在のUNIXシステムのコマンド群とroffと呼ばれる文書処理システムを構築したのでした。

今では、Linuxの普及により誰でもUNIXの環境を持つことができるようになりました。

UNIXの3大発明

今回は、「パイプでつなく」をテーマにUNIXの偉大な大発明の中から、

について例題を交えながら説明していきます。

パイプを使った処理の例

パイプを使った処理の例として、「tr」コマンドのmanページの単語の種類をカウントします。

$ man tr | tr -cs 'A-Za-z' '\n' | sort -u | wc -l
     492

このようにUNIXのコマンドをパイプでつなくことによって簡単に必要な処理をこなすことができます。

「単語処理の例」は、別にパイプを使わなくてもファイルを使って逐次的に処理できます。 パイプがファイルと決定的に異なる点は、リアルタイムの処理です。

図は、マウスイベントを別Windowにプロットする例です。

demo_pipeline.jpg

このデモでは、xevコマンドとEvent2Plot, Graphのシェルスクリプトをパイプでつないで、xplotの画面にイベントをリアルタイムでプロットします。

$ xev | Event2Plot | Graph

パイプのつなぎ方

pipeシステムコールは、本当に不思議な関数です。自分が出力したものを自分へ送るストリームの輪(パイプ)がpipeシステムによって生成されます。 マニュアルの説明を読んだだけでpipeシステムコールの使い方を理解できる人は少ないでしょう。

pipe.jpg

fork.jpg

close.jpg

dup.jpg

パイプを実現する3つのシステムコール

パイプでつなぐときに必要なシステムコール

について、おさらいも含めて説明します。

fork システムコールのおさらい

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 システムコールの使い方

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 システムコールの使い方

同様に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.jpg

のように文書整形コマンドによって、強調文字、アンダーラインが付加付加されています。 これをターミナルに出力するために、制御コードが付加されているためです。

$ 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を入力してください

リアルタイムとまではいきませんが、途中結果が表示されます。

その他のパイプ(pipe)の使われ方

パイプ(|)

初期の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

とすればよいことが分かります。

ヒアドキュメント(<<EOF)

簡単シェルスクリプト

おさらい

おきまりのパターン

シェルスクリプトTIP集

皆様のご意見、ご希望をお待ちしております。 


*1 最初の読み込みで、len=13となっているのは、文字列の終わりの0が含まれているためです
*2 GNU plotutils パッケージに含まれています
*3 Macの場合MacPortのgraphはXウィンドウをサポートしていないので、xtermから-T tekオプションで起動してください
*4 ある程度バッファに溜まるまで出力されないです
*5 実装上は別のシェルプロセスを起動し、コマンドを実行するので、このようなことが可能になります

トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
SmartDoc