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/EventMachine を使えば、FD_SETSIZE を超える数のネットワーク接続を同時に取り扱える。これは、Ruby 本体を介さずにデスクリプタを操作しているため。
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() を使っているため、問題あり。
- けっきょく、使用する拡張ライブラリのソースを全て確認するしかない。
- rb_thread_select() が API として色々な拡張ライブラリから呼ばれており、これが問題を引き起こしていることが多いように見える。(rb_thread_select() の引き数は fd_set 型へのポインタ)
Linux で TCP クライアントを実装する上での注意点・初級編
「ソケットを直に触るプログラムを書くのは初めてなんですが、何かアドバイスないですか?」みたいなことを聞かれたので、入門書には載ってなさそうな注意点をまとめてみる。
とは言っても、私自身、直にソケットを叩いて C や C++ でプログラムを書いていたのは遠い過去のことだし、その道の専門家でもないので、誤りや抜けてる項目に気づいた方や、よりベターな方法を知ってる方は指摘していただければありがたい。
前提
注意点まとめ
- あらゆるシステムコール/ライブラリ関数が失敗する可能性を考慮する。
- 入門書や解説本ではエラー処理を省略している場合がある。
fdopen()
を使ってソケットからFILE
構造体を作ってはいけない。- ライブラリのレベルでバッファリングされると困る。バグの元。
- そういうコードを見たことがあるので……。
SIGPIPE
に注意。SIGPIPE
のデフォルト動作はプロセスの終了。コマンドラインツールならその方が便利だが、ネットワーク通信では困る。signal(SIGPIPE, SIG_IGN)
で無視するなり、適切なハンドラーを書くなりして何とかする。
- システムコールで
EINTR
が返ってきたらリトライする。 - ブロックするシステムコール/ライブラリ関数に注意する。
write()
等の書き込み系システムコールでは、渡したデータが全て書き込まれるとは限らない。(partial write と呼ばれる動作)
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 本家と Debian の glibc パッケージ)を参考にした。
- Bug 4335 (glibc 本家)
- Debian Bug #471021 (Debian glibc パッケージ)
*1:文字幅の情報は EastAsianWidth.txt (unicode.org) を参照。
glibc の wcwidth() の「曖昧な文字幅」についての動作
glibc の wcwidth() の動作を自分の手できちんと検証したことがなかったので実験してみた。対象バージョンは 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
ということで、やはり glibc の wcwidth() だと 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 パッチがあてられていて*2、UTF-8 環境で aptitude とか w3m とかのレイアウトが崩れなくなるぜ……と思ったら全然ダメだった。
「なぜ?」と思って BTS への投稿(Bug#478884)とパッチの中身(40cjk_eastasian.dpatch)をよく見てみたら、、、これ、壊れてるね。ほとんど意味がない。
- cjkwidth コマンドを定義しているけど、無意味。
- グローバル変数(cjkwidth)に値を設定してるけど、どこからも参照してない。
- 肝になる utf8_isdouble() 関数は glibc の wcswidth() を呼んでいるだけ。
- GNU screen bug #16666 に添付されていた本来のパッチでは、グローバル変数 cjkwidth の値を見て、独自実装のテーブルを使って文字幅を判定していた。
- 本筋とは関係ないけど、なぜ wcwidth() を使わなかったんだろう?
- glibc の wcwidth() は、現時点では「East Asian Ambiguous Character Width」に対応していない。
ということで、このパッチは何も解決してない。
Debian lenny において screen を使って「レイアウトが崩れる!」と気になる方は、etch までと同様に独自ビルド等で何とかすべし。(中途半端にパッチがあたっている分、以前より状況が複雑になってるけど)
なお、GNU Screen の本家 BTS の cjkwidth パッチは取り込まれたようなので、GNU Screen の将来のリリース(4.1.0)では問題なくなるはず。
Debian の BTS に投稿しようかと思ったけど、glibc の wcwidth() で何が困るのかをうまく説明できる自信(と英語力)が無いので保留。
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 と同じ方法で可能。
Ruby 1.9 m17n リファレンス (不完全版)
以前書いた記事(Ruby 1.9 の新機能を調べてみた)の m17n がらみの箇所についてコメントやらトラックバックやらをいただいたので、もう少し調べてまとめてみた。
なお、1.9.0 リリース版ではなく、開発版(trunk r14835)で動作を確認している。