[インデックス 17198] ファイルの概要
このコミットは、Goランタイムにおけるデッドロック検出の誤検知(false deadlock crash)を修正するものです。具体的には、runtime/proc.c
内のスケジューラ関連のロジックが変更され、デッドロックの判定基準がより正確になるように調整されています。また、この修正を検証するための新しいTCPストレス テストが net/tcp_test.go
に追加されています。
コミット
commit 1da1030b5dd9d9610ccada4413a23e77b21c7f3b
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Aug 13 22:07:42 2013 +0400
runtime: fix false deadlock crash
Fixes #6070.
Update #6055.
R=golang-dev, nightlyone, rsc
CC=golang-dev
https://golang.org/cl/12602043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1da1030b5dd9d9610ccada4413a23e77b21c7f3b
元コミット内容
runtime: fix false deadlock crash
Fixes #6070.
Update #6055.
R=golang-dev, nightlyone, rsc
CC=golang-dev
https://golang.org/cl/12602043
変更の背景
このコミットは、Goランタイムが特定の条件下で誤ってデッドロックを検出し、プログラムをクラッシュさせてしまう問題("false deadlock crash")を修正するために行われました。コミットメッセージに記載されている #6070
と #6055
は、この問題に関連するGoのIssueトラッカーのエントリです。
-
Issue #6070: runtime: false deadlock detection with netpoll このIssueは、
netpoll
(ネットワークI/Oを処理するためのポーリングメカニズム)がアイドル状態のM(OSスレッド)を再利用しようとした際に、デッドロック検出ロジックが誤動作し、実際にはデッドロックが発生していないにもかかわらずプログラムがクラッシュするという問題が報告されています。特に、netpoll
がG(ゴルーチン)を注入する直前または直後に、別のMがシステムコールから戻り、作業がないと判断してデッドロックを報告してしまうシナリオが指摘されていました。 -
Issue #6055: runtime: deadlock detection is too aggressive このIssueは、より広範なデッドロック検出の積極性に関する議論を含んでおり、
#6070
はその具体的な症状の一つとして挙げられています。Goのランタイムは、すべてのゴルーチンがブロックされ、実行可能なゴルーチンが存在しない場合にデッドロックと判断してプログラムを終了させます。しかし、この検出ロジックが、一時的なアイドル状態や、システムコールからの復帰タイミングなど、特定の競合状態において誤った判断を下すことがありました。
これらの問題は、特にネットワークI/Oを多用するアプリケーションや、多数のゴルーチンが頻繁にシステムコールに出入りするような状況で顕在化し、Goプログラムの安定性に影響を与えていました。このコミットは、これらの誤検知を排除し、ランタイムの堅牢性を向上させることを目的としています。
前提知識の解説
このコミットの変更内容を理解するためには、Goランタイムのスケジューラとデッドロック検出に関する以下の概念を理解しておく必要があります。
Goランタイムスケジューラ (M, P, G)
Goのランタイムスケジューラは、ゴルーチン(G)、論理プロセッサ(P)、OSスレッド(M)という3つの主要なエンティティで構成されるGPMモデルを採用しています。
- G (Goroutine): Goにおける軽量な実行単位です。関数呼び出しごとに作成され、スタックサイズが小さく、数百万個のゴルーチンを同時に実行できます。
- P (Processor): 論理プロセッサであり、Goコードを実行するためのコンテキストを提供します。PはMにアタッチされ、Gを実行します。Pの数は通常、CPUコア数に設定され、並列実行の度合いを制御します。
- M (Machine/OS Thread): オペレーティングシステムのスレッドです。MはPにアタッチされ、Pが持つ実行可能なGをOS上で実行します。Mはシステムコール(ネットワークI/O、ファイルI/Oなど)の実行中にブロックされることがあります。
スケジューリングの基本的な流れ:
- MはPからGを取得し、実行します。
- Gがシステムコール(例: ネットワークI/O)を実行すると、Mはブロックされます。このとき、Pは別のMに引き渡されるか、新しいMが作成されてPにアタッチされ、他のGの実行を継続します。
- システムコールが完了すると、Mはブロック状態から解放され、実行可能な状態に戻ります。このMは、利用可能なPにアタッチされ、Gの実行を再開するか、アイドル状態になります。
デッドロック検出
Goランタイムは、プログラムがデッドロック状態に陥ったことを検出するメカニズムを持っています。デッドロックとは、すべてのゴルーチンがブロックされており、かつ、どのゴルーチンも今後実行可能になる見込みがない状態を指します。このような状態を検出すると、Goランタイムはプログラムをクラッシュさせ、デッドロックが発生したことをユーザーに通知します。
デッドロック検出の基本的なロジックは、以下の要素を監視することに基づいています。
- 実行中のMの数: 実際にGoコードを実行しているOSスレッドの数。
- アイドル状態のMの数: 作業がなく、PにアタッチされていないOSスレッドの数。
- ロックされたMの数: 特定のゴルーチンに紐付けられ、システムコールなどでブロックされているOSスレッドの数。
- 実行可能なGの数: 実行キューに存在する、または実行中のゴルーチンの数。
ランタイムは、これらのカウンタを基に、すべてのGがブロックされ、かつ、実行可能なGが存在しない状態を「デッドロック」と判断します。しかし、システムコールからの復帰やネットワークポーリングによるGの注入など、一時的にすべてのMがアイドル状態に見えるような状況で、誤ってデッドロックと判断してしまう「誤検知」が発生することがありました。
netpoll
netpoll
は、GoのネットワークI/Oを効率的に処理するためのメカニズムです。これは、OSの提供する非同期I/O機能(Linuxのepoll、macOSのkqueueなど)を利用して、複数のネットワーク接続からのイベントを効率的に監視します。netpoll
は、ネットワークイベントが発生した際に、対応するゴルーチンを「実行可能」状態に戻し、スケジューラに注入する役割を担います。
技術的詳細
このコミットの主要な変更点は、Goランタイムのデッドロック検出ロジックにおける「アイドル状態のロックされたM(OSスレッド)の数」の管理方法の改善です。
以前のデッドロック検出ロジックでは、runtime·sched.mlocked
というカウンタが使用されていました。このカウンタは、特定のゴルーチンに紐付けられ、システムコールなどでブロックされているMの数を追跡していました。しかし、このカウンタの更新タイミングが不適切であったため、特にnetpoll
がゴルーチンを注入する際や、Mがシステムコールから復帰する際に、一時的にすべてのMがアイドル状態であると誤認され、デッドロックが誤検知される可能性がありました。
このコミットでは、以下の変更が行われています。
-
mlocked
からnmidlelocked
への名称変更と意味の明確化:runtime·sched.mlocked
というフィールドがruntime·sched.nmidlelocked
に変更されました。- これに伴い、関連する関数
inclocked
もincidlelocked
に変更されました。 - この名称変更は、このカウンタが「ロックされたMの総数」ではなく、「アイドル状態のロックされたMの数」を追跡していることをより明確にするためのものです。つまり、システムコールなどでブロックされているが、まだ作業を再開していないMの数を数えるようになりました。
-
incidlelocked
の呼び出しタイミングの調整:stoplockedm
(ロックされたMを停止させる関数) やstartlockedm
(ロックされたMを開始させる関数) におけるincidlelocked
の呼び出しは、Mがアイドル状態になったり、アイドル状態から復帰したりするタイミングで適切にカウンタを増減するように調整されました。- 特に重要なのは、
sysmon
(システムモニタゴルーチン) 内のnetpoll
呼び出し後の処理です。netpoll
が実行可能なゴルーチンを返した場合、そのゴルーチンがスケジューラに注入される前にincidlelocked(-1)
が呼び出され、一時的にアイドル状態のロックされたMの数を減らします。これにより、netpoll
がゴルーチンを注入し、Mがそのゴルーチンを実行するまでの短い期間に、デッドロック検出が誤って発動するのを防ぎます。ゴルーチンが注入された後、incidlelocked(1)
が呼び出され、カウンタが元に戻されます。これは、netpoll
がGを注入した直後に、別のMがシステムコールから戻り、作業がないと判断してデッドロックを報告する状況を回避するためのものです。 - 同様に、
retake
(システムコールでブロックされているPを再取得する関数) においても、Pを再取得する直前にincidlelocked(-1)
を呼び出し、再取得後にincidlelocked(1)
を呼び出すことで、デッドロック検出の誤検知を防いでいます。これは、Pを再取得するMが、システムコールから戻り、nmidle
をインクリメントしてデッドロックを報告する可能性を考慮したものです。
-
デッドロック検出ロジックの修正:
checkdead
関数内のデッドロック判定ロジックが、runtime·sched.mlocked
の代わりにruntime·sched.nmidlelocked
を使用するように変更されました。これにより、デッドロックの判断がより正確な「アイドル状態のロックされたM」の数に基づいて行われるようになります。
これらの変更により、Goランタイムは、一時的なMのアイドル状態や、netpoll
によるゴルーチンの注入、システムコールからのMの復帰といった正常な動作中に、誤ってデッドロックと判断してクラッシュする可能性が大幅に低減されました。
コアとなるコードの変更箇所
src/pkg/net/tcp_test.go
--- a/src/pkg/net/tcp_test.go
+++ b/src/pkg/net/tcp_test.go
@@ -494,3 +494,78 @@ func TestTCPReadWriteMallocs(t *testing.T) {
t.Fatalf("Got %v allocs, want %v", mallocs, maxMallocs)
}
}
+
+func TestTCPStress(t *testing.T) {
+ const conns = 2
+ const msgs = 1e4
+ const msgLen = 512
+
+ sendMsg := func(c Conn, buf []byte) bool {
+ n, err := c.Write(buf)
+ if n != len(buf) || err != nil {
+ t.Logf("Write failed: %v", err)
+ return false
+ }
+ return true
+ }
+ recvMsg := func(c Conn, buf []byte) bool {
+ for read := 0; read != len(buf); {
+ n, err := c.Read(buf)
+ read += n
+ if err != nil {
+ t.Logf("Read failed: %v", err)
+ return false
+ }
+ }
+ return true
+ }
+
+ ln, err := Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatalf("Listen failed: %v", err)
+ }
+ defer ln.Close()
+ // Acceptor.
+ go func() {
+ for {
+ c, err := ln.Accept()
+ if err != nil {
+ break
+ }
+ // Server connection.
+ go func(c Conn) {
+ defer c.Close()
+ var buf [msgLen]byte
+ for m := 0; m < msgs; m++ {
+ if !recvMsg(c, buf[:]) || !sendMsg(c, buf[:]) {
+ break
+ }
+ }
+ }(c)
+ }
+ }()
+ done := make(chan bool)
+ for i := 0; i < conns; i++ {
+ // Client connection.
+ go func() {
+ defer func() {
+ done <- true
+ }()
+ c, err := Dial("tcp", ln.Addr().String())
+ if err != nil {
+ t.Logf("Dial failed: %v", err)
+ return
+ }
+ defer c.Close()
+ var buf [msgLen]byte
+ for m := 0; m < msgs; m++ {
+ if !sendMsg(c, buf[:]) || !recvMsg(c, buf[:]) {
+ break
+ }
+ }
+ }()
+ }
+ for i := 0; i < conns; i++ {
+ <-done
+ }
+}
src/pkg/runtime/proc.c
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -30,7 +30,7 @@ struct Sched {
M* midle; // idle m's waiting for work
int32 nmidle; // number of idle m's waiting for work
- int32 mlocked; // number of locked m's waiting for work
+ int32 nmidlelocked; // number of locked m's waiting for work
int32 mcount; // number of m's that have been created
P* pidle; // idle P's
@@ -95,7 +95,7 @@ static void stoplockedm(void);
static void startlockedm(G*);
static void sysmon(void);
static uint32 retake(int64);
-static void inclocked(int32);
+static void incidlelocked(int32);
static void checkdead(void);
static void exitsyscall0(G*);
static void park0(G*);
@@ -1019,7 +1019,7 @@ stoplockedm(void)\n p = releasep();\n handoffp(p);\n }\n- inclocked(1);\n+ incidlelocked(1);\n // Wait until another thread schedules lockedg again.\n runtime·notesleep(&m->park);\n runtime·noteclear(&m->park);\
@@ -1042,7 +1042,7 @@ startlockedm(G *gp)\n if(mp->nextp)\n runtime·throw("startlockedm: m has p");\n // directly handoff current P to the locked m\n- inclocked(-1);\n+ incidlelocked(-1);\n p = releasep();\n mp->nextp = p;\n runtime·notewakeup(&mp->park);\
@@ -1485,7 +1485,7 @@ void\n p = releasep();\n handoffp(p);\n if(g->isbackground) // do not consider blocked scavenger for deadlock detection\n- inclocked(1);\n+ incidlelocked(1);\
\n // Resave for traceback during blocked call.\n save(runtime·getcallerpc(&dummy), runtime·getcallersp(&dummy));\
@@ -1505,7 +1505,7 @@ runtime·exitsyscall(void)\n m->locks++; // see comment in entersyscall\n \n if(g->isbackground) // do not consider blocked scavenger for deadlock detection\n- inclocked(-1);\n+ incidlelocked(-1);\
\n if(exitsyscallfast()) {\n // There's a cpu for us, so we can run.\
@@ -2159,10 +2159,10 @@ releasep(void)\n }\n \n static void\n-inclocked(int32 v)\n+incidlelocked(int32 v)\n {\n runtime·lock(&runtime·sched);\n- runtime·sched.mlocked += v;\
+ runtime·sched.nmidlelocked += v;\
if(v > 0)\n checkdead();\n runtime·unlock(&runtime·sched);\
@@ -2177,12 +2177,12 @@ checkdead(void)\n int32 run, grunning, s;\n \n // -1 for sysmon\n- run = runtime·sched.mcount - runtime·sched.nmidle - runtime·sched.mlocked - 1;\
+ run = runtime·sched.mcount - runtime·sched.nmidle - runtime·sched.nmidlelocked - 1;\
if(run > 0)\n return;\n if(run < 0) {\n- runtime·printf("checkdead: nmidle=%d mlocked=%d mcount=%d\\n",\n- runtime·sched.nmidle, runtime·sched.mlocked, runtime·sched.mcount);\
+ runtime·printf("checkdead: nmidle=%d nmidlelocked=%d mcount=%d\\n",\n+ runtime·sched.nmidle, runtime·sched.nmidlelocked, runtime·sched.mcount);\
runtime·throw("checkdead: inconsistent counts");\
}\n grunning = 0;\
@@ -2238,7 +2238,18 @@ sysmon(void)\n if(lastpoll != 0 && lastpoll + 10*1000*1000 > now) {\n runtime·cas64(&runtime·sched.lastpoll, lastpoll, now);\n gp = runtime·netpoll(false); // non-blocking\n- injectglist(gp);\
+ if(gp) {\n+ // Need to decrement number of idle locked M's\n+ // (pretending that one more is running) before injectglist.\n+ // Otherwise it can lead to the following situation:\n+ // injectglist grabs all P's but before it starts M's to run the P's,\n+ // another M returns from syscall, finishes running its G,\n+ // observes that there is no work to do and no other running M's\n+ // and reports deadlock.\n+ incidlelocked(-1);\n+ injectglist(gp);\n+ incidlelocked(1);\
+ }\n }\n // retake P's blocked in syscalls\n // and preempt long running G's\
@@ -2284,15 +2295,16 @@ retake(int64 now)\n if(p->runqhead == p->runqtail &&\n runtime·atomicload(&runtime·sched.nmspinning) + runtime·atomicload(&runtime·sched.npidle) > 0)\n continue;\n- // Need to increment number of locked M's before the CAS.\n+ // Need to decrement number of idle locked M's\n+ // (pretending that one more is running) before the CAS.\n // Otherwise the M from which we retake can exit the syscall,\n // increment nmidle and report deadlock.\n- inclocked(-1);\n+ incidlelocked(-1);\n if(runtime·cas(&p->status, s, Pidle)) {\n n++;\n handoffp(p);\n }\n- inclocked(1);\n+ incidlelocked(1);\
} else if(s == Prunning) {\n // Preempt G if it's running for more than 10ms.\n if(pd->when + 10*1000*1000 > now)\
コアとなるコードの解説
src/pkg/net/tcp_test.go
の変更
このファイルには、TestTCPStress
という新しいテスト関数が追加されています。これは、Goランタイムのデッドロック検出ロジックの堅牢性を検証するためのストレス テストです。
- 目的: 複数のTCP接続を同時に確立し、大量のメッセージを送受信することで、ランタイムスケジューラとネットワークI/O処理に高い負荷をかけます。これにより、以前のデッドロック誤検知が発生しやすい競合状態を再現し、修正が正しく機能していることを確認します。
- テスト内容:
conns
(接続数) とmsgs
(メッセージ数)、msgLen
(メッセージ長) を定数として定義しています。sendMsg
とrecvMsg
というヘルパー関数が定義されており、それぞれTCP接続でのメッセージ送信と受信を処理します。- TCPリスナー (
ln
) を起動し、Acceptorゴルーチンを起動します。このAcceptorは、新しい接続を受け入れるたびに、その接続を処理する新しいサーバーゴルーチンを起動します。サーバーゴルーチンは、クライアントからメッセージを受信し、同じメッセージを返送するエコーサーバーとして機能します。 - 複数のクライアントゴルーチンを起動します。各クライアントゴルーチンは、サーバーに接続し、定義された回数 (
msgs
) だけメッセージを送信し、その応答を受信します。 done
チャネルを使用して、すべてのクライアントゴルーチンが完了するのを待ちます。
- 重要性: このテストは、ランタイムの修正が実世界のネットワークI/Oパターンにおいて、デッドロックの誤検知を引き起こさないことを保証するために不可欠です。特に、多数のゴルーチンが同時にネットワークI/Oを待機し、完了するような状況は、スケジューラの負荷が高まり、デッドロック検出ロジックが試される典型的なシナリオです。
src/pkg/runtime/proc.c
の変更
このファイルはGoランタイムのスケジューラの中核部分であり、M、P、Gの管理、システムコール処理、デッドロック検出などが行われます。
-
Sched
構造体の変更:struct Sched
内のmlocked
フィールドがnmidlelocked
に変更されました。int32 mlocked; // number of locked m's waiting for work
int32 nmidlelocked; // number of locked m's waiting for work
- この変更は、カウンタのセマンティクスをより正確に反映するためのものです。これは単に「ロックされたMの数」ではなく、「アイドル状態のロックされたMの数」を意味します。
-
関数名の変更:
static void inclocked(int32);
がstatic void incidlelocked(int32);
に変更されました。- これに伴い、
inclocked
を呼び出すすべての箇所がincidlelocked
に変更されています。
-
stoplockedm
およびstartlockedm
の変更:- これらの関数は、ロックされたMがアイドル状態になったり、アイドル状態から復帰したりする際に
incidlelocked
を呼び出すように変更されました。これは、Mの状態変化とnmidlelocked
カウンタの同期を正確に行うためです。
- これらの関数は、ロックされたMがアイドル状態になったり、アイドル状態から復帰したりする際に
-
runtime·exitsyscall
の変更:- システムコールから戻る際に、ゴルーチンがバックグラウンド(例: スカベンジャー)である場合、
incidlelocked(-1)
が呼び出されます。これは、システムコールから戻ったMが、デッドロック検出の対象から一時的に除外されるべきであることを示します。
- システムコールから戻る際に、ゴルーチンがバックグラウンド(例: スカベンジャー)である場合、
-
inclocked
/incidlelocked
関数の実装変更:- 関数名が
inclocked
からincidlelocked
に変更され、内部でruntime·sched.mlocked
ではなくruntime·sched.nmidlelocked
を更新するように修正されました。 checkdead()
の呼び出しは、nmidlelocked
が増加した場合(つまり、アイドル状態のロックされたMが増えた場合)にのみ行われます。これは、デッドロックの可能性が高まる状況でのみチェックを行うことで、不要なチェックを減らすためです。
- 関数名が
-
checkdead
関数の変更:- デッドロック検出の主要なロジックが含まれる
checkdead
関数内で、実行中のMの数を計算する際にruntime·sched.mlocked
の代わりにruntime·sched.nmidlelocked
が使用されるようになりました。run = runtime·sched.mcount - runtime·sched.nmidle - runtime·sched.mlocked - 1;
run = runtime·sched.mcount - runtime·sched.nmidle - runtime·sched.nmidlelocked - 1;
- デバッグ出力のメッセージも
mlocked
からnmidlelocked
に変更されています。
- デッドロック検出の主要なロジックが含まれる
-
sysmon
関数の変更 (最も重要):sysmon
は、Goランタイムのバックグラウンドで動作し、スケジューラの健全性を監視し、必要に応じてPの再取得やゴルーチンのプリエンプションなどを行うシステムモニタゴルーチンです。netpoll
が非ブロッキングモードで呼び出され、実行可能なゴルーチン (gp
) を返した場合の処理が変更されました。- 以前は
injectglist(gp);
が直接呼び出されていました。 - 変更後:
if(gp)
ブロックが追加され、その中で以下の処理が行われます。incidlelocked(-1);
:injectglist
が呼び出される前に、一時的にアイドル状態のロックされたMの数を減らします。これは、injectglist
がPを確保し、Mがゴルーチンを実行するまでの短い期間に、別のMがシステムコールから戻ってデッドロックを誤検知するシナリオを防ぐためです。injectglist(gp);
: 実行可能なゴルーチンをスケジューラに注入します。incidlelocked(1);
:injectglist
の後で、カウンタを元に戻します。
- 以前は
- この変更は、
netpoll
がゴルーチンを注入する際に発生する可能性のある競合状態を正確に処理し、デッドロックの誤検知を防ぐための核心的な修正です。
-
retake
関数の変更:- システムコールでブロックされているPを再取得する (
retake
) 際にも、同様のincidlelocked
の調整が行われました。 - Pを再取得する直前に
incidlelocked(-1)
を呼び出し、PのステータスがPidle
に変更され、handoffp(p)
が呼び出された後にincidlelocked(1)
を呼び出します。 - この調整は、Pを再取得するMが、システムコールから戻り、
nmidle
をインクリメントしてデッドロックを報告する可能性を考慮したものです。
- システムコールでブロックされているPを再取得する (
これらの変更は、Goランタイムのスケジューラが、Mの状態(特にシステムコールでブロックされているM)をより正確に追跡し、デッドロック検出ロジックが一時的なアイドル状態を誤ってデッドロックと判断しないようにするためのものです。これにより、Goプログラムの安定性と信頼性が向上します。
関連リンク
- Go Issue #6070: runtime: false deadlock detection with netpoll
- Go Issue #6055: runtime: deadlock detection is too aggressive
- Go CL 12602043: runtime: fix false deadlock crash (これはGitHubのコミットページと同じ内容ですが、Goの公式コードレビューシステムであるGerritのリンクです)
参考にした情報源リンク
- GoのIssueトラッカー (上記関連リンクを参照)
- Goのソースコード (特に
src/pkg/runtime/proc.c
とsrc/pkg/net/tcp_test.go
) - Goランタイムスケジューラに関するドキュメントや解説記事 (GPMモデル、デッドロック検出など)
- Go のスケジューラを理解する
- Goのスケジューラについて
- Goのランタイムスケジューラを理解する
- Goのデッドロック検出について (一般的なデッドロック検出の概念)
- Goのnet/httpパッケージの内部構造とパフォーマンス (netpollの概念に関連)
- Goのnetpollの仕組み