Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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つのファイルに影響を与えています。

  1. 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操作を再評価させる役割があります。
  2. src/pkg/runtime/netpoll.goc:

    • runtime_pollSetDeadline関数が変更されました。この関数は、PollDescのデッドラインを設定するランタイム側のCコードです。
    • pd->closingチェックのロジックが修正され、goto ret;ではなくreturn;で早期リターンするようになりました。
    • デッドラインd0ではなく、かつ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操作でブロックされているゴルーチンが即座にアンブロックされ、タイムアウトエラーを返すことが保証されます。これにより、ネットワークデッドラインの挙動がより予測可能で堅牢になりました。

関連リンク

参考にした情報源リンク

  • https://golang.org/cl/12847043
  • Goのnetパッケージのドキュメント (一般的なI/Oデッドラインの概念理解のため)
  • Goランタイムのnetpollに関する一般的な情報 (GoのI/Oモデル理解のため)
  • Goのソースコード (特にsrc/pkg/netsrc/pkg/runtimeディレクトリ)
  • GoのPollDescに関する議論やドキュメント (内部構造理解のため)
  • Goのruntime.nanotime()に関する情報 (モノトニックタイマーの理解のため)
  • Goのゴルーチンとスケジューラに関する一般的な情報