[インデックス 18298] ファイルの概要
このコミットは、Goランタイムにおける特定のデッドロック問題を解決することを目的としています。具体的には、メモリ管理とプロファイリングに関連する2つのロック(span->specialLock
と proflock
)の間で発生していた循環的なロック取得順序によるデッドロックを解消します。
コミット
commit b039abfc3ec93debe732bb4824bebd098ab7a62a
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Jan 21 10:48:37 2014 +0400
runtime: fix specials deadlock
The deadlock is between span->specialLock and proflock:
goroutine 11 [running]:
runtime.MProf_Free(0x7fa272d26508, 0xc210054180, 0xc0)
src/pkg/runtime/mprof.goc:220 +0x27
runtime.freespecial(0x7fa272d1e088, 0xc210054180, 0xc0)
src/pkg/runtime/mheap.c:691 +0x6a
runtime.freeallspecials(0x7fa272d1af50, 0xc210054180, 0xc0)
src/pkg/runtime/mheap.c:717 +0xb5
runtime.free(0xc210054180)
src/pkg/runtime/malloc.goc:190 +0xfd
selectgo(0x7fa272a5ef58)
src/pkg/runtime/chan.c:1136 +0x2d8
runtime.selectgo(0xc210054180)
src/pkg/runtime/chan.c:840 +0x12
runtime_test.func·058()
src/pkg/runtime/proc_test.go:146 +0xb4
runtime.goexit()
src/pkg/runtime/proc.c:1405
created by runtime_test.TestTimerFairness
src/pkg/runtime/proc_test.go:152 +0xd1
goroutine 12 [running]:
addspecial(0xc2100540c0, 0x7fa272d1e0a0)
src/pkg/runtime/mheap.c:569 +0x88
runtime.setprofilebucket(0xc2100540c0, 0x7fa272d26508)
src/pkg/runtime/mheap.c:668 +0x73
runtime.MProf_Malloc(0xc2100540c0, 0xc0, 0x0)
src/pkg/runtime/mprof.goc:212 +0x16b
runtime.mallocgc(0xc0, 0x0, 0xc200000000)
src/pkg/runtime/malloc.goc:142 +0x239
runtime.mal(0xbc)
src/pkg/runtime/malloc.goc:703 +0x38
newselect(0x2, 0x7fa272a5cf60)
src/pkg/runtime/chan.c:632 +0x53
runtime.newselect(0xc200000002, 0xc21005f000)
src/pkg/runtime/chan.c:615 +0x28
runtime_test.func·058()
src/pkg/runtime/proc_test.go:146 +0x37
runtime.goexit()
src/pkg/runtime/proc.c:1405
created by runtime_test.TestTimerFairness
src/pkg/runtime/proc_test.go:152 +0xd1
Fixes #7099.
R=golang-codereviews, khr
CC=golang-codereviews
https://golang.org/cl/53120043
---
src/pkg/runtime/mheap.c | 16 +++++++++++++---\n src/pkg/runtime/mprof.goc | 7 ++++++-\n 2 files changed, 19 insertions(+), 4 deletions(-)\n
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b039abfc3ec93debe732bb4824bebd098ab7a62a
元コミット内容
runtime: fix specials deadlock
The deadlock is between span->specialLock and proflock:
goroutine 11 [running]:
runtime.MProf_Free(...)
...
runtime.freeallspecials(...)
goroutine 12 [running]:
addspecial(...)
runtime.setprofilebucket(...)
runtime.MProf_Malloc(...)
...
Fixes #7099.
変更の背景
このコミットは、Goランタイム内で発生していたデッドロックを修正するために導入されました。デッドロックは、メモリの解放処理とメモリプロファイリング処理が同時に実行され、それぞれが異なるロックを異なる順序で取得しようとした際に発生していました。
具体的には、以下の2つのロックが関与していました。
span->specialLock
: メモリスパン(MSpan
)に紐付けられた特殊なオブジェクト(Special
)のリストを保護するためのロック。Special
オブジェクトは、ファイナライザやプロファイリング情報など、特別な処理が必要なメモリ領域に関連付けられます。proflock
: メモリプロファイリングデータ構造全体を保護するためのグローバルロック。
デッドロックのシナリオは以下の通りです。
-
Goroutine 11 (メモリ解放パス):
runtime.freeallspecials
関数内でspan->specialLock
を取得します。- その後、
runtime.freespecial
を呼び出し、その内部でruntime.MProf_Free
が呼び出されます。 runtime.MProf_Free
はproflock
を取得しようとします。- 結果として、Goroutine 11は
span->specialLock
を保持したままproflock
を待機します。
-
Goroutine 12 (メモリ割り当て/プロファイリングパス):
runtime.MProf_Malloc
関数内でproflock
を取得します。- その後、
runtime.setprofilebucket
を呼び出し、その内部でaddspecial
が呼び出されます。 addspecial
はspan->specialLock
を取得しようとします。- 結果として、Goroutine 12は
proflock
を保持したままspan->specialLock
を待機します。
このように、Goroutine 11は「span->specialLock
-> proflock
」の順でロックを取得しようとし、Goroutine 12は「proflock
-> span->specialLock
」の順でロックを取得しようとするため、循環的な依存関係が発生し、デッドロックに至っていました。この問題はGoのIssue #7099として報告されていました。
前提知識の解説
このコミットを理解するためには、Goランタイムの以下の概念を理解しておく必要があります。
- Goroutine (ゴルーチン): Goの軽量な並行処理単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行できます。
- Mutex (ミューテックス): 共有リソースへのアクセスを制御するための同期プリミティブです。Goランタイム内部では、データ構造の一貫性を保つためにミューテックスが広く使用されています。
runtime.lock
とruntime.unlock
は、これらのミューテックスを操作するための低レベルな関数です。 - Memory Management (メモリ管理): Goランタイムは独自のガベージコレクタ(GC)とメモリ割り当て器を持っています。
mheap
: Goのヒープメモリ管理の中核をなすコンポーネントです。メモリの割り当てと解放を管理します。MSpan
: ヒープメモリの連続したページ群を表す構造体です。MSpan
は、特定のサイズのオブジェクトを格納するために使用されます。Special
:MSpan
内の特定のメモリ領域に関連付けられた特別な情報(例: ファイナライザ、プロファイリング情報)を保持する構造体です。MSpan
はspecials
というリストを持ち、そのリストへのアクセスはspan->specialLock
によって保護されます。
- Memory Profiling (メモリプロファイリング): Goは、プログラムのメモリ使用状況を分析するためのプロファイリングツールを提供します。
mprof
: メモリプロファイリングに関連するランタイムコードです。proflock
: メモリプロファイリングデータ構造へのアクセスを保護するグローバルなミューテックスです。Stkbucket
: プロファイリング情報の一部として、呼び出しスタックのハッシュを格納するバケットです。runtime.MProf_Malloc
/runtime.MProf_Free
: メモリ割り当て/解放時にプロファイリング情報を記録するためのランタイム関数です。runtime.setprofilebucket
: 割り当てられたオブジェクトにプロファイリングバケット情報を関連付ける関数です。この関数は内部でaddspecial
を呼び出し、Special
オブジェクトをMSpan
のspecials
リストに追加します。
- Deadlock (デッドロック): 複数の並行プロセス(この場合はゴルーチン)が、互いに相手が保持しているリソースの解放を待機し、結果としてどのプロセスも進行できなくなる状態です。典型的なデッドロックは、複数のロックを異なる順序で取得しようとした場合に発生します。
技術的詳細
このデッドロックは、Goランタイムのメモリ管理とプロファイリングのサブシステムが、共有リソース(ロック)を不適切な順序で取得しようとしたために発生しました。
デッドロックのメカニズム:
-
runtime.freeallspecials
のパス:- この関数は、特定の
MSpan
から解放されるオブジェクトに関連付けられたSpecial
オブジェクトを処理します。 - 処理を開始する際に、
span->specialLock
を取得して、MSpan
のspecials
リストへの排他的アクセスを保証します。 - その後、リスト内の各
Special
オブジェクトに対してruntime.freespecial
を呼び出します。 runtime.freespecial
は、そのオブジェクトに関連付けられたプロファイリング情報を解放するためにruntime.MProf_Free
を呼び出します。runtime.MProf_Free
は、グローバルなプロファイリングデータ構造を保護するためにproflock
を取得しようとします。- この時点で、
span->specialLock
を保持したままproflock
を待機する状態になります。
- この関数は、特定の
-
runtime.MProf_Malloc
のパス:- この関数は、新しいメモリ割り当てに対してプロファイリング情報を記録します。
- 処理を開始する際に、グローバルなプロファイリングデータ構造を保護するために
proflock
を取得します。 - その後、割り当てられたオブジェクトにプロファイリングバケットを関連付けるために
runtime.setprofilebucket
を呼び出します。 runtime.setprofilebucket
は、内部でaddspecial
を呼び出し、新しいSpecial
オブジェクトをMSpan
のspecials
リストに追加します。addspecial
は、MSpan
のspecials
リストを保護するためにspan->specialLock
を取得しようとします。- この時点で、
proflock
を保持したままspan->specialLock
を待機する状態になります。
このように、span->specialLock
-> proflock
と proflock
-> span->specialLock
の2つのロック取得順序が同時に存在することで、循環的な待機が発生し、デッドロックが引き起こされていました。
解決策:
このコミットは、両方のパスでロックの取得順序を変更することでデッドロックを解消します。
-
runtime.freeallspecials
の変更:span->specialLock
を保持している間は、Special
オブジェクトを実際に解放するのではなく、解放すべきSpecial
オブジェクトを一時的なリスト(list
)に収集するだけに変更されました。Special
オブジェクトの収集が完了したら、すぐにspan->specialLock
を解放します。- その後、
span->specialLock
を解放した状態で、一時リストに収集されたSpecial
オブジェクトを順次解放します。この解放処理の中でruntime.MProf_Free
が呼び出され、proflock
が取得されます。 - これにより、
span->specialLock
とproflock
を同時に保持する期間がなくなり、ロック取得順序の循環が解消されます。
-
runtime.MProf_Malloc
の変更:proflock
を保持している間は、プロファイリングバケットの準備までを行い、runtime.setprofilebucket
の呼び出しをproflock
の解放後に行うように変更されました。runtime.setprofilebucket
はspan->specialLock
を取得する可能性があるため、proflock
を解放してから呼び出すことで、proflock
とspan->specialLock
を同時に保持する期間がなくなり、ロック取得順序の循環が解消されます。- コミットメッセージのコメントにもあるように、「
Setprofilebucket
は他の多くのミューテックスをロックするため、proflock
の外で呼び出す。これにより、潜在的な競合とデッドロックの可能性が減少する。オブジェクトはMProf_Malloc
の呼び出し中に生存している必要があるため、これを非アトミックに行っても問題ない。」と説明されています。
これらの変更により、span->specialLock
とproflock
の取得順序が常に一貫するようになり、デッドロックが回避されます。
コアとなるコードの変更箇所
src/pkg/runtime/mheap.c
--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -703,9 +703,12 @@ runtime·freespecial(Special *s, void *p, uintptr size)
void
runtime·freeallspecials(MSpan *span, void *p, uintptr size)
{
- Special *s, **t;
+ Special *s, **t, *list;
uintptr offset;
+ // first, collect all specials into the list; then, free them
+ // this is required to not cause deadlock between span->specialLock and proflock
+ list = nil;
offset = (uintptr)p - (span->start << PageShift);
runtime·lock(&span->specialLock);
t = &span->specials;
@@ -714,10 +717,17 @@ runtime·freeallspecials(MSpan *span, void *p, uintptr size)
break;
if(offset == s->offset) {
*t = s->next;
- if(!runtime·freespecial(s, p, size))
- runtime·throw("can't explicitly free an object with a finalizer");
+ s->next = list;
+ list = s;
} else
t = &s->next;
}
runtime·unlock(&span->specialLock);\n+\n+\twhile(list != nil) {\n+\t\ts = list;\n+\t\tlist = s->next;\n+\t\tif(!runtime·freespecial(s, p, size))\n+\t\t\truntime·throw("can't explicitly free an object with a finalizer");\n+\t}\n }
src/pkg/runtime/mprof.goc
--- a/src/pkg/runtime/mprof.goc
+++ b/src/pkg/runtime/mprof.goc
@@ -209,8 +209,13 @@ runtime·MProf_Malloc(void *p, uintptr size, uintptr typ)
b = stkbucket(MProf, stk, nstk, true);
b->recent_allocs++;
b->recent_alloc_bytes += size;
- runtime·setprofilebucket(p, b);
runtime·unlock(&proflock);
+\n+\t// Setprofilebucket locks a bunch of other mutexes, so we call it outside of proflock.\n+\t// This reduces potential contention and chances of deadlocks.\n+\t// Since the object must be alive during call to MProf_Malloc,\n+\t// it's fine to do this non-atomically.\n+\truntime·setprofilebucket(p, b);\n }
\n // Called when freeing a profiled block.\n```
## コアとなるコードの解説
### `src/pkg/runtime/mheap.c` の変更
`runtime·freeallspecials` 関数は、`MSpan`から特定のオブジェクトが解放される際に、そのオブジェクトに関連付けられた`Special`オブジェクト(プロファイリング情報など)を処理します。
* **変更前**: `span->specialLock`を保持したまま、ループ内で`runtime·freespecial`を直接呼び出していました。`runtime·freespecial`は内部で`runtime.MProf_Free`を呼び出し、これが`proflock`を取得しようとするため、`span->specialLock`と`proflock`の同時保持が発生していました。
* **変更後**:
1. `Special *list;` という新しいポインタ変数が追加され、一時的なリストのヘッドとして機能します。
2. `span->specialLock`を保持している間は、解放対象の`Special`オブジェクトを`span->specials`リストから削除し、それらを`list`に連結していきます(`s->next = list; list = s;`)。これにより、`span->specialLock`のクリティカルセクション内では、リストの操作のみが行われます。
3. ループが終了し、すべての解放対象`Special`オブジェクトが`list`に収集されたら、すぐに`runtime·unlock(&span->specialLock);`を呼び出して`span->specialLock`を解放します。
4. その後、`while(list != nil)`ループで、`list`に収集された`Special`オブジェクトを一つずつ取り出し、`runtime·freespecial(s, p, size)`を呼び出して実際に解放処理を行います。この時、`span->specialLock`はすでに解放されているため、`runtime·freespecial`が`proflock`を取得しようとしても、デッドロックは発生しません。
この変更により、`span->specialLock`と`proflock`の同時保持が回避され、ロック取得順序の循環が解消されます。
### `src/pkg/runtime/mprof.goc` の変更
`runtime·MProf_Malloc` 関数は、メモリ割り当て時にプロファイリング情報を記録します。
* **変更前**: `proflock`を保持したまま、`runtime·setprofilebucket(p, b);`を呼び出していました。`runtime·setprofilebucket`は内部で`addspecial`を呼び出し、これが`span->specialLock`を取得しようとするため、`proflock`と`span->specialLock`の同時保持が発生していました。
* **変更後**:
1. `runtime·setprofilebucket(p, b);`の呼び出しが、`runtime·unlock(&proflock);`の後に移動されました。
2. これにより、`proflock`を解放してから`runtime·setprofilebucket`が呼び出されるため、`proflock`と`span->specialLock`の同時保持が回避されます。
3. コメントで説明されているように、`runtime·setprofilebucket`は他のミューテックスをロックする可能性があるため、`proflock`の外で呼び出すことで、競合とデッドロックの可能性が減少します。割り当てられたオブジェクト`p`は`MProf_Malloc`の呼び出し中には生存しているため、この非アトミックな操作は安全です。
この変更により、`proflock`と`span->specialLock`の同時保持が回避され、ロック取得順序の循環が解消されます。
## 関連リンク
* Go Issue #7099: [https://github.com/golang/go/issues/7099](https://github.com/golang/go/issues/7099)
* Go CL 53120043: [https://golang.org/cl/53120043](https://golang.org/cl/53120043)
## 参考にした情報源リンク
* Goのソースコード(`src/pkg/runtime/mheap.c`, `src/pkg/runtime/mprof.goc`)
* GoのIssueトラッカー
* Goのコードレビューシステム (Gerrit)
* Goのメモリ管理に関する一般的なドキュメントと記事
* 並行処理におけるデッドロックに関する一般的な情報