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

[インデックス 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言語のコード内で直接行われるメモリアクセスを主に監視していました。しかし、ReadWriteといったシステムコールは、ユーザー空間のバッファとカーネル空間の間でデータを転送します。この際、システムコールがアクセスするユーザー空間のバッファ(例えば、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操作中に発生する可能性のあるデータ競合を正確に検出できるようになります。

具体的には、以下の変更が行われました。

  1. 新しいランタイム関数 RaceReadRangeRaceWriteRange の導入:

    • src/pkg/runtime/race.c に、runtime·RaceReadRangeruntime·RaceWriteRange というC言語で実装された関数が追加されました。これらの関数は、指定されたアドレスaddrからlenバイトのメモリ範囲に対する読み取り/書き込みアクセスをデータ競合検出器に通知します。
    • src/pkg/runtime/race.go に、これらのC関数に対応するGoの宣言が追加されました。
    • これらの関数は、rangeaccessという内部関数を呼び出しており、これが実際のメモリ範囲アクセス追跡ロジックを処理します。
  2. syscallパッケージにおけるraceReadRangeraceWriteRange のラッパー関数の導入:

    • src/pkg/syscall/race.go に、runtime.RaceReadRangeruntime.RaceWriteRange を呼び出すraceReadRangeraceWriteRange というGo関数が追加されました。これらは、データ競合検出が有効な場合にのみ使用されます。
    • src/pkg/syscall/race0.go には、データ競合検出が無効な場合にコンパイルされる、これらの関数の空のスタブが追加されました。これにより、-raceフラグなしでビルドされた場合のオーバーヘッドがなくなります。
  3. 各OSのsyscall実装におけるReadWriteの変更:

    • 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という同期プリミティブに対するraceAcquireraceReleaseMergeの呼び出しは、システムコール自体の同期を追跡するためのものであり、メモリバッファへのアクセス追跡とは独立しています。このコミットでは、メモリバッファへのアクセス追跡が追加されました。
  4. テストケース TestRaceIssue5567 の追加:

    • src/pkg/runtime/race/testdata/mop_test.go に、TestRaceIssue5567 という新しいテストケースが追加されました。このテストは、ファイルからの読み込み(os.Openf.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パッケージ間の連携を強化し、システムコールが関与するメモリ操作をデータ競合検出器が認識できるようにした点です。

  1. runtime/race.cruntime/race.go:

    • runtime·RaceReadRangeruntime·RaceWriteRange は、データ競合検出器のC言語バックエンドに、特定のメモリ範囲(アドレスと長さで指定)へのアクセスを通知するための新しいエントリポイントです。rangeaccess関数は、この範囲アクセス情報を内部的に処理し、競合の可能性をチェックします。#pragma textflag 7は、これらの関数がインライン化されないようにし、常に独立した関数呼び出しとして扱われることを保証します。
  2. syscall/race.gosyscall/race0.go:

    • syscall/race.go は、-raceビルドフラグが有効な場合にコンパイルされ、runtimeパッケージの対応するRaceReadRangeRaceWriteRange関数を呼び出す薄いラッパーを提供します。これにより、syscallパッケージはランタイムのデータ競合検出機能を利用できます。
    • syscall/race0.go は、-raceビルドフラグが無効な場合にコンパイルされ、これらの関数の空の(何もしない)実装を提供します。これにより、データ競合検出が不要なビルドでは、これらの呼び出しによるパフォーマンスオーバーヘッドが発生しません。
  3. 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操作中にバッファが他のゴルーチンによって同時にアクセスされた場合に、データ競合が正しく検出されるようになります。
  4. 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操作が関与する並行プログラムのデバッグをより効果的にします。

関連リンク

参考にした情報源リンク