Ruby と FD_SETSIZE 問題

FD_SETSIZE 問題とは、1 つのプロセスで大量のファイルを同時に open するなどしてファイルデスクリプタの値が FD_SETSIZE *1の値を超える(fd >= FD_SETSIZE になる)と標準の fd_set 型で取り扱えなくなり、select(2) が正しく実行できなくなる、という問題。有名でわりと古典的な問題だが、今でもいろんなところに埋まっている。
たとえば、同時に大量のネットワークコネクションを取り扱いたい大規模サーバではこれが問題になる。実際にこれを踏むと、範囲外アクセスを起こして Segmentation fault で落ちることが多い。
なお、select(2) そのものの実装は(現代的な OS のカーネルであれば) FD_SETSIZE には依存しておらず、呼び出し側(ユーザランド)の問題である。
また、一般的な解決方法として、子プロセスを作ってそちらにコネクションを渡してしまい、1プロセスあたりのデスクリプタ数が FD_SETSIZE を超えないように調整するという方法がある。

Ruby 1.8 系の場合

  • Ruby 本体
    • 問題あり。大量のファイルやソケットを open すると Segmentation fault で死ぬ。
    • スレッド構造体 rb_thread に fd_set が埋まっており、これを改造すると ABI が変わるため手が出せない。
    • なお、select(2) は Kernel#select 以外に色々なところで呼び出されているので(Thread の待合わせなど)、「たくさんファイルを open しても Kernel#select を使わなければ大丈夫」という訳ではない。
  • 拡張ライブラリ
    • Ruby/EventMachine を使えば、FD_SETSIZE を超える数のネットワーク接続を同時に取り扱える。これは、Ruby 本体を介さずにデスクリプタを操作しているため。
      • が、多数のソケットを開いている状態でうっかり Kernel#open などで普通にファイルを開くと、IO#fileno >= FD_SETSIZE なオブジェクトが生成されて死ぬ。
      • 実用的とは言いがたい。

Ruby 1.9 系の場合

  • Ruby 本体
    • 問題なし。(予定)
      • 1.9.1 の時点でバグ入りだが、次のリリースで既知のバグは修正される見込み。r23109
    • fd_set 型の代わりに rb_fdset_t という独自の型が導入された。これを使っている限りは FD_SETSIZE 問題は起こらないはず。
  • 拡張ライブラリ
    • rb_thread_select() が API として色々な拡張ライブラリから呼ばれており、これが問題を引き起こしていることが多いように見える。(rb_thread_select() の引き数は fd_set 型へのポインタ)
      • 代わりに rb_thread_fd_select() を使うべし。(こちらの引き数は rb_fd_set 型へのポインタ)
    • 付属の readline.so も rb_thread_select() を使っているため、問題あり。
      • もっとも、そもそも libreadline.so が中で fd_set 型を使っているため、Ruby 側のバインディングを修正しても意味が無い。(どうせ死ぬ)
    • けっきょく、使用する拡張ライブラリのソースを全て確認するしかない。

Python の場合

Ruby じゃないけど。

  • Python 本体
    • poll(2) を使っているため、普通の open() が問題になることはなさそう。
  • 拡張ライブラリ
    • select.select() は FD_SETSIZE に依存している。
    • select.epoll() を代わりに使えば OK。(Python 2.6 以降)
    • その他のライブラリは未調査。

結論めいたもの

Ruby 1.8 系は、この点においてはダメそう。将来も期待できない。
Ruby 1.9 系および Python は、本体のみを使っている限りは大丈夫そう。ただし、拡張ライブラリについては個別にソースを確認する必要がある。

*1:Linux では 1024 であることが多い

Linux で TCP クライアントを実装する上での注意点・初級編

「ソケットを直に触るプログラムを書くのは初めてなんですが、何かアドバイスないですか?」みたいなことを聞かれたので、入門書には載ってなさそうな注意点をまとめてみる。
とは言っても、私自身、直にソケットを叩いて C や C++ でプログラムを書いていたのは遠い過去のことだし、その道の専門家でもないので、誤りや抜けてる項目に気づいた方や、よりベターな方法を知ってる方は指摘していただければありがたい。

前提

  • 対象読者は最低限の知識(TCP/IP, socket, シグナル等)を持っていることが前提。
  • Linux + GNU libc だけの環境を仮定。その他のライブラリは無い。
  • TCP, IPv4 でクライアントを実装する。

注意点まとめ

  • あらゆるシステムコール/ライブラリ関数が失敗する可能性を考慮する。
    • 入門書や解説本ではエラー処理を省略している場合がある。
  • fdopen() を使ってソケットから FILE 構造体を作ってはいけない。
    • ライブラリのレベルでバッファリングされると困る。バグの元。
    • そういうコードを見たことがあるので……。
  • SIGPIPE に注意。
    • SIGPIPE のデフォルト動作はプロセスの終了。コマンドラインツールならその方が便利だが、ネットワーク通信では困る。
    • signal(SIGPIPE, SIG_IGN) で無視するなり、適切なハンドラーを書くなりして何とかする。
  • システムコールEINTR が返ってきたらリトライする。
  • ブロックするシステムコール/ライブラリ関数に注意する。
  • write() 等の書き込み系システムコールでは、渡したデータが全て書き込まれるとは限らない。(partial write と呼ばれる動作)
    • 1024 バイトのデータを渡しても 512 バイトしか書き込まれない、ということがありうる。必ず返り値(書き込んだサイズ)を確認して、適切にバッファを管理する。
    • 本当はファイルシステムへの書き込みでも同様なことは起こりうるが、滅多に起こらないので問題として認識されていない。TCP ソケットへの書き込みでは簡単に起こせる。
続きを読む

glibc の wcwidth の話の続き

glibc の wcwidth() の「曖昧な文字幅」についての動作の話の続き。
やっぱり、UTF-8 の charmap を全部書き換えてしまうのは良くないと思い、以下のようにしてみた。
まず、/usr/share/i18n/charmaps/UTF-8.gz を改造して ambiguous width な文字の幅を 2 にしたものを /usr/share/i18n/charmaps/UTF8-8-CJK.gz として設置。*1
次に UTF-8-CJK を使って locale を再作成

 $ sudo localedef -i ja_JP -c -f UTF-8-CJK ja_JP.UTF-8

あるいは、/etc/locale.gen を以下のように書き換えて locale-gen コマンドを実行しても同じ。(Debian 特有?)

en_US.UTF-8  UTF-8
ja_JP.EUC-JP EUC-JP
ja_JP.UTF-8  UTF-8-CJK

この状態で、前回作成したプログラムを実行した結果は以下のとおり。

 $ LC_CTYPE=ja_JP.utf8 ./a.out
wcwidth('A') == 1
wcwidth('α') == 2
wcwidth('あ') == 2

 $ LC_CTYPE=en_US.utf8 ./a.out
wcwidth('A') == 1
wcwidth('α') == 1
wcwidth('あ') == 2

これなら、CJK な人も non-CJK な人もそれなりに幸せになれるのではなかろうか。
もっとも、何かの拍子に(locales パッケージをアップグレードした時とか) locale-archive が更新されて元に戻ってしまいそうだけど、その時はその時で。
なお、UTF-8-CJK な charmap を作るにあたっては、以下のバグ報告(glibc 本家と Debianglibc パッケージ)を参考にした。

*1:文字幅の情報は EastAsianWidth.txt (unicode.org) を参照。

glibc の wcwidth() の「曖昧な文字幅」についての動作

glibcwcwidth() の動作を自分の手できちんと検証したことがなかったので実験してみた。対象バージョンは Debian lenny に含まれていた 2.7-18。
実験に使ったのは以下のプログラム。

#define _XOPEN_SOURCE
#include <stdio.h>
#include <locale.h>
#include <wchar.h>

void print_wcwidth(wchar_t c)
{
  printf("wcwidth('%lc') == %d\n", c, wcwidth(c));
}

int main()
{
  setlocale(LC_CTYPE, "");
  print_wcwidth(0x41);
  print_wcwidth(0x3b1);
  print_wcwidth(0x3042);
  return 0;
}

これをコンパイルして、UTF-8 なターミナルで実行してみると、以下のようになった。

 $ LC_CTYPE=ja_JP.UTF-8 ./a.out
wcwidth('A') == 1
wcwidth('α') == 1
wcwidth('あ') == 2

 $ LC_CTYPE=en_US.UTF-8 ./a.out
wcwidth('A') == 1
wcwidth('α') == 1
wcwidth('あ') == 2

 $ LC_CTYPE=ja_JP.EUC-JP ./a.out | nkf -Ew
wcwidth('A') == 1
wcwidth('α') == 2
wcwidth('あ') == 2

ということで、やはり glibcwcwidth() だと ja_JP.UTF-8 の時に哀しいことが起こることが分かった。
次に、UTF-8 charmap (/usr/share/i18n/charmaps/UTF-8.gz) の WIDTH...END WIDTH までを書き換えて ambiguous width な文字の幅を 2 に変更した後、locale-gen コマンドでロケールデータを更新すると、以下のようになった。

 $ LC_CTYPE=ja_JP.UTF-8 ./a.out
wcwidth('A') == 1
wcwidth('α') == 2
wcwidth('あ') == 2

 $ LC_CTYPE=en_US.UTF-8 ./a.out
wcwidth('A') == 1
wcwidth('α') == 2
wcwidth('あ') == 2

ということで、期待どおり CJK な人にはありがたい結果になることが分かった。代わりに en_US.UTF-8 で哀しいことが起こっているが、CJK な人だけが使っているホストなら charmap を書き換えるというのもアリかも。
なお、実際に書き換えて運用するなら dpkg-divert で本来のファイルを退避すべきかな。

 $ sudo dpkg-divert --rename --add /usr/share/i18n/charmaps/UTF-8.gz

(後日追記) glibc の wcwidth の話の続き を書いた。

Debian lenny の screen の cjkwidth パッチが壊れてる件

あるいは「Unicode の曖昧な文字幅(ambiguous width)問題 その3」。*1
Debian lenny の screen にはGNU screen bug #16666の cjkwidth パッチがあてられていて*2UTF-8 環境で aptitude とか w3m とかのレイアウトが崩れなくなるぜ……と思ったら全然ダメだった。
「なぜ?」と思って BTS への投稿(Bug#478884)とパッチの中身(40cjk_eastasian.dpatch)をよく見てみたら、、、これ、壊れてるね。ほとんど意味がない。

  1. cjkwidth コマンドを定義しているけど、無意味。
  2. 肝になる utf8_isdouble() 関数は glibcwcswidth() を呼んでいるだけ。
    • GNU screen bug #16666 に添付されていた本来のパッチでは、グローバル変数 cjkwidth の値を見て、独自実装のテーブルを使って文字幅を判定していた。
    • 本筋とは関係ないけど、なぜ wcwidth() を使わなかったんだろう?
  3. glibcwcwidth() は、現時点では「East Asian Ambiguous Character Width」に対応していない。
    • 文字幅の定義は /usr/share/i18n/charmaps/UTF-8.gz に記述されているが、この定義は言語によらず固定、つまり en_US でも ja_JP でも同じ定義が使われる。

ということで、このパッチは何も解決してない。
Debian lenny において screen を使って「レイアウトが崩れる!」と気になる方は、etch までと同様に独自ビルド等で何とかすべし。(中途半端にパッチがあたっている分、以前より状況が複雑になってるけど)
なお、GNU Screen の本家 BTS の cjkwidth パッチは取り込まれたようなので、GNU Screen の将来のリリース(4.1.0)では問題なくなるはず。
DebianBTS に投稿しようかと思ったけど、glibc の wcwidth() で何が困るのかをうまく説明できる自信(と英語力)が無いので保留。

*1:関連: id:macks:20061001 「Unicode の曖昧な文字幅問題 その2」

*2:http://bugs.debian.org/478884

WPA Supplicant on Debian etch

WPA-PSK な無線 LAN ネットワークに繋ぐ必要があったので、Debian etch でどういう風に設定するか調べてみた。

iface eth1 inet dhcp
        wpa-driver wext
        wpa-ssid ESSID
        wpa-psk PreSharedKeyString

wpasupplicant をインストールした後、/etc/network/interfaces にこれを書いただけで動いた。
拍子抜け。しかも、wpa-driver wext はデフォルト値なので省略可能*1
以下、補足。

  • 今回のネットワークは暗号化方式に AES を使ってるんだけど、特に指定しなくとも動いた。
  • interfaces(5) との統合については /usr/share/doc/wpasupplicant/README.modes.gz を参照。
  • 複数の無線 LAN 環境の使い分けは id:macks:20070317 と同じ方法で可能。

*1:使用してる無線 LAN デバイスのドライバによって変える必要あり。(今回私が使ったのは ipw3945)

Ruby 1.9 m17n リファレンス (不完全版)

以前書いた記事(Ruby 1.9 の新機能を調べてみた)の m17n がらみの箇所についてコメントやらトラックバックやらをいただいたので、もう少し調べてまとめてみた。
なお、1.9.0 リリース版ではなく、開発版(trunk r14835)で動作を確認している。

続きを読む