select(2)のfd_setは1024以上のfdをセットしようとすると落ちる


はじめに

ネット上の記述によってはselectは引数のfd_setが1024個のfdしか受け付けないから、1024個までのFDしか監視できないよ、と書いてあることが多いんですが、実際には 1024以上のfdを受け付けません。たとえ一個であれ1024以上の数字をFD_SETしようとすると落ちます。
結論は以上ですが、中身を追ってみたいと思います。

よく見るとselectのmanページにも載ってます。

fd_set は固定サイズのバッファーである。 負や FD_SETSIZE 以上の値を持つ fd に対して FD_CLR() や FD_SET() を実行した場合、 どのような動作をするかは定義されていない。

恐怖の未定義動作宣言がされています。

select(2)

ファイルディスクリプタやソケットディスクリプタを監視し、典型的にはサーバプログラムなどいつ着信があるかわからないプログラムが、着信を待ち受けるのによく使われるシステムコールです。

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

細かいことを置いておいて、大雑把に言ってしまうと、fd_setに登録されたfd(ファイルディスクリプタ)を監視してくれるシステムコールです。select(2)からread(2)を連携させることで、readをビジーwaitするより効率よくイベント駆動的にデータの読み込みができます。boost::asio::async_read の元祖みたいなやつですね。

fd_setとFD_SET

fd_setは名前の通りfdの集合を表す構造体です。私の手元のsys/select.hを見ると(かなり省略してます)

typedef long int __fd_mask;
# define __FD_SETSIZE 1024
# define __NFDBITS (8 * (int)sizeof(__fd_mask));

typedef struct
  {
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
  } fd_set;

読み解いて行くと、Linuxの64bit版だとすると、longは64bitなので、

typedef struct {
    long int fds_bits[16];
} fd_set;

ですので、結局、1024bit(64bit * 16個)の領域をもつ構造体になります。
そして、ここに含まれるfdを設定するのがFD_SETマクロです。

#define __FD_ELT(d) ((d) / __NFDBITS)
#define __FD_MASK(d) ((__fd_mask) (1UL << ((d) % __NFDBITS)))
#define __FD_SET(d, set) ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d)))
#define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp)

ちょっとわかりにくいのでマクロを解いていくと、

fdsetp->fds_bits[fd/64] |= (__fd_mask) (1UL << (fd % 64));

となり、要するにfdsetp(fd_set型変数のポインタ)のfd桁目のbitを立てる処理をしているわけです。

お分かりでしょうか?

そうです。fdが1024を超えた瞬間、buffer overflowで落ちます。
しかも、FD_SETSIZEはdefineでハードコードされており変更するのも恐ろしくてできません。redhatのQAページでもFD_SETSIZEを書き換えちゃダメだよと書いてあります。
ulimitの上限は今どきのメジャーディストリビューションでも1024が多いですが、ulimitで上限外すって言うのはサーバプログラム書く方からすると当たり前のチューニングだと思います。特に、dockerとか使っているとオプションで簡単に制限を外せます。
しかし、もしあなたのプログラムで使っているライブラリがselect(2)で実装されていたら、予期せず落ちることになります!

まとめ

select(2)を使ったプログラムでulimitの制限を外すぐらいディスクリプタを使うと、FD_SETで落ちます。
実際、某NWプロトコルのライブラリを使ってプログラムを書いてたらこの問題に遭遇しました。
これから低レイヤのNWプログラム書く方はselect(2)を使わないようにしてほしいものです。
お願いだからpoll(2)を使ってくれ!