[インデックス 16085] ファイルの概要
このコミットは、Goランタイムのsrc/pkg/runtime/race.c
ファイルに対する変更です。このファイルはGoのデータ競合検出器(Race Detector)のランタイム部分を実装しており、メモリのシャドウマッピングや競合検出ロジックの初期化を担当しています。具体的には、データセグメントとBSSセグメントのシャドウメモリを適切にマッピングするための修正が含まれています。
コミット
Goランタイムにおけるデータ競合検出器のシャドウメモリマッピングの不具合を修正しました。MapShadow()
関数に渡される値がページアラインされていない場合、mmap()
システムコールがマッピングの開始または終了を切り詰めてしまうため、競合検出器が正しく機能しない問題がありました。このコミットでは、データセグメントとBSSセグメントの範囲をページ境界にアラインすることで、この問題を解決しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/12b7db3d578f9764416ae987a4fa2e90125f379c
元コミット内容
commit 12b7db3d578f9764416ae987a4fa2e90125f379c
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Apr 4 09:11:34 2013 +1100
runtime: fix data/bss shadow memory mapping for race detector
Fixes #5175.
Race detector runtime expects values passed to MapShadow() to be page-aligned,
because they are used in mmap() call. If they are not aligned mmap() trims
either beginning or end of the mapping.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/8325043
変更の背景
Goのデータ競合検出器は、プログラムのメモリアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも一方が書き込み操作である場合に発生するデータ競合を検出します。この検出器は「シャドウメモリ」という概念を使用しており、実際のメモリ領域に対応するシャドウメモリ領域を確保し、そこにアクセス情報を記録します。
このコミットが行われる前は、データ競合検出器のランタイムがMapShadow()
関数を呼び出して、プログラムのデータセグメントとBSSセグメントに対応するシャドウメモリをマッピングしていました。しかし、このMapShadow()
関数は内部でmmap()
システムコールを使用しており、mmap()
はマッピングするアドレスとサイズがメモリページの境界にアラインされていることを期待します。
問題は、MapShadow()
に渡されるnoptrdata
(非ポインタデータセグメントの開始アドレス)とenoptrbss
(非ポインタBSSセグメントの終了アドレス)が、必ずしもページアラインされているとは限らなかった点にありました。mmap()
は、アラインされていないアドレスやサイズが指定された場合、指定された範囲を最も近いページ境界に切り詰めてマッピングします。この切り詰めが発生すると、データセグメントやBSSセグメントの全体がシャドウメモリにマッピングされず、結果として競合検出器が一部のメモリ領域での競合を見逃す可能性がありました。
この問題はGoのIssue #5175として報告されており、このコミットはその修正として導入されました。
前提知識の解説
Go Race Detector (データ競合検出器)
Goのデータ競合検出器は、Go 1.1で導入された強力なツールで、並行処理におけるデータ競合バグを特定するのに役立ちます。これは、プログラムの実行中にメモリアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、そのうち少なくとも1つが書き込み操作である場合に警告を発します。データ競合は、プログラムの予測不能な動作やクラッシュを引き起こす可能性のある、並行プログラミングにおける一般的なバグです。
競合検出器は、各メモリワードに対して「シャドウメモリ」と呼ばれる追加のメタデータを維持することで機能します。このシャドウメモリには、そのメモリワードへの最近のアクセス(読み取りまたは書き込み)に関する情報(どのゴルーチンがアクセスしたか、読み取りか書き込みかなど)が記録されます。新しいアクセスが発生すると、競合検出器はそのアクセス情報とシャドウメモリ内の既存の情報を比較し、競合があるかどうかを判断します。
メモリマッピング (mmap
)
mmap()
は、Unix系オペレーティングシステムで利用可能なシステムコールで、ファイルやデバイスをプロセスの仮想アドレス空間にマッピングするために使用されます。これにより、ファイルの内容をメモリのように直接アクセスできるようになります。mmap()
は、共有メモリ、ファイルI/Oの高速化、動的ライブラリのロードなど、様々な用途で利用されます。
mmap()
を使用する際の重要な概念は「ページアラインメント」です。オペレーティングシステムはメモリを固定サイズの「ページ」単位で管理します(一般的なページサイズは4KB)。mmap()
に渡されるアドレスとサイズは、これらのページ境界に正確にアラインされている必要があります。アラインされていないアドレスが指定された場合、mmap()
は通常、指定された範囲を最も近いページ境界に切り詰めてマッピングします。例えば、ページ境界の途中のアドレスからマッピングを開始しようとすると、mmap()
は自動的にそのアドレスを含む最も近いページ境界からマッピングを開始します。同様に、サイズがページサイズの倍数でない場合、mmap()
は指定されたサイズを含む最小のページサイズの倍数に切り上げたり、切り捨てたりすることがあります。
データセグメントとBSSセグメント
プログラムのメモリレイアウトは、通常、いくつかのセグメントに分割されます。
- データセグメント (Data Segment): 初期化されたグローバル変数や静的変数が格納される領域です。プログラムの開始時に値が設定されます。
- BSSセグメント (Block Started by Symbol Segment): 初期化されていないグローバル変数や静的変数が格納される領域です。プログラムの開始時にはゼロで初期化されます。
Goランタイムでは、これらのセグメントの開始アドレスと終了アドレスを内部的に管理しています。このコミットで言及されているnoptrdata
は非ポインタデータセグメントの開始アドレスを、enoptrbss
は非ポインタBSSセグメントの終了アドレスをそれぞれ指します。データ競合検出器は、これらのセグメント全体を監視する必要があるため、対応するシャドウメモリ領域もこれらのセグメント全体をカバーする必要があります。
技術的詳細
このコミットの核心は、mmap()
システムコールのページアラインメント要件を正しく満たすことです。
以前の実装では、runtime∕race·MapShadow
関数にnoptrdata
とenoptrbss - noptrdata
という引数が直接渡されていました。ここで、noptrdata
はデータセグメントの開始アドレス、enoptrbss - noptrdata
はそのセグメントのサイズを表します。しかし、これらの値は必ずしもメモリページの境界にアラインされているとは限りませんでした。
mmap()
は、マッピングの開始アドレスがページ境界にない場合、そのアドレスを含む最も近いページ境界まで開始アドレスを切り下げます。また、マッピングのサイズがページサイズの倍数でない場合、mmap()
は通常、指定されたサイズを含む最小のページサイズの倍数に切り上げます。しかし、このコミットの背景にある問題は、mmap()
が「切り詰める」という動作をすることでした。これは、指定された範囲がページ境界に収まらない場合に、その範囲の一部がマッピングから除外されることを意味します。特に、開始アドレスがページアラインされていない場合、mmap()
は指定された開始アドレスよりも大きなアドレスからマッピングを開始する可能性があり、これによりデータセグメントの冒頭部分がシャドウメモリの監視対象から漏れてしまう危険性がありました。
この問題に対処するため、コミットではMapShadow()
に渡す前に、開始アドレスとサイズを明示的にページアラインするロジックが追加されました。
-
開始アドレスのページアラインメント:
start = (uintptr)noptrdata & ~(PageSize-1);
この式は、noptrdata
を最も近いページ境界に切り下げます。PageSize
はシステムのメモリページサイズ(例: 4096バイト)です。PageSize-1
は、ページサイズから1を引いた値であり、そのビット表現はページサイズの下位ビットがすべて1になります(例: 4095 =0b111111111111
)。この値をビット反転(~
)すると、ページサイズの下位ビットがすべて0になり、それより上位のビットがすべて1になります。このマスクをnoptrdata
とビットAND演算することで、noptrdata
の下位ビットがクリアされ、結果としてnoptrdata
以下の最も近いページ境界アドレスが得られます。 -
サイズのページアラインメント:
size = ROUND((uintptr)enoptrbss - start, PageSize);
ここでROUND
マクロ(または関数)は、指定された値を指定されたアラインメントの倍数に切り上げるために使用されます。計算されるサイズは、ページアラインされたstart
アドレスからenoptrbss
までの範囲をカバーするようにします。enoptrbss - start
は、ページアラインされた開始アドレスからBSSセグメントの終了までの実際のバイト数です。この値をPageSize
の倍数に切り上げることで、mmap()
が要求するページアラインされたサイズが確保され、かつ元のデータ/BSSセグメント全体が確実にシャドウメモリにマッピングされるようになります。
これらの変更により、MapShadow()
に渡される引数が常にmmap()
の要件を満たすようになり、データ競合検出器がプログラムのデータ/BSSセグメント全体を正確に監視できるようになりました。
コアとなるコードの変更箇所
src/pkg/runtime/race.c
ファイルのruntime·raceinit
関数内の変更です。
--- a/src/pkg/runtime/race.c
+++ b/src/pkg/runtime/race.c
@@ -36,11 +36,14 @@ static bool onstack(uintptr argp);\n uintptr\n runtime·raceinit(void)\n {\n-\tuintptr racectx;\n+\tuintptr racectx, start, size;\n \n \tm->racecall = true;\n \truntime∕race·Initialize(&racectx);\n-\truntime∕race·MapShadow(noptrdata, enoptrbss - noptrdata);\n+\t// Round data segment to page boundaries, because it's used in mmap().\n+\tstart = (uintptr)noptrdata & ~(PageSize-1);\n+\tsize = ROUND((uintptr)enoptrbss - start, PageSize);\n+\truntime∕race·MapShadow((void*)start, size);\n \tm->racecall = false;\n \treturn racectx;\n }\n```
## コアとなるコードの解説
変更は`runtime·raceinit`関数内で行われています。この関数は、Goプログラムの起動時にデータ競合検出器を初期化する役割を担っています。
1. **変数の追加**:
`uintptr racectx, start, size;`
以前は`racectx`のみが宣言されていましたが、新たに`start`と`size`という`uintptr`型の変数が追加されました。これらは、ページアラインされたシャドウメモリ領域の開始アドレスとサイズを保持するために使用されます。
2. **シャドウメモリマッピングの引数計算**:
変更の核心は、`runtime∕race·MapShadow`関数に渡す引数を計算する部分です。
* `start = (uintptr)noptrdata & ~(PageSize-1);`
`noptrdata`は、Goランタイムが管理する非ポインタデータセグメントの開始アドレスです。この行では、`noptrdata`を最も近いページ境界に切り下げたアドレスを`start`に代入しています。`PageSize`はシステムが使用するメモリページのサイズ(通常4KB)を表す定数です。`~(PageSize-1)`は、`PageSize`の倍数にアラインするためのビットマスクを生成します。このビットマスクと`noptrdata`のビットAND演算を行うことで、`noptrdata`が指すアドレスを含む、最も近いページ境界の開始アドレスが得られます。
* `size = ROUND((uintptr)enoptrbss - start, PageSize);`
`enoptrbss`は、非ポインタBSSセグメントの終了アドレスです。この行では、ページアラインされた`start`アドレスから`enoptrbss`までの範囲をカバーするのに必要なサイズを計算し、それを`PageSize`の倍数に切り上げて`size`に代入しています。`ROUND`マクロ(または関数)は、第一引数を第二引数の倍数に切り上げる役割を果たします。これにより、データセグメントとBSSセグメント全体がシャドウメモリにマッピングされることが保証されます。
3. **`MapShadow`の呼び出し**:
`runtime∕race·MapShadow((void*)start, size);`
最後に、計算されたページアラインされた`start`アドレスと`size`を引数として`runtime∕race·MapShadow`関数が呼び出されます。これにより、`mmap()`システムコールが正しく機能し、データ競合検出器が監視すべきメモリ領域全体がシャドウメモリに正確にマッピングされるようになります。
この修正により、データ競合検出器がGoプログラムのデータセグメントとBSSセグメントにおけるすべてのメモリアクセスを確実に監視できるようになり、検出器の信頼性と正確性が向上しました。
## 関連リンク
* GitHubコミットページ: [https://github.com/golang/go/commit/12b7db3d578f9764416ae987a4fa2e90125f379c](https://github.com/golang/go/commit/12b7db3d578f9764416ae987a4fa2e90125f379c)
* Go Issue #5175: [https://go.dev/issue/5175](https://go.dev/issue/5175)
* Go Code Review: [https://golang.org/cl/8325043](https://golang.org/cl/8325043)
## 参考にした情報源リンク
* Go Race Detector Documentation: [https://go.dev/doc/articles/race_detector](https://go.dev/doc/articles/race_detector)
* `mmap` man page (Linux): `man mmap` (一般的な`mmap`の動作とページアラインメントに関する情報)
* Go Source Code (runtime package): [https://github.com/golang/go/tree/master/src/runtime](https://github.com/golang/go/tree/master/src/runtime)
* Go Memory Model: [https://go.dev/ref/mem](https://go.dev/ref/mem)
* Data Segment and BSS Segment: (一般的なOSやコンパイラのドキュメント)