From: Tanaka Akira Date: 2004-12-07T20:09:20+09:00 Subject: [ruby-dev:25101] non-stdio buffering えぇと、今回 1.9 でなにが起きたのかを私が把握している範囲でまとめてお きます。 基本的には、IO のバッファリングに stdio を使うことをやめて、自前で行う ようにしたという話です。また、実装を行った私の方針や変更が終っていない ことによっていくつかの挙動が変化しています。 そもそもなぜ stdio を使いたくないのかという点については http://pub.cozmixng.org/~the-rwiki/rw-cgi.rb?cmd=view;name=stdio にいろいろ理由が載っていますが、解決されたものもあれば解決されていない ものもあります。 * [ruby-dev:22545] HP-UX では fwrite で EAGAIN が設定されない? もし HP-UX のバグが write じゃなくて fwrite にあったとすれば、ad hoc ではない形で解決できたはずです。 * [ruby-dev:23181] ungetc を使ってよいかどうか簡単には判定できない これは簡単に判定できるようになって、ungetc の失敗を確実に判定できる ようになりました。その結果、Windows で scanf のテストが成功するよう になったようです。 http://www.dm4lab.to/~usa/ruby/d/200412a.html#id20041207_P1_1 * [ruby-bugs-ja:432] [ruby-dev:22880] [ruby-talk:93917] [ruby-talk:95953] fflush() 内の write が EAGAIN のときにバッファの データを捨ててしまう 解決されており、nonblocking mode が原因でデータが消えることはなくなっ たはずです。 * [ruby-dev:24102] [ruby-dev:24137] 読み込みバッファを破棄するときの seek の失敗を検出できない これは解決されています。seek が失敗した場合は双方向ストリームとして の動作に移行します。つまり、tty などを open したときには自動的に双方 向ストリームとして動作します。 * [ruby-dev:1991] stdio 内の読み込みバッファが空かどうか判定する関数が標準にない これも解決されており、おそらく、64bit Solaris でも問題なく動作するは ずです。 * [ruby-dev:18396] fseek の前に fflush しないといけない実装がある もはこういうわけのわからない stdio の挙動にはつきあわなくてもよくなっ たので、試行錯誤してコードを書かずとも問題なく動作するようになりまし た。 * [ruby-dev:23346] Solaris でファイルを 256個以上オープンできない この制約は依然として残っています。これは、IO オブジェクトにそれぞれ 対して、ひとつ FILE 構造体を割り当てるためです。実際の所、割り当てた FILE 構造体はほとんど使っていないので、それをなくして制約を取り除く のは可能なはずです。 * [ruby-dev:25014] trap を安全かつ遅延せずに実行させることができない これは直っていませんが、どうにかできそうだという見通しはあります。 [ruby-dev:25056] * 2byte 以上 ungetc できない 2byte 以上の ungetc は stdio と同様に保証されません。 任意長の unget をサポートするためには、バッファを長くするために realloc しないといけないのですが、安全に realloc できるタイミングが わからないというのが理由です。trap がどうにかできればこれもどうにか できるかもしれません。 また、いくつか挙動が変化します。 * rb_read_pending など、外部に公開していた I/O まわりの関数のうち、 FILE* を引数に取る関数がうまく動作しない バッファは FILE 構造体とは別の所で行われるようになったため、FILE* か らはバッファにアクセスできません。そのため FILE* を受けとってバッファ を操作する関数は適切にしないようになり、deprecated となりました。 (gcc 3.1 以降では使用すると警告を出すようにしました) 原理的には、FILE* から IO オブジェクトを検索する表を管理すればどうに かできるかも知れませんが、それでどのくらい嬉しいのかはわからないため、 いまのところはそういう対処はしていません。 なお、ruby に同梱されている中では、curses で rb_read_check(stdin) を 使っている所が残っています。これをどうするかは、curses が標準入力か ら読むときに stdio のバッファを扱っているのかどうかを調べてから考え ようと思っています。 * EOF flag を実装していない stdio には EOF を既に読んだかどうかを記録している flag があるのです が、この flag は実装しませんでした。 その結果、read(0) が常に "" を返すようになりました。 (以前は、EOF flag がセットされている時には nil を返していました) また、eof? はバッファにデータがなければ必ず read を呼び出すようにな りました。以前は EOF flag がセットされている時には呼び出しませんでし た。このため、IO の対象が端末の場合には ^D などで EOF を送るまで待つ ことが以前よりも増えています。[ruby-dev:25078] この変更は [ruby-dev:22334] で主張し、[ruby-dev:22343] で受け入れら れた EOF flag を除去するという方針を最後まで進めたものです。 * ひとつの IO オブジェクトにはひとつの file descriptor を持つ いままでの Ruby は双方向 popen を実現するのにふたつの単方向 pipe を 使っていました。そのために、ひとつの IO オブジェクトにふたつの file descriptor が入っていることがありました。 しかし、この file descriptor と IO オブジェクトのくいちがいは繁雑で あるうえ、file descriptor に対する fcntl などでは適切な動作がなんな のか不明瞭なことがありました。 そのため、今回は socketpair というもの双方向 popen を実装しすること によって、ひとつの file descriptor で済まし、ふたつの file descriptor を持つ機構を除去しました。 これの最も大きな利点は、コードが明瞭になったことだと思うのですが、 それ以外の利点としては [ruby-talk:122649] で述べている双方向パイプに 対しても fcntl でアクセスモードが得られないという問題が解決するといっ たことが思い当たります。 しかし、これには問題もあって、Windows には socketpair がないため、 Windows では双方向 popen が動かないようになってしまいました。 ただ、Windows でも TCP で (無理矢理) socketpair のようなものを実装す ることは可能なので、それが実装されたときには双方向 popen も復活する のではないかと想像しています。 なお、Windows で socketpair を実現すると、これはパイプと違って select が効くので、timeout が効き、readpartial のテストが刺さるのを 解決する糸口になるかもしれないなどの可能性があります。 * text mode を考慮していない text mode は気にしていないため、text mode に関係する挙動がどうなって いるかは不明です。いまのところ具体的にどういうトラブルが起こるのかも 不明です。 * nonblocking mode における IO#read の挙動の変化 nonblocking mode で IO#read を使用した場合、EAGAIN に出会っても即座 には返らず、blocking mode と同様に指定した長さに達するまで読むように なりました。これは [ruby-dev:17866] での判断とは異なります。 私があえてこうしたのは、O_NONBLOCK をより簡単に使うためです。 O_NONBLOCK は write でプロセス全体がブロックすることを防ぐためには必 須なのですが、O_NONBLOCK を設定したことによって IO#read の挙動が変わ ると、IO#read を使っているプログラムでは O_NONBLOCK を気楽に設定する わけにはいきません。しかし、nonblocking mode でも blocking mode と同 様に動作するのであれば、ブロックしちゃったらとりあえず O_NONBLOCK を 設定してみる、ということが気楽にできるようになります。 また、O_NONBLOCK は file descriptor (正確には file table のエントリ) を共有しているプロセス間で共有されるため、他のプロセスが勝手に変えて しまう可能性があります。そのようなことがあると、Ruby スクリプトが感 知できない所で IO#read の挙動が変わってしまうため、IO#read がどちら の挙動をとるとも期待できなくなり、期待しなかったほうの挙動が起こると、 プログラムが不適切な動作をするようになってしまいます。この危険性を除 去するためにも、blocking/nonblocking の違いに依存して挙動を変えるこ とはしないようがいいと思います。 ただし、もともと nonblocking mode の IO#read の仕様は、到着してるデー タだけを読みたいという要求から決まったわけで、その要求はそれはそれで どうにかしないといけません。しかし、1.9 にはすでに readpartial があ り、それで問題なく解決できるはずです。ただ、readpartial は 1.8 に入っ ていないので、現在のままだと 1.8 と 1.9 の両方で問題なく動作するプロ グラムを書くには場合わけが必要になってしまいます。そこで、1.8 にはそ のうち readpartial をいれて、IO#read で挙動が変わるところを利用した 時 (EAGAIN になった時) には警告を出すようにしたらいいんじゃないかと 思っています。 また、現在の実装は最終的なものではなく、まだ変化する可能性があります。 具体的には次のことを考えています。 * OpenFile 構造体から f を除去する こうすると、FILE 構造体を使わなくなるため、Solaris での 256個制限が 回避できるはずです。 ただし、これを行うと、ext/openssl/ossl_bio.c の BIO_new_fp のように 拡張ライブラリで GetOpenFile を使って FILE* を得ている部分で困るかも 知れません。まぁ、その部分で fdopen すればいいのではないかという気も するのですが、使っているところのメンテナと相談したい所です。 また、これを行うと popen(3) を使うコードを捨てる必要があります。しか し、じつは最近の Ruby ではそこの部分にバグがあって、コンパイルできな かったはずです。したがって、捨てても大きな影響はないはずだと考えてい ます。 (popen を使うのは HAVE_FORK も _WIN32 も定義されていない時なのですが、 そこで fpr という変数を使っていたにもかかわらず、その変数は _WIN32 が定義されているときしか定義されていませんでした) * text mode をどうにかする? Unix では関係ないのですが、それ以外での text mode をどうにかする必要 があるかも知れません。 いまのところどういう問題が生じているのかも不明ですが、今まで stdio がやっていたような処理 (というのが何なのかよくわかっていないのですが) を行うべきか、あるいは chomp などを使ってスクリプトレベルで処理した ほうがいいのか議論が必要でしょう。 * 安全な trap の処理 まぁ、これはやっていけない理由はないと思うのでとくに議論は必要ないと 思います。実現されたときには trap がもうちょっと安定して使用できるよ うになる、はずです。 -- [田中 哲][たなか あきら][Tanaka Akira]