[[FrontPage]]

2008/08/12からのアクセス回数 &counter;

#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のように文字を表示し、バックスペースでもどし、再度文字を出力

アンダラインでは、
- _  \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
}}
正しい単語の数を得ることができました。

このように一つずつ確認しながらパイプをつなぐことによって短時間に正しい結果を得ることができます。

*** 複雑な処理を、単純な処理をパイプでつなぐ [#t78619d9]
例えば、WebのアクセスログからGoogleの検索でブログにアクセスしたログを抽出したいとします。
- Googleの検索からアクセスされたログには、http://www.google.co.jp/searchという文字が含まれ
- PukiWikiを使ったブログには、index.phpという文字が含まれます

この条件を使って
#pre{{
$ 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
のようにパイプでつなぐのが簡単かつ確実な方法です。特に急いでいるときには難しい表現はさけ、簡単な処理をパイプでつないでいく方が堅実です。

*** 途中経過を確認しながら、パイプでつなぐ [#i6e9f386]
2つ目のサンプル、
#pre{{
 xev | Event2Plot | Graph
}}
では、Event2PlotとGraphというシェルスクリプトを使いました。

引数やオプションが多い場合には、それをシェルスクリプトに書いて一つにまとめると便利です。
Graphは、graphコマンド((GNU plotutils パッケージに含まれています))のオプションを以下のようにまとめたものです。
#pre{{
#! /bin/sh
graph -T X -x 0 100 -y 0 100 -m 0 -S 5
}}

- 1行目の#! は、使用するシェル(コマンド)を指定するおまじないです。ここでは/bin/shを使っています
- graphのコマンドとオプションをセットしています。

エディタでGraphを入力した後に、シェルで実行できるように実行権限をセットします。
#pre{{
$ chmod +x Graph
}}

Graphを起動して、X,Yの座標をブランク区切りで入力すると、リアルタイムに座標が画面にプロットされます。
#pre{{
$ Graph
10 30
50 50
70 20
}}
これでGraphの完成です。((Macの場合MacPortのgraphはXウィンドウをサポートしていないので、xtermから-T tekオプションで起動してください))

次にxevコマンドを実行し、太い線の正方形の中で、マウスを動かしてみてください。

#pre{{
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です。
先ほどと同様に
#pre{{
#! /bin/sh
awk -F, '
/MotionNotify/ { 
        getline; 
        gsub(/\(/, "", $4);
        gsub(/\)/, "", $5);
        printf("%d %d\n", $4, 100 - $5); 
}'
}}
をEvent2Plotに入力して、
 chmod +x Event2Plot
を実行してください。

xevとEvent2Plotをぱいぷでつないで、
#pre{{
$ xev | Event2Plot
6 59
27 85
37 82
42 81
43 80
42 80
41 79
40 79
39 79
38 79
以下省略
}}
最初、ある程度マウスを動かすと画面に座標が表示されます。((ある程度バッファに溜まるまで出力されないです))

ここまで、確認できたらGraphと連結してみましょう。
#pre{{
$ xev | Event2Plot | Graph
}}

無事、最初の図と同じように出力されたでしょうか。

グラフにプロットされるデータがどのようになっているか知りたいことはありませんか。
このような場合には、teeコマンドで途中のデータをファイルに出力し、tailコマンドで表示します。

#pre{{
$ 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)の使われ方 [#j46d5541]
*** パイプ(|) [#r0f82b15]
初期のUNIXでは、ファイルシステムを跨ぐファイルのコピーには、cpコマンドが使えませんでした。
そこで、tarコマンドとパイプを使って以下のようにコピーしました。
#pre{{
$ tar cf - . | (cd /mnt/bak; tar -xf -)
}}

これまでは、()で括ってパイプを使ったことはありませんでしたが、これはグルーピングと呼ばれる
処理で、()で括ったコマンドは入出力が共通になります。((実装上は別のシェルプロセスを起動し、コマンドを実行するので、このようなことが可能になります))

*** バッククォート(`) [#p3d27cbb]
バッククォートで括られたコマンドの出力をパイプでつないで、その結果を別のコマンドの引数にします。

例えば、変数NOWに今の時刻をセットしたい場合に、
#pre{{
$ NOW=`date`
$ echo $NOW
Wed Aug 12 20:15:11 JST 2009
}}
とすると、変数NOWにdateコマンドの実行結果がセットされ、echo $NOWでセットした時刻が表示されます。

また、あるコマンドの実行結果を別のコマンドの引数にしたい場合にも便利です。
例)変数iの値に1を足した値を表示する場合
#pre{{
$ i=1
$ echo `expr $i + 1`
2
}}
これを使えば、変数の値を変える場合には、
#pre{{
$ i=`expr $i + 1`
$ echo $i
2
}}
とすればよいことが分かります。

*** ヒアドキュメント(<<EOF) [#u89a4f77]
シェルスクリプト内に記述したドキュメントにシェル変数の置換を行い、その結果を別のコマンドの入力とします。

** 簡単シェルスクリプト [#vf5f7efd]
*** おさらい [#b051ca79]
- リダイレクト
- ファイル展開
- シェル変数と環境変数
- 特殊変数
- クォーティング

*** おきまりのパターン [#a20dec8e]
- 引数チェック
- 各ファイルに対する繰り返し
- 制御コマンド
-- if文
-- while文
-- testコマンド

** シェルスクリプトTIP集 [#x2bd3c3c]
- パイプをまとめる括弧
- バックグラウンド処理
- 縦をよこにする
- 一意なファイル名の生成
- 出力の結合
- 特殊ファイル


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

#comment


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