ablog

不器用で落着きのない技術者のメモ

ノンブロッキング I/O について調べてみた

ノンブロッキングI/Oについて調べてみました*1。ノンブロッキングI/OはSQLの「SELECT ... FOR UPDATE NOWAIT」のイメージに近いのかなと思いました。通常は待つケースで待たずにエラーが返ってきて、アプリケーション側でエラーハンドリングしてリトライするといった使い方になるようですね。

ポイントは以下の点だと思います。

  • 通常、ノンブロッキングI/OはネットワークI/Oに使い、ディスクI/Oには使わない。
  • open()にO_NONBLOCKフラグを渡したり、 ioctl(fd, FIONBIO, 1)でノンブロッキングモードにすることができる。
  • ノンブロッキングモードでは通常はブロックされるケースで、ブロックされずにerrnoにEAGAINが設定されて返ってくる。
  • シングルスレッドで複数の接続を処理できるのでC10K問題を回避できる。

膨大な接続があるが、常に全ての接続がアクティブじゃないし、アクティブでもI/Oできる状態でないこともあるネットワークI/Oにおいて有効な方法ではないかと思いました。


Linuxシステムプログラミング

Linuxシステムプログラミング

P.26

O_NONBLOCK
可能ならば、ファイルをノンブロッキングモード(nonblocking mode)でオープンします。open()だけではなく、他のI/O操作でもプロセスがブロック(休眠、待ち状態、block、sleep)されることはありません。*2この動作が必要になるのはFIFOだけと言ってよいでしょう。

P.33

2.2.3. ノンブロッキング I/O
読み取れるデータがまだない場合に、read()がブロックされずにリターンし、データがまだないことを判断する必要がある場合もあります。この動作をノンブロッキングI/O(nonblocking I/O)と訕れると言います。))この動作が必要になるのはFIFOだけと言ってよいでしょう。

P.33

2.2.3. ノンブロッキング I/O
読み取れるデータがまだない場合に、read()がブロックされずにリターンし、データがまだないことを判断する必要がある場合もあります。この動作をノンブロッキングI/O(nonblocking I/O)と言います。ノンブロッキングI/Oでは、複数のファイルに対しも、データがまだないという原因ではブロックされずに、I/O を発行できるようになります。
この場合はerrnoがEAGAINになっているか否かが重要になります。前述のようにファイルをノンブロッキングモードでオープンし(open()に O_NONBLOCK フラグを渡した場合。「2.1.1.1 open()のフラグ」を参照)、読み取れるデータがまだ存在しない場合にread()はブロックされずにその場で-1を返し、errnoへはEAGAINを設定します。ノンブロッキングI/Oを使用する場合はEAGAINの確認が必須です。この確認を怠ると、単にデータがまだない場合と重大なエラーを混同してしまう恐れがあります。

P.36

ノンブロッキング書込み
O_NONBLOCK フラグを使用し、ファイルをノンブッキングモードでオープンすると、本来はブロックされるような書き込みの場合に、write()システムコールは-1を返し、errnoへEAGAINを設定します。書き込みは後でやり直すことになります。この状況は通常ファイルの場合では一般的に発生しません。


Linuxネットワークプログラミング

Linuxネットワークプログラミング

P.166

ソケットは、通常状態では「ブロッキングモード」になっています。ブロッキングモードでは、ソケット入出力システムコールは何らかの処理を行う準備が整うまで制御を返しません。
しかし、シングルスレッドでプログラムを記述したいときや、データが取得可能かを調べるだけのためにシステムコールを使いたい場合もあります。そのような用途のために、ソケットがブロッキングを行わない「ノンブロッキングモード」が用意されています。
一例として、connect()システムコールは通常設定(ブロッキングモード)でブロックします。シングルスレッドでのプログラミングを行っているときに、接続先が応答せずに接続タイムアウト待ちになってしまうと、connect()システムコールでブロックしたままの状態が発生し、プログラム全体が停止する可能性があります。さらにconntect()システムコールタイムアウトカーネルが決定するために、ユーザ側から任意の時間にタイムアウトすることができません。
connect()システムコール利用時にブロッキング状態を発生させずに、任意の時間でタイムアウトさせたいプログラムを書くときには、ソケットをノンブロッキングモードにする必要があります。
(中略)
Linux では、ioctl()システムコールにFIONBIOを渡すことにより、ブロッキング/ノンブロッキングの設定をできます。


そういえば、Linux の iostat の出力結果を銀行のATMに例えて説明してみる - ablog を書いたときに、

% git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git

でとってきた Linux Kernel 2.6 のソースコードが手元にあるので見よう。

  • fs/ioctl.c 468-489 行目
static int ioctl_fionbio(struct file *filp, int __user *argp)
{
	unsigned int flag;
	int on, error;

	error = get_user(on, argp);
	if (error)
		return error;
	flag = O_NONBLOCK;
#ifdef __sparc__
	/* SunOS compatibility item. */
	if (O_NONBLOCK != O_NDELAY)
		flag |= O_NDELAY;
#endif
	spin_lock(&filp->f_lock);
	if (on)
		filp->f_flags |= flag;
	else
		filp->f_flags &= ~flag;
	spin_unlock(&filp->f_lock);
	return error;
}
  • fs/ioctl.c 546-564 行目
int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd,
	     unsigned long arg)
{
	int error = 0;
	int __user *argp = (int __user *)arg;
	struct inode *inode = filp->f_path.dentry->d_inode;

	switch (cmd) {
	case FIOCLEX:
		set_close_on_exec(fd, 1);
		break;

	case FIONCLEX:
		set_close_on_exec(fd, 0);
		break;

	case FIONBIO:
		error = ioctl_fionbio(filp, argp);
		break;

*1:Linuxに限った話ではないですが、自宅の本棚はLinuxの書籍が充実しているので、Linuxに限って調べました。

*2:訳者注:ブロッキングモードでは、例えば同じファイルに対し複数のプロセスが同時にwrite(2)すると、片方のプロセスは競合するwrite(2)が完了するまで待つことになります。この状態をプロセスがスリープする、またはブロックされると言います。