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

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

このコミットは、Goランタイムにおける特定のデッドロック問題を解決することを目的としています。具体的には、メモリ管理とプロファイリングに関連する2つのロック(span->specialLockproflock)の間で発生していた循環的なロック取得順序によるデッドロックを解消します。

コミット

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つのロックが関与していました。

  1. span->specialLock: メモリスパン(MSpan)に紐付けられた特殊なオブジェクト(Special)のリストを保護するためのロック。Specialオブジェクトは、ファイナライザやプロファイリング情報など、特別な処理が必要なメモリ領域に関連付けられます。
  2. proflock: メモリプロファイリングデータ構造全体を保護するためのグローバルロック。

デッドロックのシナリオは以下の通りです。

  • Goroutine 11 (メモリ解放パス):

    • runtime.freeallspecials関数内でspan->specialLockを取得します。
    • その後、runtime.freespecialを呼び出し、その内部でruntime.MProf_Freeが呼び出されます。
    • runtime.MProf_Freeproflockを取得しようとします。
    • 結果として、Goroutine 11はspan->specialLockを保持したままproflockを待機します。
  • Goroutine 12 (メモリ割り当て/プロファイリングパス):

    • runtime.MProf_Malloc関数内でproflockを取得します。
    • その後、runtime.setprofilebucketを呼び出し、その内部でaddspecialが呼び出されます。
    • addspecialspan->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.lockruntime.unlockは、これらのミューテックスを操作するための低レベルな関数です。
  • Memory Management (メモリ管理): Goランタイムは独自のガベージコレクタ(GC)とメモリ割り当て器を持っています。
    • mheap: Goのヒープメモリ管理の中核をなすコンポーネントです。メモリの割り当てと解放を管理します。
    • MSpan: ヒープメモリの連続したページ群を表す構造体です。MSpanは、特定のサイズのオブジェクトを格納するために使用されます。
    • Special: MSpan内の特定のメモリ領域に関連付けられた特別な情報(例: ファイナライザ、プロファイリング情報)を保持する構造体です。MSpanspecialsというリストを持ち、そのリストへのアクセスはspan->specialLockによって保護されます。
  • Memory Profiling (メモリプロファイリング): Goは、プログラムのメモリ使用状況を分析するためのプロファイリングツールを提供します。
    • mprof: メモリプロファイリングに関連するランタイムコードです。
    • proflock: メモリプロファイリングデータ構造へのアクセスを保護するグローバルなミューテックスです。
    • Stkbucket: プロファイリング情報の一部として、呼び出しスタックのハッシュを格納するバケットです。
    • runtime.MProf_Malloc / runtime.MProf_Free: メモリ割り当て/解放時にプロファイリング情報を記録するためのランタイム関数です。
    • runtime.setprofilebucket: 割り当てられたオブジェクトにプロファイリングバケット情報を関連付ける関数です。この関数は内部でaddspecialを呼び出し、SpecialオブジェクトをMSpanspecialsリストに追加します。
  • Deadlock (デッドロック): 複数の並行プロセス(この場合はゴルーチン)が、互いに相手が保持しているリソースの解放を待機し、結果としてどのプロセスも進行できなくなる状態です。典型的なデッドロックは、複数のロックを異なる順序で取得しようとした場合に発生します。

技術的詳細

このデッドロックは、Goランタイムのメモリ管理とプロファイリングのサブシステムが、共有リソース(ロック)を不適切な順序で取得しようとしたために発生しました。

デッドロックのメカニズム:

  1. runtime.freeallspecialsのパス:

    • この関数は、特定のMSpanから解放されるオブジェクトに関連付けられたSpecialオブジェクトを処理します。
    • 処理を開始する際に、span->specialLockを取得して、MSpanspecialsリストへの排他的アクセスを保証します。
    • その後、リスト内の各Specialオブジェクトに対してruntime.freespecialを呼び出します。
    • runtime.freespecialは、そのオブジェクトに関連付けられたプロファイリング情報を解放するためにruntime.MProf_Freeを呼び出します。
    • runtime.MProf_Freeは、グローバルなプロファイリングデータ構造を保護するためにproflockを取得しようとします。
    • この時点で、span->specialLockを保持したままproflockを待機する状態になります。
  2. runtime.MProf_Mallocのパス:

    • この関数は、新しいメモリ割り当てに対してプロファイリング情報を記録します。
    • 処理を開始する際に、グローバルなプロファイリングデータ構造を保護するためにproflockを取得します。
    • その後、割り当てられたオブジェクトにプロファイリングバケットを関連付けるためにruntime.setprofilebucketを呼び出します。
    • runtime.setprofilebucketは、内部でaddspecialを呼び出し、新しいSpecialオブジェクトをMSpanspecialsリストに追加します。
    • addspecialは、MSpanspecialsリストを保護するためにspan->specialLockを取得しようとします。
    • この時点で、proflockを保持したままspan->specialLockを待機する状態になります。

このように、span->specialLock -> proflockproflock -> span->specialLock の2つのロック取得順序が同時に存在することで、循環的な待機が発生し、デッドロックが引き起こされていました。

解決策:

このコミットは、両方のパスでロックの取得順序を変更することでデッドロックを解消します。

  1. runtime.freeallspecialsの変更:

    • span->specialLockを保持している間は、Specialオブジェクトを実際に解放するのではなく、解放すべきSpecialオブジェクトを一時的なリスト(list)に収集するだけに変更されました。
    • Specialオブジェクトの収集が完了したら、すぐにspan->specialLockを解放します。
    • その後、span->specialLockを解放した状態で、一時リストに収集されたSpecialオブジェクトを順次解放します。この解放処理の中でruntime.MProf_Freeが呼び出され、proflockが取得されます。
    • これにより、span->specialLockproflockを同時に保持する期間がなくなり、ロック取得順序の循環が解消されます。
  2. runtime.MProf_Mallocの変更:

    • proflockを保持している間は、プロファイリングバケットの準備までを行い、runtime.setprofilebucketの呼び出しをproflockの解放後に行うように変更されました。
    • runtime.setprofilebucketspan->specialLockを取得する可能性があるため、proflockを解放してから呼び出すことで、proflockspan->specialLockを同時に保持する期間がなくなり、ロック取得順序の循環が解消されます。
    • コミットメッセージのコメントにもあるように、「Setprofilebucketは他の多くのミューテックスをロックするため、proflockの外で呼び出す。これにより、潜在的な競合とデッドロックの可能性が減少する。オブジェクトはMProf_Mallocの呼び出し中に生存している必要があるため、これを非アトミックに行っても問題ない。」と説明されています。

これらの変更により、span->specialLockproflockの取得順序が常に一貫するようになり、デッドロックが回避されます。

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

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のメモリ管理に関する一般的なドキュメントと記事
*   並行処理におけるデッドロックに関する一般的な情報