[インデックス 15631] ファイルの概要
このコミットは、Go言語のネットワークパッケージ(net
)における重要なリファクタリングを扱っています。特に、I/OポーリングメカニズムをGoランタイムに統合するための準備作業として、pollServer
関連のコードがfd_unix.go
からfd_poll_unix.go
へと移動され、カスタム初期化や終了処理を可能にするための変更が加えられています。
コミット
- コミットハッシュ:
b000f2286c3c9d462cf390084e1ea4ae16649fe9
- Author: Dmitriy Vyukov dvyukov@google.com
- Date: Thu Mar 7 21:44:24 2013 +0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b000f2286c3c9d462cf390084e1ea4ae16649fe9
元コミット内容
net: more refactoring in preparation for runtime integrated pollster
Move pollServer from fd_unix.go to fd_poll_unix.go.
Add pollServerInit(*NetFD) to allow custom initialization.
Add pollServer.Close(*NetFD) to allow custom finalization.
Move setDeadline() to fd_poll_unix.go to allow custom handling of deadlines.
Move newPollServer() to fd_poll_unix.go to allow custom initialization.
No logical code changes.
The next step will be to turn off fd_poll_unix.go for some platform
(I have changes for darwin/linux) and redirect it into runtime. See:
https://golang.org/cl/7569043/diff/2001/src/pkg/net/fd_poll_runtime.go
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/7513045
変更の背景
このコミットの主な背景は、Go言語のネットワークI/Oポーリングメカニズムを、より効率的でスケーラブルな「ランタイム統合ポーラスター(runtime integrated pollster)」へと移行するための準備です。
GoのネットワークI/Oは、内部的にはノンブロッキングI/Oを使用しています。しかし、Goのプログラマーから見ると、Read
やWrite
のようなI/O操作はブロッキングであるかのように振る舞います。この透過性を実現するために、Goランタイムは「ネットワークポーラー(netpoller)」と呼ばれるコンポーネントを使用しています。これは、OSが提供する効率的なI/O多重化メカニズム(Linuxのepoll
、macOS/BSDのkqueue
、WindowsのIoCompletionPort
など)を利用して、多数のファイルディスクリプタ(ネットワーク接続)の準備状況を監視します。
以前のGoのバージョンでは、このポーリングロジックの一部がnet
パッケージ内に存在していました。しかし、より高度な最適化と、ランタイムスケジューラとの密接な連携を実現するためには、このポーリング機能をGoランタイム自体に深く統合する必要がありました。これにより、I/O待ちでブロックされたゴルーチンを効率的に「パーク(park)」し、I/Oが準備できたときに「アンパーク(unpark)」してスケジューラに戻すことが可能になります。
このコミットは、その統合に向けた最初の一歩として、pollServer
というポーリング関連の主要な構造体とそのメソッドを、net
パッケージ内のより適切なファイルに移動し、将来のランタイム統合に備えてインターフェースを整理することを目的としています。特に、カスタムの初期化や終了処理、デッドライン(タイムアウト)のハンドリングを可能にすることで、プラットフォーム固有のポーリング実装をランタイムにリダイレクトする際の柔軟性を高めています。
前提知識の解説
ファイルディスクリプタ (File Descriptor, FD)
Unix系OSにおいて、ファイルやソケットなどのI/Oリソースを識別するために使用される整数値です。Goのネットワーク接続も内部的にはファイルディスクリプタとして扱われます。
ノンブロッキングI/O (Non-blocking I/O)
通常のブロッキングI/O操作では、データが利用可能になるまで(または書き込みが完了するまで)プログラムの実行が停止します。一方、ノンブロッキングI/Oでは、I/O操作がすぐに完了しない場合でも、エラー(例: EAGAIN
やEWOULDBLOCK
)を返してすぐに制御を呼び出し元に戻します。これにより、プログラムはI/O待ちの間に他の処理を実行できます。
I/O多重化 (I/O Multiplexing)
複数のI/O操作を同時に監視し、いずれかのI/Oが準備できたときに通知を受け取るメカニズムです。これにより、単一のスレッドで多数のI/O接続を効率的に処理できます。主要なI/O多重化APIには以下のようなものがあります。
select
/poll
: 比較的基本的なAPIで、監視対象のFDのリストをOSに渡し、準備ができたFDを返します。epoll
(Linux):select
/poll
よりも高性能で、大規模な接続数に適しています。イベント駆動型で、監視対象のFDの追加・削除が効率的です。kqueue
(FreeBSD, macOS, NetBSD, OpenBSD):epoll
と同様に高性能なイベント通知インターフェースです。IoCompletionPort
(IOCP, Windows): Windowsにおける高性能な非同期I/Oメカニズムです。
Goのネットポーラー (Netpoller)
Goランタイムの重要なコンポーネントで、GoのゴルーチンがブロッキングI/Oを実行しているかのように見せかけつつ、内部的にはノンブロッキングI/OとOSのI/O多重化メカニズムを組み合わせて効率的なネットワークI/Oを実現します。
- ゴルーチンがネットワークI/O操作(例:
Read
)を開始します。 - データがすぐに利用できない場合、GoランタイムはFDをノンブロッキングモードに設定し、OSのI/O多重化APIにFDを登録します。
- I/O待ちのゴルーチンは「パーク」され、そのゴルーチンを実行していたOSスレッドは他のゴルーチンを実行するために解放されます。
- OSのI/O多重化APIが、FDがI/O可能になったことをネットポーラーに通知します。
- ネットポーラーは、対応するゴルーチンを「アンパーク」し、Goスケジューラの実行キューに戻します。
- ゴルーチンはI/O操作を再開し、今度はデータが利用可能になっているため、ブロッキングすることなく完了します。
pollServer
(旧称)
このコミット以前のGoのnet
パッケージに存在した、I/Oポーリングを管理する内部的な構造体です。netFD
(ネットワークファイルディスクリプタ)がEAGAIN
(一時的にI/Oができない状態)を受け取った際に、I/Oを再試行するタイミングを決定するのを助ける役割を担っていました。
netFD
Goのnet
パッケージにおけるネットワーク接続を表す内部的な構造体です。OSのファイルディスクリプタ(sysfd
)をラップし、読み書きのデッドライン(タイムアウト)や、ポーリング関連のチャネルなどを保持します。
技術的詳細
このコミットは、Goのnet
パッケージ内のI/Oポーリングロジックを再編成し、将来的にGoランタイムのより深いレベルでポーリングを統合するための基盤を構築しています。論理的なコード変更は行われておらず、主にコードの移動とインターフェースの調整に焦点を当てています。
pollServer
の移動
- 以前は
src/pkg/net/fd_unix.go
に定義されていたpollServer
構造体と関連するメソッド(AddFD
,Evict
,Wakeup
,LookupFD
,WakeFD
,CheckDeadlines
,Run
,PrepareRead
,PrepareWrite
,WaitRead
,WaitWrite
)が、新しく作成されたsrc/pkg/net/fd_poll_unix.go
に移動されました。 newPollServer()
関数も同様にsrc/pkg/net/newpollserver_unix.go
からsrc/pkg/net/fd_poll_unix.go
に移動され、src/pkg/net/newpollserver_unix.go
は削除されました。
この移動により、pollServer
に関するコードがnet
パッケージ内でより独立したモジュールとして扱われるようになり、ランタイム統合の際にnet
パッケージの他の部分への影響を最小限に抑えることができます。
カスタム初期化と終了処理の導入
pollServerInit(*NetFD)
関数が導入されました。これは、netFD
が作成される際に、そのnetFD
に関連付けられるpollServer
の初期化をカスタムで行えるようにするためのものです。以前はnewFD
内で直接server(fd)
を呼び出してpollServer
を取得していましたが、pollServerInit
を介することで、初期化ロジックを抽象化し、将来的にランタイムが提供するポーラーに切り替える際の柔軟性が増します。pollServer.Close(*NetFD)
メソッドが追加されました。これは、netFD
がクローズされる際に、pollServer
がカスタムの終了処理を実行できるようにするためのものです。これにより、pollServer
が管理しているリソース(例: OSのポーリングメカニズムに登録されたFD)を適切に解放する機会が提供されます。
デッドラインハンドリングの移動
setReadDeadline()
,setWriteDeadline()
,setDeadline()
関数がsrc/pkg/net/sockopt_posix.go
からsrc/pkg/net/fd_poll_unix.go
に移動されました。src/pkg/net/sockopt_windows.go
にも同様の関数が追加されました。
これにより、デッドライン(タイムアウト)の管理ロジックがpollServer
のコードとより密接に連携するようになり、ポーリングメカニズムがデッドラインの期限切れをより効率的に検出・処理できるようになります。特に、pollServer
のCheckDeadlines
メソッドが、登録されたFDのデッドラインを監視し、期限切れのFDをポーリングリストから削除してタイムアウトエラーを通知する役割を担います。
論理的な変更なし
コミットメッセージに明記されている通り、これらの変更は「No logical code changes.(論理的なコード変更なし)」です。これは、既存の機能の振る舞いを変更するものではなく、コードの構造と配置を改善し、将来の大きな変更(ランタイム統合)に備えるためのリファクタリングであることを意味します。
次のステップへの言及
コミットメッセージでは、次のステップとして「fd_poll_unix.go
を一部のプラットフォーム(darwin/linux)で無効にし、ランタイムにリダイレクトする」ことが述べられています。これは、net
パッケージがOS固有のポーリング実装を直接扱うのではなく、Goランタイムが提供する汎用的なポーリングインターフェースを介してI/Oを処理するようになることを示唆しています。これにより、Goのネットワークスタックのパフォーマンスとスケーラビリティがさらに向上することが期待されます。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
-
src/pkg/net/fd_poll_unix.go
:- 新規作成されたファイル。
pollServer
構造体とそのすべてのメソッド(AddFD
,Evict
,Wakeup
,LookupFD
,WakeFD
,CheckDeadlines
,Run
,PrepareRead
,PrepareWrite
,WaitRead
,WaitWrite
)がfd_unix.go
から移動されました。newPollServer()
関数がnewpollserver_unix.go
から移動されました。sysInit()
,startServer()
,pollServerInit(*netFD)
,pollServer.Close(*netFD)
といった、pollServer
の初期化と管理に関連する関数が追加されました。setReadDeadline()
,setWriteDeadline()
,setDeadline()
関数がsockopt_posix.go
から移動されました。
-
src/pkg/net/fd_unix.go
:pollServer
構造体とその関連メソッドの定義が削除されました。runtime
パッケージのインポートが削除されました(pollServer
が移動したため)。newFD
関数内で、netfd.cr
,netfd.cw
のチャネル作成とnetfd.pollServer
の設定が、新しく導入されたpollServerInit(netfd)
の呼び出しに置き換えられました。decref
関数内で、fd.pollServer.Close(fd)
の呼び出しが追加され、netFD
がクローズされる際にpollServer
のカスタム終了処理が実行されるようになりました。server(fd)
関数が削除されました。
-
src/pkg/net/newpollserver_unix.go
:- ファイル全体が削除されました。
newPollServer()
関数がfd_poll_unix.go
に移動されたためです。
- ファイル全体が削除されました。
-
src/pkg/net/sock_posix.go
:dialTimeout
関数内で、デッドラインの設定とクリアにsetWriteDeadline
関数が使用されるようになりました。以前はfd.wdeadline.setTime
を直接呼び出していました。
-
src/pkg/net/sockopt_posix.go
:setReadDeadline()
,setWriteDeadline()
,setDeadline()
関数が削除されました。これらはfd_poll_unix.go
に移動されました。time
パッケージのインポートが削除されました。
-
src/pkg/net/sockopt_windows.go
:time
パッケージのインポートが追加されました。setReadDeadline()
,setWriteDeadline()
,setDeadline()
関数が追加されました。これは、Unix系OSと同様のデッドライン設定関数をWindowsにも提供するためです。
コアとなるコードの解説
src/pkg/net/fd_poll_unix.go
(新規ファイル)
このファイルは、GoのネットワークI/Oにおけるポーリングメカニズムの心臓部を担います。
pollServer
構造体
type pollServer struct {
pr, pw *os.File
poll *pollster // low-level OS hooks
sync.Mutex // controls pending and deadline
pending map[int]*netFD
deadline int64 // next deadline (nsec since 1970)
}
pr
,pw
: 内部的なパイプで、pollServer
へのウェイクアップ通知に使用されます。poll
: OS固有のポーリングメカニズム(epoll
,kqueue
など)を抽象化したインターフェース(pollster
)へのポインタです。sync.Mutex
:pending
マップとdeadline
へのアクセスを保護するためのミューテックスです。pending
: ポーリングを待っているnetFD
のマップです。キーはファイルディスクリプタと読み書きのモードを組み合わせたものです。deadline
: 次に期限切れになるデッドラインの時刻(ナノ秒単位)です。
newPollServer()
関数
func newPollServer() (s *pollServer, err error) {
// ... (パイプの作成、ノンブロッキング設定、pollsterの初期化など)
go s.Run() // ポーリングループを別ゴルーチンで開始
return s, nil
}
新しいpollServer
インスタンスを作成し、内部的なパイプを設定し、OS固有のポーリングフック(pollster
)を初期化します。そして、s.Run()
を新しいゴルーチンで起動し、ポーリングループを開始します。
AddFD(fd *netFD, mode int) error
メソッド
func (s *pollServer) AddFD(fd *netFD, mode int) error {
s.Lock()
// ... (FDの有効性チェック、デッドラインの更新、pollsterへのFD登録)
s.Unlock()
if wake || doWakeup {
s.Wakeup() // 必要に応じてポーリングループをウェイクアップ
}
return nil
}
指定されたnetFD
をpollServer
の監視リストに追加します。mode
は読み込み('r'
)または書き込み('w'
)を示します。デッドラインが設定されている場合、pollServer
の次のデッドラインを更新し、必要であればポーリングループをウェイクアップします。
Evict(fd *netFD) bool
メソッド
func (s *pollServer) Evict(fd *netFD) bool {
// ... (pendingマップからFDを削除、pollsterからFDを削除、WakeFDを呼び出して待機中のゴルーチンを解除)
return doWakeup
}
pollServer
の監視リストからnetFD
を削除し、そのFDで待機しているゴルーチンを解除します。主にnetFD.Close()
時に呼び出されます。
Wakeup()
メソッド
func (s *pollServer) Wakeup() { s.pw.Write(wakeupbuf[0:]) }
内部パイプに少量のデータを書き込むことで、pollServer
のRun
ループをウェイクアップします。これにより、pollServer
はすぐにポーリング状態を再評価できます。
Run()
メソッド
func (s *pollServer) Run() {
s.Lock()
defer s.Unlock()
for {
// ... (タイムアウト計算、pollster.WaitFD呼び出し)
if fd < 0 {
// Timeout happened.
s.CheckDeadlines() // タイムアウトが発生した場合、デッドラインをチェック
continue
}
if fd == int(s.pr.Fd()) {
// Drain our wakeup pipe
s.pr.Read(scratch[0:])
s.CheckDeadlines() // ウェイクアップされた場合もデッドラインをチェック
} else {
netfd := s.LookupFD(fd, mode) // 準備ができたFDを検索
if netfd == nil {
// ... (既に削除されているFDの場合の処理)
continue
}
s.WakeFD(netfd, mode, nil) // 待機中のゴルーチンを解除
}
}
}
pollServer
のメインのポーリングループです。pollster.WaitFD
を呼び出してI/Oイベントを待ちます。タイムアウトが発生した場合や、内部パイプ経由でウェイクアップされた場合、CheckDeadlines()
を呼び出して期限切れのデッドラインを処理します。I/Oが準備できたFDが見つかった場合、LookupFD
で対応するnetFD
を取得し、WakeFD
を呼び出して待機中のゴルーチンを解除します。
pollServerInit(fd *netFD) error
関数
func pollServerInit(fd *netFD) error {
pollN := runtime.GOMAXPROCS(0)
if pollN > pollMaxN {
pollN = pollMaxN
}
k := fd.sysfd % pollN // FDに基づいてpollServerを選択
startServersOnce[k]() // 選択されたpollServerを一度だけ初期化
fd.pollServer = pollservers[k]
fd.cr = make(chan error, 1) // 読み込み完了チャネル
fd.cw = make(chan error, 1) // 書き込み完了チャネル
return nil
}
netFD
が作成される際に呼び出され、そのnetFD
に適切なpollServer
を割り当て、読み書き完了用のチャネルを初期化します。複数のpollServer
が存在する場合、FDの番号に基づいて分散させます。
(s *pollServer) Close(fd *netFD)
メソッド
func (s *pollServer) Close(fd *netFD) {
// 現在は何も処理しないが、将来的にカスタム終了処理を追加するためのフック
}
netFD
がクローズされる際に呼び出されます。このコミット時点では具体的な処理は含まれていませんが、将来的にpollServer
が管理するリソースのクリーンアップなど、カスタムの終了処理を追加するためのプレースホルダーとして機能します。
src/pkg/net/fd_unix.go
(変更点)
newFD
関数
func newFD(fd, family, sotype int, net string) (*netFD, error) {
// ...
netfd := &netFD{
// ...
}
if err := pollServerInit(netfd); err != nil { // pollServerInitを呼び出すように変更
return nil, err
}
return netfd, nil
}
netFD
の初期化ロジックが変更され、pollServerInit
関数を呼び出すことで、pollServer
の割り当てとチャネルの初期化が行われるようになりました。これにより、newFD
のコードが簡潔になり、ポーリング関連の初期化ロジックがpollServerInit
に集約されました。
decref
メソッド
func (fd *netFD) decref() {
// ...
if fd.closing && fd.sysref == 0 && fd.sysfile != nil {
fd.sysfile.Close()
fd.pollServer.Close(fd) // pollServer.Closeを呼び出すように変更
fd.sysfile = nil
fd.sysfd = -1
}
}
netFD
の参照カウントがゼロになり、クローズされる際に、fd.pollServer.Close(fd)
が呼び出されるようになりました。これにより、pollServer
はnetFD
の終了を認識し、必要に応じて関連するリソースをクリーンアップできます。
関連リンク
- 次のステップで言及されている変更セット:
参考にした情報源リンク
- GitHub上のコミットページ:
- Goのネットポーラーに関する情報:
- morsmachine.dk - The Go netpoller
- goperf.dev - Go's Netpoller: How it works
- stackoverflow.com - How does Go's network poller work?
- medium.com - Go Concurrency: The Netpoller
- rickyboyd.dev - Go's Netpoller
- sobyte.net - Go's Netpoller
- hashnode.dev - Understanding Go's Netpoller
- google.com - Go's Netpoller