[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[FDclone-users:00997] Re: Re: パイプ先を間違えることができない?



 しらいです。

In Message-Id <20120713093746.E5DF24806A7@yuka.unixusers.net>
        Takashi SHIRAI <shirai@unixusers.net>writes:
> しらいです。

> posixsh.c:gettermio() の頭にある if 条件節から
> ttypgrp == pgrp という boolean を削除します。
> 多分これだけで解消する筈。
> 解説は追って帰宅後に。

 帰宅したので解説させて頂きます。まずは先に上記の修正を施す
patch です。昨日の分は不要なので捨てて下さい。前半部は上の記
述どおりですが、後半部は後ろの方で解説します。

---- Cut Here ----
diff -u ../old/FD-3.00l/posixsh.c ./posixsh.c
--- ../old/FD-3.00l/posixsh.c	2012-06-30 00:00:00.000000000 +0900
+++ ./posixsh.c	2012-07-14 00:53:05.000000000 +0900
@@ -112,8 +112,7 @@
 	int ret;
 	sigmask_t mask, omask;
 
-	if (ttypgrp < (p_id_t)0 || pgrp < (p_id_t)0 || pgrp == ttypgrp)
-		return(0);
+	if (ttypgrp < (p_id_t)0 || pgrp < (p_id_t)0) return(0);
 	else if (!job) {
 		ttypgrp = pgrp;
 		return(0);
diff -u ../old/FD-3.00l/system.c ./system.c
--- ../old/FD-3.00l/system.c	2012-06-30 00:00:00.000000000 +0900
+++ ./system.c	2012-07-14 03:49:17.000000000 +0900
@@ -2769,7 +2769,7 @@
 	if (trapok >= 0) trapok = 0;
 
 # ifndef	NOJOB
-	if (mypid == orgpgrp) {
+	if (mypid == orgpgrp && (childpgrp < (p_id_t)0 || pid == childpgrp)) {
 		VOID_C gettermio(orgpgrp, jobok);
 #  ifdef	FD
 		checkscreen(-1, -1);
---- Cut Here ----

 これだけで症状が再現しなくなる筈です。では以下解説。


 fork() と waitpid() の順序が期待通りにならないケースが原因
という昨日の説自体に間違いはありませんし、昨日の patch も別
に問題がある訳ではありません。単に無駄なことしてるだけで。
 Linux kernel の fork() の実装は、thread 処理だかなんだか余
計なことを色々している関係で、一般的に期待される処理順序と大
きく外れるタイミングで処理されることがままあります。
 Linux 以外で再現しなかったのはそういう理由で、普通は fork()
の親子で処理完了のタイミングがそう大きく異なることがないため、
期待通りの処理順序となり、bug は表面化しません。

 では、その期待通りケースと期待を裏切るケースとの処理の流れ
を図示してみます。
[期待通り]
p0 -+--> %1 --+--> %2 ----> *3 --------> *4
c2  |         +--> *2 --> X
c1  +--> *1 -------------------------> X
[期待外れ]
p0 -+--> %1 --+--> %2 ----> *3 --------> *4
c2  |         +--> *2 --> X
c1  +-------------------------> *1 --> X
 縦軸は process で横軸が時間経過です。縦軸は上から順に親、
pipe 後段、pipe 前段の各 process です。前後が逆なのは単に図
の見易さのためだけなので気にしないで下さい。
 「+」のところで fork() して処理が分岐します。「X」は子が死
ぬところで、*1-*4 が制御端末の移管処理です。gettermio() とい
う関数がその処理を行ないます。
 pipe 処理の場合、c1, c2 は process group を形成していて c1
が session leader になるので、*1, *2 はどちらも c1 に移管し
ようとします。
 *3, *4 は waitpid() で親が子を看取った後の処理なので、どち
らも p0 に移管しようとします。

 さて、こう書くと *1, *2 及び *3, *4 は同じ処理をしようとし
ているので、処理が無駄になることが判ると思います。私もそう考
えていて、同じだったら処理を省略することにしていました。
 このために、移管した先の process を ttypgrp という変数で保
持していた訳です。上記 patch の修正前の箇所で == で比較して
いるのがそれです。
 ところがこれがいけなかった。

 上図の %1, %2 は fork() 後の親の処理ですが、ここでは子が正
しく制御端末の移管を完了していると信じ、無条件に ttypgrp の
値に c1 を代入していました。
 ところが、現実には後者のケースが存在した訳で、このケースで
は実際の制御端末の所有者と ttypgrp の値が一致しません。昨日
の patch はこの一致の為に %1, %2 で待つ処理を入れています。
 この処理は、後者のケースを許さないという対処法になります。
これはこれで正しいのですが、待つと言ってもいつまで待てば良い
のか保証がないし、しかもコードが複雑になります。

 今日の patch はもっと simple です。無駄を排除しないで、必
要な process にその都度制御端末を移管します。そうすることで、
処理順序に関係なく適切に制御端末を移管出来る訳です。
 例えば後者の *2 では、%1 で c1 に制御が移ったと思い込んで
いる p0 のせいで c2 もそう思い込んでいます。従来はそのせいで
移管を省略していましたが、省略しなければ問題なくなります。


 さて、実は ttypgrp には他に二つの役割があります。負数の場
合には端末制御が行なわれていないことを示し、自分以外を指して
いる場合には制御端末を失っていることを示します。
 今回の patch ではこの二つの役割に対しては配慮しないことに
なりますので、それで支障が生じないかどうかをこれ以降で検証し
ておきます。
 みなさんとっくに読み飛ばしているかも知れませんが、ここまで
読み進めた方はもう暫くおつき合い下さい。

 まず負数のケースですが、この場合は端末制御しないので移管も
当然行ないません。なので常に負数のままで、状況には何ら影響を
及ぼしません。実際は shell script 等のケースですね。
 続いて自分を指しているか否かの問題です。上図の後者ではこの
点に関して実際に誤認が生じています。このことが支障を生じさせ
ないでしょうか?

 %1 -> %2 の期間、実際には p0 に制御がありますが p0, c2 は
c1 に制御が移管済と思い込んでいます。c2 に関しては生後すぐに
gettermio() を呼ぶので誤認期間が短く特に支障ありません。
 p0 に関しては「自分のものなのに他人のものだと認識」という
間違い方なので支障ありません。逆だと他人のものを無理に使おう
としてしまい支障を生じますが、使えるのに使わないだけです。
 そもそも fork() 後の親の役割は子を待って看取るのが大きな仕
事で、仮に制御端末を持っていたとしてもそれを活用するような仕
事はありません。使えなくても何ら支障ないでしょう。

 もう一つの誤認は *1 -> *4 の期間で発生します。この間、実際
には c1 に制御がありますが、p0 は *3 で自分に戻したので自分
のものだと誤認し続けてしまいます。
 さっきの「逆だと支障あり」のケースですね。しかし、これはそ
もそも *3 の時点で自分に戻すのがおかしい訳です。ということで、
*4 の時のみ戻すようにしたのが今日の patch の後半部です。
 前半部だけ適用しても bug 回避に繋がりますが、実は後半部だ
け適用しても bug は回避出来ます。*4 で移管を省略した元凶であ
る *3 をやめる訳ですから。

 今回のケースだとどちらでも構わないのですが、他のケースを考
えると一方だけでは片手落ちになります。
 前者だけだと上の *1 -> *4 の期間に p0 が session leader と
して振舞うので、例えばその間に trap で仕込んだコマンドが機能
してしまいます。これは c1 が死ぬまで待つべき。
 後者だけだと上の *2 で移管が発生しないので、pipe 前段が制
御端末を持てません。具体例は思いつきませんが、そこで制御端末
を操作されると SIGTTIN/SIGTTOU になるでしょうね。

 という訳で解説でした。


 ところで、今回のケースは Linux の特殊な fork() 実装に依存
しているのでなかなか再現しにくい一面がありました。これを簡単
に再現する patch を最後に紹介しておきます。
 行数を調整して工夫したので、上記 patch の適用前でも適用後
でも大丈夫だと思います。この patch を当てるとほぼ 100% の再
現率になると思います。
 飽くまでも検証用ということでご利用下さい。当然 release に
は含めません。

---- Cut Here ----
diff -u ../old/FD-3.00l/system.c ./system.c
--- ../old/FD-3.00l/system.c	2012-06-30 00:00:00.000000000 +0900
+++ ./system.c	2012-07-14 03:49:17.000000000 +0900
@@ -2644,7 +2644,7 @@
 		if (parentfd >= 0) addmychild(pid);
 #  endif
 	}
-	else {
+	else if (tty >= (p_id_t)0 || (usleep(1000000L), 1)) {
 		if (jobok && setpgroup(mypid, childpgrp) < 0) {
 			doperror(NULL, "fatal error");
 			prepareexit(-1);
@@ -6315,7 +6315,7 @@
 # endif
 		pid = (p_id_t)-1;
 	}
-	else if ((pid = makechild(tty, ppid, -1)) < (p_id_t)0) {
+	else if ((pid = makechild(-tty, ppid, -1)) < (p_id_t)0) {
 # ifdef	FAKEUNINIT
 		fd = -1;	/* fake for -Wuninitialized */
 # endif
---- Cut Here ----

                                               しらい たかし