[インデックス 12259] ファイルの概要
このコミットは、Go言語のランタイムプロファイリングツール pprof がmacOS (旧称 OS X) 上でCPUプロファイリングをサポートするための重要な変更を含んでいます。特に、macOSカーネルのバグに対する回避策の導入、addr2line および objdump というGo独自のツールの追加、そしてpprofがこれらの新しいツールを利用するように適応された点が主な内容です。これにより、macOS環境でのGoアプリケーションのパフォーマンス分析能力が向上しました。
コミット
commit 6e2ae0a12c0f73da56d4f465e68208731b4b16be
Author: Russ Cox <rsc@golang.org>
Date: Tue Feb 28 16:18:24 2012 -0500
runtime/pprof: support OS X CPU profiling
Work around profiling kernel bug with signal masks.
Still broken on 64-bit Snow Leopard kernel,
but I think we can ignore that one and let people
upgrade to Lion.
Add new trivial tools addr2line and objdump to take
the place of the GNU tools of the same name, since
those are not installed on OS X.
Adapt pprof to invoke 'go tool addr2line' and
'go tool objdump' if the system tools do not exist.
Clean up disassembly of base register on amd64.
Fixes #2008.
R=golang-dev, bradfitz, mikioh.mikioh, r, iant
CC=golang-dev
https://golang.org/cl/5697066
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6e2ae0a12c0f73da56d4f465e68208731b4b16be
元コミット内容
このコミットは、Goのプロファイリングツール pprof がmacOS上でCPUプロファイリングを適切に機能させるための複数の変更を導入しています。
主な変更点:
- macOSカーネルのプロファイリングバグへの対応: シグナルマスクを操作することで、プロファイリングシグナルがスリープ中のスレッドに誤って配信されるmacOSカーネルのバグ(特にSnow Leopard 64-bitカーネルで顕著)を回避します。
- Go独自の
addr2lineとobjdumpツールの追加: macOSにはGNUのaddr2lineやobjdumpが標準でインストールされていないため、Goツールチェーン内にこれらの機能を提供する軽量なバージョンが追加されました。これらはgo tool addr2lineおよびgo tool objdumpとして利用されます。 pprofの適応:pprofスクリプトが、システムにaddr2lineやobjdumpが存在しない場合に、新しく追加されたgo toolバージョンを自動的に使用するように変更されました。- amd64における逆アセンブルの改善: ベースレジスタの逆アセンブル表示がクリーンアップされました。
- 関連するIssueの修正: このコミットは、GoのIssue #2008 を修正します。
ファイル変更の概要:
misc/pprof:addr2lineとobjdumpのフォールバックロジックを追加。src/cmd/addr2line/main.c:addr2lineツールのC言語実装を新規追加。src/cmd/objdump/main.c:objdumpツールのC言語実装を新規追加。src/cmd/dist/build.c:addr2lineとobjdumpをビルドプロセスに追加。src/libmach/8db.c: amd64の逆アセンブルロジックを修正。src/pkg/runtime/*: プロファイリングシグナルハンドリング、特にmacOSにおけるSIGPROFの挙動を制御するための変更。runtime·setprof関数の追加と、notesleep,notetsleep,entersyscall,exitsyscall,resetcpuprofiler,semasleepなどの関数でのsetprofの呼び出し。src/pkg/runtime/pprof/pprof.go: macOSのCPUプロファイリングに関する既知のバグについてのコメントを更新。src/pkg/runtime/pprof/pprof_test.go: Snow Leopard 64-bitカーネルでのテストスキップロジックを追加。
変更の背景
このコミットの主要な背景には、macOS環境におけるGo言語のCPUプロファイリングの課題がありました。
-
macOSカーネルのプロファイリングバグ: 当時のmacOS (特にSnow Leopard) のカーネルには、CPUプロファイリングに使用される
SIGPROFシグナルの配信に関するバグが存在しました。通常、SIGPROFはCPU時間を消費しているスレッドに送られるべきですが、macOSではスリープ中のスレッドにも送られてしまうことがありました。これはプロファイリングデータの精度を著しく低下させる問題でした。Goのランタイムは、このシグナルを利用してプロファイル情報を収集するため、このバグはGoアプリケーションのCPUプロファイリングを困難にしていました。コミットメッセージでは、64-bit Snow Leopardカーネルでは依然として問題が残るものの、Lionへのアップグレードを推奨することでこの問題を「無視できる」と判断しています。 -
GNUツールの非存在:
pprofツールは、プロファイルデータを人間が読める形式(関数名やソースコードの行番号など)に変換するために、通常はGNU Binutilsに含まれるaddr2lineやobjdumpといった外部ツールに依存していました。しかし、macOS環境ではこれらのGNUツールが標準でインストールされておらず、ユーザーが別途インストールする必要がありました。これはGo開発者にとって不便であり、プロファイリングの利用を妨げる要因となっていました。
これらの問題を解決し、macOS上でのGoアプリケーションのプロファイリング体験を向上させることが、このコミットの目的でした。特に、Go独自のaddr2lineとobjdumpを導入することで、外部依存を減らし、Goツールチェーンの自己完結性を高める狙いがありました。
前提知識の解説
このコミットを理解するためには、以下の技術的な概念について知っておく必要があります。
-
CPUプロファイリング:
- 目的: プロファイリングは、プログラムの実行中にどの部分が最も多くのCPU時間を消費しているかを特定する手法です。これにより、パフォーマンスのボトルネックを特定し、最適化の対象を見つけることができます。
- サンプリングプロファイリング: GoのCPUプロファイリングは、サンプリングベースで行われます。これは、一定の間隔(例えば100Hz、つまり1秒間に100回)でプログラムの実行を一時停止し、その時点でのコールスタック(関数呼び出しの履歴)を記録する方式です。これにより、プログラムがどの関数で時間を費やしているかの統計的な情報を得ることができます。
SIGPROFシグナル: Unix系システムでは、ITIMER_PROFタイマーとSIGPROFシグナルがCPUプロファイリングによく利用されます。ITIMER_PROFは、プロセスが消費したCPU時間に基づいて定期的にSIGPROFシグナルを生成します。このシグナルを受け取ったプロセスは、シグナルハンドラ内で現在のコールスタックを記録します。
-
pprofツール:- Go言語の標準プロファイリングツールです。Goアプリケーションから生成されたプロファイルデータ(通常は
pprof形式)を解析し、テキスト、グラフ(SVG、PDFなど)、Webインターフェースなど、様々な形式で可視化します。 pprofは、プロファイルデータ内のメモリアドレスを実際の関数名やソースコードの行番号に変換するために、デバッグ情報(シンボル情報)を必要とします。この変換プロセスでaddr2lineやobjdumpのような外部ツールが利用されます。
- Go言語の標準プロファイリングツールです。Goアプリケーションから生成されたプロファイルデータ(通常は
-
addr2line:- GNU Binutilsに含まれるコマンドラインツールの一つです。実行可能ファイル内のメモリアドレスを受け取り、対応するソースファイル名と行番号を出力します。デバッグやプロファイリングにおいて、生のメモリアドレスを人間が読める情報に変換するために不可欠です。
-
objdump:- GNU Binutilsに含まれるもう一つのコマンドラインツールです。実行可能ファイルやオブジェクトファイルの情報を表示します。特に、逆アセンブル機能(機械語コードをアセンブリ言語に変換する機能)は、特定のメモリアドレス範囲のコードを詳細に分析する際に使用されます。
pprofでは、プロファイルされたコードの逆アセンブル表示に利用されます。
- GNU Binutilsに含まれるもう一つのコマンドラインツールです。実行可能ファイルやオブジェクトファイルの情報を表示します。特に、逆アセンブル機能(機械語コードをアセンブリ言語に変換する機能)は、特定のメモリアドレス範囲のコードを詳細に分析する際に使用されます。
-
シグナルとシグナルマスク:
- シグナル: Unix系システムにおけるソフトウェア割り込みの一種で、非同期イベント(例: 外部からの終了要求、エラー発生、タイマー満了など)をプロセスに通知するメカニズムです。
- シグナルマスク: プロセスが現在ブロックしている(つまり、受信してもすぐに処理せず保留する)シグナルのセットです。シグナルマスクを操作することで、特定のシグナルがスレッドに配信されるタイミングを制御できます。
SIG_BLOCKはシグナルをブロックし、SIG_UNBLOCKはブロックを解除します。
-
Goランタイム:
- Goプログラムの実行を管理するシステムです。ガベージコレクション、スケジューリング、システムコールインターフェース、シグナルハンドリングなど、低レベルの機能を提供します。プロファイリング機能もランタイムの一部として実装されています。
-
Goのビルドシステム (
src/cmd/dist):- Goのソースコードをビルドし、Goツールチェーン全体を構築するための内部ツールです。新しいツール(
addr2lineやobjdump)をGoのビルドプロセスに組み込むために、この部分の変更が必要になります。
- Goのソースコードをビルドし、Goツールチェーン全体を構築するための内部ツールです。新しいツール(
これらの知識が、コミットが解決しようとしている問題、その解決策、およびコード変更の具体的な内容を理解する上で基盤となります。
技術的詳細
このコミットの技術的詳細は、macOSにおけるCPUプロファイリングの課題を解決するための多角的なアプローチにあります。
1. macOSカーネルのプロファイリングバグへの回避策
コミットメッセージとsrc/pkg/runtime/pprof/pprof.goのコメントで言及されているように、当時のmacOSカーネル(特にSnow Leopard 64-bit)には、SIGPROFシグナルの配信に関するバグがありました。このバグは、CPU時間を消費しているスレッドではなく、スリープ中のスレッドにSIGPROFが誤って配信されるというものでした。これはプロファイリングの精度を著しく低下させます。
Goランタイムは、この問題を回避するために、シグナルマスクを動的に操作する戦略を採用しました。
runtime·setprof(bool on)関数の導入: この新しい関数は、現在のスレッドがSIGPROFシグナルを受信できるかどうかを制御します。onがtrueの場合(プロファイリングを有効にする場合)、SIGPROFがシグナルマスクからSIG_UNBLOCKされます。onがfalseの場合(プロファイリングを一時停止する場合)、SIGPROFがシグナルマスクにSIG_BLOCKされます。
- ブロッキングシステムコール中のプロファイリング一時停止:
runtime·notesleep(セマフォ待機),runtime·notetsleep(タイムアウト付きセマフォ待機),runtime·entersyscall(システムコール開始),runtime·exitsyscall(システムコール終了),runtime·semasleep(セマフォ待機) などの関数が変更され、スレッドがブロッキング状態に入る直前にruntime·setprof(false)を呼び出してSIGPROFをブロックし、ブロッキング状態から抜けた直後にruntime·setprof(true)を呼び出してSIGPROFのブロックを解除するようにしました。- このアプローチにより、カーネルは
SIGPROFをスリープ中のスレッドに送ることができなくなり、結果として実際にCPUを消費しているスレッドにシグナルが配信される可能性が高まります。これにより、プロファイリングデータの関連性が向上します。
runtime·resetcpuprofilerの変更: CPUプロファイラのリセット関数も、SIGPROFのハンドリングをruntime·setprofに委譲するように変更されました。
ただし、コミットメッセージにもあるように、この回避策は64-bit Snow Leopardカーネルでは完全ではありませんでした。そのカーネルでは、シグナルマスクに関わらずスリープ中のスレッドにSIGPROFが配信され、さらにスレッドが自発的に目覚めるまでシグナルが配信されないという問題が残っていました。このため、GoチームはユーザーにLionへのアップグレードを推奨しています。
2. Go独自のaddr2lineとobjdumpツールの追加
macOSにはGNU Binutilsのaddr2lineやobjdumpが標準でインストールされていないため、Goはこれらの機能を提供する独自の軽量ツールを導入しました。
src/cmd/addr2line/main.c:- このファイルは、
addr2lineのGoバージョンをC言語で実装しています。 - Goの内部ライブラリである
libmach(Goのバイナリ形式を解析するためのライブラリ)を利用して、実行可能ファイルからシンボル情報(関数名、ファイル名、行番号)を抽出します。 - 標準入力からメモリアドレスを読み込み、対応する関数名と
ファイル:行番号の形式で標準出力に出力します。これは、pprofがプロファイルデータをシンボル解決する際に必要とする最小限の機能を提供します。
- このファイルは、
src/cmd/objdump/main.c:- このファイルは、
objdumpのGoバージョンをC言語で実装しています。 - これも
libmachを利用して、指定された実行可能ファイルの特定のアドレス範囲を逆アセンブルします。 pprofがコードの逆アセンブル表示を行う際に利用されます。
- このファイルは、
src/cmd/dist/build.cの変更:- Goのビルドシステムに、これらの新しいツール(
cmd/addr2lineとcmd/objdump)をビルド対象として追加しました。これにより、Goのインストール時にこれらのツールも自動的にビルドされ、go toolコマンドを通じて利用可能になります。
- Goのビルドシステムに、これらの新しいツール(
3. pprofツールの適応
既存のmisc/pprofスクリプト(Perlで書かれている)が、これらの新しいGoツールを認識し、利用するように変更されました。
- フォールバックロジック:
pprofスクリプトは、addr2lineやobjdumpコマンドを実行する前に、システムにこれらのツールが存在するかどうかをsystem("$tool --help >/dev/null 2>&1")のようなコマンドでチェックします。 go toolの利用: もしシステムツールが見つからない場合、pprofは自動的にgo tool addr2lineやgo tool objdumpにフォールバックするように設定されました。これにより、macOSユーザーは追加のツールをインストールすることなく、Goのプロファイリング機能を利用できるようになります。
4. その他の改善
src/libmach/8db.cの修正: amd64アーキテクチャにおける逆アセンブルの出力がクリーンアップされました。特に、ベースレジスタの表示に関する冗長な情報が削除され、より読みやすい形式になりました。src/pkg/runtime/signal_darwin_386.cおよびsrc/pkg/runtime/signal_darwin_amd64.cの変更:SIGPROFハンドラ内で、gp != m->g0 && gp != m->gsignalという条件が追加されました。これは、プロファイリングシグナルがGoランタイムの内部ゴルーチン(g0やgsignal)に配信された場合にはruntime·sigprofを呼び出さないようにすることで、プロファイリングデータのノイズを減らすためのものです。
これらの変更は、GoのプロファイリングエコシステムをmacOS環境に適合させ、より堅牢で使いやすいものにするための包括的な取り組みを示しています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は多岐にわたりますが、特に重要なのは以下のファイル群です。
-
misc/pprof:Disassembleサブルーチン内で、objdumpが見つからない場合にgo tool objdumpにフォールバックするロジックが追加されました。MapToSymbolsサブルーチン内で、addr2lineが見つからない場合にgo tool addr2lineにフォールバックするロジックが追加されました。
--- a/misc/pprof +++ b/misc/pprof @@ -1234,6 +1234,13 @@ sub Disassemble { my $cmd = sprintf("$objdump -C -d -l --no-show-raw-insn " . "--start-address=0x$start_addr " . "--stop-address=0x$end_addr $prog"); + + if (system("$objdump --help >/dev/null 2>&1") != 0) { + # objdump must not exist. Fall back to go tool objdump. + $objdump = "go tool objdump"; + $cmd = "$objdump $prog 0x$start_addr 0x$end_addr"; + } + open(OBJDUMP, "$cmd |") || error("$objdump: $!\\n"); my @result = (); my $filename = ""; @@ -4391,6 +4398,12 @@ sub MapToSymbols { $cmd = "$addr2line --demangle -f -C -e $image"; } + if (system("$addr2line --help >/dev/null 2>&1") != 0) { + # addr2line must not exist. Fall back to go tool addr2line. + $addr2line = "go tool addr2line"; + $cmd = "$addr2line $image"; + } + # If "addr2line" isn't installed on the system at all, just use # nm to get what info we can (function names, but not line numbers). if (system("$addr2line --help >/dev/null 2>&1") != 0) { -
src/cmd/addr2line/main.c(新規ファイル):- Go独自の
addr2lineツールのC言語実装。crackhdr,syminit,findsym,filelineといったlibmachの関数を使用して、バイナリからシンボル情報を取得します。
// Copyright 2012 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. /* * addr2line simulation - only enough to make pprof work on Macs */ #include <u.h> #include <libc.h> #include <bio.h> #include <mach.h> // ... (usage function) ... void main(int argc, char **argv) { int fd; char *p; uvlong pc; Symbol s; Fhdr fhdr; Biobuf bin, bout; char file[1024]; // ... (argument parsing) ... fd = open(argv[0], OREAD); if(fd < 0) sysfatal("open %s: %r", argv[0]); if(crackhdr(fd, &fhdr) <= 0) sysfatal("crackhdr: %r"); machbytype(fhdr.type); if(syminit(fd, &fhdr) <= 0) sysfatal("syminit: %r"); Binit(&bin, 0, OREAD); Binit(&bout, 1, OWRITE); for(;;) { p = Brdline(&bin, '\n'); if(p == nil) break; p[Blinelen(&bin)-1] = '\0'; pc = strtoull(p, 0, 16); if(!findsym(pc, CTEXT, &s)) s.name = "??"; if(!fileline(file, sizeof file, pc)) strcpy(file, "??:0"); Bprint(&bout, "%s\n%s\n", s.name, file); } Bflush(&bout); exits(0); } - Go独自の
-
src/cmd/objdump/main.c(新規ファイル):- Go独自の
objdumpツールのC言語実装。crackhdr,syminit,loadmap,machdata->das,machdata->instsizeといったlibmachの関数を使用して、バイナリを逆アセンブルします。
// Copyright 2012 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. /* * objdump simulation - only enough to make pprof work on Macs */ #include <u.h> #include <libc.h> #include <bio.h> #include <mach.h> // ... (usage function) ... void main(int argc, char **argv) { int fd, n; uvlong pc, start, stop; Fhdr fhdr; Biobuf bout; char buf[1024]; Map *text; // ... (argument parsing) ... fd = open(argv[0], OREAD); if(fd < 0) sysfatal("open %s: %r", argv[0]); if(crackhdr(fd, &fhdr) <= 0) sysfatal("crackhdr: %r"); machbytype(fhdr.type); if(syminit(fd, &fhdr) <= 0) sysfatal("syminit: %r"); text = loadmap(nil, fd, &fhdr); if(text == nil) sysfatal("loadmap: %r"); Binit(&bout, 1, OWRITE); for(pc=start; pc<stop; ) { if(fileline(buf, sizeof buf, pc)) Bprint(&bout, "%s\n", buf); buf[0] = '\0'; machdata->das(text, pc, 0, buf, sizeof buf); Bprint(&bout, " %llx: %s\n", pc, buf); n = machdata->instsize(text, pc); if(n <= 0) break; pc += n; } Bflush(&bout); exits(0); } - Go独自の
-
src/pkg/runtime/runtime.h:runtime·setprof関数の宣言が追加されました。
--- a/src/pkg/runtime/runtime.h +++ b/src/pkg/runtime/runtime.h @@ -730,3 +730,13 @@ bool runtime·showframe(Func*); void runtime·ifaceE2I(struct InterfaceType*, Eface, Iface*); uintptr runtime·memlimit(void); + +// If appropriate, ask the operating system to control whether this +// thread should receive profiling signals. This is only necessary on OS X. +// An operating system should not deliver a profiling signal to a +// thread that is not actually executing (what good is that?), but that's +// what OS X prefers to do. When profiling is turned on, we mask +// away the profiling signal when threads go to sleep, so that OS X +// is forced to deliver the signal to a thread that's actually running. +// This is a no-op on other systems. +void runtime·setprof(bool); -
src/pkg/runtime/thread_darwin.c:runtime·setprof関数の実装が追加されました。これはSIGPROFシグナルをブロックまたはアンブロックするためにruntime·sigprocmaskを呼び出します。runtime·semasleep内でruntime·setprofが呼び出され、セマフォ待機中にプロファイリングを一時停止します。runtime·minit内で、プロファイリングが有効な場合にSIGPROFをアンブロックするように初期シグナルマスクを設定します。
--- a/src/pkg/runtime/thread_darwin.c +++ b/src/pkg/runtime/thread_darwin.c @@ -11,6 +11,7 @@ extern SigTab runtime·sigtab[]; static Sigset sigset_all = ~(Sigset)0; static Sigset sigset_none; +static Sigset sigset_prof = 1<<(SIGPROF-1); static void unimplemented(int8 *name) @@ -23,7 +24,14 @@ unimplemented(int8 *name) int32 runtime·semasleep(int64 ns) { -\treturn runtime·mach_semacquire(m->waitsema, ns); +\tint32 v; +\n+\tif(m->profilehz > 0)\n+\t\truntime·setprof(false);\ +\tv = runtime·mach_semacquire(m->waitsema, ns);\ +\tif(m->profilehz > 0)\n+\t\truntime·setprof(true);\ +\treturn v; } void @@ -98,7 +106,11 @@ runtime·minit(void) // Initialize signal handling. m->gsignal = runtime·malg(32*1024);\t// OS X wants >=8K, Linux >=2K runtime·signalstack(m->gsignal->stackguard - StackGuard, 32*1024);\ -\truntime·sigprocmask(SIG_SETMASK, &sigset_none, nil); +\n+\tif(m->profilehz > 0)\n+\t\truntime·sigprocmask(SIG_SETMASK, &sigset_none, nil);\ +\telse +\t\truntime·sigprocmask(SIG_SETMASK, &sigset_prof, nil);\ } // Mach IPC, to get at semaphores @@ -434,3 +446,34 @@ runtime·memlimit(void) // the limit. return 0; } + +// NOTE(rsc): On OS X, when the CPU profiling timer expires, the SIGPROF +// signal is not guaranteed to be sent to the thread that was executing to +// cause it to expire. It can and often does go to a sleeping thread, which is +// not interesting for our profile. This is filed Apple Bug Report #9177434,\n// copied to http://code.google.com/p/go/source/detail?r=35b716c94225. +// To work around this bug, we disable receipt of the profiling signal on +// a thread while in blocking system calls. This forces the kernel to deliver +// the profiling signal to an executing thread. +// +// The workaround fails on OS X machines using a 64-bit Snow Leopard kernel. +// In that configuration, the kernel appears to want to deliver SIGPROF to the +// sleeping threads regardless of signal mask and, worse, does not deliver +// the signal until the thread wakes up on its own. +// +// If necessary, we can switch to using ITIMER_REAL for OS X and handle +// the kernel-generated SIGALRM by generating our own SIGALRMs to deliver +// to all the running threads. SIGALRM does not appear to be affected by +// the 64-bit Snow Leopard bug. However, as of this writing Mountain Lion +// is in preview, making Snow Leopard two versions old, so it is unclear how +// much effort we need to spend on one buggy kernel. + +// Control whether profiling signal can be delivered to this thread. +void +runtime·setprof(bool on) +{ +\tif(on)\n+\t\truntime·sigprocmask(SIG_UNBLOCK, &sigset_prof, nil);\n+\telse +\t\truntime·sigprocmask(SIG_BLOCK, &sigset_prof, nil);\n+} -
src/pkg/runtime/proc.c:runtime·entersyscallとruntime·exitsyscall内でruntime·setprofが呼び出され、システムコール中にプロファイリングを一時停止します。
--- a/src/pkg/runtime/proc.c +++ b/src/pkg/runtime/proc.c @@ -916,6 +915,9 @@ runtime·entersyscall(void) { uint32 v; +\tif(m->profilehz > 0)\n+\t\truntime·setprof(false);\n+\n // Leave SP around for gc and traceback. runtime·gosave(&g->sched); g->gcsp = g->sched.sp; @@ -979,6 +981,9 @@ runtime·exitsyscall(void) // Garbage collector isn't running (since we are), // so okay to clear gcstack. g->gcstack = nil; +\n+\t\tif(m->profilehz > 0)\n+\t\t\truntime·setprof(true);\ return; }
これらの変更は、macOSでのプロファイリングの信頼性を向上させるためのGoランタイムの低レベルな調整と、pprofツールが外部依存なしで機能するためのGoツールチェーンの拡張を示しています。
コアとなるコードの解説
ここでは、上記の「コアとなるコードの変更箇所」で示したコードスニペットについて、その役割と重要性を詳しく解説します。
1. misc/pprofにおけるフォールバックロジック
if (system("$objdump --help >/dev/null 2>&1") != 0) {
# objdump must not exist. Fall back to go tool objdump.
$objdump = "go tool objdump";
$cmd = "$objdump $prog 0x$start_addr 0x$end_addr";
}
このPerlコードスニペットは、pprofスクリプトがobjdumpコマンドを呼び出す前に実行されるチェックです。
system("$objdump --help >/dev/null 2>&1") != 0: これは、現在のシステムパスでobjdumpコマンドが見つかるかどうか、または実行可能であるかどうかをテストします。$objdump --help:objdumpコマンドに--helpオプションを付けて実行します。これは通常、コマンドが存在し、実行可能であれば何らかのヘルプメッセージを出力します。>/dev/null 2>&1: 標準出力と標準エラー出力を/dev/nullにリダイレクトします。これにより、ヘルプメッセージがコンソールに表示されるのを防ぎます。system(...) != 0:system関数は、実行したコマンドの終了ステータスを返します。終了ステータスが0であれば成功(コマンドが見つかり、実行できた)、0以外であれば失敗(コマンドが見つからない、実行できないなど)を意味します。
# objdump must not exist. Fall back to go tool objdump.: コマンドが見つからなかった場合のコメントです。$objdump = "go tool objdump";:objdumpコマンドのパスを、Goツールチェーンに含まれるgo tool objdumpに再設定します。$cmd = "$objdump $prog 0x$start_addr 0x$end_addr";: 実際に実行するコマンド文字列を、新しいobjdumpパスとGoツールに合わせた引数形式で再構築します。
同様のロジックがaddr2lineに対しても適用されています。このフォールバックメカニズムにより、macOSユーザーはGNU Binutilsを別途インストールすることなく、Goのプロファイリング機能を利用できるようになり、ユーザーエクスペリエンスが大幅に向上しました。
2. src/cmd/addr2line/main.cとsrc/cmd/objdump/main.cの新規追加
これらのファイルは、Goツールチェーンに組み込まれる軽量なaddr2lineとobjdumpの実装です。C言語で書かれており、Goの内部ライブラリであるlibmachを利用しています。
libmachの利用:libmachは、Goの実行可能ファイルやオブジェクトファイルの構造を解析し、シンボルテーブルやセクション情報にアクセスするためのライブラリです。crackhdr(fd, &fhdr): ファイルディスクリプタfdから実行可能ファイルのヘッダ情報を解析し、fhdr構造体に格納します。syminit(fd, &fhdr): シンボルテーブルを初期化し、シンボル情報をメモリにロードします。findsym(pc, CTEXT, &s): 指定されたプログラムカウンタpcに対応する関数シンボルsを検索します。fileline(file, sizeof file, pc): 指定されたプログラムカウンタpcに対応するソースファイル名と行番号を取得します。machdata->das(text, pc, 0, buf, sizeof buf): 指定されたテキストセクションtext内のプログラムカウンタpcから命令を逆アセンブルし、結果をbufに格納します。machdata->instsize(text, pc): 指定されたプログラムカウンタpcにある命令のサイズを返します。
これらのツールは、GNUの同名ツールに比べて機能は限定的ですが、pprofがプロファイルデータをシンボル解決し、逆アセンブル表示を行うために必要な最小限の機能を提供します。これにより、Goツールチェーンの自己完結性が高まり、macOS環境での外部依存が解消されました。
3. runtime·setprof(bool on)関数の導入と利用
// src/pkg/runtime/runtime.h
void runtime·setprof(bool);
// src/pkg/runtime/thread_darwin.c
void
runtime·setprof(bool on)
{
if(on)
runtime·sigprocmask(SIG_UNBLOCK, &sigset_prof, nil);
else
runtime·sigprocmask(SIG_BLOCK, &sigset_prof, nil);
}
runtime·setprofの役割: この関数は、現在のスレッドがSIGPROFシグナルを受信できるかどうかを制御します。onがtrueの場合、SIGPROFはシグナルマスクから解除され、スレッドはシグナルを受信できるようになります。onがfalseの場合、SIGPROFはシグナルマスクに追加され、スレッドはシグナルをブロックします。sigset_prof:SIGPROFシグナルのみを含むシグナルセットです。runtime·sigprocmask: Unix系システムコールsigprocmaskのGoランタイムラッパーです。スレッドのシグナルマスクを変更するために使用されます。SIG_UNBLOCKは指定されたシグナルをマスクから削除し、SIG_BLOCKは追加します。
この関数は、Goランタイムの様々な場所で呼び出されます。
-
src/pkg/runtime/lock_futex.cとsrc/pkg/runtime/lock_sema.c:runtime·notesleep(セマフォ待機) やruntime·notetsleep(タイムアウト付きセマフォ待機) のような、スレッドが長時間ブロッキング状態に入る可能性のある関数内で、ブロッキングに入る直前にruntime·setprof(false)を呼び出し、ブロッキングから抜けた直後にruntime·setprof(true)を呼び出します。- これにより、スレッドがスリープしている間は
SIGPROFがブロックされ、カーネルがスリープ中のスレッドにシグナルを誤って配信するのを防ぎます。
-
src/pkg/runtime/proc.c:runtime·entersyscall(システムコールに入る前) とruntime·exitsyscall(システムコールから出る後) でruntime·setprofが呼び出されます。- システムコール中はスレッドがカーネルモードで実行されるため、この間も
SIGPROFをブロックすることで、プロファイリングの精度を維持しようとします。
このシグナルマスクの動的な操作は、macOSカーネルのバグに対する主要な回避策であり、GoのCPUプロファイリングがより正確な結果を生成できるようにするための重要な変更です。
4. src/pkg/runtime/signal_darwin_386.cおよびsrc/pkg/runtime/signal_darwin_amd64.cのSIGPROFハンドラ
if(sig == SIGPROF) {
if(gp != m->g0 && gp != m->gsignal)
runtime·sigprof((uint8*)r->eip, (uint8*)r->esp, nil, gp);
return;
}
このコードは、SIGPROFシグナルがGoランタイムのシグナルハンドラに到達した際に実行されます。
if(gp != m->g0 && gp != m->gsignal): この条件は非常に重要です。gpは現在のゴルーチン(Goの軽量スレッド)へのポインタです。m->g0: Goランタイムのスケジューラやガベージコレクタなどの内部処理を実行するための特別なゴルーチンです。m->gsignal: シグナルハンドラが実行される際に使用される特別なゴルーチンです。
- この条件は、「現在のゴルーチンが、Goランタイムの内部処理やシグナルハンドリングのための特別なゴルーチンではない場合のみ、プロファイリング情報を記録する」ことを意味します。
- 目的: Goランタイムの内部ゴルーチンが
SIGPROFを受け取った場合、それはアプリケーションコードの実行とは直接関係のないCPU時間を示している可能性があります。これらのシグナルを無視することで、プロファイリングデータからノイズを取り除き、ユーザーのアプリケーションコードのパフォーマンスに焦点を当てた、よりクリーンで関連性の高いプロファイル情報を得ることができます。
これらのコアとなるコードの変更は、GoがmacOS環境でのプロファイリングの課題にどのように対処し、Goツールチェーンの独立性と堅牢性を高めたかを示しています。
関連リンク
- Go Issue #2008: runtime/pprof: support OS X CPU profiling
- Go Change List 5697066: runtime/pprof: support OS X CPU profiling
- Apple Bug Report #9177434 (Goのコメントで言及):
- 直接的な公開リンクは見つかりませんが、Goのソースコードコメントで言及されています。
- http://code.google.com/p/go/source/detail?r=35b716c94225 (これはGoの古いSVNリポジトリの変更履歴へのリンクで、Appleのバグ報告に関する詳細なコメントが含まれています。)
参考にした情報源リンク
- Go Issue #2008: https://github.com/golang/go/issues/2008
- Go Change List 5697066: https://golang.org/cl/5697066
- GNU Binutils - addr2line: https://sourceware.org/binutils/docs/binutils/addr2line.html
- GNU Binutils - objdump: https://sourceware.org/binutils/docs/binutils/objdump.html
- Go pprof documentation: https://pkg.go.dev/runtime/pprof
- Go blog: Profiling Go Programs: https://go.dev/blog/pprof
- Unix signals (general concept): https://en.wikipedia.org/wiki/Signal_(IPC)
- sigprocmask(2) - Linux man page: https://man7.org/linux/man-pages/man2/sigprocmask.2.html
- ITIMER_PROF: https://man7.org/linux/man-pages/man2/setitimer.2.html
- Go source code comments (specifically in
src/pkg/runtime/thread_darwin.candsrc/pkg/runtime/pprof/pprof.gorelated to Apple Bug Report #9177434): - Go
libmach(internal library for Mach-O parsing): (具体的な公開ドキュメントは少ないが、Goのソースコード内で利用されている)src/libmach/ディレクトリ内のファイル群
これらの情報源は、コミットの背景、技術的詳細、および関連する概念を理解するために参照されました。 I have completed the request. I have read the commit data, performed web searches for context, and generated a comprehensive technical explanation in Markdown format, adhering to all specified sections and requirements. The output is printed to standard output only, as requested.# [インデックス 12259] ファイルの概要
このコミットは、Go言語のランタイムプロファイリングツール pprof がmacOS (旧称 OS X) 上でCPUプロファイリングをサポートするための重要な変更を含んでいます。特に、macOSカーネルのバグに対する回避策の導入、addr2line および objdump というGo独自のツールの追加、そしてpprofがこれらの新しいツールを利用するように適応された点が主な内容です。これにより、macOS環境でのGoアプリケーションのパフォーマンス分析能力が向上しました。
コミット
commit 6e2ae0a12c0f73da56d4f465e68208731b4b16be
Author: Russ Cox <rsc@golang.org>
Date: Tue Feb 28 16:18:24 2012 -0500
runtime/pprof: support OS X CPU profiling
Work around profiling kernel bug with signal masks.
Still broken on 64-bit Snow Leopard kernel,
but I think we can ignore that one and let people
upgrade to Lion.
Add new trivial tools addr2line and objdump to take
the place of the GNU tools of the same name, since
those are not installed on OS X.
Adapt pprof to invoke 'go tool addr2line' and
'go tool objdump' if the system tools do not exist.
Clean up disassembly of base register on amd64.
Fixes #2008.
R=golang-dev, bradfitz, mikioh.mikioh, r, iant
CC=golang-dev
https://golang.org/cl/5697066
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6e2ae0a12c0f73da56d4f465e68208731b4b16be
元コミット内容
このコミットは、Goのプロファイリングツール pprof がmacOS上でCPUプロファイリングを適切に機能させるための複数の変更を導入しています。
主な変更点:
- macOSカーネルのプロファイリングバグへの対応: シグナルマスクを操作することで、プロファイリングシグナルがスリープ中のスレッドに誤って配信されるmacOSカーネルのバグ(特にSnow Leopard 64-bitカーネルで顕著)を回避します。
- Go独自の
addr2lineとobjdumpツールの追加: macOSにはGNUのaddr2lineやobjdumpが標準でインストールされていないため、Goツールチェーン内にこれらの機能を提供する軽量なバージョンが追加されました。これらはgo tool addr2lineおよびgo tool objdumpとして利用されます。 pprofの適応:pprofスクリプトが、システムにaddr2lineやobjdumpが存在しない場合に、新しく追加されたgo toolバージョンを自動的に使用するように変更されました。- amd64における逆アセンブルの改善: ベースレジスタの逆アセンブル表示がクリーンアップされました。
- 関連するIssueの修正: このコミットは、GoのIssue #2008 を修正します。
ファイル変更の概要:
misc/pprof:addr2lineとobjdumpのフォールバックロジックを追加。src/cmd/addr2line/main.c:addr2lineツールのC言語実装を新規追加。src/cmd/objdump/main.c:objdumpツールのC言語実装を新規追加。src/cmd/dist/build.c:addr2lineとobjdumpをビルドプロセスに追加。src/libmach/8db.c: amd64の逆アセンブルロジックを修正。src/pkg/runtime/*: プロファイリングシグナルハンドリング、特にmacOSにおけるSIGPROFの挙動を制御するための変更。runtime·setprof関数の追加と、notesleep,notetsleep,entersyscall,exitsyscall,resetcpuprofiler,semasleepなどの関数でのsetprofの呼び出し。src/pkg/runtime/pprof/pprof.go: macOSのCPUプロファイリングに関する既知のバグについてのコメントを更新。src/pkg/runtime/pprof/pprof_test.go: Snow Leopard 64-bitカーネルでのテストスキップロジックを追加。
変更の背景
このコミットの主要な背景には、macOS環境におけるGo言語のCPUプロファイリングの課題がありました。
-
macOSカーネルのプロファイリングバグ: 当時のmacOS (特にSnow Leopard) のカーネルには、CPUプロファイリングに使用される
SIGPROFシグナルの配信に関するバグが存在しました。通常、SIGPROFはCPU時間を消費しているスレッドに送られるべきですが、macOSではスリープ中のスレッドにも送られてしまうことがありました。これはプロファイリングデータの精度を著しく低下させる問題でした。Goのランタイムは、このシグナルを利用してプロファイル情報を収集するため、このバグはGoアプリケーションのCPUプロファイリングを困難にしていました。コミットメッセージでは、64-bit Snow Leopardカーネルでは依然として問題が残るものの、Lionへのアップグレードを推奨することでこの問題を「無視できる」と判断しています。 -
GNUツールの非存在:
pprofツールは、プロファイルデータを人間が読める形式(関数名やソースコードの行番号など)に変換するために、通常はGNU Binutilsに含まれるaddr2lineやobjdumpといった外部ツールに依存していました。しかし、macOS環境ではこれらのGNUツールが標準でインストールされておらず、ユーザーが別途インストールする必要がありました。これはGo開発者にとって不便であり、プロファイリングの利用を妨げる要因となっていました。
これらの問題を解決し、macOS上でのGoアプリケーションのプロファイリング体験を向上させることが、このコミットの目的でした。特に、Go独自のaddr2lineとobjdumpを導入することで、外部依存を減らし、Goツールチェーンの自己完結性を高める狙いがありました。
前提知識の解説
このコミットを理解するためには、以下の技術的な概念について知っておく必要があります。
-
CPUプロファイリング:
- 目的: プロファイリングは、プログラムの実行中にどの部分が最も多くのCPU時間を消費しているかを特定する手法です。これにより、パフォーマンスのボトルネックを特定し、最適化の対象を見つけることができます。
- サンプリングプロファイリング: GoのCPUプロファイリングは、サンプリングベースで行われます。これは、一定の間隔(例えば100Hz、つまり1秒間に100回)でプログラムの実行を一時停止し、その時点でのコールスタック(関数呼び出しの履歴)を記録する方式です。これにより、プログラムがどの関数で時間を費やしているかの統計的な情報を得ることができます。
SIGPROFシグナル: Unix系システムでは、ITIMER_PROFタイマーとSIGPROFシグナルがCPUプロファイリングによく利用されます。ITIMER_PROFは、プロセスが消費したCPU時間に基づいて定期的にSIGPROFシグナルを生成します。このシグナルを受け取ったプロセスは、シグナルハンドラ内で現在のコールスタックを記録します。
-
pprofツール:- Go言語の標準プロファイリングツールです。Goアプリケーションから生成されたプロファイルデータ(通常は
pprof形式)を解析し、テキスト、グラフ(SVG、PDFなど)、Webインターフェースなど、様々な形式で可視化します。 pprofは、プロファイルデータ内のメモリアドレスを実際の関数名やソースコードの行番号に変換するために、デバッグ情報(シンボル情報)を必要とします。この変換プロセスでaddr2lineやobjdumpのような外部ツールが利用されます。
- Go言語の標準プロファイリングツールです。Goアプリケーションから生成されたプロファイルデータ(通常は
-
addr2line:- GNU Binutilsに含まれるコマンドラインツールの一つです。実行可能ファイル内のメモリアドレスを受け取り、対応するソースファイル名と行番号を出力します。デバッグやプロファイリングにおいて、生のメモリアドレスを人間が読める情報に変換するために不可欠です。
-
objdump:- GNU Binutilsに含まれるもう一つのコマンドラインツールです。実行可能ファイルやオブジェクトファイルの情報を表示します。特に、逆アセンブル機能(機械語コードをアセンブリ言語に変換する機能)は、特定のメモリアドレス範囲のコードを詳細に分析する際に使用されます。
pprofでは、プロファイルされたコードの逆アセンブル表示に利用されます。
- GNU Binutilsに含まれるもう一つのコマンドラインツールです。実行可能ファイルやオブジェクトファイルの情報を表示します。特に、逆アセンブル機能(機械語コードをアセンブリ言語に変換する機能)は、特定のメモリアドレス範囲のコードを詳細に分析する際に使用されます。
-
シグナルとシグナルマスク:
- シグナル: Unix系システムにおけるソフトウェア割り込みの一種で、非同期イベント(例: 外部からの終了要求、エラー発生、タイマー満了など)をプロセスに通知するメカニズムです。
- シグナルマスク: プロセスが現在ブロックしている(つまり、受信してもすぐに処理せず保留する)シグナルのセットです。シグナルマスクを操作することで、特定のシグナルがスレッドに配信されるタイミングを制御できます。
SIG_BLOCKはシグナルをブロックし、SIG_UNBLOCKはブロックを解除します。
-
Goランタイム:
- Goプログラムの実行を管理するシステムです。ガベージコレクション、スケジューリング、システムコールインターフェース、シグナルハンドリングなど、低レベルの機能を提供します。プロファイリング機能もランタイムの一部として実装されています。
-
Goのビルドシステム (
src/cmd/dist):- Goのソースコードをビルドし、Goツールチェーン全体を構築するための内部ツールです。新しいツール(
addr2lineやobjdump)をGoのビルドプロセスに組み込むために、この部分の変更が必要になります。
- Goのソースコードをビルドし、Goツールチェーン全体を構築するための内部ツールです。新しいツール(
これらの知識が、コミットが解決しようとしている問題、その解決策、およびコード変更の具体的な内容を理解する上で基盤となります。
技術的詳細
このコミットの技術的詳細は、macOSにおけるCPUプロファイリングの課題を解決するための多角的なアプローチにあります。
1. macOSカーネルのプロファイリングバグへの回避策
コミットメッセージとsrc/pkg/runtime/pprof/pprof.goのコメントで言及されているように、当時のmacOSカーネル(特にSnow Leopard 64-bit)には、SIGPROFシグナルの配信に関するバグがありました。このバグは、CPU時間を消費しているスレッドではなく、スリープ中のスレッドにSIGPROFが誤って配信されるというものでした。これはプロファイリングの精度を著しく低下させます。
Goランタイムは、この問題を回避するために、シグナルマスクを動的に操作する戦略を採用しました。
runtime·setprof(bool on)関数の導入: この新しい関数は、現在のスレッドがSIGPROFシグナルを受信できるかどうかを制御します。onがtrueの場合(プロファイリングを有効にする場合)、SIGPROFがシグナルマスクからSIG_UNBLOCKされます。onがfalseの場合(プロファイリングを一時停止する場合)、SIGPROFがシグナルマスクにSIG_BLOCKされます。
- ブロッキングシステムコール中のプロファイリング一時停止:
runtime·notesleep(セマフォ待機),runtime·notetsleep(タイムアウト付きセマフォ待機),runtime·entersyscall(システムコール開始),runtime·exitsyscall(システムコール終了),runtime·semasleep(セマフォ待機) などの関数が変更され、スレッドがブロッキング状態に入る直前にruntime·setprof(false)を呼び出してSIGPROFをブロックし、ブロッキング状態から抜けた直後にruntime·setprof(true)を呼び出してSIGPROFのブロックを解除するようにしました。- このアプローチにより、カーネルは
SIGPROFをスリープ中のスレッドに送ることができなくなり、結果として実際にCPUを消費しているスレッドにシグナルが配信される可能性が高まります。これにより、プロファイリングデータの関連性が向上します。
runtime·resetcpuprofilerの変更: CPUプロファイラのリセット関数も、SIGPROFのハンドリングをruntime·setprofに委譲するように変更されました。
ただし、コミットメッセージにもあるように、この回避策は64-bit Snow Leopardカーネルでは完全ではありませんでした。そのカーネルでは、シグナルマスクに関わらずスリープ中のスレッドにSIGPROFが配信され、さらにスレッドが自発的に目覚めるまでシグナルが配信されないという問題が残っていました。このため、GoチームはユーザーにLionへのアップグレードを推奨しています。
2. Go独自のaddr2lineとobjdumpツールの追加
macOSにはGNU Binutilsのaddr2lineやobjdumpが標準でインストールされていないため、Goはこれらの機能を提供する独自の軽量ツールを導入しました。
src/cmd/addr2line/main.c:- このファイルは、
addr2lineのGoバージョンをC言語で実装しています。 - Goの内部ライブラリである
libmach(Goのバイナリ形式を解析するためのライブラリ)を利用して、実行可能ファイルからシンボル情報(関数名、ファイル名、行番号)を抽出します。 - 標準入力からメモリアドレスを読み込み、対応する関数名と
ファイル:行番号の形式で標準出力に出力します。これは、pprofがプロファイルデータをシンボル解決する際に必要とする最小限の機能を提供します。
- このファイルは、
src/cmd/objdump/main.c:- このファイルは、
objdumpのGoバージョンをC言語で実装しています。 - これも
libmachを利用して、指定された実行可能ファイルの特定のアドレス範囲を逆アセンブルします。 pprofがコードの逆アセンブル表示を行う際に利用されます。
- このファイルは、
src/cmd/dist/build.cの変更:- Goのビルドシステムに、これらの新しいツール(
cmd/addr2lineとcmd/objdump)をビルド対象として追加しました。これにより、Goのインストール時にこれらのツールも自動的にビルドされ、go toolコマンドを通じて利用可能になります。
- Goのビルドシステムに、これらの新しいツール(
3. pprofツールの適応
既存のmisc/pprofスクリプト(Perlで書かれている)が、これらの新しいGoツールを認識し、利用するように変更されました。
- フォールバックロジック:
pprofスクリプトは、addr2lineやobjdumpコマンドを実行する前に、システムにこれらのツールが存在するかどうかをsystem("$tool --help >/dev/null 2>&1")のようなコマンドでチェックします。 go toolの利用: もしシステムツールが見つからない場合、pprofは自動的にgo tool addr2lineやgo tool objdumpにフォールバックするように設定されました。これにより、macOSユーザーは追加のツールをインストールすることなく、Goのプロファイリング機能を利用できるようになります。
4. その他の改善
src/libmach/8db.cの修正: amd64アーキテクチャにおける逆アセンブルの出力がクリーンアップされました。特に、ベースレジスタの表示に関する冗長な情報が削除され、より読みやすい形式になりました。src/pkg/runtime/signal_darwin_386.cおよびsrc/pkg/runtime/signal_darwin_amd64.cの変更:SIGPROFハンドラ内で、gp != m->g0 && gp != m->gsignalという条件が追加されました。これは、プロファイリングシグナルがGoランタイムの内部ゴルーチン(g0やgsignal)に配信された場合にはruntime·sigprofを呼び出さないようにすることで、プロファイリングデータのノイズを減らすためのものです。
これらの変更は、GoのプロファイリングエコシステムをmacOS環境に適合させ、より堅牢で使いやすいものにするための包括的な取り組みを示しています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は多岐にわたりますが、特に重要なのは以下のファイル群です。
-
misc/pprof:Disassembleサブルーチン内で、objdumpが見つからない場合にgo tool objdumpにフォールバックするロジックが追加されました。MapToSymbolsサブルーチン内で、addr2lineが見つからない場合にgo tool addr2lineにフォールバックするロジックが追加されました。
--- a/misc/pprof +++ b/misc/pprof @@ -1234,6 +1234,13 @@ sub Disassemble { my $cmd = sprintf("$objdump -C -d -l --no-show-raw-insn " . "--start-address=0x$start_addr " . "--stop-address=0x$end_addr $prog"); + + if (system("$objdump --help >/dev/null 2>&1") != 0) { + # objdump must not exist. Fall back to go tool objdump. + $objdump = "go tool objdump"; + $cmd = "$objdump $prog 0x$start_addr 0x$end_addr"; + } + open(OBJDUMP, "$cmd |") || error("$objdump: $!\\n"); my @result = (); my $filename = ""; @@ -4391,6 +4398,12 @@ sub MapToSymbols { $cmd = "$addr2line --demangle -f -C -e $image"; } + if (system("$addr2line --help >/dev/null 2>&1") != 0) { + # addr2line must not exist. Fall back to go tool addr2line. + $addr2line = "go tool addr2line"; + $cmd = "$addr2line $image"; + } + # If "addr2line" isn't installed on the system at all, just use # nm to get what info we can (function names, but not line numbers). if (system("$addr2line --help >/dev/null 2>&1") != 0) { -
src/cmd/addr2line/main.c(新規ファイル):- Go独自の
addr2lineツールのC言語実装。crackhdr,syminit,findsym,filelineといったlibmachの関数を使用して、バイナリからシンボル情報を取得します。
// Copyright 2012 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. /* * addr2line simulation - only enough to make pprof work on Macs */ #include <u.h> #include <libc.h> #include <bio.h> #include <mach.h> // ... (usage function) ... void main(int argc, char **argv) { int fd; char *p; uvlong pc; Symbol s; Fhdr fhdr; Biobuf bin, bout; char file[1024]; // ... (argument parsing) ... fd = open(argv[0], OREAD); if(fd < 0) sysfatal("open %s: %r", argv[0]); if(crackhdr(fd, &fhdr) <= 0) sysfatal("crackhdr: %r"); machbytype(fhdr.type); if(syminit(fd, &fhdr) <= 0) sysfatal("syminit: %r"); Binit(&bin, 0, OREAD); Binit(&bout, 1, OWRITE); for(;;) { p = Brdline(&bin, '\n'); if(p == nil) break; p[Blinelen(&bin)-1] = '\0'; pc = strtoull(p, 0, 16); if(!findsym(pc, CTEXT, &s)) s.name = "??"; if(!fileline(file, sizeof file, pc)) strcpy(file, "??:0"); Bprint(&bout, "%s\n%s\n", s.name, file); } Bflush(&bout); exits(0); } - Go独自の
-
src/cmd/objdump/main.c(新規ファイル):- Go独自の
objdumpツールのC言語実装。crackhdr,syminit,loadmap,machdata->das,machdata->instsizeといったlibmachの関数を使用して、バイナリを逆アセンブルします。
// Copyright 2012 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. /* * objdump simulation - only enough to make pprof work on Macs */ #include <u.h> #include <libc.h> #include <bio.h> #include <mach.h> // ... (usage function) ... void main(int argc, char **argv) { int fd, n; uvlong pc, start, stop; Fhdr fhdr; Biobuf bout; char buf[1024]; Map *text; // ... (argument parsing) ... fd = open(argv[0], OREAD); if(fd < 0) sysfatal("open %s: %r", argv[0]); if(crackhdr(fd, &fhdr) <= 0) sysfatal("crackhdr: %r"); machbytype(fhdr.type); if(syminit(fd, &fhdr) <= 0) sysfatal("syminit: %r"); text = loadmap(nil, fd, &fhdr); if(text == nil) sysfatal("loadmap: %r"); Binit(&bout, 1, OWRITE); for(pc=start; pc<stop; ) { if(fileline(buf, sizeof buf, pc)) Bprint(&bout, "%s\n", buf); buf[0] = '\0'; machdata->das(text, pc, 0, buf, sizeof buf); Bprint(&bout, " %llx: %s\n", pc, buf); n = machdata->instsize(text, pc); if(n <= 0) break; pc += n; } Bflush(&bout); exits(0); } - Go独自の
-
src/pkg/runtime/runtime.h:runtime·setprof関数の宣言が追加されました。
--- a/src/pkg/runtime/runtime.h +++ b/src/pkg/runtime/runtime.h @@ -730,3 +730,13 @@ bool runtime·showframe(Func*); void runtime·ifaceE2I(struct InterfaceType*, Eface, Iface*); uintptr runtime·memlimit(void); + +// If appropriate, ask the operating system to control whether this +// thread should receive profiling signals. This is only necessary on OS X. +// An operating system should not deliver a profiling signal to a +// thread that is not actually executing (what good is that?), but that's +// what OS X prefers to do. When profiling is turned on, we mask +// away the profiling signal when threads go to sleep, so that OS X +// is forced to deliver the signal to a thread that's actually running. +// This is a no-op on other systems. +void runtime·setprof(bool); -
src/pkg/runtime/thread_darwin.c:runtime·setprof関数の実装が追加されました。これはSIGPROFシグナルをブロックまたはアンブロックするためにruntime·sigprocmaskを呼び出します。runtime·semasleep内でruntime·setprofが呼び出され、セマフォ待機中にプロファイリングを一時停止します。runtime·minit内で、プロファイリングが有効な場合にSIGPROFをアンブロックするように初期シグナルマスクを設定します。
--- a/src/pkg/runtime/thread_darwin.c +++ b/src/pkg/runtime/thread_darwin.c @@ -11,6 +11,7 @@ extern SigTab runtime·sigtab[]; static Sigset sigset_all = ~(Sigset)0; static Sigset sigset_none; +static Sigset sigset_prof = 1<<(SIGPROF-1); static void unimplemented(int8 *name) @@ -23,7 +24,14 @@ unimplemented(int8 *name) int32 runtime·semasleep(int64 ns) { -\treturn runtime·mach_semacquire(m->waitsema, ns); +\tint32 v; +\n+\tif(m->profilehz > 0)\n+\t\truntime·setprof(false);\n+\tv = runtime·mach_semacquire(m->waitsema, ns);\n+\tif(m->profilehz > 0)\n+\t\truntime·setprof(true);\n+\treturn v; } void @@ -98,7 +106,11 @@ runtime·minit(void) // Initialize signal handling. m->gsignal = runtime·malg(32*1024);\t// OS X wants >=8K, Linux >=2K runtime·signalstack(m->gsignal->stackguard - StackGuard, 32*1024);\ -\truntime·sigprocmask(SIG_SETMASK, &sigset_none, nil); +\n+\tif(m->profilehz > 0)\n+\t\truntime·sigprocmask(SIG_SETMASK, &sigset_none, nil);\ +\telse +\t\truntime·sigprocmask(SIG_SETMASK, &sigset_prof, nil);\ } // Mach IPC, to get at semaphores @@ -434,3 +446,34 @@ runtime·memlimit(void) // the limit. return 0; } + +// NOTE(rsc): On OS X, when the CPU profiling timer expires, the SIGPROF +// signal is not guaranteed to be sent to the thread that was executing to +// cause it to expire. It can and often does go to a sleeping thread, which is +// not interesting for our profile. This is filed Apple Bug Report #9177434,\n// copied to http://code.google.com/p/go/source/detail?r=35b716c94225. +// To work around this bug, we disable receipt of the profiling signal on +// a thread while in blocking system calls. This forces the kernel to deliver +// the profiling signal to an executing thread. +// +// The workaround fails on OS X machines using a 64-bit Snow Leopard kernel. +// In that configuration, the kernel appears to want to deliver SIGPROF to the +// sleeping threads regardless of signal mask and, worse, does not deliver +// the signal until the thread wakes up on its own. +// +// If necessary, we can switch to using ITIMER_REAL for OS X and handle +// the kernel-generated SIGALRM by generating our own SIGALRMs to deliver +// to all the running threads. SIGALRM does not appear to be affected by +// the 64-bit Snow Leopard bug. However, as of this writing Mountain Lion +// is in preview, making Snow Leopard two versions old, so it is unclear how +// much effort we need to spend on one buggy kernel. + +// Control whether profiling signal can be delivered to this thread. +void +runtime·setprof(bool on) +{ +\tif(on)\n+\t\truntime·sigprocmask(SIG_UNBLOCK, &sigset_prof, nil);\n+\telse +\t\truntime·sigprocmask(SIG_BLOCK, &sigset_prof, nil);\n+} -
src/pkg/runtime/proc.c:runtime·entersyscallとruntime·exitsyscall内でruntime·setprofが呼び出され、システムコール中にプロファイリングを一時停止します。
--- a/src/pkg/runtime/proc.c +++ b/src/pkg/runtime/proc.c @@ -916,6 +915,9 @@ runtime·entersyscall(void) { uint32 v; +\tif(m->profilehz > 0)\n+\t\truntime·setprof(false);\n+\n // Leave SP around for gc and traceback. runtime·gosave(&g->sched); g->gcsp = g->sched.sp; @@ -979,6 +981,9 @@ runtime·exitsyscall(void) // Garbage collector isn't running (since we are), // so okay to clear gcstack. g->gcstack = nil; +\n+\t\tif(m->profilehz > 0)\n+\t\t\truntime·setprof(true);\ return; }
これらの変更は、macOSでのプロファイリングの信頼性を向上させるためのGoランタイムの低レベルな調整と、pprofツールが外部依存なしで機能するためのGoツールチェーンの拡張を示しています。
コアとなるコードの解説
ここでは、上記の「コアとなるコードの変更箇所」で示したコードスニペットについて、その役割と重要性を詳しく解説します。
1. misc/pprofにおけるフォールバックロジック
if (system("$objdump --help >/dev/null 2>&1") != 0) {
# objdump must not exist. Fall back to go tool objdump.
$objdump = "go tool objdump";
$cmd = "$objdump $prog 0x$start_addr 0x$end_addr";
}
このPerlコードスニペットは、pprofスクリプトがobjdumpコマンドを呼び出す前に実行されるチェックです。
system("$objdump --help >/dev/null 2>&1") != 0: これは、現在のシステムパスでobjdumpコマンドが見つかるかどうか、または実行可能であるかどうかをテストします。$objdump --help:objdumpコマンドに--helpオプションを付けて実行します。これは通常、コマンドが存在し、実行可能であれば何らかのヘルプメッセージを出力します。>/dev/null 2>&1: 標準出力と標準エラー出力を/dev/nullにリダイレクトします。これにより、ヘルプメッセージがコンソールに表示されるのを防ぎます。system(...) != 0:system関数は、実行したコマンドの終了ステータスを返します。終了ステータスが0であれば成功(コマンドが見つかり、実行できた)、0以外であれば失敗(コマンドが見つからない、実行できないなど)を意味します。
# objdump must not exist. Fall back to go tool objdump.: コマンドが見つからなかった場合のコメントです。$objdump = "go tool objdump";:objdumpコマンドのパスを、Goツールチェーンに含まれるgo tool objdumpに再設定します。$cmd = "$objdump $prog 0x$start_addr 0x$end_addr";: 実際に実行するコマンド文字列を、新しいobjdumpパスとGoツールに合わせた引数形式で再構築します。
同様のロジックがaddr2lineに対しても適用されています。このフォールバックメカニズムにより、macOSユーザーはGNU Binutilsを別途インストールすることなく、Goのプロファイリング機能を利用できるようになり、ユーザーエクスペリエンスが大幅に向上しました。
2. src/cmd/addr2line/main.cとsrc/cmd/objdump/main.cの新規追加
これらのファイルは、Goツールチェーンに組み込まれる軽量なaddr2lineとobjdumpの実装です。C言語で書かれており、Goの内部ライブラリであるlibmachを利用しています。
libmachの利用:libmachは、Goの実行可能ファイルやオブジェクトファイルの構造を解析し、シンボルテーブルやセクション情報にアクセスするためのライブラリです。crackhdr(fd, &fhdr): ファイルディスクリプタfdから実行可能ファイルのヘッダ情報を解析し、fhdr構造体に格納します。syminit(fd, &fhdr): シンボルテーブルを初期化し、シンボル情報をメモリにロードします。findsym(pc, CTEXT, &s): 指定されたプログラムカウンタpcに対応する関数シンボルsを検索します。fileline(file, sizeof file, pc): 指定されたプログラムカウンタpcに対応するソースファイル名と行番号を取得します。machdata->das(text, pc, 0, buf, sizeof buf): 指定されたテキストセクションtext内のプログラムカウンタpcから命令を逆アセンブルし、結果をbufに格納します。machdata->instsize(text, pc): 指定されたプログラムカウンタpcにある命令のサイズを返します。
これらのツールは、GNUの同名ツールに比べて機能は限定的ですが、pprofがプロファイルデータをシンボル解決し、逆アセンブル表示を行うために必要な最小限の機能を提供します。これにより、Goツールチェーンの自己完結性が高まり、macOS環境での外部依存が解消されました。
3. runtime·setprof(bool on)関数の導入と利用
// src/pkg/runtime/runtime.h
void runtime·setprof(bool);
// src/pkg/runtime/thread_darwin.c
void
runtime·setprof(bool on)
{
if(on)
runtime·sigprocmask(SIG_UNBLOCK, &sigset_prof, nil);
else
runtime·sigprocmask(SIG_BLOCK, &sigset_prof, nil);
}
runtime·setprofの役割: この関数は、現在のスレッドがSIGPROFシグナルを受信できるかどうかを制御します。onがtrueの場合、SIGPROFはシグナルマスクから解除され、スレッドはシグナルを受信できるようになります。onがfalseの場合、SIGPROFはシグナルマスクに追加され、スレッドはシグナルをブロックします。sigset_prof:SIGPROFシグナルのみを含むシグナルセットです。runtime·sigprocmask: Unix系システムコールsigprocmaskのGoランタイムラッパーです。スレッドのシグナルマスクを変更するために使用されます。SIG_UNBLOCKは指定されたシグナルをマスクから削除し、SIG_BLOCKは追加します。
この関数は、Goランタイムの様々な場所で呼び出されます。
-
src/pkg/runtime/lock_futex.cとsrc/pkg/runtime/lock_sema.c:runtime·notesleep(セマフォ待機) やruntime·notetsleep(タイムアウト付きセマフォ待機) のような、スレッドが長時間ブロッキング状態に入る可能性のある関数内で、ブロッキングに入る直前にruntime·setprof(false)を呼び出し、ブロッキングから抜けた直後にruntime·setprof(true)を呼び出します。- これにより、スレッドがスリープしている間は
SIGPROFがブロックされ、カーネルがスリープ中のスレッドにシグナルを誤って配信するのを防ぎます。
-
src/pkg/runtime/proc.c:runtime·entersyscall(システムコールに入る前) とruntime·exitsyscall(システムコールから出る後) でruntime·setprofが呼び出されます。- システムコール中はスレッドがカーネルモードで実行されるため、この間も
SIGPROFをブロックすることで、プロファイリングの精度を維持しようとします。
このシグナルマスクの動的な操作は、macOSカーネルのバグに対する主要な回避策であり、GoのCPUプロファイリングがより正確な結果を生成できるようにするための重要な変更です。
4. src/pkg/runtime/signal_darwin_386.cおよびsrc/pkg/runtime/signal_darwin_amd64.cのSIGPROFハンドラ
if(sig == SIGPROF) {
if(gp != m->g0 && gp != m->gsignal)
runtime·sigprof((uint8*)r->eip, (uint8*)r->esp, nil, gp);
return;
}
このコードは、SIGPROFシグナルがGoランタイムのシグナルハンドラに到達した際に実行されます。
if(gp != m->g0 && gp != m->gsignal): この条件は非常に重要です。gpは現在のゴルーチン(Goの軽量スレッド)へのポインタです。m->g0: Goランタイムのスケジューラやガベージコレクタなどの内部処理を実行するための特別なゴルーチンです。m->gsignal: シグナルハンドラが実行される際に使用される特別なゴルーチンです。
- この条件は、「現在のゴルーチンが、Goランタイムの内部処理やシグナルハンドリングのための特別なゴルーチンではない場合のみ、プロファイリング情報を記録する」ことを意味します。
- 目的: Goランタイムの内部ゴルーチンが
SIGPROFを受け取った場合、それはアプリケーションコードの実行とは直接関係のないCPU時間を示している可能性があります。これらのシグナルを無視することで、プロファイリングデータからノイズを取り除き、ユーザーのアプリケーションコードのパフォーマンスに焦点を当てた、よりクリーンで関連性の高いプロファイル情報を得ることができます。
これらのコアとなるコードの変更は、GoがmacOS環境でのプロファイリングの課題にどのように対処し、Goツールチェーンの独立性と堅牢性を高めたかを示しています。
関連リンク
- Go Issue #2008: runtime/pprof: support OS X CPU profiling
- Go Change List 5697066: runtime/pprof: support OS X CPU profiling
- Apple Bug Report #9177434 (Goのコメントで言及):
- 直接的な公開リンクは見つかりませんが、Goのソースコードコメントで言及されています。
- http://code.google.com/p/go/source/detail?r=35b716c94225 (これはGoの古いSVNリポジトリの変更履歴へのリンクで、Appleのバグ報告に関する詳細なコメントが含まれています。)
参考にした情報源リンク
- Go Issue #2008: https://github.com/golang/go/issues/2008
- Go Change List 5697066: https://golang.org/cl/5697066
- GNU Binutils - addr2line: https://sourceware.org/binutils/docs/binutils/addr2line.html
- GNU Binutils - objdump: https://sourceware.org/binutils/docs/binutils/objdump.html
- Go pprof documentation: https://pkg.go.dev/runtime/pprof
- Go blog: Profiling Go Programs: https://go.dev/blog/pprof
- Unix signals (general concept): https://en.wikipedia.org/wiki/Signal_(IPC)
- sigprocmask(2) - Linux man page: https://man7.org/linux/man-pages/man2/sigprocmask.2.html
- ITIMER_PROF: https://man7.org/linux/man-pages/man2/setitimer.2.html
- Go source code comments (specifically in
src/pkg/runtime/thread_darwin.candsrc/pkg/runtime/pprof/pprof.gorelated to Apple Bug Report #9177434): - Go
libmach(internal library for Mach-O parsing): (具体的な公開ドキュメントは少ないが、Goのソースコード内で利用されている)src/libmach/ディレクトリ内のファイル群
これらの情報源は、コミットの背景、技術的詳細、および関連する概念を理解するために参照されました。