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

[インデックス 1603] ファイルの概要

このコミットは、Linux上でのスレッドデバッグの改善を目的としています。特に、ptraceによってトレースされているプロセス内でcloneシステムコールが呼び出された際に、新しく生成された子プロセス(またはスレッド)が自動的にアタッチされ、停止する挙動をより適切に扱うように変更が加えられています。これにより、デバッガがマルチスレッドアプリケーションをより正確に制御できるようになります。

コミット

marginally better thread debugging on Linux.

if you clone inside a traced pid, the child
is automatically attached and stopped,
apparently.

R=r
DELTA=63  (41 added, 12 deleted, 10 changed)
OCL=24096
CL=24106

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/fb88a01cc26700a49783f0a0b09d487c450cdbb3

元コミット内容

marginally better thread debugging on Linux. if you clone inside a traced pid, the child is automatically attached and stopped, apparently.

変更の背景

Linuxにおけるプロセスデバッグは、ptraceシステムコールを介して行われます。しかし、マルチスレッドアプリケーションの場合、新しいスレッドがcloneシステムコールによって作成されると、その新しいスレッド(カーネルからは独立したプロセスとして扱われることが多い)がデバッガによって自動的にトレース対象としてアタッチされ、停止されるかどうかの挙動が常に明確ではありませんでした。

このコミット以前は、ptraceで親プロセスをトレースしている最中にcloneで子プロセス/スレッドが生成された場合、その子が自動的にデバッガにアタッチされ、停止状態になるという挙動が「どうやら」発生するものの、それをデバッガ側で適切に検知・処理できていなかった可能性があります。この曖昧さや不完全な処理は、マルチスレッドプログラムのデバッグを困難にしていました。

この変更は、この「自動アタッチ・停止」の挙動をデバッガ側で明示的に検知し、それに基づいてptraceのアタッチ処理を最適化することで、Linux上でのスレッドデバッグの信頼性と効率を向上させることを目的としています。また、特定のプロセス(スレッド)にシグナルを送る際に、プロセス全体ではなくスレッド単位で制御できるtkillシステムコールを使用することで、より粒度の細かいデバッグ制御を可能にしています。

前提知識の解説

  • ptraceシステムコール: Linuxにおけるプロセスデバッグの根幹をなすシステムコールです。これにより、あるプロセス(トレーサー)が別のプロセス(トレース対象、tracee)の実行を監視・制御できます。具体的には、トレース対象のメモリやレジスタの読み書き、シグナルの送受信、システムコールの監視などが可能です。デバッガ(GDBなど)は内部的にptraceを利用しています。
  • /procファイルシステム: Linuxカーネルが提供する仮想ファイルシステムです。実行中のプロセスに関する情報やシステムの状態をファイルとして公開しています。/proc/<pid>/statファイルは、特定のプロセスID (<pid>) を持つプロセスの状態(実行中、停止中、ゾンビなど)やその他の詳細情報を含んでいます。
  • プロセス状態(/proc/<pid>/statにおける文字):
    • R (Running): 実行中または実行可能キューにある。
    • S (Sleeping): スリープ中(イベントを待っている)。
    • D (Disk Sleep): ディスクI/O待ちで中断できないスリープ状態。
    • T (Stopped): シグナルによって停止している(例: SIGSTOP, SIGTSTP, ptraceによる停止)。
    • t (Tracing stop): ptraceによって停止している。
    • Z (Zombie): ゾンビプロセス。子プロセスが終了したが、親プロセスがwait()システムコールを呼び出してその終了ステータスを回収していない状態。
    • X (Dead): 終了したプロセス(通常は表示されない)。
  • cloneシステムコール: forkに似ていますが、より細かく親プロセスと子プロセス間で共有するリソース(メモリ空間、ファイルディスクリプタ、シグナルハンドラなど)を指定できるシステムコールです。Linuxでは、スレッドはcloneを使って、親プロセスとメモリ空間を共有する形で実装されることが多いです。
  • kill vs tkillシステムコール:
    • kill(pid, sig): 指定されたプロセスID (pid) またはプロセスグループID (-pid) にシグナル (sig) を送信します。通常、プロセスグループ全体に影響を与えます。
    • tkill(tid, sig): 特定のスレッドID (tid) にシグナル (sig) を送信します。これは、プロセス内の特定のスレッドのみを対象としたい場合に非常に有用です。__NR_tkilltkillシステムコールの番号です。

技術的詳細

このコミットの主要な技術的改善点は以下の通りです。

  1. procstate関数の導入とisstoppedの置き換え:
    • 以前のisstopped関数は、/proc/<pid>/statを読み取り、プロセスの状態が'T'(停止)であるかどうかのみを真偽値で返していました。
    • 新しく導入されたprocstate関数は、プロセスの状態文字そのものを返し、さらにトレーサーのPID(tpid)も取得できるようになりました。これにより、プロセスの詳細な状態をより正確に把握し、デバッグロジックに活用できるようになります。特に、tpidを取得できるようになったことで、そのプロセスがどのトレーサーによってトレースされているかを識別できるようになりました。
  2. attached関数の導入:
    • attached(pid)関数は、procstateを利用して、指定されたpidのプロセスが'T'状態であり、かつそのトレーサーPIDがpid自身である(つまり、自身がトレーサーとしてアタッチされ、停止している)場合に真を返します。これは、cloneによって生成された子プロセスが自動的にアタッチ・停止される挙動を検知するために重要です。
  3. ptraceattachの最適化:
    • ptrace(PTRACE_ATTACH, ...)の呼び出しが、!attached(pid)という条件でガードされるようになりました。これは、もし子プロセスがcloneによって既に自動的にアタッチされ、停止している場合、二重にPTRACE_ATTACHを試みることを避けるためです。これにより、デバッガのアタッチ処理がより堅牢になります。
  4. tkillの使用:
    • ctlproc関数内で、プロセスを停止させるためにkill(pid, SIGSTOP)syscall(__NR_tkill, pid, SIGSTOP)に置き換えられました。これは、SIGSTOPシグナルをプロセス全体ではなく、特定のPID(この場合はスレッドID)に送信することを意味します。マルチスレッド環境では、特定のスレッドのみを停止させたい場合にtkillが不可欠であり、より正確なスレッド制御を可能にします。
  5. procstatusの拡張:
    • procstatus関数は、procstateから取得した詳細なプロセス状態文字に基づいて、「Stopped」、「Zombie」、「Running」といったより具体的なステータス文字列を返すようになりました。これにより、デバッガのユーザーインターフェースやログ出力がより分かりやすくなります。

これらの変更は、Linuxカーネルのptracecloneの挙動、そして/procファイルシステムから得られるプロセス情報をより深く理解し、それをデバッガのロジックに反映させることで、マルチスレッドアプリケーションのデバッグ体験を向上させるものです。

コアとなるコードの変更箇所

変更はすべて src/libmach_amd64/linux.c ファイル内で行われています。

主な変更点は以下の通りです。

  • isstopped 関数が削除され、procstate 関数が追加されました。
  • attached 関数が新しく追加されました。
  • waitstop 関数内で isstopped の呼び出しが procstate の呼び出しに置き換えられました。
  • ptraceattach 関数内で ptrace(PTRACE_ATTACH, ...) の呼び出しに !attached(pid) の条件が追加されました。
  • ctlproc 関数内で kill(pid, SIGSTOP)syscall(__NR_tkill, pid, SIGSTOP) に変更されました。
  • procstatus 関数内で isstopped の呼び出しが procstate の呼び出しに置き換えられ、より詳細な状態に応じた文字列を返すように拡張されました。

コアとなるコードの解説

procstate 関数

static int
procstate(int pid, int *tpid)
{
	char buf[1024];
	int fd, n;
	char *p;

	snprint(buf, sizeof buf, "/proc/%d/stat", pid);
	if((fd = open(buf, OREAD)) < 0)
		return -1;
	n = read(fd, buf, sizeof buf-1);
	close(fd);
	if(n <= 0)
		return -1;
	buf[n] = 0;

	/* command name is in parens, no parens afterward */
	p = strrchr(buf, ')');
	if(p == nil || *++p != ' ')
		return -1;
	++p;

	/* p is now state letter.  p+1 is tracer pid */
	if(tpid)
		*tpid = atoi(p+1);
	return *p;
}

この関数は、指定されたpidのプロセスの/proc/<pid>/statファイルを読み込み、そのプロセスの状態文字(例: 'R', 'S', 'T')を返します。また、オプションでトレーサーのPIDをtpidポインタを通じて返します。/proc/<pid>/statのフォーマットは固定されており、コマンド名が括弧で囲まれ、その後に状態文字、そしてトレーサーPIDなどが続きます。この関数は、そのフォーマットを解析して必要な情報を抽出します。

attached 関数

static int
attached(int pid)
{
	int tpid;

	return procstate(pid, &tpid) == 'T' && tpid == pid;
}

このヘルパー関数は、procstateを利用して、プロセスが'T'(停止)状態であり、かつそのトレーサーPIDがプロセス自身のPIDと一致する場合(つまり、自身がトレーサーとしてアタッチされ、停止している状態)に真を返します。これは、cloneによって生成された子プロセスが自動的にアタッチされたかどうかを効率的にチェックするために使用されます。

ptraceattach 関数の変更

// 変更前:
// if(ptrace(PTRACE_ATTACH, pid, 0, 0) < 0){

// 変更後:
if(!attached(pid) && ptrace(PTRACE_ATTACH, pid, 0, 0) < 0){

ptrace(PTRACE_ATTACH, ...)の呼び出しの前に!attached(pid)という条件が追加されました。これにより、もしプロセスが既にptraceによってアタッチされ、停止している状態であれば、再度アタッチを試みることを避けます。これは、cloneによって生成された子プロセスが自動的にアタッチされる挙動を考慮した最適化です。

ctlproc 関数の変更

// 変更前:
// if(kill(pid, SIGSTOP) < 0)

// 変更後:
if(syscall(__NR_tkill, pid, SIGSTOP) < 0)

プロセスを停止させるためにkillシステムコールを使用していた箇所が、syscall(__NR_tkill, pid, SIGSTOP)に置き換えられました。tkillは特定のPID(スレッドID)にシグナルを送信するため、マルチスレッド環境でより正確なスレッド制御が可能になります。

procstatus 関数の変更

// 変更前:
// if(isstopped(pid))
// 	return "Stopped";
// return "Running";

// 変更後:
int c;

c = procstate(pid, nil);
if(c < 0)
	return "Dead";
switch(c) {
case 'T':
	return "Stopped";
case 'Z':
	return "Zombie";
case 'R':
	return "Running";
// TODO: translate more characters here
}
return "Running";

isstoppedの代わりにprocstateを使用してプロセスの状態文字を取得し、その文字に基づいてより詳細なステータス文字列("Stopped", "Zombie", "Running")を返すようになりました。これにより、デバッガがプロセスの状態をより正確にユーザーに伝えることができます。

関連リンク

参考にした情報源リンク

  • 上記のLinux man page
  • Go言語のソースコード(src/libmach_amd64/linux.c
  • 一般的なLinuxシステムプログラミングとデバッグに関する知識