[インデックス 17188] ファイルの概要
このコミットは、GoランタイムにおけるネットワークI/Oのデッドライン(期限)処理に関するバグ修正です。具体的には、既に発行されているI/O操作に対してもデッドラインが適切に適用されるように改善されています。これにより、ネットワーク操作がデッドラインを超過した場合に、より確実に操作が中断されるようになります。
コミット
commit aaab94694342599e69678a9f96363e54f21bafb9
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Aug 13 19:11:42 2013 +0400
runtime: fix handling of network deadlines
Ensure that deadlines affect already issued IO.
R=golang-dev, mikioh.mikioh, bradfitz
CC=golang-dev
https://golang.org/cl/12847043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/aaab94694342599e69678a9f96363e54f21bafb9
元コミット内容
このコミットは、Goランタイムがネットワークデッドラインを処理する方法における問題を修正します。主な目的は、「既に発行されたI/O操作に対してもデッドラインが影響を与えること」を保証することです。これは、ネットワーク接続に対して読み取りまたは書き込みのデッドラインが設定された際に、その時点でブロックされている可能性のあるI/O操作が、デッドラインの変更によって適切に中断されるべきであることを意味します。
変更の背景
GoのネットワークI/Oは、内部的にノンブロッキングI/Oとポーリングメカニズム(netpoll
)を使用しています。デッドラインは、特定の時間までにI/O操作が完了しない場合に、その操作をタイムアウトさせるための機能です。このコミット以前は、デッドラインが設定または変更された際に、既にブロック状態に入っているI/O操作(例えば、ネットワークからのデータ受信を待っている状態)が、新しいデッドラインによって適切に「起こされない(unblockされない)」という問題がありました。
具体的には、net.Conn
インターフェースのSetDeadline
, SetReadDeadline
, SetWriteDeadline
メソッドが呼び出された際に、その変更が即座に反映されず、既にブロックされているゴルーチンがデッドライン超過によって解放されない可能性がありました。これは、アプリケーションが期待するタイムアウト動作と異なる挙動を引き起こし、場合によってはゴルーチンが永久にブロックされる「ゴルーチンリーク」につながる可能性もありました。
この修正は、デッドラインが過去の時刻に設定された場合(つまり、即座にタイムアウトすべき場合)に、現在ブロックされているI/O操作を明示的にアンブロックするメカニズムを導入することで、この問題を解決しようとしています。
前提知識の解説
- GoのネットワークI/Oモデル: Goの標準ライブラリ
net
パッケージは、OSのノンブロッキングI/O機能(Linuxのepoll, macOSのkqueueなど)を抽象化して利用しています。Goランタイムは、これらのシステムコールと連携し、I/O操作がブロックされる際にゴルーチンをスケジューラから外し、I/Oが準備できたときに再度スケジューラに戻すことで、多数の同時接続を効率的に扱います。 PollDesc
構造体:runtime
パッケージ内部で使用されるPollDesc
構造体は、ファイルディスクリプタ(FD)の状態と、それに関連するI/O操作(読み取り、書き込み)のデッドライン情報を管理します。各net.Conn
は内部的にPollDesc
を保持しています。netpoll
: Goランタイムのネットワークポーリングメカニズムです。I/O操作がブロックされると、ゴルーチンはnetpoll
に登録され、I/Oイベントが発生するまで待機します。イベントが発生すると、netpoll
は対応するゴルーチンを「ready」状態にし、スケジューラがそのゴルーチンを再開できるようにします。- デッドライン(Deadlines):
net.Conn
インターフェースには、SetDeadline(t time.Time)
,SetReadDeadline(t time.Time)
,SetWriteDeadline(t time.Time)
メソッドがあります。これらは、それぞれ読み取りと書き込み、読み取りのみ、書き込みのみのI/O操作に対してタイムアウト時刻を設定します。指定された時刻までに操作が完了しない場合、I/O操作はエラー(通常はnet.ErrTimeout
)を返して中断されます。 runtime.nanotime()
: Goランタイム内部で使用される、モノトニックな(単調増加する)高分解能タイマーです。システム時刻の変更に影響されず、経過時間を正確に測定するために使用されます。デッドラインの比較に利用されます。- ゴルーチン(Goroutines)とスケジューラ: Goの軽量スレッドです。Goランタイムのスケジューラは、多数のゴルーチンを少数のOSスレッド上で効率的に多重化して実行します。I/O操作がブロックされると、スケジューラは現在のゴルーチンを一時停止し、別の実行可能なゴルーチンにCPUを割り当てます。I/Oが完了すると、停止していたゴルーチンは再開されます。
技術的詳細
このコミットは主に2つのファイルに影響を与えています。
-
src/pkg/net/fd_poll_unix.go
:setDeadline
,setReadDeadline
,setWriteDeadline
の各メソッドが、新しいヘルパー関数setDeadlineImpl
を呼び出すように変更されました。setDeadlineImpl
関数が新しく導入されました。この関数は、fd.incref()
とfd.decref()
を使ってファイルディスクリプタの参照カウントを適切に管理します。setDeadlineImpl
内で、読み取りデッドライン(fd.pd.rdeadline.setTime(t)
)と書き込みデッドライン(fd.pd.wdeadline.setTime(t)
)が設定されます。- 最も重要な変更は、デッドライン設定後に
fd.pd.Wakeup()
が呼び出されるようになった点です。Wakeup()
は、PollDesc
に関連付けられたポーリングメカニズムに対して、デッドラインが変更されたことを通知し、ブロックされている可能性のあるI/O操作を再評価させる役割があります。
-
src/pkg/runtime/netpoll.goc
:runtime_pollSetDeadline
関数が変更されました。この関数は、PollDesc
のデッドラインを設定するランタイム側のCコードです。pd->closing
チェックのロジックが修正され、goto ret;
ではなくreturn;
で早期リターンするようになりました。- デッドライン
d
が0
ではなく、かつruntime·nanotime()
(現在の時刻)以下である場合(つまり、デッドラインが過去に設定された場合)、d
を-1
に設定するロジックが追加されました。-1
は「即座にタイムアウト」を意味します。 - 最も重要な変更は、デッドライン設定後に追加された以下のロジックです。
このコードブロックは、設定された読み取りデッドライン(// If we set the new deadline in the past, unblock currently pending IO if any. rg = nil; wg = nil; if(pd->rd < 0) rg = netpollunblock(pd, 'r', false); if(pd->wd < 0) wg = netpollunblock(pd, 'w', false); runtime·unlock(pd); if(rg) runtime·ready(rg); if(wg) runtime·ready(wg);
pd->rd
)または書き込みデッドライン(pd->wd
)が-1
(即座にタイムアウト)である場合に、netpollunblock
を呼び出して、現在ブロックされている読み取りまたは書き込みのゴルーチンを明示的にアンブロックします。アンブロックされたゴルーチンは、runtime·ready
によってスケジューラに「実行可能」としてマークされ、デッドライン超過エラーで再開されます。
これらの変更により、デッドラインが設定または変更された際に、その変更が即座にポーリングメカニズムに伝達され、既にブロックされているI/O操作が新しいデッドラインに基づいて適切にタイムアウトするようになりました。
コアとなるコードの変更箇所
src/pkg/net/fd_poll_unix.go
--- a/src/pkg/net/fd_poll_unix.go
+++ b/src/pkg/net/fd_poll_unix.go
@@ -351,20 +351,29 @@ func (pd *pollDesc) Init(fd *netFD) error {
return nil
}
-// TODO(dfc) these unused error returns could be removed
-
func (fd *netFD) setDeadline(t time.Time) error {
- fd.setReadDeadline(t)
- fd.setWriteDeadline(t)
- return nil
+ return setDeadlineImpl(fd, t, true, true)
}
func (fd *netFD) setReadDeadline(t time.Time) error {
- fd.pd.rdeadline.setTime(t)
- return nil
+ return setDeadlineImpl(fd, t, true, false)
}
func (fd *netFD) setWriteDeadline(t time.Time) error {
- fd.pd.wdeadline.setTime(t)
+ return setDeadlineImpl(fd, t, false, true)
+}
+
+func setDeadlineImpl(fd *netFD, t time.Time, read, write bool) error {
+ if err := fd.incref(); err != nil {
+ return err
+ }
+ defer fd.decref()
+ if read {
+ fd.pd.rdeadline.setTime(t)
+ }
+ if write {
+ fd.pd.wdeadline.setTime(t)
+ }
+ fd.pd.Wakeup()
return nil
}
src/pkg/runtime/netpoll.goc
--- a/src/pkg/runtime/netpoll.goc
+++ b/src/pkg/runtime/netpoll.goc
@@ -134,9 +134,13 @@ func runtime_pollWaitCanceled(pd *PollDesc, mode int) {
}
func runtime_pollSetDeadline(pd *PollDesc, d int64, mode int) {
+ G *rg, *wg;
+
runtime·lock(pd);
- if(pd->closing)
- goto ret;
+ if(pd->closing) {
+ runtime·unlock(pd);
+ return;
+ }
pd->seq++; // invalidate current timers
// Reset current timers.
if(pd->rt.fv) {
@@ -148,9 +152,8 @@ func runtime_pollSetDeadline(pd *PollDesc, d int64, mode int) {
pd->wt.fv = nil;
}
// Setup new timers.
- if(d != 0 && d <= runtime·nanotime()) {
+ if(d != 0 && d <= runtime·nanotime())
d = -1;
- }
if(mode == 'r' || mode == 'r'+'w')
pd->rd = d;
if(mode == 'w' || mode == 'r'+'w')
@@ -180,8 +183,18 @@ func runtime_pollSetDeadline(pd *PollDesc, d int64, mode int) {
if(pd->wt.fv) {
runtime·addtimer(&pd->wt);
}
}
-ret:
+ // If we set the new deadline in the past, unblock currently pending IO if any.
+ rg = nil;
+ wg = nil;
+ if(pd->rd < 0)
+ rg = netpollunblock(pd, 'r', false);
+ if(pd->wd < 0)
+ wg = netpollunblock(pd, 'w', false);
runtime·unlock(pd);
+ if(rg)
+ runtime·ready(rg);
+ if(wg)
+ runtime·ready(wg);
}
func runtime_pollUnblock(pd *PollDesc) {
コアとなるコードの解説
src/pkg/net/fd_poll_unix.go
の変更
setDeadlineImpl
関数の導入: 以前はsetDeadline
などが直接デッドラインを設定していましたが、共通のロジックをsetDeadlineImpl
に集約しました。これにより、コードの重複が減り、保守性が向上します。- 参照カウントの管理:
fd.incref()
とfd.decref()
は、ファイルディスクリプタの参照カウントを増減させます。これは、FDが使用中に閉じられないようにするための重要なメカニズムです。デッドライン設定中にFDが閉じられることを防ぎます。 fd.pd.Wakeup()
の呼び出し: これがこのファイルの変更の核心です。デッドラインが設定された後、Wakeup()
を呼び出すことで、netpoll
システムにデッドラインの変更を通知します。Wakeup()
は、PollDesc
に関連付けられたポーリングイベントをトリガーし、もしゴルーチンがそのFDでブロックされている場合、そのゴルーチンを再評価させます。これにより、新しいデッドラインが過去の時刻である場合、ブロックされているゴルーチンが即座にタイムアウトエラーを受け取って再開されるようになります。
src/pkg/runtime/netpoll.goc
の変更
pd->closing
の修正:goto ret;
からreturn;
への変更は、C言語のコーディングスタイルにおける改善です。goto
の使用を減らし、関数の制御フローをより明確にします。- 即時タイムアウトのロジック:
この行は、設定されたデッドラインif(d != 0 && d <= runtime·nanotime()) d = -1;
d
が現在の時刻(runtime·nanotime()
)以下である場合、つまりデッドラインが既に過ぎているか、現在時刻である場合に、デッドラインを-1
に設定します。-1
は、netpoll
システムにおいて「即座にタイムアウトすべき」という特別な意味を持ちます。 - ブロックされたI/Oの明示的なアンブロック:
これがこのコミットの最も重要な部分です。// If we set the new deadline in the past, unblock currently pending IO if any. rg = nil; wg = nil; if(pd->rd < 0) rg = netpollunblock(pd, 'r', false); if(pd->wd < 0) wg = netpollunblock(pd, 'w', false); runtime·unlock(pd); if(rg) runtime·ready(rg); if(wg) runtime·ready(wg);
pd->rd < 0
またはpd->wd < 0
は、読み取りまたは書き込みのデッドラインが-1
(即座にタイムアウト)に設定されたことを意味します。netpollunblock(pd, 'r', false)
またはnetpollunblock(pd, 'w', false)
は、指定されたPollDesc
とモード(読み取りまたは書き込み)で現在ブロックされているゴルーチンを強制的にアンブロックします。これにより、そのゴルーチンはブロック状態から解放されます。runtime·ready(rg)
またはruntime·ready(wg)
は、アンブロックされたゴルーチンをGoスケジューラに「実行可能」として登録します。これにより、スケジューラはこれらのゴルーチンを速やかに再開し、デッドライン超過によるエラーをアプリケーションに返せるようになります。
これらの変更により、デッドラインが設定された瞬間に、そのデッドラインが過去の時刻である場合、関連するI/O操作でブロックされているゴルーチンが即座にアンブロックされ、タイムアウトエラーを返すことが保証されます。これにより、ネットワークデッドラインの挙動がより予測可能で堅牢になりました。
関連リンク
- Go CL 12847043: https://golang.org/cl/12847043
参考にした情報源リンク
- https://golang.org/cl/12847043
- Goの
net
パッケージのドキュメント (一般的なI/Oデッドラインの概念理解のため) - Goランタイムの
netpoll
に関する一般的な情報 (GoのI/Oモデル理解のため) - Goのソースコード (特に
src/pkg/net
とsrc/pkg/runtime
ディレクトリ) - Goの
PollDesc
に関する議論やドキュメント (内部構造理解のため) - Goの
runtime.nanotime()
に関する情報 (モノトニックタイマーの理解のため) - Goのゴルーチンとスケジューラに関する一般的な情報