linuxのスレッド生成処理に関するメモ

linuxのスレッド生成処理に関するメモ。
sys_cloneから呼び出されるdo_forkあたりについて。

1. スレッドの生成

以下のプログラムを実行すると、スレッドを一つ生成する。

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>

#define THREADMAX 1

pid_t gettid(void)
{
    return syscall(SYS_gettid);
}

void *thread_function(void* arg)
{
	int *n = (int*)arg;
	pid_t pid = gettid();
	
	printf("[%d] %d\n", pid, *n);
	return arg;
}

int main(int argc, char **argv)
{
	int i;
	pthread_t thread[THREADMAX];
	int data[THREADMAX];
	pid_t pid = gettid();
	printf("[%d] %p\n", pid, &argc);

	for(i = 0; i < THREADMAX; ++i) {
		data[i] = i;
		pthread_create(&thread[i], NULL, thread_function, &data[i]);
	}

	for(i = 0; i < THREADMAX; ++i) {
		pthread_join(thread[i], NULL);
	}
	return 0;
}

straceで上記プログラムから呼び出すシステムコールを監視すると、pthread_create()が実際にはclone()を呼び出している事がわかる。

$strace ./a.out
...
clone(child_stack=0x7f0c9cf99fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f0c9cf9a9d0, tls=0x7f0c9cf9a700, child_tidptr=0x7f0c9cf9a9d0) = 9067
...

clone()はsys_clone()を呼び出して子プロセスを作成する。
この後、詳解Linuxカーネル(P125)によると、子プロセスのスタックを書き換えて、引数を積み、
子プロセスの戻り値アドレスをpthread_createで指定した関数ポインタに変更する。

2. do_fork呼び出し

sys_clone()は以下のように実装されており(linux 3.5.0)、さらにdo_fork()を呼び出す。

long sys_clone(unsigned long clone_flags, unsigned long newsp,
	       void __user *parent_tid, void __user *child_tid)
{
	long ret;

	if (!newsp)
		newsp = UPT_SP(&current->thread.regs.regs);
	current->thread.forking = 1;
	ret = do_fork(clone_flags, newsp, &current->thread.regs, 0, parent_tid,
		      child_tid);
	current->thread.forking = 0;
	return ret;
}

以下のsys_clone.stpを用いて、上記スレッド作成プログラムでdo_fork呼び出し時の引数を確認する。

probe kernel.function("do_fork@kernel/fork.c")
{ 	
	printf("do_fork   %s\n", $$vars);
}

probe kernel.function("sys_clone*")
{
	printf("sys_clone %s\n", $$vars);
}

probe kernel.function("do_fork@kernel/fork.c").return
{ 	
	printf("<- do_fork    %d %d\n", kernel_int($parent_tidptr), kernel_int($child_tidptr));
}

probe kernel.function("sys_clone*").return
{
	printf("<- sys_clone  %d %d\n", kernel_int($parent_tid), kernel_int($child_tid));
}
$ sudo stap sys_clone.stp -c ./a.out 
[9602] 0x7fff675580ec
[9605] 0
sys_clone clone_flags=0x3d0f00 newsp=0x7ff643872fb0 parent_tid=0x7ff6438739d0 child_tid=0x7ff6438739d0 regs=0xffff88008141df58
do_fork   clone_flags=0x3d0f00 stack_start=0x7ff643872fb0 regs=0xffff88008141df58 stack_size=0x0 parent_tidptr=0x7ff6438739d0 child_tidptr=0x7ff6438739d0 p=0x3d28620000 trace=? nr=?
<- do_fork    9605 9605
<- sys_clone  9605 9605

3. do_forkの処理

do_forkの処理は以下のような処理を行う。

3.1 do_forkのインタフェースと、変数の宣言
long do_fork(unsigned long clone_flags, /* clone呼び出し時フラグ */
	      unsigned long stack_start, /* 子プロセスに割り当てるスタックの開始位置 */
	      struct pt_regs *regs,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)
{
	struct task_struct *p;
	int trace = 0;
	long nr;
3.2 do_forkのフラグ有効性確認
	/*
	 * Do some preliminary argument and permissions checking before we
	 * actually start allocating stuff
	 */
	if (clone_flags & CLONE_NEWUSER) {
		if (clone_flags & CLONE_THREAD)
			return -EINVAL;
		/* hopefully this check will go away when userns support is
		 * complete
		 */
		if (!capable(CAP_SYS_ADMIN) || !capable(CAP_SETUID) ||
				!capable(CAP_SETGID))
			return -EPERM;
	}
3.3 トレース状態の確認

ユーザーモードでかつ、非トレースでなければ、trace変数にトレースの状態を設定する。
このtrace変数は、後で呼ばれるcopy_process()で使う。

	/*
	 * Determine whether and which event to report to ptracer.  When
	 * called from kernel_thread or CLONE_UNTRACED is explicitly
	 * requested, no event is reported; otherwise, report if the event
	 * for the type of forking is enabled.
	 */

	if (likely(user_mode(regs)) && !(clone_flags & CLONE_UNTRACED)) {
		if (clone_flags & CLONE_VFORK)
			trace = PTRACE_EVENT_VFORK;
		else if ((clone_flags & CSIGNAL) != SIGCHLD)
			trace = PTRACE_EVENT_CLONE;
		else
			trace = PTRACE_EVENT_FORK;

		if (likely(!ptrace_event_enabled(current, trace)))
			trace = 0;
	}
3.4 プロセスディスクリプタをコピーする

プロセスディスクリプタのコピーを作成する。
戻り値は新しくコピーされたtask_structオブジェクトへのポインタ。

	p = copy_process(clone_flags, stack_start, regs, stack_size,
			 child_tidptr, NULL, trace);
3.5 新しいタスクを起床する

新しいPIDをユーザー空間にコピーして、新しいプロセスを起床する。

	/*
	 * Do this prior waking up the new thread - the thread pointer
	 * might get invalid after that point, if the thread exits quickly.
	 */
	if (!IS_ERR(p)) {
		struct completion vfork;

		trace_sched_process_fork(current, p);

		nr = task_pid_vnr(p);

		if (clone_flags & CLONE_PARENT_SETTID)
			put_user(nr, parent_tidptr);

		if (clone_flags & CLONE_VFORK) {
			p->vfork_done = &vfork;
			init_completion(&vfork);
			get_task_struct(p);
		}

		wake_up_new_task(p);

		/* forking complete and child started to run, tell ptracer */
		if (unlikely(trace))
			ptrace_event(trace, nr);

		if (clone_flags & CLONE_VFORK) {
			if (!wait_for_vfork_done(p, &vfork))
				ptrace_event(PTRACE_EVENT_VFORK_DONE, nr);
		}
	} else {
		nr = PTR_ERR(p);
	}
	return nr;
}