Goのos.ReadはO_NONBLOCK+EAGAINでもブロックする
Go言語で/dev/kmsgのようなデバイスファイルをO_NONBLOCKフラグ付きで開いてos.File.Readしても、C言語のようにEAGAINを受け取ってすぐ制御が戻ってくることを期待するとハングする。
原因
Goのos.Read(Linux実装)は、内部のread(2)システムコールがEAGAINを返した場合、それをアプリケーション側にエラーとして返さない。代わりにfd.pd.pollable()がtrueのときfd.pd.waitReadが呼ばれ、readyになるまで(=データが来るまで)内部で待機し続ける。
GoランタイムはnetpollerというepollベースのI/Oポーリング機構を使い、ノンブロッキングI/Oをアプリケーションから見て「あたかもブロッキングI/Oであるかのように」隠蔽する設計になっている。そのため、呼び出し元はEAGAINを観測できず、O_NONBLOCKフラグを立てただけでは真にノンブロッキングな挙動にならない。
検証方法
元記事の著者は以下の手順で原因を突き止めている。
straceで追跡し、①readがO_NONBLOCK付きで実行されEAGAINを返している、②その直後にepoll_pwaitがtimeout=-1(無限待ち)で呼ばれていることを確認。gdbのcatch syscall機能で、そのepoll_pwait呼び出しがruntime.schedule → runtime.findRunnableというGoランタイムのスケジューラ内部から発生していることを突き止めた。
回避策
SetReadDeadlineでタイムアウトを設定する —os.ErrDeadlineExceededを検知して抜ける。SyscallConnインターフェースを使う — 低レベルのread(2)を直接呼び、netpollerの自動待機を経由せずEAGAINを自分でハンドリングする。