2011年4月3日日曜日

第14回 V7から始めるUNIX講座 復習とまとめ(パイプ)

第14回 V7から始めるUNIX講座 復習とまとめ(パイプ)

前回の補足というか余談

System360ではアドレスは24bitしかなかった。データは32bit
上位の8ビットをタグに使っていたりしたが、ハードウエアが変わったらプログラムが動かなくなった。

68000でもアドレスは24bitしかなかったので、上位の8bitをタグに使ったりしていたプログラムがあったけど、それは68020になったときに動かなくなった。
→あるものは使うというのは共通のようです(^^;)

シグナルを送る命令が「kill」というのは
→おそらくsignalという機能はプロセスを殺すためだけにあったのだろう。その後に色々な種類が追加されたと思われます。

signalは実行中のシステムコールを中断することがある。そのときシステムコールはEINTRを返す。
→なのでシステムコールを呼ぶような処理を書くときはEINTRが返ってくる場合の再実行を実装する必要があります。
例として以下のサイトを参考にして下さい。
http://kzk9.net/column/write.html

シグナル(非同期通信)という言葉から、プロセス同士が何かやりとりしているようなイメージを抱きますが
実際は相手のプロセスのu構造体の中身を書き換えているだけ。対象のプロセスが再開するときにシグナルがあったことを認識して処理を行います。
→もともとプロセス間通信とはそういうものだそうです。



●パイプ
さて、今回は目からうろこが落ちまくりのパイプです。

例えば
$ ls | wc -l
$ sort file | uniq

というパイプ"|"でコマンドの入力と出力を接続することが出来ます。
こういった考え方が生まれたのはメモリ空間が小さいため、大きなものが作れないため、小さいものをつなぐ形になったと考えられます。

・MS-DOSの擬似パイプ
MS-DOSにもパイプの機能はありますが、これは後ろでこっそり一時的な中間ファイルを介しています。

・ファイルディスクリプタ(FD)
Openシステムコールを実行するとFile Descriptor(FD)と呼ばれる小さな整数(V7の場合は0~19)が返ってきます。
これらはプロセス単位で管理しています。
u構造体の中に20個の配列で保持しています。これが最大20個までのファイルを指しています。

user構造体
http://www.tamacom.com/tour/kernel/unix/S/80.html

53行目:NOFILEは20です。struct file(ファイル構造体)でOpenしているファイルを管理しています。



53 struct file *u_ofile[NOFILE]; /* pointers to file structures of open files */



file構造体
http://www.tamacom.com/tour/kernel/unix/S/58.html#L8

file構造体は以下のような構造です。
12行目にinode構造体がありますので、これで小さな整数(0~19)とファイル(実体はinode)の結びつけを行っています。
詳細は後述します。


8 struct file
9 {
10 char f_flag;
11 char f_count; /* reference count */
12 struct inode *f_inode; /* pointer to inode structure */
13 union {
14 off_t f_offset; /* read/write character pointer */
15 struct chan *f_chan; /* mpx channel pointer */
16 } f_un;
17 };



標準入力、標準出力、標準エラー出力
FDは0~19までですが、0,1,2は既に割り付けられています。
割付はinitとgettyで行います





種類普通の接続先CFD
標準入力キーボードstdin0
標準出力画面stdout1
標準エラー出力画面stderr2





●リダイレクション

$ ls > file
とすればlsの結果がfileに書き込まれます。

親プロセス(Shell)がforkで子プロセスを生成して子プロセスがexecでlsになります。
execの前に子プロセスの標準出力(1)をCloseして、fileをOpenするとFDの1番はfileを指します。
ls自体は標準出力(1)に出力しているつもりですが、実際はfileに書き込まれます。

画像


●ファイルディスクリプタとファイル構造体とinodeの関係
Unixのread, writeシステムコールはシーケンシャルアクセスを前提としていてoffsetを明示的に指定しません。
今どこまで読み書きしたかという情報はFile構造体の中に格納されています。

先ほど出てきたfile構造体のf_offsetがoffsetになります。
lseekはこのoffsetの値を返します。


14 off_t f_offset; /* read/write character pointer */



file構造体にinodeを格納しています。

以下のような関係になります。

画像


●pipe
pipe(2)システムコールを見ていきます。
まずはマニュアルを
http://plan9.bell-labs.com/7thEdMan/bswv7.html
v7vol1.pdf
P245です。

pipe ? create an interprocess channel
pipe(fildes)
int fildes[2];

int型の配列(要素数が2)を渡します。

The pipe system call creates an I/O mechanism called a pipe. The file descriptors returned can be used
in read and write operations. When the pipe is written using the descriptor fildes[1] up to 4096 bytes of
data are buffered before the writing process is suspended. A read using the descriptor fildes[0] will pick
up the data. Writes with a count of 4096 bytes or less are atomic; no other process can intersperse data.

(要約)
パイプシステムコールはpipeと呼ばれるI/Oメカニズムを作成します。リードライト操作できるファイルディスクリプタを返します。
ファイルディスクリプタ:fildes[1]に4KByte書くと止まります。
ファイルディスクリプタ:fildes[0]を使ってデータを読みます。
(4KByte読むと止まる)
書き終わる(書くものがなくなる)とEOFが返ります。

協調してリードライトが出来るわけです。

画像


ソースコードを見てみます
http://www.tamacom.com/tour/kernel/unix/S/91.html#L25

31行目:iallocでinodeを生成します。
パラメータで「pipedev」を指定しています。inodeはファイルシステム「pipedev」上に作成されます。
34行目:Read用のファイル構造体を作成します。★
39行目:Read用のFDをrに退避します。
40行目:Write用のファイル構造体を作成します。★
47行目:user構造体のシステムコールのリターン値1(Write用のFD)をリターン値2に格納します。
48行目:user構造体のシステムコールのリターン値1に退避しておいたRead用のFDを格納します。
49行目:Write用のファイル構造体のf_flagに書き込み用でパイプであることを示すフラグを立てます
50行目:Write用のファイル構造体のf_inodeに生成したinodeを格納します
51行目:Read用のファイル構造体のf_flagに読み込み用でパイプであることを示すフラグを立てます
52行目:Read用のファイル構造体のf_inodeに生成したinodeを格納します
53行目:inodeの参照カウントを2(read/write)にする
54行目:inodeのタイプをレギュラー(通常)にします
55行目:inodeのフラグに更新されたことを示すフラグを立てます

47、48行目でシステムコールのリターン値を格納する領域の
1番目:Read用のFD
2番目:Write用のFD
を格納することになります。


25 pipe()
26 {
27 register struct inode *ip;
28 register struct file *rf, *wf;
29 int r;
30
31 ip = ialloc(pipedev);
32 if(ip == NULL)
33 return;
34 rf = falloc();
35 if(rf == NULL) {
36 iput(ip);
37 return;
38 }
39 r = u.u_r.r_val1;
40 wf = falloc();
41 if(wf == NULL) {
42 rf->f_count = 0;
43 u.u_ofile[r] = NULL;
44 iput(ip);
45 return;
46 }
47 u.u_r.r_val2 = u.u_r.r_val1;
48 u.u_r.r_val1 = r;
49 wf->f_flag = FWRITE|FPIPE;
50 wf->f_inode = ip;
51 rf->f_flag = FREAD|FPIPE;
52 rf->f_inode = ip;
53 ip->i_count = 2;
54 ip->i_mode = IFREG;
55 ip->i_flag = IACC|IUPD|ICHG;
56 }


★falloc
http://www.tamacom.com/tour/kernel/unix/S/85.html#L242

247行目:user構造体のu_ofile(配列)の空いてる場所を探して、その番号(FD)を取得します。
その番号はuser構造体のu_r.r_val1(システムコールのリターン値1)に格納しておきます。
250~255行目
file構造体の配列の空いてるところを探して、u_ofileのFDが指す場所に格納します。


241 struct file *
242 falloc()
243 {
244 register struct file *fp;
245 register i;
246
247 i = ufalloc();
248 if(i < 0)
249 return(NULL);
250 for(fp = &file[0]; fp < &file[NFILE]; fp++)
251 if(fp->f_count == 0) {
252 u.u_ofile[i] = fp;
253 fp->f_count++;
254 fp->f_un.f_offset = 0;
255 return(fp);
256 }
257 printf("no file\n");
258 u.u_error = ENFILE;
259 return(NULL);
260 }


ufalloc
fallocから呼ばれます(247行目)
user構造体から空いているFDの場所を返します。

222行目:0から19までループして
223行目:u_ofileがNULLである一番最初を見つけたら
224行目:システムコールのリターン値1にi(FD)を格納
225行目:ファイルをオープンしているかのを示すu_pofileのi番目を0に設定
226行目:i(FD)を返します
228、229行目:u_ofileを全て使用している場合は、エラーを設定して(-1)を返します。


218 ufalloc()
219 {
220 register i;
221
222 for(i = 0; i < NOFILE; i++)
223 if(u.u_ofile[i] == NULL) {
224 u.u_r.r_val1 = i;
225 u.u_pofile[i] = 0;
226 return(i);
227 }
228 u.u_error = EMFILE;
229 return(-1);
230 }


●質問
pipeの実体は専用のファイルシステム(pipedev)上に生成されたinode及びデータブロックだと思って良いでしょうか?



上記でpipeの概念は分かりましたが、何故これでプロセス間で入出力を繋げられるのでしょうか?

●親子間での接続
まず親でpipeシステムコールを使用して、Read/Write用のFDを取得します。
これだと自分で書いて自分で読むだけの状態です。
この状態でforkします。
inodeは上述したように共有するのでコピーされません。
inode以外の情報はforkした子プロセスにコピーされます。(下図参照)

画像


この状態で親プロセスのRead用のFDをクローズ、子プロセスのWrite用のFDをクローズすると
親子間がパイプを通じて繋がりました。

画像





●標準出力と標準入力の接続
元々は
$ ls | wc
のようにlsの標準出力(1)とwcの標準入力(0)をつなげるという話でした。
上記の親子間の接続ではまだ繋がっていません。
それを実現するためのシステムコールが「dup」です。

では早速マニュアルを
P221です。
dup, dup2 ? duplicate an open file descriptor

dup(fildes)
int fildes;
dup2(fildes, fildes2)
int fildes, fildes2;

Given a file descriptor returned from an open, pipe, or creat call, dup allocates another file descriptor
synonymous with the original. The new file descriptor is returned.

とても簡単です。FDを引数として渡すとコピーして新しいFDを返します。
新しいFDは空いてるところの一番小さい値を返します。(これがミソ)
例えば、0,1,2を使っていたとして、dup(0)とすると3が返ります。

画像


dupはFDをコピーするだけです。これでどうしてlsの標準出力とwcの標準入力が繋がるのでしょうか?
実現するには、
・lsの標準出力(1)をpipeの書き込み用(fildes[1])に切り替え
・wcの標準入力(0)をpipeの読み込み用(fildes[0])に切り替え
を行えば実現します。

まずは標準出力の切り替えです。
1、標準出力(1)をクローズします。(これで1が空きます)
2、dup(fildes[1])を実行します。リターン値は1(最初に空いてるところを見つけるから)
→これで標準出力(1)はfildes[1]のコピーになります。つまりpipeの書き込み用を指します
3、close(fildes[1])を実行します
4、close(fildes[0])を実行します

次に標準入力側です。
1、標準入力(0)をクローズします。(これで0が空きます)
2、dup(fildes[0])を実行します。リターン値は0(最初に空いてるところを見つけるから)
→これで標準入力(0)はfildes[0]のコピーになります。つまりpipeの読み込み用を指します
3、close(fildes[0])を実行します
4、close(fildes[1])を実行します

以下の図を参照してください
画像




●リダイレクションの再説明
リダイレクションの説明のところでは、dupが出て来てなかったので詳細説明は省きました。
リダイレクションもdupを使って実現しています。

1、fileをcreateシステムコールを使用して作成(FDを得る)
2、標準出力をクローズする。(これで1が空きます)
3、dup(fd)を実行する。fdは1で取得したもの。これで標準出力(1)はfdのコピーになります。
4、close(fd)を実行する。

この作業により、標準出力の結果がファイルに書き込まれます。



●参考資料
パイプについては以下の資料を参照下さい

・UNIXカーネルの設計
P95 パイプ、P99 dup、P198 シェルについて
特にシェルについてを読むと、より分かります。

・UNIXの1/4世紀
P55 Cとパイプ:1971年から1973年
にパイプについて記載があります。パイプはThirdEdtionで実装されたようです。
パイプが動作するようになった時に、その場にいた全員がこいつはすごいと悟った
と記載があります。その当時でも素晴らしく画期的な出来事だったのが分かります。

pipeもdupも動作自体は簡単ですがそれを組み合わせることで、機能を実現しているのが驚きです。

0 件のコメント:

コメントを投稿