[インデックス 18469] ファイルの概要
このコミットは、Goランタイムのネットワークポーリング(netpoll
)におけるI/Oイベントのトリガーメカニズムに関するリファクタリングです。特に、Solarisオペレーティングシステムに特化していたレベルトリガーI/Oの扱いを汎用化し、netpollarmread
とnetpollarmwrite
という2つの関数をnetpollarm
という単一の関数に統合することで、コードの重複を排除し、インターフェースの一貫性を向上させています。
コミット
commit 2ea859a7797472d6c7e401057d313c1d468a7a09
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Feb 12 22:24:29 2014 +0400
runtime: refactor level-triggered IO support
Remove GOOS_solaris ifdef from netpoll code,
instead introduce runtime edge/level triggered IO flag.
Replace armread/armwrite with a single arm(mode) function,
that's how all other interfaces look like and these functions
will need to do roughly the same thing anyway.
LGTM=rsc
R=golang-codereviews, dave, rsc
CC=golang-codereviews
https://golang.org/cl/55500044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2ea859a7797472d6c7e401057d313c1d468a7a09
元コミット内容
runtime: refactor level-triggered IO support
Remove GOOS_solaris ifdef from netpoll code,
instead introduce runtime edge/level triggered IO flag.
Replace armread/armwrite with a single arm(mode) function,
that's how all other interfaces look like and these functions
will need to do roughly the same thing anyway.
変更の背景
このコミットの主な背景には、GoランタイムのネットワークI/Oポーリングメカニズムの改善と、コードの汎用性および保守性の向上が挙げられます。
-
Solaris固有のI/O処理の抽象化: 以前のコードでは、Solarisオペレーティングシステムに特化したレベルトリガーI/Oの処理が
#ifdef GOOS_solaris
というプリプロセッサディレクティブによって直接埋め込まれていました。これは、コードの可読性を損ない、他のOSでのI/Oモデルとの一貫性を欠いていました。このコミットは、Solaris固有の条件分岐を削除し、ランタイム全体で利用可能な「エッジ/レベルトリガーI/Oフラグ」を導入することで、より抽象的で汎用的なI/O処理の枠組みを構築しようとしています。これにより、将来的に他のOSがレベルトリガーI/Oを必要とする場合でも、コードの変更を最小限に抑えることができます。 -
関数インターフェースの一貫性:
netpollarmread
とnetpollarmwrite
という2つの関数が存在していましたが、これらは本質的に類似した操作(I/Oイベントの監視を「アーム」する、つまり有効にする)を行っていました。Goランタイムの他のインターフェースが単一の関数でモードを引数として受け取る形式であることに倣い、これらをnetpollarm(mode)
という単一の関数に統合することで、APIの一貫性を高め、コードの重複を削減しています。これは、コードベース全体の設計原則に合致させるためのリファクタリングです。 -
コードの簡素化と保守性向上: 上記の変更により、
netpoll.goc
内の条件付きコンパイルブロックが削除され、netpollarm
という統一されたインターフェースが提供されます。これにより、コードがより簡潔になり、異なるOSやI/Oモデルに対応する際の保守が容易になります。
要するに、このコミットは、GoランタイムのI/Oサブシステムをよりクリーンで、汎用的で、保守しやすいものにするための重要なステップでした。
前提知識の解説
このコミットを理解するためには、以下の概念について知っておく必要があります。
-
Goランタイム (Go Runtime): Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクション、スケジューラ(ゴルーチンの管理)、メモリ管理、そしてネットワークI/Oの処理などが含まれます。Goの並行処理モデル(ゴルーチンとチャネル)は、このランタイムによって効率的に実現されています。
-
ネットワークポーリング (Network Polling / netpoll): GoのネットワークI/Oは、ノンブロッキングI/Oとイベント駆動型モデルに基づいて実装されています。
netpoll
は、複数のネットワーク接続(ファイルディスクリプタ)からのI/Oイベント(読み込み可能、書き込み可能など)を効率的に監視するためのメカニズムです。OSが提供するI/O多重化メカニズム(Linuxのepoll、macOS/FreeBSDのkqueue、WindowsのIOCPなど)を利用して、I/O準備ができたファイルディスクリプタを検出し、それに対応するゴルーチンをスケジューラに投入します。これにより、多数の同時接続を効率的に処理できます。 -
ファイルディスクリプタ (File Descriptor, FD): Unix系OSにおいて、ファイルやソケットなどのI/Oリソースを識別するために使用される整数値です。ネットワークI/Oでは、ソケットがファイルディスクリプタとして扱われます。
-
エッジトリガー (Edge-triggered) と レベルトリガー (Level-triggered) I/O: これは、I/O多重化メカニズム(epoll, kqueueなど)がイベントを通知する方法に関する重要な概念です。
-
レベルトリガー (Level-triggered): I/Oリソースが「準備完了」状態にある限り、イベントが繰り返し通知されます。例えば、ソケットに読み取り可能なデータがある限り、
read
イベントが通知され続けます。データが完全に読み取られるまで、またはバッファが満杯になるまで、イベントは発生し続けます。これは、イベントを処理し忘れた場合でも、次のポーリングサイクルで再度通知されるため、プログラミングが比較的容易ですが、イベントの重複処理に注意が必要です。Solarisの/dev/poll
や一部の古いselect
/poll
実装はレベルトリガーです。 -
エッジトリガー (Edge-triggered): I/Oリソースの状態が「変化した」ときにのみ、イベントが一度だけ通知されます。例えば、ソケットに新しいデータが到着したときに一度だけ
read
イベントが通知されます。そのイベントを受け取ったら、アプリケーションは利用可能なデータをすべて読み取る責任があります。もし一部だけ読み取って残りのデータを放置した場合、そのデータに対しては二度とイベントが通知されません(新しいデータが到着しない限り)。これは、イベントの処理をより厳密に制御する必要があり、プログラミングは複雑になりますが、イベントの重複通知がないため、より効率的です。Linuxのepoll
はデフォルトでエッジトリガーモードをサポートしており、高性能なサーバーアプリケーションでよく利用されます。
-
-
#ifdef GOOS_solaris
: C言語のプリプロセッサディレクティブで、GOOS_solaris
というマクロが定義されている場合にのみ、そのブロック内のコードがコンパイルされることを意味します。GOOS
はGoのビルドシステムが提供する環境変数で、ターゲットOSを示します。これは、特定のOSに依存するコードを条件付きで含めるために使用されます。 -
runtime·netpollarmread
/runtime·netpollarmwrite
: これらは、Goランタイム内部で使用される関数で、特定のファイルディスクリプタ(fd
)に対して読み取り(read
)または書き込み(write
)イベントの監視を「アーム」(有効化)する役割を担っていました。つまり、これらの関数が呼び出されることで、netpoll
システムがそのfd
からのI/Oイベントを監視し始めます。
これらの知識を前提として、コミットはSolaris固有のレベルトリガーI/Oの扱いを汎用化し、I/Oイベント監視のアーム関数を統合することで、GoランタイムのI/Oサブシステムをより洗練されたものにしていると理解できます。
技術的詳細
このコミットは、Goランタイムのnetpoll
(ネットワークポーリング)サブシステムにおけるI/Oイベントの監視方法をリファクタリングしています。主要な変更点は以下の通りです。
-
Solaris固有のレベルトリガーI/O処理の抽象化:
- 以前の
src/pkg/runtime/netpoll.goc
では、runtime_pollWait
関数内で#ifdef GOOS_solaris
というプリプロセッサディレクティブを使用して、Solarisの場合にのみruntime·netpollarmread
またはruntime·netpollarmwrite
を呼び出していました。これは、SolarisのI/O多重化メカニズム(例えば/dev/poll
)がレベルトリガーであるため、イベントを消費した後に再度アームする必要があったためと考えられます。 - このコミットでは、この
#ifdef
ブロックを削除し、代わりにif(Solaris)
というランタイムチェックを導入しています。ここでいうSolaris
は、Goランタイム内で定義されるブール型のフラグであり、現在のOSがSolarisであるかどうかを示します。これにより、コンパイル時に特定のOSに依存するのではなく、実行時にOSを判別してレベルトリガーI/Oのアーム処理を行うようになります。 - さらに、
runtime·netpollarmread(pd->fd)
とruntime·netpollarmwrite(pd->fd)
の呼び出しが、runtime·netpollarm(pd->fd, mode)
という単一の呼び出しに置き換えられています。これは、後述する関数の統合と関連しています。
- 以前の
-
netpollarmread
とnetpollarmwrite
のnetpollarm
への統合:- 以前は、読み取りイベントのアームには
runtime·netpollarmread
、書き込みイベントのアームにはruntime·netpollarmwrite
という個別の関数が使用されていました。 - このコミットでは、これらの関数を
runtime·netpollarm(uintptr fd, int32 mode)
という単一の関数に統合しています。mode
引数('r'
または'w'
)によって、読み取りイベントをアームするのか、書き込みイベントをアームするのかを区別します。 src/pkg/runtime/runtime.h
の関数プロトタイプが変更され、netpollarmread
とnetpollarmwrite
の宣言が削除され、netpollarm
の宣言が追加されています。src/pkg/runtime/netpoll_epoll.c
、src/pkg/runtime/netpoll_kqueue.c
、src/pkg/runtime/netpoll_windows.c
といった各OS固有のnetpoll
実装ファイルには、新しく追加されたruntime·netpollarm
関数のスタブが追加されています。これらのスタブは、runtime·throw("unused")
を呼び出すことで、これらのOSではnetpollarm
が使用されないことを明示しています。これは、Linux (epoll)、macOS/FreeBSD (kqueue)、Windows (IOCP) など、他の主要なOSのI/O多重化メカニズムが主にエッジトリガーであるか、またはレベルトリガーであってもイベントを消費した後の再アームが不要な設計になっているためと考えられます。Solarisのみが、このnetpollarm
関数を実際に利用する特殊なケースとして扱われます。
- 以前は、読み取りイベントのアームには
-
runtime_pollWaitCanceled
関数のコメント更新:runtime_pollWaitCanceled
関数内のコメントが更新され、この関数が「Windowsでのみ、保留中の非同期I/O操作のキャンセルが失敗した後に使用される」ことが明記されています。以前のコメントはSolaris固有の#ifdef
ブロックと関連していましたが、そのブロックが削除されたため、コメントも実態に合わせて修正されました。
これらの変更により、Goランタイムのnetpoll
コードは、OS固有のI/Oトリガーモデルの違いをより抽象化し、統一されたインターフェースを通じて処理できるようになりました。特に、SolarisのレベルトリガーI/Oの特殊性を、より汎用的な方法で扱うための基盤が整備されています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。
-
src/pkg/runtime/netpoll.goc
:runtime_pollWait
関数内の#ifdef GOOS_solaris
ブロックが削除され、代わりにif(Solaris)
というランタイムチェックが導入されました。runtime·netpollarmread(pd->fd)
とruntime·netpollarmwrite(pd->fd)
の呼び出しが、runtime·netpollarm(pd->fd, mode)
という単一の呼び出しに置き換えられました。runtime_pollWaitCanceled
関数内のコメントが更新されました。
--- a/src/pkg/runtime/netpoll.goc +++ b/src/pkg/runtime/netpoll.goc @@ -134,12 +134,9 @@ ret: func runtime_pollWait(pd *PollDesc, mode int) (err int) { err = checkerr(pd, mode); if(err == 0) { -#ifdef GOOS_solaris -\t\tif(mode == 'r') -\t\t\truntime·netpollarmread(pd->fd);\n -\t\telse if(mode == 'w') -\t\t\truntime·netpollarmwrite(pd->fd);\n -#endif +\t\t// As for now only Solaris uses level-triggered IO. +\t\tif(Solaris) +\t\t\truntime·netpollarm(pd->fd, mode); while(!netpollblock(pd, mode, false)) { \terr = checkerr(pd, mode); \tif(err != 0) @@ -152,13 +149,8 @@ func runtime_pollWait(pd *PollDesc, mode int) (err int) { } func runtime_pollWaitCanceled(pd *PollDesc, mode int) { -#ifdef GOOS_solaris -\tif(mode == 'r') -\t\truntime·netpollarmread(pd->fd);\n -\telse if(mode == 'w') -\t\truntime·netpollarmwrite(pd->fd);\n -#endif -\t// wait for ioready, ignore closing or timeouts.\n +\t// This function is used only on windows after a failed attempt to cancel\n +\t// a pending async IO operation. Wait for ioready, ignore closing or timeouts.\n while(!netpollblock(pd, mode, true)) \t;\n }
-
src/pkg/runtime/netpoll_epoll.c
,src/pkg/runtime/netpoll_kqueue.c
,src/pkg/runtime/netpoll_windows.c
:- これらのファイルに、新しい
runtime·netpollarm
関数のスタブが追加されました。これらのスタブは、対応するOSではこの関数が使用されないため、runtime·throw("unused")
を呼び出して未使用であることを示しています。
--- a/src/pkg/runtime/netpoll_epoll.c +++ b/src/pkg/runtime/netpoll_epoll.c @@ -52,6 +52,13 @@ runtime·netpollclose(uintptr fd) return -res; } +void +runtime·netpollarm(uintptr fd, int32 mode) +{ +\tUSED(fd, mode); +\truntime·throw("unused"); +} + // polls for ready network connections // returns list of goroutines that become runnable G*
(
netpoll_kqueue.c
とnetpoll_windows.c
も同様の変更) - これらのファイルに、新しい
-
src/pkg/runtime/runtime.h
:runtime·netpollarmread
とruntime·netpollarmwrite
の関数プロトタイプが削除されました。- 新しい
runtime·netpollarm
関数のプロトタイプが追加されました。
--- a/src/pkg/runtime/runtime.h +++ b/src/pkg/runtime/runtime.h @@ -893,8 +893,7 @@ int32 runtime·netpollopen(uintptr, PollDesc*); int32 runtime·netpollclose(uintptr); void runtime·netpollready(G**, PollDesc*, int32); uintptr runtime·netpollfd(PollDesc*); -void runtime·netpollarmread(uintptr fd); -void runtime·netpollarmwrite(uintptr fd); +void runtime·netpollarm(uintptr, int32); void runtime·crash(void); void runtime·parsedebugvars(void); void _rt0_go(void);
これらの変更は、GoランタイムのI/Oポーリング層における抽象化と汎用性を高めることを目的としています。
コアとなるコードの解説
このコミットのコアとなる変更は、src/pkg/runtime/netpoll.goc
内のruntime_pollWait
関数と、runtime.h
における関数プロトタイプの変更、そして各OS固有のnetpoll
実装ファイルへのruntime·netpollarm
スタブの追加です。
src/pkg/runtime/netpoll.goc
の変更
runtime_pollWait
関数は、GoのネットワークI/Oにおいて、特定のファイルディスクリプタ(fd
)がI/O準備完了になるまでゴルーチンをブロックし、イベントを待機する役割を担っています。
変更前は、この関数内に以下のようなSolaris固有の条件分岐がありました。
#ifdef GOOS_solaris
if(mode == 'r')
runtime·netpollarmread(pd->fd);
else if(mode == 'w')
runtime·netpollarmwrite(pd->fd);
#endif
これは、SolarisのI/O多重化メカニズムがレベルトリガーであるため、イベントを処理した後も、そのイベントを再度監視対象として「アーム」(有効化)する必要があったためです。#ifdef
はコンパイル時にSolarisの場合にのみこのコードを含めることを意味します。
変更後、このブロックは以下のように変わりました。
// As for now only Solaris uses level-triggered IO.
if(Solaris)
runtime·netpollarm(pd->fd, mode);
-
#ifdef GOOS_solaris
の削除とif(Solaris)
への置き換え: これは、コンパイル時ではなく、ランタイム時(実行時)にOSがSolarisであるかをチェックするように変更されたことを意味します。Solaris
はGoランタイム内で定義されるグローバルなブール変数で、現在のOSがSolarisかどうかを示します。これにより、コードがより汎用的になり、特定のOSに依存するコンパイル時ディレクティブが減少し、コードの可読性と保守性が向上します。 -
runtime·netpollarmread
/runtime·netpollarmwrite
からruntime·netpollarm
への統合: 以前は読み取りと書き込みで個別の関数が呼び出されていましたが、これらがruntime·netpollarm(pd->fd, mode)
という単一の関数に統合されました。mode
引数('r'
または'w'
)によって、読み取りイベントをアームするのか、書き込みイベントをアームするのかを区別します。これは、インターフェースの一貫性を高め、コードの重複を排除するためのリファクタリングです。
runtime_pollWaitCanceled
関数のコメント変更も重要です。以前はSolarisの#ifdef
ブロックと関連していましたが、そのブロックが削除されたため、この関数がWindowsでのみ使用されることを明確にするようにコメントが更新されました。
src/pkg/runtime/runtime.h
の変更
このヘッダーファイルは、GoランタイムのC部分で利用される関数のプロトタイプを定義しています。
変更前は、以下の2つの関数プロトタイプがありました。
void runtime·netpollarmread(uintptr fd);
void runtime·netpollarmwrite(uintptr fd);
変更後、これらは削除され、新しい統合された関数のプロトタイプが追加されました。
void runtime·netpollarm(uintptr, int32);
これは、GoランタイムのC部分のAPIが、読み取りと書き込みのアーム操作を単一の関数で提供するように変更されたことを示しています。
各OS固有の netpoll
実装ファイル (netpoll_epoll.c
, netpoll_kqueue.c
, netpoll_windows.c
) の変更
これらのファイルは、Linux (epoll)、macOS/FreeBSD (kqueue)、Windows (IOCP) など、各OS固有のI/O多重化メカニズムの実装を含んでいます。
これらのファイルには、新しく追加されたruntime·netpollarm
関数のスタブが追加されました。
void
runtime·netpollarm(uintptr fd, int32 mode)
{
USED(fd, mode);
runtime·throw("unused");
}
USED(fd, mode);
: これは、コンパイラの警告を抑制するためのマクロで、引数fd
とmode
が関数内で使用されていないことを示します。runtime·throw("unused");
: これは、この関数がこれらのOSでは実際に呼び出されるべきではないことを示すためのものです。もし誤って呼び出された場合、ランタイムパニックを引き起こします。
このスタブの存在は、runtime·netpollarm
関数が主にSolarisのようなレベルトリガーI/Oを必要とするOSでのみ意味を持ち、他の主要なOSではその機能が不要であることを明確にしています。これらのOSのI/O多重化メカニズムは、イベントを一度通知すれば、アプリケーションがすべてのデータを消費するまで再アームする必要がない(エッジトリガー)か、またはレベルトリガーであっても再アームの必要がない設計になっているためです。
これらの変更全体として、GoランタイムのI/Oサブシステムが、異なるOSのI/Oモデルの特性をより抽象化し、コードの重複を減らし、将来的な拡張性を高めるように設計されていることがわかります。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
- Goの
net
パッケージドキュメント: https://pkg.go.dev/net - Goの
runtime
パッケージドキュメント: https://pkg.go.dev/runtime - epoll(7) - Linux man page: https://man7.org/linux/man-pages/man7/epoll.7.html
- kqueue(2) - FreeBSD man page: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
- I/O Completion Ports (IOCP) - Microsoft Learn: https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports
参考にした情報源リンク
- Goのコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/
- GoのIssue Tracker: https://go.dev/issue
- Stack Overflow (I/O多重化、エッジ/レベルトリガーに関する一般的な情報): https://stackoverflow.com/
- Wikipedia (epoll, kqueue, I/O多重化): https://ja.wikipedia.org/wiki/Epoll, https://ja.wikipedia.org/wiki/Kqueue, https://ja.wikipedia.org/wiki/I/O%E5%A4%9A%E9%87%8D%E5%8C%96
- Goのソースコード (特に
src/pkg/runtime/netpoll*.c
およびsrc/pkg/runtime/runtime.h
): https://github.com/golang/go/tree/master/src/runtime - Goの設計に関するブログ記事やドキュメント (I/Oモデルに関するもの): Goの公式ブログや設計ドキュメントは、GoのI/Oモデルやランタイムの設計思想を理解する上で非常に役立ちます。