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

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

このコミットは、Go言語のデータ競合検出機能(Data Race Detector)の一部として、syscallパッケージにおける変更を導入するものです。特に、Read()およびWrite()システムコールに対して粗粒度(coarse-grained)の同期メカニズムを追加し、データ競合の検出を可能にすることを目的としています。

コミット

commit cffbfaeb1819bfb6770848c4d57615f0ee1a46ba
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Oct 9 20:51:58 2012 +0400

    race: syscall changes
    This is a part of a bigger change that adds data race detection feature:
    https://golang.org/cl/6456044
    The purpose of this patch is to provide coarse-grained synchronization
    between all Read() and Write() calls.
    
    R=rsc, bradfitz, alex.brainman
    CC=golang-dev
    https://golang.org/cl/6610064

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

https://github.com/golang/go/commit/cffbfaeb1819bfb6770848c4d57615f0ee1a46ba

元コミット内容

race: syscall changes
This is a part of a bigger change that adds data race detection feature:
https://golang.org/cl/6456044
The purpose of this patch is to provide coarse-grained synchronization
between all Read() and Write() calls.

変更の背景

このコミットは、Go言語にデータ競合検出機能(Data Race Detector)を導入する大規模な変更の一部です。データ競合は、複数のゴルーチンが共有変数に同時にアクセスし、そのうち少なくとも1つが書き込み操作である場合に、適切な同期なしで発生するバグの一種です。これらのバグは非決定論的であり、再現が困難なため、デバッグが非常に難しいことで知られています。

Goのデータ競合検出器は、コンパイル時または実行時にメモリアクセスを計測し、同期されていない共有変数へのアクセスを監視することで、これらの競合を特定します。このコミットの具体的な目的は、syscallパッケージ内のRead()およびWrite()システムコール間で粗粒度の同期を提供することです。これにより、これらのI/O操作がデータ競合検出器によって適切に監視され、関連する競合が報告されるようになります。

前提知識の解説

データ競合 (Data Race)

データ競合は、並行プログラミングにおける一般的なバグの一種です。以下の3つの条件がすべて満たされたときに発生します。

  1. 複数のゴルーチン(またはスレッド)が同じメモリ位置にアクセスする。
  2. 少なくとも1つのアクセスが書き込み操作である。
  3. アクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。

データ競合が発生すると、プログラムの動作が非決定論的になり、予期せぬ結果やクラッシュを引き起こす可能性があります。

Goのデータ競合検出器 (Go Data Race Detector)

Go言語には、データ競合を検出するための組み込みツールが用意されています。これは、go test -racego run -racego build -raceなどのコマンドに-raceフラグを追加することで有効にできます。

  • 動作原理: データ競合検出器は、プログラムのメモリアクセスを計測します。これは、C/C++のThreadSanitizerランタイムライブラリに基づいています。計測されたアクセスは、Goランタイムによって監視され、共有変数に対する同期されていない操作が検出されると、競合が報告されます。
  • 利点: データ競合はデバッグが非常に困難ですが、検出器は競合が検出された際に明確なレポートを提供し、バグの特定を支援します。誤検知(false positives)は発生しないように設計されています。
  • 考慮事項: 検出器を有効にすると、メモリ使用量が5〜10倍、実行時間が2〜20倍増加するなど、パフォーマンスにオーバーヘッドが生じます。そのため、本番環境での常時有効化は推奨されず、テスト時、特に並行処理を含むコードの単体テスト、結合テスト、負荷テスト中に使用するのが最適です。検出器は、実行中に実際にトリガーされた競合のみを検出するため、包括的なテストカバレッジが重要です。

syscallパッケージ

syscallパッケージは、Goプログラムがオペレーティングシステム(OS)のプリミティブなシステムコールに直接アクセスするための機能を提供します。ファイルI/O、ネットワーク通信、プロセス管理など、OSレベルの操作を行う際に使用されます。このパッケージは、OS固有のシステムコールを抽象化し、Goプログラムから利用できるようにします。

unsafe.Pointer

unsafe.Pointerは、Go言語において型安全性をバイパスし、任意の型のポインタを表現できる特殊なポインタ型です。これにより、Goの型システムでは通常許可されないメモリ操作(例: 異なる型のポインタ間の変換、ポインタとuintptr間の変換)が可能になります。データ競合検出器のような低レベルのメモリ操作を伴う機能では、unsafe.Pointerが使用されることがあります。

技術的詳細

このコミットの主要な技術的変更点は、syscallパッケージ内のRead()およびWrite()システムコールにデータ競合検出のための同期メカニズムを組み込むことです。これは、主に以下の要素によって実現されています。

  1. race.gorace0.goの導入:

    • race.goは、ビルドタグ+build raceが指定された場合にコンパイルされるファイルです。このファイルには、データ競合検出が有効な場合にのみ使用されるraceAcquireraceReleaseMerge関数が定義されています。これらの関数は、runtimeパッケージのRaceAcquireRaceReleaseMergeを呼び出します。また、raceenabled定数がtrueに設定されます。
    • race0.goは、ビルドタグ+build !raceが指定された場合(つまり、データ競合検出が無効な場合)にコンパイルされるファイルです。このファイルでは、raceAcquireraceReleaseMerge関数は空の関数として定義され、raceenabled定数はfalseに設定されます。これにより、データ競合検出が無効なビルドでは、これらの同期呼び出しがコンパイル時に最適化されて削除され、パフォーマンスオーバーヘッドが発生しないようになっています。
  2. ioSync変数の導入:

    • ioSyncというint64型の変数が導入されました。この変数は、Read()およびWrite()操作間の同期の対象となる共有メモリ位置として機能します。unsafe.Pointer(&ioSync)としてそのアドレスがraceAcquireおよびraceReleaseMergeに渡されます。
  3. Read()およびWrite()システムコールの変更:

    • 各OS固有のsyscallパッケージ(darwin, freebsd, linux, netbsd, openbsd, plan9, windows)において、Read()およびWrite()システムコールの実装が変更されました。
    • Read()関数では、システムコールが成功し、エラーが発生しなかった場合にraceAcquire(unsafe.Pointer(&ioSync))が呼び出されます。これは、読み取り操作が完了した後に、ioSyncに対する「取得」操作をデータ競合検出器に通知します。
    • Write()関数では、システムコールが実行される前にraceReleaseMerge(unsafe.Pointer(&ioSync))が呼び出されます。これは、書き込み操作が開始される前に、ioSyncに対する「解放」操作をデータ競合検出器に通知します。
    • これらのraceAcquireraceReleaseMergeの呼び出しは、ioSync変数を介してRead()Write()操作の間に同期関係を確立します。これにより、データ競合検出器は、これらのI/O操作が並行して実行された場合に発生する可能性のある競合を検出できるようになります。
  4. システムコール関数のリネームとラッパーの導入:

    • 多くのOS固有のsyscallファイルにおいて、既存のReadおよびWrite関数がreadおよびwrite(小文字)にリネームされました。
    • そして、新しいReadおよびWrite(大文字)のラッパー関数が導入され、その中でデータ競合検出のためのraceAcquireraceReleaseMergeの呼び出しが行われ、その後、リネームされた小文字のreadwrite関数が呼び出される構造になっています。
    • また、一部のOSでは、readwriteの低レベルなシステムコールラッパーがreadlenwritelenといった名前に変更され、SYS_READSYS_WRITEといったシステムコール番号が明示的に割り当てられています。これは、Goのシステムコールバインディングの生成プロセス(zsyscall_*.goファイル群)と連携しています。

これらの変更により、Goのデータ競合検出器は、ファイルディスクリプタを介したRead()およびWrite()操作における並行アクセスを監視し、潜在的なデータ競合を報告できるようになります。

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

このコミットで変更された主要なファイルと関数は以下の通りです。

  • src/pkg/syscall/race.go: 新規追加。データ競合検出が有効な場合のraceAcquireraceReleaseMergeの実装。
  • src/pkg/syscall/race0.go: 新規追加。データ競合検出が無効な場合のraceAcquireraceReleaseMergeのスタブ実装。
  • src/pkg/syscall/exec_unix.go: read関数呼び出しがreadlenに変更。
  • src/pkg/syscall/syscall_darwin.go: ReadWritesys定義が小文字のreadwriteに変更。readwriteの低レベルなsys定義がreadlenwritelenに変更され、SYS_READ/SYS_WRITEが明示的に指定。
  • src/pkg/syscall/syscall_freebsd.go: syscall_darwin.goと同様の変更。
  • src/pkg/syscall/syscall_linux.go: syscall_darwin.goと同様の変更。
  • src/pkg/syscall/syscall_netbsd.go: syscall_darwin.goと同様の変更。
  • src/pkg/syscall/syscall_openbsd.go: syscall_darwin.goと同様の変更。
  • src/pkg/syscall/syscall_plan9.go: ReadWrite関数にraceenabledチェックとraceAcquire/raceReleaseMerge呼び出し、およびioSync変数の追加。
  • src/pkg/syscall/syscall_unix.go: ReadWrite関数にraceenabledチェックとraceAcquire/raceReleaseMerge呼び出し、およびioSync変数の追加。
  • src/pkg/syscall/syscall_windows.go: ReadWrite関数にraceenabledチェックとraceAcquire/raceReleaseMerge呼び出し、およびioSync変数の追加。
  • src/pkg/syscall/zsyscall_darwin_386.go (および他のzsyscall_*.goファイル):
    • Read関数がreadにリネーム。
    • Write関数がwriteにリネーム。
    • 低レベルなread関数がreadlenにリネーム。
    • 低レベルなwrite関数がwritelenにリネーム。

コアとなるコードの解説

このコミットの核心は、syscallパッケージにおけるReadWrite操作にデータ競合検出のフックを組み込むことです。

race.gorace0.go

これらのファイルは、データ競合検出機能の有効/無効を切り替えるための条件付きコンパイルを可能にします。

src/pkg/syscall/race.go (データ競合検出有効時):

// +build race

package syscall

import (
	"runtime"
	"unsafe"
)

const raceenabled = true

func raceAcquire(addr unsafe.Pointer) {
	runtime.RaceAcquire(addr)
}

func raceReleaseMerge(addr unsafe.Pointer) {
	runtime.RaceReleaseMerge(addr)
}
  • +build raceタグにより、go build -raceのように-raceフラグが指定された場合にのみコンパイルされます。
  • raceenabled定数がtrueに設定されます。
  • raceAcquireraceReleaseMergeは、runtimeパッケージの対応する関数を呼び出します。これらは、特定のメモリ位置(addr)に対するメモリ操作の同期イベントをデータ競合検出器に通知するために使用されます。RaceAcquireはメモリの「取得」を、RaceReleaseMergeはメモリの「解放とマージ」を意味します。

src/pkg/syscall/race0.go (データ競合検出無効時):

// +build !race

package syscall

import (
	"unsafe"
)

const raceenabled = false

func raceAcquire(addr unsafe.Pointer) {
}

func raceReleaseMerge(addr unsafe.Pointer) {
}
  • +build !raceタグにより、-raceフラグが指定されていない場合にコンパイルされます。
  • raceenabled定数がfalseに設定されます。
  • raceAcquireraceReleaseMergeは空の関数として定義されます。これにより、データ競合検出が不要なビルドでは、これらの関数呼び出しがインライン化され、実質的にコードから削除されるため、実行時のオーバーヘッドがなくなります。

Read()Write()関数の変更例 (syscall_unix.goの場合)

多くのOS固有のsyscallファイルで同様のパターンが適用されていますが、syscall_unix.goの変更は代表的です。

変更前 (概念):

func Read(fd int, p []byte) (n int, err error) {
	// システムコール呼び出し
	// ...
}

func Write(fd int, p []byte) (n int, err error) {
	// システムコール呼び出し
	// ...
}

変更後:

func Read(fd int, p []byte) (n int, err error) {
	n, err = read(fd, p) // 実際のシステムコールは小文字のreadが担当
	if raceenabled && err == nil {
		raceAcquire(unsafe.Pointer(&ioSync)) // 読み取り完了後にacquire
	}
	return
}

func Write(fd int, p []byte) (n int, err error) {
	if raceenabled {
		raceReleaseMerge(unsafe.Pointer(&ioSync)) // 書き込み前にrelease
	}
	return write(fd, p) // 実際のシステムコールは小文字のwriteが担当
}

var ioSync int64 // 同期対象となる共有変数
  • ReadWriteは、それぞれ実際のシステムコールを行う小文字のreadwrite関数を呼び出すラッパー関数になりました。
  • raceenabledtrueの場合(つまり、データ競合検出が有効な場合)にのみ、raceAcquireまたはraceReleaseMergeが呼び出されます。
  • ioSyncというint64型の変数が導入され、この変数のアドレスがraceAcquireraceReleaseMergeに渡されます。これは、ReadWrite操作がこのioSync変数を介して同期されることをデータ競合検出器に示します。
    • Write操作の前にraceReleaseMergeが呼び出されることで、書き込み操作がioSyncに対する「解放」イベントとして扱われます。
    • Read操作の後にraceAcquireが呼び出されることで、読み取り操作がioSyncに対する「取得」イベントとして扱われます。
  • これにより、ReadWriteが並行して実行され、ioSyncに対するアクセスが適切に同期されていないとデータ競合検出器が判断した場合に、警告が発せられます。これは、ファイルディスクリプタを介したI/O操作における潜在的な競合を検出するために重要です。

zsyscall_*.goファイルの変更

zsyscall_*.goファイル群は、Goのツールチェーンによって自動生成されるシステムコールバインディングです。このコミットでは、これらのファイル内の関数名も変更されています。

例えば、src/pkg/syscall/zsyscall_darwin_386.goでは、以下のような変更が見られます。

変更前:

func Read(fd int, p []byte) (n int, err error) { ... }
func Write(fd int, p []byte) (n int, err error) { ... }
func read(fd int, buf *byte, nbuf int) (n int, err error) { ... } // 低レベルなread
func write(fd int, buf *byte, nbuf int) (n int, err error) { ... } // 低レベルなwrite

変更後:

func read(fd int, p []byte) (n int, err error) { ... } // 旧Readがreadに
func write(fd int, p []byte) (n int, err error) { ... } // 旧Writeがwriteに
func readlen(fd int, buf *byte, nbuf int) (n int, err error) { ... } = SYS_READ // 旧readがreadlenに
func writelen(fd int, buf *byte, nbuf int) (n int, err error) { ... } = SYS_WRITE // 旧writeがwritelenに

この変更は、Goのsyscallパッケージの内部構造を整理し、データ競合検出のためのラッパー関数(大文字のRead/Write)と、実際のシステムコールを呼び出す低レベルな関数(小文字のread/write、さらにreadlen/writelen)を明確に区別するために行われました。

これらの変更全体として、Goのデータ競合検出器がsyscallパッケージを介したI/O操作における並行アクセスを効果的に監視し、開発者が並行処理のバグを早期に発見できるようにするための基盤を構築しています。

関連リンク

参考にした情報源リンク