[インデックス 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 言語に関する貢献 (コミットの作者の他の仕事から文脈を得るため)