Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 関数が追加され、libmachprocthreadpids を利用して、ターゲットプロセスのすべてのアクティブなスレッドIDを取得し、それらにアタッチします。これにより、prof ツールは複数のスレッドから同時にプロファイリングデータを収集できるようになりました。
    • samples 関数が各スレッドに対して sample 関数を呼び出すように変更され、各スレッドのレジスタ情報(特にPCとSP)を個別に取得します。
  • pprof スタイルのヒストグラム:
    • PC 構造体に callerpc フィールドが追加され、プログラムカウンタと呼び出し元プログラムカウンタのペアでプロファイリングデータを集計できるようになりました。これにより、関数間の呼び出し関係を考慮した、より詳細なプロファイル情報(コールグラフの基礎)を収集できます。
    • addtohistogram 関数が pccallerpc の両方をキーとして使用するように変更されました。
    • 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 関数の追加: libmachprocthreadpids を呼び出してスレッドリストを更新。
  • sample 関数が Map *map を引数にとるように変更され、特定のスレッドのレジスタを読み取るように。
  • addtohistogram 関数が pccallerpc の両方でインデックス付けするように変更。
  • Func 構造体と findfunc 関数の追加: シンボル情報に基づいて関数を特定し、onstackleaf カウントを管理。
  • dumphistogram 関数の大幅な改修: Func 構造体と onstack, leaf カウントを使用して pprof スタイルのプロファイルレポートを生成。
  • main 関数におけるスレッドアタッチとプロファイリングループの変更: 各スレッドに対して ctlprocsample を呼び出すように。

include/mach_amd64.h

  • procthreadpids 関数のシグネチャ変更: int procthreadpids(int pid, int *tid, int ntid);

コアとなるコードの解説

src/libmach_amd64/linux.cattachthreadwait1

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.cdumphistogram

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 関数は、プロファイリングの核心部分です。

  1. pc, callerpc ペアの集計: sample 関数で収集された pc, callerpc ペアのカウント (counters 配列に格納) を基に、各関数 (Func 構造体) の onstackleaf カウントを計算します。
    • onstack: その関数がスタック上に存在した(つまり、実行中または呼び出しチェーン上にあった)総サンプル数。これは pprof の「累積時間」に相当します。
    • leaf: その関数が実際に命令を実行した(つまり、他の関数を呼び出さずに自身がCPU時間を使った)総サンプル数。これは pprof の「自己時間」に相当します。x->pc に対応する関数の leaf を増やし、x->callerpc に対応する関数の leaf を減らすことで、呼び出し元ではない真のリーフ関数(CPUを消費した関数)の時間を正確に集計しています。
  2. ソートと出力: 計算された Func 構造体の配列を leaf カウントでソートし、各関数の leafonstack のパーセンテージ、および関数名を出力します。これにより、どの関数が最もCPU時間を消費しているか、またその関数が直接消費しているのか、それとも呼び出し元として多くの時間を消費しているのかを把握できるようになります。これは pprof の基本的なレポート形式に非常に近いです。

関連リンク

  • Go言語のプロファイリング: Go言語の公式ドキュメントには、pprof を使ったプロファイリングに関する詳細な情報があります。このコミットは、その基礎を築く初期のステップです。
  • Linux ptrace システムコール: libmach のLinux実装の根幹をなすシステムコールです。
  • /proc ファイルシステム: Linuxにおけるプロセス情報を提供する仮想ファイルシステム。libmach がスレッド情報を取得するために利用しています。

参考にした情報源リンク

[インデックス 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 関数が追加され、libmachprocthreadpids を利用して、ターゲットプロセスのすべてのアクティブなスレッドIDを取得し、それらにアタッチします。これにより、prof ツールは複数のスレッドから同時にプロファイリングデータを収集できるようになりました。
    • samples 関数が各スレッドに対して sample 関数を呼び出すように変更され、各スレッドのレジスタ情報(特にPCとSP)を個別に取得します。
  • pprof スタイルのヒストグラム:
    • PC 構造体に callerpc フィールドが追加され、プログラムカウンタと呼び出し元プログラムカウンタのペアでプロファイリングデータを集計できるようになりました。これにより、関数間の呼び出し関係を考慮した、より詳細なプロファイル情報(コールグラフの基礎)を収集できます。
    • addtohistogram 関数が pccallerpc の両方をキーとして使用するように変更されました。
    • 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 関数の追加: libmachprocthreadpids を呼び出してスレッドリストを更新。
  • sample 関数が Map *map を引数にとるように変更され、特定のスレッドのレジスタを読み取るように。
  • addtohistogram 関数が pccallerpc の両方でインデックス付けするように変更。
  • Func 構造体と findfunc 関数の追加: シンボル情報に基づいて関数を特定し、onstackleaf カウントを管理。
  • dumphistogram 関数の大幅な改修: Func 構造体と onstack, leaf カウントを使用して pprof スタイルのプロファイルレポートを生成。
  • main 関数におけるスレッドアタッチとプロファイリングループの変更: 各スレッドに対して ctlprocsample を呼び出すように。

include/mach_amd64.h

  • procthreadpids 関数のシグネチャ変更: int procthreadpids(int pid, int *tid, int ntid);

コアとなるコードの解説

src/libmach_amd64/linux.cattachthreadwait1

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.cdumphistogram

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 関数は、プロファイリングの核心部分です。

  1. pc, callerpc ペアの集計: sample 関数で収集された pc, callerpc ペアのカウント (counters 配列に格納) を基に、各関数 (Func 構造体) の onstackleaf カウントを計算します。
    • onstack: その関数がスタック上に存在した(つまり、実行中または呼び出しチェーン上にあった)総サンプル数。これは pprof の「累積時間」に相当します。
    • leaf: その関数が実際に命令を実行した(つまり、他の関数を呼び出さずに自身がCPU時間を使った)総サンプル数。これは pprof の「自己時間」に相当します。x->pc に対応する関数の leaf を増やし、x->callerpc に対応する関数の leaf を減らすことで、呼び出し元ではない真のリーフ関数(CPUを消費した関数)の時間を正確に集計しています。
  2. ソートと出力: 計算された Func 構造体の配列を leaf カウントでソートし、各関数の leafonstack のパーセンテージ、および関数名を出力します。これにより、どの関数が最もCPU時間を消費しているか、またその関数が直接消費しているのか、それとも呼び出し元として多くの時間を消費しているのかを把握できるようになります。これは pprof の基本的なレポート形式に非常に近いです。

関連リンク

  • Go言語のプロファイリング: Go言語の公式ドキュメントには、pprof を使ったプロファイリングに関する詳細な情報があります。このコミットは、その基礎を築く初期のステップです。
  • Linux ptrace システムコール: libmach のLinux実装の根幹をなすシステムコールです。
  • /proc ファイルシステム: Linuxにおけるプロセス情報を提供する仮想ファイルシステム。libmach がスレッド情報を取得するために利用しています。

参考にした情報源リンク