[インデックス 16526] ファイルの概要
このコミットは、Goランタイムのデータ競合検出器(Race Detector)が、Read
およびWrite
システムコールによってアクセスされるメモリ領域を正しく追跡できるようにするための変更を導入しています。これにより、システムコールを介したI/O操作中に発生する可能性のあるデータ競合を検出できるようになり、並行プログラムの信頼性とデバッグ可能性が向上します。
コミット
commit fc8076479205f467242eaaa67c51123b4dcc25ee
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Jun 10 22:40:35 2013 +0400
runtime/race: tell race detector what memory Read/Write syscalls touch
Fixes #5567.
R=golang-dev, dave, iant
CC=golang-dev
https://golang.org/cl/10085043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fc8076479205f467242eaaa67c51123b4dcc25ee
元コミット内容
runtime/race: tell race detector what memory Read/Write syscalls touch
Fixes #5567.
R=golang-dev, dave, iant
CC=golang-dev
https://golang.org/cl/10085043
変更の背景
Goのデータ競合検出器は、並行実行されるゴルーチン間で共有されるメモリへの非同期アクセスによって発生するデータ競合を特定するために設計されています。データ競合は、プログラムの予測不能な動作やクラッシュの原因となる深刻なバグです。
従来のデータ競合検出器は、Go言語のコード内で直接行われるメモリアクセスを主に監視していました。しかし、Read
やWrite
といったシステムコールは、ユーザー空間のバッファとカーネル空間の間でデータを転送します。この際、システムコールがアクセスするユーザー空間のバッファ(例えば、Read
がデータを書き込むバイトスライスや、Write
がデータを読み取るバイトスライス)は、他のゴルーチンによって同時にアクセスされる可能性があります。
このコミット以前は、システムコールによるメモリへのアクセスがデータ競合検出器に適切に通知されていなかったため、システムコールを介して発生するデータ競合を見落とす可能性がありました。特に、Read
システムコールがバッファにデータを書き込む操作や、Write
システムコールがバッファからデータを読み取る操作は、他のゴルーチンによる同じバッファへのアクセスと競合する可能性があります。
この問題は、内部的に#5567
として追跡されていたようです。src/pkg/runtime/race/testdata/mop_test.go
に追加されたTestRaceIssue5567
テストケースは、この特定の競合シナリオを再現し、修正を検証するために作成されました。このテストは、os.Open
でファイルを開き、f.Read(b)
でバイトスライスb
に読み込み、そのb
を別のゴルーチンでsha1.New().Write(b)
でハッシュ計算するという、典型的なI/Oと並行処理のパターンで競合が発生することを示唆しています。
前提知識の解説
- データ競合 (Data Race): 複数のゴルーチンが同時に同じメモリ位置にアクセスし、そのうち少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生するバグです。データ競合は、プログラムの動作を非決定的にし、デバッグを困難にします。
- Go Race Detector: Go言語に組み込まれているツールで、実行時にデータ競合を検出します。プログラムを特別なビルドフラグ(
-race
)付きでコンパイルし、実行時にメモリアクセスを監視することで機能します。競合が検出されると、詳細なレポートが出力されます。 - システムコール (Syscall): オペレーティングシステムが提供するサービス(ファイルI/O、ネットワーク通信、メモリ管理など)をプログラムが利用するためのインターフェースです。Goプログラムがファイルからデータを読み込んだり、ネットワークにデータを書き込んだりする際には、内部的にシステムコールが呼び出されます。
runtime.RaceRead
/runtime.RaceWrite
: Goのランタイムが提供する関数で、データ競合検出器に単一のメモリ位置への読み取り/書き込みアクセスを通知します。runtime.RaceAcquire
/runtime.RaceRelease
/runtime.RaceReleaseMerge
: データ競合検出器に同期プリミティブ(ミューテックスなど)の取得/解放を通知するための関数です。これにより、同期によって保護されたメモリアクセスが競合として誤検出されるのを防ぎます。unsafe.Pointer
: Goの型システムをバイパスして、任意の型のポインタを表現できる特殊な型です。システムプログラミングや、C言語との相互運用など、低レベルの操作が必要な場合に使用されます。データ競合検出器にメモリ範囲を通知する際に、アドレスを渡すために使用されます。textflag 7
(pragma textflag 7): Goのコンパイラディレクティブの一つで、関数がインライン化されないように指示します。これは、特定のランタイム関数が常に独立した関数として呼び出されることを保証するために使用されます。
技術的詳細
このコミットの核心は、Read
およびWrite
システムコールが操作するメモリ領域を、Goのデータ競合検出器に明示的に通知することです。これにより、システムコールを介したI/O操作中に発生する可能性のあるデータ競合を正確に検出できるようになります。
具体的には、以下の変更が行われました。
-
新しいランタイム関数
RaceReadRange
とRaceWriteRange
の導入:src/pkg/runtime/race.c
に、runtime·RaceReadRange
とruntime·RaceWriteRange
というC言語で実装された関数が追加されました。これらの関数は、指定されたアドレスaddr
からlen
バイトのメモリ範囲に対する読み取り/書き込みアクセスをデータ競合検出器に通知します。src/pkg/runtime/race.go
に、これらのC関数に対応するGoの宣言が追加されました。- これらの関数は、
rangeaccess
という内部関数を呼び出しており、これが実際のメモリ範囲アクセス追跡ロジックを処理します。
-
syscall
パッケージにおけるraceReadRange
とraceWriteRange
のラッパー関数の導入:src/pkg/syscall/race.go
に、runtime.RaceReadRange
とruntime.RaceWriteRange
を呼び出すraceReadRange
とraceWriteRange
というGo関数が追加されました。これらは、データ競合検出が有効な場合にのみ使用されます。src/pkg/syscall/race0.go
には、データ競合検出が無効な場合にコンパイルされる、これらの関数の空のスタブが追加されました。これにより、-race
フラグなしでビルドされた場合のオーバーヘッドがなくなります。
-
各OSの
syscall
実装におけるRead
とWrite
の変更:src/pkg/syscall/syscall_unix.go
(Unix系OS)、src/pkg/syscall/syscall_windows.go
(Windows)、src/pkg/syscall/syscall_plan9.go
(Plan 9) の各ファイルで、Read
およびWrite
システムコールの実装が変更されました。Read
システムコール: データをバッファに読み込む操作であるため、読み込みが成功し、実際にデータがバッファに書き込まれた場合(n > 0
)、raceWriteRange(unsafe.Pointer(&p[0]), n)
を呼び出して、読み込まれたバイトスライスp
の先頭からn
バイトが書き込みアクセスされたことをデータ競合検出器に通知します。Write
システムコール: バッファからデータを書き出す操作であるため、書き込みが成功し、実際にデータがバッファから読み取られた場合(n > 0
)、raceReadRange(unsafe.Pointer(&p[0]), n)
を呼び出して、書き出されたバイトスライスp
の先頭からn
バイトが読み取りアクセスされたことをデータ競合検出器に通知します。ioSync
という同期プリミティブに対するraceAcquire
やraceReleaseMerge
の呼び出しは、システムコール自体の同期を追跡するためのものであり、メモリバッファへのアクセス追跡とは独立しています。このコミットでは、メモリバッファへのアクセス追跡が追加されました。
-
テストケース
TestRaceIssue5567
の追加:src/pkg/runtime/race/testdata/mop_test.go
に、TestRaceIssue5567
という新しいテストケースが追加されました。このテストは、ファイルからの読み込み(os.Open
とf.Read
)と、その読み込んだデータを別のゴルーチンで処理(sha1.New().Write
)する際に発生するデータ競合を意図的に再現します。このテストが競合を検出することで、変更が正しく機能していることを検証します。
これらの変更により、Goのデータ競合検出器は、システムコールを介したI/O操作によって共有メモリがアクセスされるシナリオにおいても、より包括的にデータ競合を検出できるようになりました。
コアとなるコードの変更箇所
src/pkg/runtime/race.c
// func RaceReadRange(addr unsafe.Pointer, len int)
#pragma textflag 7
void
runtime·RaceReadRange(void *addr, intgo len)
{
rangeaccess(addr, len, 1, 0, (uintptr)runtime·getcallerpc(&addr), false);
}
// func RaceWriteRange(addr unsafe.Pointer, len int)
#pragma textflag 7
void
runtime·RaceWriteRange(void *addr, intgo len)
{
rangeaccess(addr, len, 1, 0, (uintptr)runtime·getcallerpc(&addr), true);
}
src/pkg/runtime/race.go
func RaceRead(addr unsafe.Pointer)
func RaceWrite(addr unsafe.Pointer)
+func RaceReadRange(addr unsafe.Pointer, len int)
+func RaceWriteRange(addr unsafe.Pointer, len int)
src/pkg/syscall/race.go
func raceAcquire(addr unsafe.Pointer) {
runtime.RaceAcquire(addr)
}
func raceRelease(addr unsafe.Pointer) {
runtime.RaceRelease(addr)
}
func raceReleaseMerge(addr unsafe.Pointer) {
runtime.RaceReleaseMerge(addr)
}
+func raceReadRange(addr unsafe.Pointer, len int) {
+ runtime.RaceReadRange(addr, len)
+}
+
+func raceWriteRange(addr unsafe.Pointer, len int) {
+ runtime.RaceWriteRange(addr, len)
+}
src/pkg/syscall/race0.go
func raceAcquire(addr unsafe.Pointer) {
}
func raceRelease(addr unsafe.Pointer) {
}
func raceReleaseMerge(addr unsafe.Pointer) {
}
+func raceReadRange(addr unsafe.Pointer, len int) {
+}
+
+func raceWriteRange(addr unsafe.Pointer, len int) {
+}
src/pkg/syscall/syscall_unix.go
(例: Unix系OSのRead/Write)
func Read(fd int, p []byte) (n int, err error) {
n, err = read(fd, p)
if raceenabled {
if n > 0 {
raceWriteRange(unsafe.Pointer(&p[0]), n) // Readはバッファに書き込む
}
if err == nil {
raceAcquire(unsafe.Pointer(&ioSync))
}
}
return
}
func Write(fd int, p []byte) (n int, err error) {
if raceenabled {
raceReleaseMerge(unsafe.Pointer(&ioSync))
}
n, err = write(fd, p)
if raceenabled && n > 0 {
raceReadRange(unsafe.Pointer(&p[0]), n) // Writeはバッファから読み込む
}
return
}
src/pkg/runtime/race/testdata/mop_test.go
+func TestRaceIssue5567(t *testing.T) {
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(4))
+ in := make(chan []byte)
+ res := make(chan error)
+ go func() {
+ var err error
+ defer func() {
+ close(in)
+ res <- err
+ }()
+ path := "mop_test.go"
+ f, err := os.Open(path)
+ if err != nil {
+ return
+ }
+ defer f.Close()
+ var n, total int
+ b := make([]byte, 17) // the race is on b buffer
+ for err == nil {
+ n, err = f.Read(b)
+ total += n
+ if n > 0 {
+ in <- b[:n]
+ }
+ }
+ if err == io.EOF {
+ err = nil
+ }
+ }()
+ h := sha1.New()
+ for b := range in {
+ h.Write(b)
+ }
+ _ = h.Sum(nil)
+ err := <-res
+ if err != nil {
+ t.Fatal(err)
+ }
+}
コアとなるコードの解説
このコミットの主要な変更は、Goのランタイムとsyscall
パッケージ間の連携を強化し、システムコールが関与するメモリ操作をデータ競合検出器が認識できるようにした点です。
-
runtime/race.c
とruntime/race.go
:runtime·RaceReadRange
とruntime·RaceWriteRange
は、データ競合検出器のC言語バックエンドに、特定のメモリ範囲(アドレスと長さで指定)へのアクセスを通知するための新しいエントリポイントです。rangeaccess
関数は、この範囲アクセス情報を内部的に処理し、競合の可能性をチェックします。#pragma textflag 7
は、これらの関数がインライン化されないようにし、常に独立した関数呼び出しとして扱われることを保証します。
-
syscall/race.go
とsyscall/race0.go
:syscall/race.go
は、-race
ビルドフラグが有効な場合にコンパイルされ、runtime
パッケージの対応するRaceReadRange
とRaceWriteRange
関数を呼び出す薄いラッパーを提供します。これにより、syscall
パッケージはランタイムのデータ競合検出機能を利用できます。syscall/race0.go
は、-race
ビルドフラグが無効な場合にコンパイルされ、これらの関数の空の(何もしない)実装を提供します。これにより、データ競合検出が不要なビルドでは、これらの呼び出しによるパフォーマンスオーバーヘッドが発生しません。
-
syscall/syscall_*.go
(各OS固有の実装):- 各OSの
Read
およびWrite
システムコールの実装に、raceenabled
(データ競合検出が有効かどうかを示すグローバル変数)のチェックが追加されました。 Read
システムコールは、カーネルからユーザー空間のバッファにデータを「書き込む」操作であるため、読み込みが成功してn
バイトがバッファp
に書き込まれた場合、raceWriteRange(unsafe.Pointer(&p[0]), n)
が呼び出されます。これは、p
の先頭からn
バイトが書き込みアクセスされたことを検出器に通知します。Write
システムコールは、ユーザー空間のバッファからカーネルにデータを「読み出す」操作であるため、書き込みが成功してn
バイトがバッファp
から読み取られた場合、raceReadRange(unsafe.Pointer(&p[0]), n)
が呼び出されます。これは、p
の先頭からn
バイトが読み取りアクセスされたことを検出器に通知します。- これにより、I/O操作中にバッファが他のゴルーチンによって同時にアクセスされた場合に、データ競合が正しく検出されるようになります。
- 各OSの
-
src/pkg/runtime/race/testdata/mop_test.go
:TestRaceIssue5567
は、この変更の重要性を示す具体的な例です。このテストは、os.Open
でファイルを開き、f.Read(b)
でバイトスライスb
にデータを読み込みます。同時に、別のゴルーチンがそのb
をチャネル経由で受け取り、sha1.New().Write(b)
でハッシュ計算を行います。f.Read(b)
がb
に書き込んでいる最中に、sha1.New().Write(b)
がb
から読み込もうとすると、同期メカニズムがないためデータ競合が発生します。このコミット以前は、f.Read(b)
による書き込みがデータ競合検出器に適切に通知されなかったため、この競合が見過ごされる可能性がありました。- このテストケースの追加と、それが競合を検出できるようになったことは、このコミットが解決しようとした問題と、その解決策が機能していることを明確に示しています。
これらの変更は、Goのデータ競合検出器の堅牢性を大幅に向上させ、I/O操作が関与する並行プログラムのデバッグをより効果的にします。
関連リンク
- Go Race Detectorの公式ドキュメント: https://go.dev/doc/articles/race_detector
- Goのシステムコールパッケージに関するドキュメント: https://pkg.go.dev/syscall
参考にした情報源リンク
- Goのコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/10085043
は、Gerritの変更リストへのリンクです。) - GoのIssue Tracker (GitHub): https://github.com/golang/go/issues (ただし、
#5567
は直接見つかりませんでした。これは内部的な追跡番号であるか、非常に古いIssueである可能性があります。) - Goのソースコード: https://github.com/golang/go