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

[インデックス 14532] ファイルの概要

このコミットは、Go言語の標準ライブラリnetパッケージにおける、ネットワークファイルディスクリプタ(netFD)の読み書きデッドライン(期限)管理に関する変更を元に戻すものです。具体的には、以前のコミット(CL 6855110 / 869253ef7009)で導入されたsync/atomicパッケージを使用した64ビットアトミック操作によるデッドライン管理が、32ビットシステム上で問題を引き起こしたため、その変更を巻き戻しています。

コミット

commit 5b425cc3ab2c9ce4752a5baa9a52ab86bda96036
Author: Dave Cheney <dave@cheney.net>
Date:   Fri Nov 30 20:02:30 2012 +1100

    undo CL 6855110 / 869253ef7009
    
    64bit atomics are broken on 32bit systems. This is issue 599.
    
    linux/arm builders all broke with this change, I am concerned that the other 32bit builders are silently impacted.
    
    ««« original CL description
    net: fix data races on deadline vars
    
    Fixes #4434.
    
    R=mikioh.mikioh, bradfitz, dvyukov, alex.brainman
    CC=golang-dev
    https://golang.org/cl/6855110
    »»»
    
    R=rsc, mikioh.mikioh, dvyukov, minux.ma
    CC=golang-dev
    https://golang.org/cl/6852105

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/5b425cc3ab2c9ce4752a5baa9a52ab86bda96036

元コミット内容

このコミットは、以下の元のコミット(CL 6855110)の変更を元に戻しています。

net: fix data races on deadline vars

Fixes #4434.

R=mikioh.mikioh, bradfitz, dvyukov, alex.brainman
CC=golang-dev
https://golang.org/cl/6855110

元のコミットは、netパッケージ内のデッドライン変数におけるデータ競合を修正することを目的としていました。

変更の背景

このコミットの主な背景は、以前のコミット(CL 6855110)が導入した変更が、32ビットシステム上で予期せぬ問題を引き起こしたことです。

  1. データ競合の修正試行: 元のコミット(CL 6855110)は、ネットワーク操作のデッドライン(タイムアウト)を管理する変数に発生する可能性のあるデータ競合を解決しようとしました。これは、複数のゴルーチンが同時に同じデッドライン変数にアクセスする際に、競合状態が発生し、予期しない動作やバグにつながるのを防ぐためです。
  2. sync/atomicの導入: このデータ競合を修正するために、元のコミットではsync/atomicパッケージが導入され、デッドライン変数をアトミックに操作するようになりました。アトミック操作は、ロックを使用せずに共有変数への安全なアクセスを可能にする低レベルのプリミティブです。
  3. 32ビットシステムでの問題: しかし、このアトミック操作、特に64ビット整数に対するアトミック操作が、32ビットアーキテクチャ(例: linux/arm)上で正しく機能しないという問題(Issue 599)が発覚しました。32ビットシステムでは、64ビットの値を一度に読み書きするアトミック操作は、ハードウェアレベルで直接サポートされていない場合が多く、ソフトウェアエミュレーションが必要になります。このエミュレーションがバグを含んでいたか、特定の環境で正しく動作しなかったため、ビルドが失敗するなどの問題が発生しました。
  4. 安定性の回復: linux/armビルドが壊れたことに加え、他の32ビットシステムでも同様の問題が静かに発生している可能性が懸念されたため、安定性を回復するために、問題の原因となった変更を元に戻すことが決定されました。

このコミットは、機能改善よりもシステムの安定性と互換性を優先した、典型的な「リバート(巻き戻し)」コミットです。

前提知識の解説

このコミットを理解するためには、以下の技術的な概念を把握しておく必要があります。

  1. Go言語のnetパッケージ:

    • Go言語の標準ライブラリの一部で、TCP/IPネットワークプログラミングのための基本的な機能を提供します。ソケットの作成、接続、データの送受信、リスニングなどを扱います。
    • netFD (network file descriptor) は、ネットワーク接続を表す内部構造体で、実際のOSのファイルディスクリプタ(ソケット)をラップし、I/O操作やデッドライン管理を行います。
    • pollServer は、非同期I/O操作を管理し、ファイルディスクリプタが読み書き可能になったり、デッドラインに達したりしたときに通知する役割を担います。
  2. デッドライン(Deadlines):

    • ネットワークI/O操作(読み込み、書き込み、接続など)に設定されるタイムアウトのことです。例えば、「5秒以内にデータが読み込めなければエラーとする」といった挙動を定義します。
    • Goのnetパッケージでは、SetReadDeadlineSetWriteDeadlineSetDeadlineなどのメソッドを通じて設定されます。これらのデッドラインは、Unixエポックからのナノ秒単位の時刻として内部的に保持されることが多いです。
  3. データ競合(Data Races):

    • 複数のゴルーチン(またはスレッド)が同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生するプログラミング上のバグです。
    • データ競合が発生すると、プログラムの動作が予測不能になり、クラッシュ、不正なデータ、セキュリティ脆弱性などにつながる可能性があります。
    • Go言語では、go run -raceコマンドでデータ競合を検出できます。
  4. sync/atomicパッケージ:

    • Go言語の標準ライブラリで、ミューテックス(sync.Mutex)のようなロック機構を使わずに、共有変数へのアトミックな(不可分な)操作を提供するパッケージです。
    • アトミック操作は、その操作が完了するまで他のゴルーチンから中断されないことが保証されます。これにより、データ競合を回避しつつ、ロックのオーバーヘッドを避けることができます。
    • atomic.LoadInt64atomic.StoreInt64は、64ビット整数をアトミックに読み書きするための関数です。
  5. 64ビットアトミック操作と32ビットシステム:

    • 64ビットのCPUアーキテクチャ(例: x86-64)では、64ビットの整数を一度のCPU命令でアトミックに読み書きできます。
    • しかし、32ビットのCPUアーキテクチャ(例: ARMv5, x86)では、64ビットの整数は2つの32ビットワードとして扱われます。このため、64ビットの値をアトミックに読み書きするには、特別なハードウェアサポート(例: CMPXCHG8B命令)が必要になるか、ソフトウェアによるエミュレーション(ロックや割り込みの無効化など)が必要になります。
    • ソフトウェアエミュレーションは複雑で、特定のコンパイラやOS、CPUの組み合わせでバグが発生しやすい領域です。このコミットの背景にある問題は、まさにこの「32ビットシステム上での64ビットアトミック操作の信頼性」に関するものでした。
  6. syscall.EAGAIN:

    • Unix系システムコールが返すエラーコードの一つで、非ブロッキングI/O操作(例: read, write)が、すぐに完了できない(データがまだ利用できない、またはバッファがいっぱい)場合に返されます。
    • このエラーを受け取ったアプリケーションは、通常、後で操作を再試行するか、I/O多重化メカニズム(poll, epoll, kqueueなど)を使用してファイルディスクリプタが準備できるのを待ちます。Goのnetパッケージでは、pollServerがこの待機を抽象化しています。

これらの概念を理解することで、このコミットがなぜ行われたのか、そしてその技術的な影響が何であるかを深く把握できます。

技術的詳細

このコミットは、Goのnetパッケージにおけるデッドライン管理のメカニズムを、sync/atomicパッケージを使用したアトミック操作から、よりシンプルなint64型への直接的な代入に戻すことで、以前のコミット(CL 6855110)の変更を完全に巻き戻しています。

元のCL 6855110では、netFD構造体内のrdeadlinewdeadlineフィールドが、int64の値をラップし、atomic.LoadInt64atomic.StoreInt64を使用して値を読み書きするカスタム型deadlineに変更されました。これは、これらのデッドライン変数への並行アクセスにおけるデータ競合を解決するための試みでした。

しかし、このアプローチは32ビットシステム(特にlinux/arm)で問題を引き起こしました。32ビットアーキテクチャでは、64ビット整数に対するアトミック操作がネイティブにサポートされていない場合があり、ソフトウェアエミュレーションが必要になります。このエミュレーションが不安定であったり、バグを含んでいたりすると、システムがクラッシュしたり、予期しない動作をしたりする原因となります。コミットメッセージにある「64bit atomics are broken on 32bit systems. This is issue 599.」という記述がこれを明確に示しています。

このコミットで行われた具体的な変更は以下の通りです。

  1. deadline型の削除: src/pkg/net/fd_unix.gosrc/pkg/net/fd_windows.goから、deadline型(int64をラップし、アトミック操作を提供する)の定義と、そのexpired(), value(), set(), setTime()メソッドが削除されました。
  2. sync/atomicのインポート削除: src/pkg/net/fd_unix.goから"sync/atomic"パッケージのインポートが削除されました。これは、アトミック操作が不要になったためです。
  3. netFDフィールドの変更: netFD構造体内のrdeadlinewdeadlineフィールドの型が、カスタムのdeadline型から、元のシンプルなint64型に戻されました。
  4. デッドラインアクセスロジックの変更:
    • 以前はfd.rdeadline.value()fd.wdeadline.value()のようにメソッド呼び出しでデッドライン値を取得していましたが、変更後はfd.rdeadlinefd.wdeadlineのように直接int64フィールドにアクセスするようになりました。
    • デッドラインの期限切れチェックも、fd.rdeadline.expired()のようなメソッド呼び出しから、fd.rdeadline > 0 && time.Now().UnixNano() >= fd.rdeadlineのような直接的な比較ロジックに変更されました。
    • デッドラインの設定も、fd.rdeadline.setTime(t)のようなメソッド呼び出しから、fd.rdeadline = t.UnixNano()fd.rdeadline = 0のような直接的な代入に変更されました。
  5. テストファイルの削除: src/pkg/net/fd_posix_test.goが削除されました。このテストファイルは、おそらくdeadline型とアトミック操作のテストに関連していたため、その型が削除されたことに伴い不要になりました。
  6. sendfile関連ファイルの修正: src/pkg/net/sendfile_freebsd.gosrc/pkg/net/sendfile_linux.goでは、syscall.EAGAINエラー処理の条件に&& c.wdeadline >= 0が追加されました。これは、wdeadlineint64に戻されたことで、0が「デッドラインなし」を意味するようになったため、デッドラインが設定されている場合にのみ待機処理を行うようにするための調整です。

このリバートにより、Goのnetパッケージは、32ビットシステムでの64ビットアトミック操作の不安定性という問題を回避し、より広範なアーキテクチャでの安定性を確保しました。データ競合の潜在的な問題は残るかもしれませんが、システムのクラッシュやビルドの失敗といったより深刻な問題を解決するためのトレードオフとして行われました。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更は、主にsrc/pkg/net/fd_unix.gosrc/pkg/net/fd_windows.goに集中しています。

src/pkg/net/fd_unix.go (および fd_windows.go も同様)

  1. sync/atomicパッケージの削除:

    --- a/src/pkg/net/fd_unix.go
    +++ b/src/pkg/net/fd_unix.go
    @@ -11,7 +11,6 @@ import (
      	"os"
      	"runtime"
      	"sync"
    -	"sync/atomic"
      	"syscall"
      	"time"
     )
    
  2. netFD構造体内のデッドラインフィールドの型変更: deadline型からint64型へ変更。

    --- a/src/pkg/net/fd_unix.go
    +++ b/src/pkg/net/fd_unix.go
    @@ -38,11 +37,11 @@ type netFD struct {
      	laddr       Addr
      	raddr       Addr
      
    -	// serialize access to Read and Write methods
    -	rio, wio sync.Mutex
    -
    -	// read and write deadlines
    -	rdeadline, wdeadline deadline
    +	// owned by client
    +	rdeadline int64
    +	rio       sync.Mutex
    +	wdeadline int64
    +	wio       sync.Mutex
      
      	// owned by fd wait server
      	ncr, ncw int
    
  3. deadline型定義とそのメソッドの削除: expired(), value(), set(), setTime()メソッドを含むdeadline型全体が削除されました。

    --- a/src/pkg/net/fd_unix.go
    +++ b/src/pkg/net/fd_unix.go
    @@ -51,31 +50,6 @@ type netFD struct {
      	pollServer *pollServer
      }
      
    -// deadline is an atomically-accessed number of nanoseconds since 1970
    -// or 0, if no deadline is set.
    -type deadline int64
    -
    -func (d *deadline) expired() bool {
    -	t := d.value()
    -	return t > 0 && time.Now().UnixNano() >= t
    -}
    -
    -func (d *deadline) value() int64 {
    -	return atomic.LoadInt64((*int64)(d))
    -}
    -
    -func (d *deadline) set(v int64) {
    -	atomic.StoreInt64((*int64)(d), v)
    -}
    -
    -func (d *deadline) setTime(t time.Time) {
    -	if t.IsZero() {
    -		d.set(0)
    -	} else {
    -		d.set(t.UnixNano())
    -	}
    -}
    -
      // A pollServer helps FDs determine when to retry a non-blocking
      // read or write after they get EAGAIN.  When an FD needs to wait,
      // call s.WaitRead() or s.WaitWrite() to pass the request to the poll server.
    
  4. デッドライン値の直接アクセスへの変更: fd.rdeadline.value()fd.wdeadline.value()のようなメソッド呼び出しが、直接fd.rdeadlinefd.wdeadlineに置き換えられました。

    --- a/src/pkg/net/fd_unix.go
    +++ b/src/pkg/net/fd_unix.go
    @@ -108,11 +82,11 @@ func (s *pollServer) AddFD(fd *netFD, mode int) error {
      	key := intfd << 1
      	if mode == 'r' {
      		fd.ncr++
    -		t = fd.rdeadline.value()
    +		t = fd.rdeadline
      	} else {
      		fd.ncw++
      		key++
    -		t = fd.wdeadline.value()
    +		t = fd.wdeadline
      	}
      	s.pending[key] = fd
      	doWakeup := false
    
  5. デッドライン期限切れチェックロジックの変更: fd.rdeadline.expired()のようなメソッド呼び出しが、直接的なint64の比較に置き換えられました。

    --- a/src/pkg/net/fd_unix.go
    +++ b/src/pkg/net/fd_unix.go
    @@ -439,9 +417,11 @@ func (fd *netFD) Read(p []byte) (n int, err error) {
      	}
      	defer fd.decref()
      	for {
    -		if fd.rdeadline.expired() {
    -			err = errTimeout
    -			break
    +		if fd.rdeadline > 0 {
    +			if time.Now().UnixNano() >= fd.rdeadline {
    +				err = errTimeout
    +				break
    +			}
      		}
      		n, err = syscall.Read(int(fd.sysfd), p)
      		if err != nil {
    

    同様の変更がReadFrom, ReadMsg, Write, WriteTo, WriteMsgにも適用されています。

src/pkg/net/sock_posix.go および src/pkg/net/sockopt_posix.go

デッドライン設定関数が、setTimeメソッドの呼び出しから、直接int64フィールドへの代入に変更されました。

--- a/src/pkg/net/sock_posix.go
+++ b/src/pkg/net/sock_posix.go
@@ -57,14 +57,16 @@ func socket(net string, f, t, p int, ipv6only bool, ulsa, ursa syscall.Sockaddr,
 	}
 
 	if ursa != nil {
-		fd.wdeadline.setTime(deadline)
+		if !deadline.IsZero() {
+			fd.wdeadline = deadline.UnixNano()
+		}
 		if err = fd.connect(ursa); err != nil {
 			closesocket(s)
 			fd.Close()
 			return nil, err
 		}
 		fd.isConnected = true
-		fd.wdeadline.set(0)
+		fd.wdeadline = 0
 	}
 
 	lsa, _ := syscall.Getsockname(s)
--- a/src/pkg/net/sockopt_posix.go
+++ b/src/pkg/net/sockopt_posix.go
@@ -119,22 +119,29 @@ func setWriteBuffer(fd *netFD, bytes int) error {
 	return os.NewSyscallError("setsockopt", syscall.SOL_SOCKET, syscall.SO_SNDBUF, bytes))
 }
 
-// TODO(dfc) these unused error returns could be removed
-
 func setReadDeadline(fd *netFD, t time.Time) error {
-	fd.rdeadline.setTime(t)
+	if t.IsZero() {
+		fd.rdeadline = 0
+	} else {
+		fd.rdeadline = t.UnixNano()
+	}
 	return nil
 }
 
 func setWriteDeadline(fd *netFD, t time.Time) error {
-	fd.wdeadline.setTime(t)
+	if t.IsZero() {
+		fd.wdeadline = 0
+	} else {
+		fd.wdeadline = t.UnixNano()
+	}
 	return nil
 }
 
 func setDeadline(fd *netFD, t time.Time) error {
-	setReadDeadline(fd, t)
-	setWriteDeadline(fd, t)
-	return nil
+	if err := setReadDeadline(fd, t); err != nil {
+		return err
+	}
+	return setWriteDeadline(fd, t)
 }
 
 func setKeepAlive(fd *netFD, keepalive bool) error {

src/pkg/net/fd_posix_test.go

このファイルは完全に削除されました。

--- a/src/pkg/net/fd_posix_test.go
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright 2012 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// +build darwin freebsd linux netbsd openbsd windows
-
-package net
-
-import (
-	"testing"
-	"time"
-)
-
-var deadlineSetTimeTests = []struct {
-	input    time.Time
-	expected int64
-}{
-	{time.Time{}, 0},
-	{time.Date(2009, 11, 10, 23, 00, 00, 00, time.UTC), 1257894000000000000}, // 2009-11-10 23:00:00 +0000 UTC
-}
-
-func TestDeadlineSetTime(t *testing.T) {
-	for _, tt := range deadlineSetTimeTests {
-		var d deadline
-		d.setTime(tt.input)
-		actual := d.value()
-		expected := int64(0)
-		if !tt.input.IsZero() {
-			expected = tt.input.UnixNano()
-		}
-		if actual != expected {
-			t.Errorf("set/value failed: expected %v, actual %v", expected, actual)
-		}
-	}
-}
-
-var deadlineExpiredTests = []struct {
-	deadline time.Time
-	expired  bool
-}{
-	// note, times are relative to the start of the test run, not
-	// the start of TestDeadlineExpired
-	{time.Now().Add(5 * time.Minute), false},
-	{time.Now().Add(-5 * time.Minute), true},
-	{time.Time{}, false}, // no deadline set
-}
-
-func TestDeadlineExpired(t *testing.T) {
-	for _, tt := range deadlineExpiredTests {
-		var d deadline
-		d.set(tt.deadline.UnixNano())
-		expired := d.expired()
-		if expired != tt.expired {
-			t.Errorf("expire failed: expected %v, actual %v", tt.expired, expired)
-		}
-	}
-}

コアとなるコードの解説

このコミットの核心は、GoのnetパッケージがネットワークI/Oのデッドラインをどのように管理するかという、根本的なアプローチの変更を元に戻すことにあります。

元のコミット(CL 6855110)では、netFD構造体内のrdeadline(読み込みデッドライン)とwdeadline(書き込みデッドライン)を、単なるint64型ではなく、deadlineというカスタム型で定義していました。このdeadline型は、内部的にint64のタイムスタンプを保持し、sync/atomicパッケージのatomic.LoadInt64atomic.StoreInt64を使って、そのタイムスタンプをアトミックに読み書きするメソッド(value(), set()など)を提供していました。

このアトミック操作の導入は、複数のゴルーチンが同時にデッドラインにアクセスする際に発生しうるデータ競合を防止することを目的としていました。例えば、あるゴルーチンがデッドラインを更新している最中に、別のゴルーチンがそのデッドラインを読み取ろうとすると、不完全な値が読み取られる可能性があります。アトミック操作は、このような中間状態を他のゴルーチンから隠蔽し、常に完全な値が読み書きされることを保証します。

しかし、このアプローチは32ビットシステムで問題を引き起こしました。32ビットCPUは、64ビットの値を一度の命令でアトミックに操作する能力を持たないことが一般的です。そのため、Goランタイムは、このような操作をソフトウェアでエミュレートする必要があります。このエミュレーションの実装にバグがあったか、特定の32ビット環境で信頼性が低かったため、linux/armなどのビルドが失敗する原因となりました。

このコミットは、この問題に対処するため、deadlineカスタム型とそれに伴うアトミック操作を完全に削除し、rdeadlinewdeadlineを再びシンプルなint64型に戻しました。これにより、デッドラインの読み書きは、アトミック操作ではなく、通常のint64の読み書き(これは32ビットシステムでは非アトミックになる可能性がある)に戻りました。

結果として、この変更は、データ競合の潜在的なリスクを再導入する可能性がありますが、32ビットシステムでのビルドの失敗やランタイムの不安定性という、より深刻な問題を解決しました。これは、特定のアーキテクチャでの安定性を優先し、パフォーマンスや厳密なデータ競合回避よりも、広範な互換性と信頼性を重視した判断と言えます。

デッドラインのチェックロジックも、fd.rdeadline.expired()のような抽象化されたメソッド呼び出しから、fd.rdeadline > 0 && time.Now().UnixNano() >= fd.rdeadlineのような直接的なタイムスタンプ比較に変わりました。これは、deadline型が提供していた抽象化レイヤーが不要になったためです。

このリバートは、Go言語の進化の過程で、特定の最適化や並行処理の改善が、予期せぬプラットフォーム固有の問題を引き起こすことがあるという良い例を示しています。このような場合、安定性を最優先し、問題のある変更を元に戻すことが、健全なソフトウェア開発において重要な判断となります。

関連リンク

参考にした情報源リンク

  • Go言語のsync/atomicパッケージのドキュメント: https://pkg.go.dev/sync/atomic
  • Go言語のnetパッケージのドキュメント: https://pkg.go.dev/net
  • Go言語のtimeパッケージのドキュメント: https://pkg.go.dev/time
  • Go言語のsyscallパッケージのドキュメント: https://pkg.go.dev/syscall
  • アトミック操作に関する一般的な情報(Wikipediaなど)
  • 32ビットシステムにおける64ビットアトミック操作の課題に関する情報(CPUアーキテクチャ、コンパイラ、OSのドキュメントなど)