[インデックス 1608] ファイルの概要
このコミットは、Go言語のプロファイリングツール prof
と、デバッグ・プロファイリングをサポートする低レベルライブラリ libmach
の改善に焦点を当てています。特に、Linux環境におけるスレッドハンドリングの大幅な改善と、prof
ツールにおける複数スレッドのサポート、そしてpprof
のようなグラフ生成に向けたヒストグラム機能の強化が含まれています。
コミット
commit 736903c170a78582a67ff92dc73a19a880831380
Author: Russ Cox <rsc@golang.org>
Date: Tue Feb 3 15:00:09 2009 -0800
libmach:
* heuristic to go farther during stack traces.
* significantly improved Linux thread handing.
acid:
* update to new libmach interface.
prof:
* use new libmach interface.
* multiple thread support (derived from Rob's copy).
* first steps toward pprof-like graphs:
keep counters indexed by pc,callerpc pairs.
R=r
DELTA=909 (576 added, 123 deleted, 210 changed)
OCL=24240
CL=24259
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/736903c170a78582a67ff92dc73a19a880831380
元コミット内容
libmach:
* heuristic to go farther during stack traces.
* significantly improved Linux thread handing.
acid:
* update to new libmach interface.
prof:
* use new libmach interface.
* multiple thread support (derived from Rob's copy).
* first steps toward pprof-like graphs:
keep counters indexed by pc,callerpc pairs.
変更の背景
このコミットが行われた2009年2月は、Go言語がまだ一般に公開される前の開発初期段階にあたります。当時のGo言語のランタイムやツールチェインは、現在のような成熟した状態ではなく、特にデバッグやプロファイリングの機能は発展途上でした。
libmach
の改善:libmach
は、Go言語のデバッガやプロファイラがターゲットプロセスと対話するための低レベルなインターフェースを提供するライブラリです。特にLinux環境では、プロセスのスレッド情報を正確に取得し、各スレッドの状態を制御することが複雑であり、初期の実装では不十分な点があったと考えられます。スタックトレースの精度向上も、デバッグ体験を改善するために不可欠でした。prof
ツールの機能強化:prof
ツールは、Goプログラムのプロファイリングデータを収集・分析するためのものです。当時のGo言語は並行処理を重視しており、複数のゴルーチン(Go言語の軽量スレッド)が同時に動作する環境でのプロファイリングは重要でした。しかし、初期のprof
ツールは単一スレッドのプロファイリングに限定されていたか、複数スレッドの情報を十分に活用できていなかった可能性があります。pprof
のようなグラフ表示機能は、プロファイリング結果を視覚的に理解しやすくするために非常に有効であり、その基礎を築くことが目的でした。acid
の更新:acid
は、Go言語のデバッガの一つであり、libmach
を利用してターゲットプロセスを操作します。libmach
のインターフェースが変更されたことに伴い、acid
もその変更に適応する必要がありました。
これらの背景から、このコミットはGo言語のデバッグ・プロファイリング基盤の安定性と機能性を向上させるための重要なステップであったと言えます。
前提知識の解説
1. libmach
libmach
は、Go言語のデバッガやプロファイラが、実行中のプロセス(ターゲットプロセス)のメモリ、レジスタ、スレッド情報などにアクセスするための抽象化レイヤーを提供するライブラリです。異なるOS(Linux, Darwinなど)やアーキテクチャ(AMD64など)に対応するために、それぞれのプラットフォーム固有の実装を持ちます。このライブラリは、ptrace
のようなOSのデバッグインターフェースをラップし、より高レベルな操作を可能にします。
2. ptrace
(Linux)
ptrace
は、Linuxカーネルが提供するシステムコールで、プロセスが別のプロセス(ターゲットプロセス)の実行を監視・制御することを可能にします。デバッガやプロファイラは ptrace
を利用して、ターゲットプロセスのレジスタの読み書き、メモリの読み書き、シグナルの送信、システムコールの監視などを行います。
ptrace
の主な機能と関連する概念:
PTRACE_ATTACH
: ターゲットプロセスにアタッチし、そのプロセスを停止させる。PTRACE_DETACH
: ターゲットプロセスからデタッチし、そのプロセスを再開させる。PTRACE_CONT
: 停止中のターゲットプロセスを再開させる。PTRACE_SINGLESTEP
: ターゲットプロセスを1命令だけ実行させる。PTRACE_GETREGS
/PTRACE_SETREGS
: ターゲットプロセスのレジスタの読み書き。PTRACE_PEEKDATA
/PTRACE_POKEDATA
: ターゲットプロセスのメモリの読み書き。PTRACE_O_TRACEFORK
,PTRACE_O_TRACECLONE
,PTRACE_O_TRACEEXEC
など:ptrace
イベントオプション。これらのオプションを設定することで、ターゲットプロセスがfork
,clone
,exec
などのシステムコールを呼び出した際に、デバッガに通知されるようになります。特にclone
はLinuxにおけるスレッド作成のシステムコールであり、マルチスレッドアプリケーションのデバッグには不可欠です。waitpid
: 子プロセスの状態変化(停止、終了など)を待機するシステムコール。ptrace
と組み合わせて、ターゲットプロセスの状態を監視するために使用されます。
3. prof
ツールとプロファイリング
prof
は、Go言語のプログラムの実行プロファイルを収集・分析するためのツールです。プロファイリングとは、プログラムの実行中に、どの関数がどれくらいのCPU時間を使っているか、どのメモリがどれくらい割り当てられているか、といった情報を収集し、プログラムのパフォーマンスボトルネックを特定する手法です。
- ヒストグラム: プロファイリングデータの一種で、特定のイベント(例: 関数呼び出し)の発生回数や、それに費やされた時間などを集計したものです。
pc
(Program Counter): プログラムカウンタ。現在実行中の命令のアドレスを指すレジスタ。callerpc
(Caller Program Counter): 呼び出し元のプログラムカウンタ。関数が呼び出された際、呼び出し元の命令のアドレスを指します。pc, callerpc
のペアでプロファイリングデータを集計することで、関数間の呼び出し関係を考慮した、より詳細なプロファイル情報を得ることができます。これは、pprof
のようなコールグラフ(Call Graph)を生成するための基礎となります。pprof
: Go言語の標準的なプロファイリングツールで、プロファイリングデータを視覚的に分析するための豊富な機能を提供します。テキスト形式のレポートだけでなく、コールグラフやフレームグラフなどのグラフィカルな表示も可能です。このコミットは、prof
ツールがpprof
のような高度な機能を持つための初期ステップと位置づけられています。
4. スレッドとプロセス (Linux)
Linuxでは、プロセスとスレッドはカーネルレベルでは同じ「タスク」として扱われます。fork
は新しいプロセスを作成し、clone
は新しいタスク(スレッド)を作成します。ptrace
は個々のタスク(スレッド)に対してアタッチ・デタッチが可能です。マルチスレッドアプリケーションを正確にプロファイリング・デバッグするためには、すべてのスレッドを適切に管理し、その状態変化を追跡する必要があります。
技術的詳細
このコミットの技術的詳細は、主に libmach
のLinux実装 (src/libmach_amd64/linux.c
) と prof
ツール (src/cmd/prof/main.c
) の変更に集約されます。
libmach
のLinuxスレッドハンドリング改善
以前の libmach
のLinux実装は、単一プロセス(メインスレッド)のデバッグに特化していたか、マルチスレッド環境での挙動が不安定だったと考えられます。このコミットでは、ptrace
の高度な機能とLinuxの /proc
ファイルシステムを利用して、より堅牢なスレッド管理メカニズムを導入しています。
LinuxThread
構造体の導入: 各スレッドの状態を追跡するためのLinuxThread
構造体が導入されました。これにより、各スレッドのPID (実際にはTID: Thread ID)、状態 (Detached, Attached, Stopped, Runningなど)、シグナル情報、子プロセス情報などを一元的に管理できるようになりました。attachthread
関数: 特定のPID/TIDを持つスレッドにアタッチし、その状態を管理リストに追加します。PTRACE_ATTACH
を使用してスレッドにアタッチし、waitpid
で停止を待ちます。特に重要なのは、PTRACE_SETOPTIONS
を使用してPTRACE_O_TRACEFORK
,PTRACE_O_TRACECLONE
,PTRACE_O_TRACEEXEC
などのイベントオプションを設定している点です。これにより、ターゲットプロセスが新しいスレッドを作成したり、exec
を実行したりする際に、デバッガがそのイベントを捕捉できるようになります。attachallthreads
関数: 特定のプロセスIDに属するすべてのスレッドを列挙し、それぞれにアタッチします。これは/proc/<pid>/task
ディレクトリを読み取り、その中の各エントリ(TID)に対してattachthread
を呼び出すことで実現されます。これにより、プロファイリングやデバッグの開始時に、ターゲットプロセスのすべてのスレッドを確実に捕捉できるようになります。wait1
関数:waitpid
を使用して、アタッチされたスレッドの状態変化を監視します。WIFSTOPPED
,WIFCONTINUED
,WIFEXITED
,WIFSIGNALED
などのマクロを使用して、スレッドが停止したのか、再開したのか、終了したのか、シグナルによって終了したのかを判断し、LinuxThread
構造体の状態を更新します。特にSIGTRAP
シグナルとptrace
イベント(PTRACE_EVENT_FORK
,PTRACE_EVENT_CLONE
など)の処理が追加され、新しいスレッドの生成などを正確に追跡できるようになりました。ctlproc
関数の改善:ctlproc
は、ターゲットプロセス(またはスレッド)を制御するための関数です。このコミットでは、LinuxThread
構造体と新しいスレッド管理ロジックに基づいて、stop
,start
,kill
,step
などの操作が各スレッドに対して正確に適用されるように変更されました。特に、tkill
システムコール(特定のTIDにシグナルを送信する)がstop
処理に利用されています。- スタックトレースのヒューリスティック改善 (
8db.c
):i386trace
関数において、スタックポインタの調整後にPCが不正になるケースに対応するためのヒューリスティックが追加されました。これにより、スタックトレースの精度が向上し、より多くのフレームを正確に辿れるようになりました。
prof
ツールの改善
prof
ツールは、新しい libmach
インターフェースを活用し、マルチスレッドプロファイリングと pprof
スタイルのヒストグラムをサポートするように大幅に改修されました。
- 複数スレッドのサポート:
pid
,nthread
,thread[]
,map[]
といったグローバル変数が導入され、メインプロセスID、スレッド数、各スレッドのPID、および各スレッドに対応するメモリマップを管理します。getthreads
関数が追加され、libmach
のprocthreadpids
を利用して、ターゲットプロセスのすべてのアクティブなスレッドIDを取得し、それらにアタッチします。これにより、prof
ツールは複数のスレッドから同時にプロファイリングデータを収集できるようになりました。samples
関数が各スレッドに対してsample
関数を呼び出すように変更され、各スレッドのレジスタ情報(特にPCとSP)を個別に取得します。
pprof
スタイルのヒストグラム:PC
構造体にcallerpc
フィールドが追加され、プログラムカウンタと呼び出し元プログラムカウンタのペアでプロファイリングデータを集計できるようになりました。これにより、関数間の呼び出し関係を考慮した、より詳細なプロファイル情報(コールグラフの基礎)を収集できます。addtohistogram
関数がpc
とcallerpc
の両方をキーとして使用するように変更されました。dumphistogram
関数が大幅に改修され、収集されたpc, callerpc
ペアのヒストグラムデータを分析し、関数ごとの実行時間(またはサンプル数)を計算します。特に、findfunc
関数とFunc
構造体が導入され、シンボル情報に基づいて関数を特定し、その関数がスタック上にあった回数 (onstack
) と、その関数がリーフ関数(他の関数を呼び出さない)として実行された回数 (leaf
) を集計します。これにより、pprof
のような「自己時間」と「累積時間」に近い概念のプロファイリングメトリクスを計算する基礎が築かれました。- ヒストグラムの出力形式も改善され、各関数の
leaf
カウント(自己時間に近い)とonstack
カウント(累積時間に近い)をパーセンテージで表示できるようになりました。
- コマンドラインオプションの変更:
-c
(collapse) オプションが削除されました。これは、新しいpc, callerpc
ペアに基づくヒストグラム集計とFunc
構造体による関数ごとの集計により、より洗練された方法で関数呼び出しのコンテキストを扱うようになったためと考えられます。-s
(stacks) オプションが複数回指定可能になり、スタックトレースの表示レベルを制御できるようになりました (stacks++
)。-hs
(include stack info in histograms) オプションが追加され、ヒストグラムにスタック情報を含めるかどうかの制御が可能になりました。
mach_amd64.h
の変更
procthreadpids
関数のシグネチャが変更されました。以前は int **thread
でスレッドIDの配列を返していましたが、新しいシグネチャ int *tid, int ntid
では、呼び出し元が提供するバッファにスレッドIDを書き込む形式になりました。これは、メモリ管理の責任を呼び出し元に委ねることで、より安全で効率的なインターフェースを提供するためと考えられます。
darwin.c
の変更
Darwin版の procthreadpids
もLinux版と同様にシグネチャが変更され、呼び出し元が提供するバッファにスレッドIDを書き込む形式になりました。
コアとなるコードの変更箇所
src/libmach_amd64/linux.c
LinuxThread
構造体の追加と、スレッドの状態を管理するthr
配列の導入。attachthread
関数の追加:ptrace(PTRACE_ATTACH)
とPTRACE_SETOPTIONS
を使用してスレッドにアタッチし、イベントオプションを設定。wait1
関数の追加:waitpid
を使用してスレッドの状態変化を監視し、ptrace
イベント(PTRACE_EVENT_FORK
,PTRACE_EVENT_CLONE
など)を処理。attachallthreads
関数の追加:/proc/<pid>/task
を読み取り、すべてのスレッドにアタッチ。ctlproc
関数の大幅な改修:LinuxThread
構造体とwait1
を利用したスレッド制御ロジックの導入。procthreadpids
関数の実装変更:LinuxThread
リストからアクティブなスレッドIDを抽出して返す。
src/cmd/prof/main.c
nthread
,thread[]
,map[]
といった複数スレッド管理用のグローバル変数の追加。PC
構造体にcallerpc
フィールドを追加。getthreads
関数の追加:libmach
のprocthreadpids
を呼び出してスレッドリストを更新。sample
関数がMap *map
を引数にとるように変更され、特定のスレッドのレジスタを読み取るように。addtohistogram
関数がpc
とcallerpc
の両方でインデックス付けするように変更。Func
構造体とfindfunc
関数の追加: シンボル情報に基づいて関数を特定し、onstack
とleaf
カウントを管理。dumphistogram
関数の大幅な改修:Func
構造体とonstack
,leaf
カウントを使用してpprof
スタイルのプロファイルレポートを生成。main
関数におけるスレッドアタッチとプロファイリングループの変更: 各スレッドに対してctlproc
とsample
を呼び出すように。
include/mach_amd64.h
procthreadpids
関数のシグネチャ変更:int procthreadpids(int pid, int *tid, int ntid);
コアとなるコードの解説
src/libmach_amd64/linux.c
の attachthread
と wait1
static LinuxThread*
attachthread(int pid, int tid, int *new, int newstate)
{
// ... (既存スレッドの検索、新規スレッドの割り当て)
if(t->state == Detached) {
if(ptrace(PTRACE_ATTACH, tid, 0, 0) < 0) {
// ... エラー処理
return nil;
}
t->state = Attached;
}
if(t->state == Attached) {
// wait for stop, so we can set options
if(waitpid(tid, &status, __WALL|WUNTRACED|WSTOPPED) < 0)
return nil;
if(!WIFSTOPPED(status)) {
// ... エラー処理
return nil;
}
t->state = AttachStop;
}
if(t->state == AttachStop) {
// set options so we'll find out about new threads
flags = PTRACE_O_TRACEFORK |
PTRACE_O_TRACEVFORK |
PTRACE_O_TRACECLONE |
PTRACE_O_TRACEEXEC |
PTRACE_O_TRACEVFORKDONE |
PTRACE_O_TRACEEXIT;
if(ptrace(PTRACE_SETOPTIONS, tid, 0, (void*)flags) < 0) {
// ... エラー処理
return nil;
}
t->state = Stopped;
}
return t;
}
static int
wait1(int nohang)
{
// ... (waitpid呼び出し)
if(WIFSTOPPED(status)) {
t->state = Stopped;
t->signal = WSTOPSIG(status);
if(t->signal == SIGTRAP && (event = status>>16) != 0) { // ptrace event
switch(event) {
case PTRACE_EVENT_FORK:
case PTRACE_EVENT_VFORK:
case PTRACE_EVENT_CLONE:
// 新しい子プロセス/スレッドのPIDを取得し、attachthreadでアタッチ
if(ptrace(PTRACE_GETEVENTMSG, t->tid, 0, &data) < 0) { /* ... */ }
t->child = data;
attachthread(t->pid, t->child, &new, Running);
break;
case PTRACE_EVENT_EXEC:
t->state = Execing;
break;
case PTRACE_EVENT_EXIT:
// 終了コードを取得
if(ptrace(PTRACE_GETEVENTMSG, t->tid, 0, &data) < 0) { /* ... */ }
t->exitcode = data;
break;
}
}
}
// ... (WIFCONTINUED, WIFEXITED, WIFSIGNALED の処理)
return 1;
}
attachthread
は、ptrace(PTRACE_ATTACH)
でターゲットスレッドにアタッチし、waitpid
でそのスレッドが停止するのを待ちます。その後、PTRACE_SETOPTIONS
を使って PTRACE_O_TRACEFORK
, PTRACE_O_TRACECLONE
などのイベントを有効にしています。これにより、ターゲットプロセスが新しいスレッドを生成した際に、デバッガがそのイベントを捕捉できるようになります。
wait1
は、waitpid
をノンブロッキングモード (WNOHANG
) またはブロッキングモードで呼び出し、アタッチされたスレッドの状態変化を監視します。特に SIGTRAP
シグナルと ptrace
イベント(status>>16
で取得)をチェックし、PTRACE_EVENT_CLONE
などのイベントが発生した場合は、新しく生成されたスレッドのTIDを取得し、attachthread
を再帰的に呼び出してそのスレッドも監視対象に加えます。これにより、動的に生成されるスレッドも正確に追跡できるようになります。
src/cmd/prof/main.c
の dumphistogram
void
dumphistogram()
{
// ... (countersからFunc構造体への集計)
// assign counts to functions.
for(h = 0; h < Ncounters; h++) {
for(x = counters[h]; x != NULL; x = x->next) {
f = findfunc(x->pc); // 現在のPCに対応する関数を検索
if(f) {
f->onstack += x->count; // その関数がスタック上にあった回数を加算
f->leaf += x->count; // その関数がリーフ関数として実行された回数を加算
}
f = findfunc(x->callerpc); // 呼び出し元のPCに対応する関数を検索
if(f)
f->leaf -= x->count; // 呼び出し元関数はリーフではないので減算
}
}
// ... (Func構造体の配列構築とソート)
// print.
print("%d samples (avg %.1g threads)\\n", nsample, (double)nsamplethread/nsample);
for(i = 0; i < nfunc; i++) {
f = ff[i];
print("%6.2f%%\\t", 100.0*(double)f->leaf/nsample); // リーフカウント(自己時間)のパーセンテージ
if(stacks)
print("%6.2f%%\\t", 100.0*(double)f->onstack/nsample); // onstackカウント(累積時間)のパーセンテージ
print("%s\\n", f->s.name); // 関数名
}
}
dumphistogram
関数は、プロファイリングの核心部分です。
pc, callerpc
ペアの集計:sample
関数で収集されたpc, callerpc
ペアのカウント (counters
配列に格納) を基に、各関数 (Func
構造体) のonstack
とleaf
カウントを計算します。onstack
: その関数がスタック上に存在した(つまり、実行中または呼び出しチェーン上にあった)総サンプル数。これはpprof
の「累積時間」に相当します。leaf
: その関数が実際に命令を実行した(つまり、他の関数を呼び出さずに自身がCPU時間を使った)総サンプル数。これはpprof
の「自己時間」に相当します。x->pc
に対応する関数のleaf
を増やし、x->callerpc
に対応する関数のleaf
を減らすことで、呼び出し元ではない真のリーフ関数(CPUを消費した関数)の時間を正確に集計しています。
- ソートと出力: 計算された
Func
構造体の配列をleaf
カウントでソートし、各関数のleaf
とonstack
のパーセンテージ、および関数名を出力します。これにより、どの関数が最もCPU時間を消費しているか、またその関数が直接消費しているのか、それとも呼び出し元として多くの時間を消費しているのかを把握できるようになります。これはpprof
の基本的なレポート形式に非常に近いです。
関連リンク
- Go言語のプロファイリング: Go言語の公式ドキュメントには、
pprof
を使ったプロファイリングに関する詳細な情報があります。このコミットは、その基礎を築く初期のステップです。 - Linux
ptrace
システムコール:libmach
のLinux実装の根幹をなすシステムコールです。 /proc
ファイルシステム: Linuxにおけるプロセス情報を提供する仮想ファイルシステム。libmach
がスレッド情報を取得するために利用しています。
参考にした情報源リンク
- Go言語のソースコード (GitHub)
- Linux man pages
- Go Blog: Profiling Go Programs
- Go言語の初期開発に関する議論やメーリングリストのアーカイブ (もし公開されていれば) (このコミットの時期の具体的な議論を見つけるのは難しいかもしれませんが、一般的な情報源として)
- The Go Programming Language Specification (Go言語の基本的な概念理解のため)
- Go言語のデバッグツールに関する情報 (当時のデバッグ環境の理解のため)
- Rob Pike の Go 言語に関する初期のプレゼンテーションや論文 (Go言語の設計思想や初期のツールに関する洞察を得るため)
- Russ Cox の Go 言語に関する貢献 (コミットの作者の他の仕事から文脈を得るため)
[インデックス 1608] ファイルの概要
このコミットは、Go言語のプロファイリングツール prof
と、デバッグ・プロファイリングをサポートする低レベルライブラリ libmach
の改善に焦点を当てています。特に、Linux環境におけるスレッドハンドリングの大幅な改善と、prof
ツールにおける複数スレッドのサポート、そしてpprof
のようなグラフ生成に向けたヒストグラム機能の強化が含まれています。
コミット
commit 736903c170a78582a67ff92dc73a19a880831380
Author: Russ Cox <rsc@golang.org>
Date: Tue Feb 3 15:00:09 2009 -0800
libmach:
* heuristic to go farther during stack traces.
* significantly improved Linux thread handing.
acid:
* update to new libmach interface.
prof:
* use new libmach interface.
* multiple thread support (derived from Rob's copy).
* first steps toward pprof-like graphs:
keep counters indexed by pc,callerpc pairs.
R=r
DELTA=909 (576 added, 123 deleted, 210 changed)
OCL=24240
CL=24259
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/736903c170a78582a67ff92dc73a19a880831380
元コミット内容
libmach:
* heuristic to go farther during stack traces.
* significantly improved Linux thread handing.
acid:
* update to new libmach interface.
prof:
* use new libmach interface.
* multiple thread support (derived from Rob's copy).
* first steps toward pprof-like graphs:
keep counters indexed by pc,callerpc pairs.
変更の背景
このコミットが行われた2009年2月は、Go言語がまだ一般に公開される前の開発初期段階にあたります。当時のGo言語のランタイムやツールチェインは、現在のような成熟した状態ではなく、特にデバッグやプロファイリングの機能は発展途上でした。
libmach
の改善:libmach
は、Go言語のデバッガやプロファイラがターゲットプロセスと対話するための低レベルなインターフェースを提供するライブラリです。特にLinux環境では、プロセスのスレッド情報を正確に取得し、各スレッドの状態を制御することが複雑であり、初期の実装では不十分な点があったと考えられます。スタックトレースの精度向上も、デバッグ体験を改善するために不可欠でした。prof
ツールの機能強化:prof
ツールは、Goプログラムのプロファイリングデータを収集・分析するためのものです。当時のGo言語は並行処理を重視しており、複数のゴルーチン(Go言語の軽量スレッド)が同時に動作する環境でのプロファイリングは重要でした。しかし、初期のprof
ツールは単一スレッドのプロファイリングに限定されていたか、複数スレッドの情報を十分に活用できていなかった可能性があります。pprof
のようなグラフ表示機能は、プロファイリング結果を視覚的に理解しやすくするために非常に有効であり、その基礎を築くことが目的でした。acid
の更新:acid
は、Go言語のデバッガの一つであり、libmach
を利用してターゲットプロセスを操作します。libmach
のインターフェースが変更されたことに伴い、acid
もその変更に適応する必要がありました。
これらの背景から、このコミットはGo言語のデバッグ・プロファイリング基盤の安定性と機能性を向上させるための重要なステップであったと言えます。
前提知識の解説
1. libmach
libmach
は、Go言語のデバッガやプロファイラが、実行中のプロセス(ターゲットプロセス)のメモリ、レジスタ、スレッド情報などにアクセスするための抽象化レイヤーを提供するライブラリです。異なるOS(Linux, Darwinなど)やアーキテクチャ(AMD64など)に対応するために、それぞれのプラットフォーム固有の実装を持ちます。このライブラリは、ptrace
のようなOSのデバッグインターフェースをラップし、より高レベルな操作を可能にします。
2. ptrace
(Linux)
ptrace
は、Linuxカーネルが提供するシステムコールで、プロセスが別のプロセス(ターゲットプロセス)の実行を監視・制御することを可能にします。デバッガやプロファイラは ptrace
を利用して、ターゲットプロセスのレジスタの読み書き、メモリの読み書き、シグナルの送信、システムコールの監視などを行います。
ptrace
の主な機能と関連する概念:
PTRACE_ATTACH
: ターゲットプロセスにアタッチし、そのプロセスを停止させる。PTRACE_DETACH
: ターゲットプロセスからデタッチし、そのプロセスを再開させる。PTRACE_CONT
: 停止中のターゲットプロセスを再開させる。PTRACE_SINGLESTEP
: ターゲットプロセスを1命令だけ実行させる。PTRACE_GETREGS
/PTRACE_SETREGS
: ターゲットプロセスのレジスタの読み書き。PTRACE_PEEKDATA
/PTRACE_POKEDATA
: ターゲットプロセスのメモリの読み書き。PTRACE_O_TRACEFORK
,PTRACE_O_TRACECLONE
,PTRACE_O_TRACEEXEC
など:ptrace
イベントオプション。これらのオプションを設定することで、ターゲットプロセスがfork
,clone
,exec
などのシステムコールを呼び出した際に、デバッガに通知されるようになります。特にclone
はLinuxにおけるスレッド作成のシステムコールであり、マルチスレッドアプリケーションのデバッグには不可欠です。waitpid
: 子プロセスの状態変化(停止、終了など)を待機するシステムコール。ptrace
と組み合わせて、ターゲットプロセスの状態を監視するために使用されます。
3. prof
ツールとプロファイリング
prof
は、Go言語のプログラムの実行プロファイルを収集・分析するためのツールです。プロファイリングとは、プログラムの実行中に、どの関数がどれくらいのCPU時間を使っているか、どのメモリがどれくらい割り当てられているか、といった情報を収集し、プログラムのパフォーマンスボトルネックを特定する手法です。
- ヒストグラム: プロファイリングデータの一種で、特定のイベント(例: 関数呼び出し)の発生回数や、それに費やされた時間などを集計したものです。
pc
(Program Counter): プログラムカウンタ。現在実行中の命令のアドレスを指すレジスタ。callerpc
(Caller Program Counter): 呼び出し元のプログラムカウンタ。関数が呼び出された際、呼び出し元の命令のアドレスを指します。pc, callerpc
のペアでプロファイリングデータを集計することで、関数間の呼び出し関係を考慮した、より詳細なプロファイル情報を得ることができます。これは、pprof
のようなコールグラフ(Call Graph)を生成するための基礎となります。pprof
: Go言語の標準的なプロファイリングツールで、プロファイリングデータを視覚的に分析するための豊富な機能を提供します。テキスト形式のレポートだけでなく、コールグラフやフレームグラフなどのグラフィカルな表示も可能です。このコミットは、prof
ツールがpprof
のような高度な機能を持つための初期ステップと位置づけられています。
4. スレッドとプロセス (Linux)
Linuxでは、プロセスとスレッドはカーネルレベルでは同じ「タスク」として扱われます。fork
は新しいプロセスを作成し、clone
は新しいタスク(スレッド)を作成します。ptrace
は個々のタスク(スレッド)に対してアタッチ・デタッチが可能です。マルチスレッドアプリケーションを正確にプロファイリング・デバッグするためには、すべてのスレッドを適切に管理し、その状態変化を追跡する必要があります。
技術的詳細
このコミットの技術的詳細は、主に libmach
のLinux実装 (src/libmach_amd64/linux.c
) と prof
ツール (src/cmd/prof/main.c
) の変更に集約されます。
libmach
のLinuxスレッドハンドリング改善
以前の libmach
のLinux実装は、単一プロセス(メインスレッド)のデバッグに特化していたか、マルチスレッド環境での挙動が不安定だったと考えられます。このコミットでは、ptrace
の高度な機能とLinuxの /proc
ファイルシステムを利用して、より堅牢なスレッド管理メカニズムを導入しています。
LinuxThread
構造体の導入: 各スレッドの状態を追跡するためのLinuxThread
構造体が導入されました。これにより、各スレッドのPID (実際にはTID: Thread ID)、状態 (Detached, Attached, Stopped, Runningなど)、シグナル情報、子プロセス情報などを一元的に管理できるようになりました。attachthread
関数: 特定のPID/TIDを持つスレッドにアタッチし、その状態を管理リストに追加します。PTRACE_ATTACH
を使用してスレッドにアタッチし、waitpid
で停止を待ちます。特に重要なのは、PTRACE_SETOPTIONS
を使用してPTRACE_O_TRACEFORK
,PTRACE_O_TRACECLONE
,PTRACE_O_TRACEEXEC
などのイベントオプションを設定している点です。これにより、ターゲットプロセスが新しいスレッドを作成したり、exec
を実行したりする際に、デバッガがそのイベントを捕捉できるようになります。attachallthreads
関数: 特定のプロセスIDに属するすべてのスレッドを列挙し、それぞれにアタッチします。これは/proc/<pid>/task
ディレクトリを読み取り、その中の各エントリ(TID)に対してattachthread
を呼び出すことで実現されます。これにより、プロファイリングやデバッグの開始時に、ターゲットプロセスのすべてのスレッドを確実に捕捉できるようになります。wait1
関数:waitpid
を使用して、アタッチされたスレッドの状態変化を監視します。WIFSTOPPED
,WIFCONTINUED
,WIFEXITED
,WIFSIGNALED
などのマクロを使用して、スレッドが停止したのか、再開したのか、終了したのか、シグナルによって終了したのかを判断し、LinuxThread
構造体の状態を更新します。特にSIGTRAP
シグナルとptrace
イベント(PTRACE_EVENT_FORK
,PTRACE_EVENT_CLONE
など)の処理が追加され、新しいスレッドの生成などを正確に追跡できるようになりました。ctlproc
関数の改善:ctlproc
は、ターゲットプロセス(またはスレッド)を制御するための関数です。このコミットでは、LinuxThread
構造体と新しいスレッド管理ロジックに基づいて、stop
,start
,kill
,step
などの操作が各スレッドに対して正確に適用されるように変更されました。特に、tkill
システムコール(特定のTIDにシグナルを送信する)がstop
処理に利用されています。- スタックトレースのヒューリスティック改善 (
8db.c
):i386trace
関数において、スタックポインタの調整後にPCが不正になるケースに対応するためのヒューリスティックが追加されました。これにより、スタックトレースの精度が向上し、より多くのフレームを正確に辿れるようになりました。
prof
ツールの改善
prof
ツールは、新しい libmach
インターフェースを活用し、マルチスレッドプロファイリングと pprof
スタイルのヒストグラムをサポートするように大幅に改修されました。
- 複数スレッドのサポート:
pid
,nthread
,thread[]
,map[]
といったグローバル変数が導入され、メインプロセスID、スレッド数、各スレッドのPID、および各スレッドに対応するメモリマップを管理します。getthreads
関数が追加され、libmach
のprocthreadpids
を利用して、ターゲットプロセスのすべてのアクティブなスレッドIDを取得し、それらにアタッチします。これにより、prof
ツールは複数のスレッドから同時にプロファイリングデータを収集できるようになりました。samples
関数が各スレッドに対してsample
関数を呼び出すように変更され、各スレッドのレジスタ情報(特にPCとSP)を個別に取得します。
pprof
スタイルのヒストグラム:PC
構造体にcallerpc
フィールドが追加され、プログラムカウンタと呼び出し元プログラムカウンタのペアでプロファイリングデータを集計できるようになりました。これにより、関数間の呼び出し関係を考慮した、より詳細なプロファイル情報(コールグラフの基礎)を収集できます。addtohistogram
関数がpc
とcallerpc
の両方をキーとして使用するように変更されました。dumphistogram
関数が大幅に改修され、収集されたpc, callerpc
ペアのヒストグラムデータを分析し、関数ごとの実行時間(またはサンプル数)を計算します。特に、findfunc
関数とFunc
構造体が導入され、シンボル情報に基づいて関数を特定し、その関数がスタック上にあった回数 (onstack
) と、その関数がリーフ関数(他の関数を呼び出さない)として実行された回数 (leaf
) を集計します。これにより、pprof
のような「自己時間」と「累積時間」に近い概念のプロファイリングメトリクスを計算する基礎が築かれました。- ヒストグラムの出力形式も改善され、各関数の
leaf
カウント(自己時間に近い)とonstack
カウント(累積時間に近い)をパーセンテージで表示できるようになりました。
- コマンドラインオプションの変更:
-c
(collapse) オプションが削除されました。これは、新しいpc, callerpc
ペアに基づくヒストグラム集計とFunc
構造体による関数ごとの集計により、より洗練された方法で関数呼び出しのコンテキストを扱うようになったためと考えられます。-s
(stacks) オプションが複数回指定可能になり、スタックトレースの表示レベルを制御できるようになりました (stacks++
)。-hs
(include stack info in histograms) オプションが追加され、ヒストグラムにスタック情報を含めるかどうかの制御が可能になりました。
mach_amd64.h
の変更
procthreadpids
関数のシグネチャが変更されました。以前は int **thread
でスレッドIDの配列を返していましたが、新しいシグネチャ int *tid, int ntid
では、呼び出し元が提供するバッファにスレッドIDを書き込む形式になりました。これは、メモリ管理の責任を呼び出し元に委ねることで、より安全で効率的なインターフェースを提供するためと考えられます。
darwin.c
の変更
Darwin版の procthreadpids
もLinux版と同様にシグネチャが変更され、呼び出し元が提供するバッファにスレッドIDを書き込む形式になりました。
コアとなるコードの変更箇所
src/libmach_amd64/linux.c
LinuxThread
構造体の追加と、スレッドの状態を管理するthr
配列の導入。attachthread
関数の追加:ptrace(PTRACE_ATTACH)
とPTRACE_SETOPTIONS
を使用してスレッドにアタッチし、イベントオプションを設定。wait1
関数の追加:waitpid
を使用してスレッドの状態変化を監視し、ptrace
イベント(PTRACE_EVENT_FORK
,PTRACE_EVENT_CLONE
など)を処理。attachallthreads
関数の追加:/proc/<pid>/task
を読み取り、すべてのスレッドにアタッチ。ctlproc
関数の大幅な改修:LinuxThread
構造体とwait1
を利用したスレッド制御ロジックの導入。procthreadpids
関数の実装変更:LinuxThread
リストからアクティブなスレッドIDを抽出して返す。
src/cmd/prof/main.c
nthread
,thread[]
,map[]
といった複数スレッド管理用のグローバル変数の追加。PC
構造体にcallerpc
フィールドを追加。getthreads
関数の追加:libmach
のprocthreadpids
を呼び出してスレッドリストを更新。sample
関数がMap *map
を引数にとるように変更され、特定のスレッドのレジスタを読み取るように。addtohistogram
関数がpc
とcallerpc
の両方でインデックス付けするように変更。Func
構造体とfindfunc
関数の追加: シンボル情報に基づいて関数を特定し、onstack
とleaf
カウントを管理。dumphistogram
関数の大幅な改修:Func
構造体とonstack
,leaf
カウントを使用してpprof
スタイルのプロファイルレポートを生成。main
関数におけるスレッドアタッチとプロファイリングループの変更: 各スレッドに対してctlproc
とsample
を呼び出すように。
include/mach_amd64.h
procthreadpids
関数のシグネチャ変更:int procthreadpids(int pid, int *tid, int ntid);
コアとなるコードの解説
src/libmach_amd64/linux.c
の attachthread
と wait1
static LinuxThread*
attachthread(int pid, int tid, int *new, int newstate)
{
// ... (既存スレッドの検索、新規スレッドの割り当て)
if(t->state == Detached) {
if(ptrace(PTRACE_ATTACH, tid, 0, 0) < 0) {
// ... エラー処理
return nil;
}
t->state = Attached;
}
if(t->state == Attached) {
// wait for stop, so we can set options
if(waitpid(tid, &status, __WALL|WUNTRACED|WSTOPPED) < 0)
return nil;
if(!WIFSTOPPED(status)) {
// ... エラー処理
return nil;
}
t->state = AttachStop;
}
if(t->state == AttachStop) {
// set options so we'll find out about new threads
flags = PTRACE_O_TRACEFORK |
PTRACE_O_TRACEVFORK |
PTRACE_O_TRACECLONE |
PTRACE_O_TRACEEXEC |
PTRACE_O_TRACEVFORKDONE |
PTRACE_O_TRACEEXIT;
if(ptrace(PTRACE_SETOPTIONS, tid, 0, (void*)flags) < 0) {
// ... エラー処理
return nil;
}
t->state = Stopped;
}
return t;
}
static int
wait1(int nohang)
{
// ... (waitpid呼び出し)
if(WIFSTOPPED(status)) {
t->state = Stopped;
t->signal = WSTOPSIG(status);
if(t->signal == SIGTRAP && (event = status>>16) != 0) { // ptrace event
switch(event) {
case PTRACE_EVENT_FORK:
case PTRACE_EVENT_VFORK:
case PTRACE_EVENT_CLONE:
// 新しい子プロセス/スレッドのPIDを取得し、attachthreadでアタッチ
if(ptrace(PTRACE_GETEVENTMSG, t->tid, 0, &data) < 0) { /* ... */ }
t->child = data;
attachthread(t->pid, t->child, &new, Running);
break;
case PTRACE_EVENT_EXEC:
t->state = Execing;
break;
case PTRACE_EVENT_EXIT:
// 終了コードを取得
if(ptrace(PTRACE_GETEVENTMSG, t->tid, 0, &data) < 0) { /* ... */ }
t->exitcode = data;
break;
}
}
}
// ... (WIFCONTINUED, WIFEXITED, WIFSIGNALED の処理)
return 1;
}
attachthread
は、ptrace(PTRACE_ATTACH)
でターゲットスレッドにアタッチし、waitpid
でそのスレッドが停止するのを待ちます。その後、PTRACE_SETOPTIONS
を使って PTRACE_O_TRACEFORK
, PTRACE_O_TRACECLONE
などのイベントを有効にしています。これにより、ターゲットプロセスが新しいスレッドを生成した際に、デバッガがそのイベントを捕捉できるようになります。
wait1
は、waitpid
をノンブロッキングモード (WNOHANG
) またはブロッキングモードで呼び出し、アタッチされたスレッドの状態変化を監視します。特に SIGTRAP
シグナルと ptrace
イベント(status>>16
で取得)をチェックし、PTRACE_EVENT_CLONE
などのイベントが発生した場合は、新しく生成されたスレッドのTIDを取得し、attachthread
を再帰的に呼び出してそのスレッドも監視対象に加えます。これにより、動的に生成されるスレッドも正確に追跡できるようになります。
src/cmd/prof/main.c
の dumphistogram
void
dumphistogram()
{
// ... (countersからFunc構造体への集計)
// assign counts to functions.
for(h = 0; h < Ncounters; h++) {
for(x = counters[h]; x != NULL; x = x->next) {
f = findfunc(x->pc); // 現在のPCに対応する関数を検索
if(f) {
f->onstack += x->count; // その関数がスタック上にあった回数を加算
f->leaf += x->count; // その関数がリーフ関数として実行された回数を加算
}
f = findfunc(x->callerpc); // 呼び出し元のPCに対応する関数を検索
if(f)
f->leaf -= x->count; // 呼び出し元関数はリーフではないので減算
}
}
// ... (Func構造体の配列構築とソート)
// print.
print("%d samples (avg %.1g threads)\\n", nsample, (double)nsamplethread/nsample);
for(i = 0; i < nfunc; i++) {
f = ff[i];
print("%6.2f%%\\t", 100.0*(double)f->leaf/nsample); // リーフカウント(自己時間)のパーセンテージ
if(stacks)
print("%6.2f%%\\t", 100.0*(double)f->onstack/nsample); // onstackカウント(累積時間)のパーセンテージ
print("%s\\n", f->s.name); // 関数名
}
}
dumphistogram
関数は、プロファイリングの核心部分です。
pc, callerpc
ペアの集計:sample
関数で収集されたpc, callerpc
ペアのカウント (counters
配列に格納) を基に、各関数 (Func
構造体) のonstack
とleaf
カウントを計算します。onstack
: その関数がスタック上に存在した(つまり、実行中または呼び出しチェーン上にあった)総サンプル数。これはpprof
の「累積時間」に相当します。leaf
: その関数が実際に命令を実行した(つまり、他の関数を呼び出さずに自身がCPU時間を使った)総サンプル数。これはpprof
の「自己時間」に相当します。x->pc
に対応する関数のleaf
を増やし、x->callerpc
に対応する関数のleaf
を減らすことで、呼び出し元ではない真のリーフ関数(CPUを消費した関数)の時間を正確に集計しています。
- ソートと出力: 計算された
Func
構造体の配列をleaf
カウントでソートし、各関数のleaf
とonstack
のパーセンテージ、および関数名を出力します。これにより、どの関数が最もCPU時間を消費しているか、またその関数が直接消費しているのか、それとも呼び出し元として多くの時間を消費しているのかを把握できるようになります。これはpprof
の基本的なレポート形式に非常に近いです。
関連リンク
- Go言語のプロファイリング: Go言語の公式ドキュメントには、
pprof
を使ったプロファイリングに関する詳細な情報があります。このコミットは、その基礎を築く初期のステップです。 - Linux
ptrace
システムコール:libmach
のLinux実装の根幹をなすシステムコールです。 /proc
ファイルシステム: Linuxにおけるプロセス情報を提供する仮想ファイルシステム。libmach
がスレッド情報を取得するために利用しています。
参考にした情報源リンク
- Go言語のソースコード (GitHub)
- Linux man pages
- Go Blog: Profiling Go Programs
- Go言語の初期開発に関する議論やメーリングリストのアーカイブ (もし公開されていれば) (このコミットの時期の具体的な議論を見つけるのは難しいかもしれませんが、一般的な情報源として)
- The Go Programming Language Specification (Go言語の基本的な概念理解のため)
- Go言語のデバッグツールに関する情報 (当時のデバッグ環境の理解のため)
- Rob Pike の Go 言語に関する初期のプレゼンテーションや論文 (Go言語の設計思想や初期のツールに関する洞察を得るため)
- Russ Cox の Go 言語に関する貢献 (コミットの作者の他の仕事から文脈を得るため)