[インデックス 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ビットシステム上で予期せぬ問題を引き起こしたことです。
- データ競合の修正試行: 元のコミット(CL 6855110)は、ネットワーク操作のデッドライン(タイムアウト)を管理する変数に発生する可能性のあるデータ競合を解決しようとしました。これは、複数のゴルーチンが同時に同じデッドライン変数にアクセスする際に、競合状態が発生し、予期しない動作やバグにつながるのを防ぐためです。
sync/atomic
の導入: このデータ競合を修正するために、元のコミットではsync/atomic
パッケージが導入され、デッドライン変数をアトミックに操作するようになりました。アトミック操作は、ロックを使用せずに共有変数への安全なアクセスを可能にする低レベルのプリミティブです。- 32ビットシステムでの問題: しかし、このアトミック操作、特に64ビット整数に対するアトミック操作が、32ビットアーキテクチャ(例:
linux/arm
)上で正しく機能しないという問題(Issue 599)が発覚しました。32ビットシステムでは、64ビットの値を一度に読み書きするアトミック操作は、ハードウェアレベルで直接サポートされていない場合が多く、ソフトウェアエミュレーションが必要になります。このエミュレーションがバグを含んでいたか、特定の環境で正しく動作しなかったため、ビルドが失敗するなどの問題が発生しました。 - 安定性の回復:
linux/arm
ビルドが壊れたことに加え、他の32ビットシステムでも同様の問題が静かに発生している可能性が懸念されたため、安定性を回復するために、問題の原因となった変更を元に戻すことが決定されました。
このコミットは、機能改善よりもシステムの安定性と互換性を優先した、典型的な「リバート(巻き戻し)」コミットです。
前提知識の解説
このコミットを理解するためには、以下の技術的な概念を把握しておく必要があります。
-
Go言語の
net
パッケージ:- Go言語の標準ライブラリの一部で、TCP/IPネットワークプログラミングのための基本的な機能を提供します。ソケットの作成、接続、データの送受信、リスニングなどを扱います。
netFD
(network file descriptor) は、ネットワーク接続を表す内部構造体で、実際のOSのファイルディスクリプタ(ソケット)をラップし、I/O操作やデッドライン管理を行います。pollServer
は、非同期I/O操作を管理し、ファイルディスクリプタが読み書き可能になったり、デッドラインに達したりしたときに通知する役割を担います。
-
デッドライン(Deadlines):
- ネットワークI/O操作(読み込み、書き込み、接続など)に設定されるタイムアウトのことです。例えば、「5秒以内にデータが読み込めなければエラーとする」といった挙動を定義します。
- Goの
net
パッケージでは、SetReadDeadline
、SetWriteDeadline
、SetDeadline
などのメソッドを通じて設定されます。これらのデッドラインは、Unixエポックからのナノ秒単位の時刻として内部的に保持されることが多いです。
-
データ競合(Data Races):
- 複数のゴルーチン(またはスレッド)が同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生するプログラミング上のバグです。
- データ競合が発生すると、プログラムの動作が予測不能になり、クラッシュ、不正なデータ、セキュリティ脆弱性などにつながる可能性があります。
- Go言語では、
go run -race
コマンドでデータ競合を検出できます。
-
sync/atomic
パッケージ:- Go言語の標準ライブラリで、ミューテックス(
sync.Mutex
)のようなロック機構を使わずに、共有変数へのアトミックな(不可分な)操作を提供するパッケージです。 - アトミック操作は、その操作が完了するまで他のゴルーチンから中断されないことが保証されます。これにより、データ競合を回避しつつ、ロックのオーバーヘッドを避けることができます。
atomic.LoadInt64
やatomic.StoreInt64
は、64ビット整数をアトミックに読み書きするための関数です。
- Go言語の標準ライブラリで、ミューテックス(
-
64ビットアトミック操作と32ビットシステム:
- 64ビットのCPUアーキテクチャ(例: x86-64)では、64ビットの整数を一度のCPU命令でアトミックに読み書きできます。
- しかし、32ビットのCPUアーキテクチャ(例: ARMv5, x86)では、64ビットの整数は2つの32ビットワードとして扱われます。このため、64ビットの値をアトミックに読み書きするには、特別なハードウェアサポート(例:
CMPXCHG8B
命令)が必要になるか、ソフトウェアによるエミュレーション(ロックや割り込みの無効化など)が必要になります。 - ソフトウェアエミュレーションは複雑で、特定のコンパイラやOS、CPUの組み合わせでバグが発生しやすい領域です。このコミットの背景にある問題は、まさにこの「32ビットシステム上での64ビットアトミック操作の信頼性」に関するものでした。
-
syscall.EAGAIN
:- Unix系システムコールが返すエラーコードの一つで、非ブロッキングI/O操作(例:
read
,write
)が、すぐに完了できない(データがまだ利用できない、またはバッファがいっぱい)場合に返されます。 - このエラーを受け取ったアプリケーションは、通常、後で操作を再試行するか、I/O多重化メカニズム(
poll
,epoll
,kqueue
など)を使用してファイルディスクリプタが準備できるのを待ちます。Goのnet
パッケージでは、pollServer
がこの待機を抽象化しています。
- Unix系システムコールが返すエラーコードの一つで、非ブロッキングI/O操作(例:
これらの概念を理解することで、このコミットがなぜ行われたのか、そしてその技術的な影響が何であるかを深く把握できます。
技術的詳細
このコミットは、Goのnet
パッケージにおけるデッドライン管理のメカニズムを、sync/atomic
パッケージを使用したアトミック操作から、よりシンプルなint64
型への直接的な代入に戻すことで、以前のコミット(CL 6855110)の変更を完全に巻き戻しています。
元のCL 6855110では、netFD
構造体内のrdeadline
とwdeadline
フィールドが、int64
の値をラップし、atomic.LoadInt64
とatomic.StoreInt64
を使用して値を読み書きするカスタム型deadline
に変更されました。これは、これらのデッドライン変数への並行アクセスにおけるデータ競合を解決するための試みでした。
しかし、このアプローチは32ビットシステム(特にlinux/arm
)で問題を引き起こしました。32ビットアーキテクチャでは、64ビット整数に対するアトミック操作がネイティブにサポートされていない場合があり、ソフトウェアエミュレーションが必要になります。このエミュレーションが不安定であったり、バグを含んでいたりすると、システムがクラッシュしたり、予期しない動作をしたりする原因となります。コミットメッセージにある「64bit atomics are broken on 32bit systems. This is issue 599.」という記述がこれを明確に示しています。
このコミットで行われた具体的な変更は以下の通りです。
deadline
型の削除:src/pkg/net/fd_unix.go
とsrc/pkg/net/fd_windows.go
から、deadline
型(int64
をラップし、アトミック操作を提供する)の定義と、そのexpired()
,value()
,set()
,setTime()
メソッドが削除されました。sync/atomic
のインポート削除:src/pkg/net/fd_unix.go
から"sync/atomic"
パッケージのインポートが削除されました。これは、アトミック操作が不要になったためです。netFD
フィールドの変更:netFD
構造体内のrdeadline
とwdeadline
フィールドの型が、カスタムのdeadline
型から、元のシンプルなint64
型に戻されました。- デッドラインアクセスロジックの変更:
- 以前は
fd.rdeadline.value()
やfd.wdeadline.value()
のようにメソッド呼び出しでデッドライン値を取得していましたが、変更後はfd.rdeadline
やfd.wdeadline
のように直接int64
フィールドにアクセスするようになりました。 - デッドラインの期限切れチェックも、
fd.rdeadline.expired()
のようなメソッド呼び出しから、fd.rdeadline > 0 && time.Now().UnixNano() >= fd.rdeadline
のような直接的な比較ロジックに変更されました。 - デッドラインの設定も、
fd.rdeadline.setTime(t)
のようなメソッド呼び出しから、fd.rdeadline = t.UnixNano()
やfd.rdeadline = 0
のような直接的な代入に変更されました。
- 以前は
- テストファイルの削除:
src/pkg/net/fd_posix_test.go
が削除されました。このテストファイルは、おそらくdeadline
型とアトミック操作のテストに関連していたため、その型が削除されたことに伴い不要になりました。 sendfile
関連ファイルの修正:src/pkg/net/sendfile_freebsd.go
とsrc/pkg/net/sendfile_linux.go
では、syscall.EAGAIN
エラー処理の条件に&& c.wdeadline >= 0
が追加されました。これは、wdeadline
がint64
に戻されたことで、0
が「デッドラインなし」を意味するようになったため、デッドラインが設定されている場合にのみ待機処理を行うようにするための調整です。
このリバートにより、Goのnet
パッケージは、32ビットシステムでの64ビットアトミック操作の不安定性という問題を回避し、より広範なアーキテクチャでの安定性を確保しました。データ競合の潜在的な問題は残るかもしれませんが、システムのクラッシュやビルドの失敗といったより深刻な問題を解決するためのトレードオフとして行われました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主にsrc/pkg/net/fd_unix.go
とsrc/pkg/net/fd_windows.go
に集中しています。
src/pkg/net/fd_unix.go
(および fd_windows.go
も同様)
-
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" )
-
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
-
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.
-
デッドライン値の直接アクセスへの変更:
fd.rdeadline.value()
やfd.wdeadline.value()
のようなメソッド呼び出しが、直接fd.rdeadline
やfd.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
-
デッドライン期限切れチェックロジックの変更:
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.LoadInt64
とatomic.StoreInt64
を使って、そのタイムスタンプをアトミックに読み書きするメソッド(value()
, set()
など)を提供していました。
このアトミック操作の導入は、複数のゴルーチンが同時にデッドラインにアクセスする際に発生しうるデータ競合を防止することを目的としていました。例えば、あるゴルーチンがデッドラインを更新している最中に、別のゴルーチンがそのデッドラインを読み取ろうとすると、不完全な値が読み取られる可能性があります。アトミック操作は、このような中間状態を他のゴルーチンから隠蔽し、常に完全な値が読み書きされることを保証します。
しかし、このアプローチは32ビットシステムで問題を引き起こしました。32ビットCPUは、64ビットの値を一度の命令でアトミックに操作する能力を持たないことが一般的です。そのため、Goランタイムは、このような操作をソフトウェアでエミュレートする必要があります。このエミュレーションの実装にバグがあったか、特定の32ビット環境で信頼性が低かったため、linux/arm
などのビルドが失敗する原因となりました。
このコミットは、この問題に対処するため、deadline
カスタム型とそれに伴うアトミック操作を完全に削除し、rdeadline
とwdeadline
を再びシンプルなint64
型に戻しました。これにより、デッドラインの読み書きは、アトミック操作ではなく、通常のint64
の読み書き(これは32ビットシステムでは非アトミックになる可能性がある)に戻りました。
結果として、この変更は、データ競合の潜在的なリスクを再導入する可能性がありますが、32ビットシステムでのビルドの失敗やランタイムの不安定性という、より深刻な問題を解決しました。これは、特定のアーキテクチャでの安定性を優先し、パフォーマンスや厳密なデータ競合回避よりも、広範な互換性と信頼性を重視した判断と言えます。
デッドラインのチェックロジックも、fd.rdeadline.expired()
のような抽象化されたメソッド呼び出しから、fd.rdeadline > 0 && time.Now().UnixNano() >= fd.rdeadline
のような直接的なタイムスタンプ比較に変わりました。これは、deadline
型が提供していた抽象化レイヤーが不要になったためです。
このリバートは、Go言語の進化の過程で、特定の最適化や並行処理の改善が、予期せぬプラットフォーム固有の問題を引き起こすことがあるという良い例を示しています。このような場合、安定性を最優先し、問題のある変更を元に戻すことが、健全なソフトウェア開発において重要な判断となります。
関連リンク
- 元のコミット(CL 6855110): https://golang.org/cl/6855110
- 関連するGo Issue #599: コミットメッセージで言及されている「64bit atomics are broken on 32bit systems」に関する問題。
- 関連するGo Issue #4434: 元のコミット(CL 6855110)が修正しようとしたデータ競合に関する問題。
参考にした情報源リンク
- 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のドキュメントなど)