NetBSD-current /sbin/initが起動されるまで(4)

ちょっとシステムコールへの理解が弱くてそもそもの動きをきちんと把握し切れてなかった(反省)。

ここらへんでDebianな人が似たような所を解説していたりするがLinuxでもBSDでも基本的に考え方は同じはずで、

  1. forkして新しいプロセスを作る
  2. fork先プロセスで実行したいプログラムをexecveする
  3. fork先プロセスが新しいプログラムで置き換わって実行される

というような感じになる。

forkは新しいプロセスを作るが、ユーザランドプログラムを実行する事はしない。
execveはユーザランドプログラムを現在のプロセスを上書きして実行する。
となっており、新しいプロセスでユーザランドプログラムを起動するにはfork->execveと実行する必要がある。

という事で、forkについても中を詳しく見ていこうと思う。

	p1 = l1->l_proc;

p1が現在のプロセス(親プロセスになる)。

	/*
	 * Enforce limits.
	 */
	count = chgproccnt(uid, 1);
	if (uid != 0 &&
	    __predict_false(count > p1->p_rlimit[RLIMIT_NPROC].rlim_cur)) {
		(void)chgproccnt(uid, -1);
		atomic_dec_uint(&nprocs);
		if (forkfsleep)
			kpause("forkulim", false, forkfsleep, NULL);
		return (EAGAIN);
	}

chgproccnt()でuidを変え、countで現在のプロセス数を取得、続くif文でroot以外のユーザの場合はrlimitのNPROCを超えていないかチェックし、超えていたら実行を許可しない。

	/*
	 * Allocate virtual address space for the U-area now, while it
	 * is still easy to abort the fork operation if we're out of
	 * kernel virtual address space.  The actual U-area pages will
	 * be allocated and wired in uvm_fork() if needed.
	 */

	inmem = uvm_uarea_alloc(&uaddr);
	if (__predict_false(uaddr == 0)) {
		(void)chgproccnt(uid, -1);
		atomic_dec_uint(&nprocs);
		return (ENOMEM);
	}

ユーザランドのバーチャルアドレスすペースを割り当て?U-areaという言葉遣いが若干気になるのだが、恐らくはそういう事だと思う。

	/* Allocate new proc. */
	p2 = proc_alloc();

proc構造体をアロケート。
p2が子プロセスになる。
そこからしばらくはproc構造体の初期化関連のコード。

	/*
	 * Create signal actions for the child process.
	 */
	p2->p_sigacts = sigactsinit(p1, flags & FORK_SHARESIGS);
	mutex_enter(p1->p_lock);
	p2->p_sflag |=
	    (p1->p_sflag & (PS_STOPFORK | PS_STOPEXEC | PS_NOCLDSTOP));
	sched_proc_fork(p1, p2);
	mutex_exit(p1->p_lock);

シグナルの初期化かな?

ちなみにkern/sched_4bsd.cのsched_proc_fork()はこんな感じ:

void
sched_proc_fork(struct proc *parent, struct proc *child)
{
	lwp_t *pl;

	KASSERT(mutex_owned(parent->p_lock));

	pl = LIST_FIRST(&parent->p_lwps);
	child->p_estcpu_inherited = pl->l_estcpu;
	child->p_forktime = sched_pstats_ticks;
}

p1のestcpuをp2にコピーしてforktimeを設定しただけ。

	/*
	 * p_stats.
	 * Copy parts of p_stats, and zero out the rest.
	 */
	p2->p_stats = pstatscopy(p1->p_stats);

statsをコピー。

	/*
	 * If emulation has process fork hook, call it now.
	 */
	if (p2->p_emul->e_proc_fork)
		(*p2->p_emul->e_proc_fork)(p2, p1, flags);

emulation hookを実行。
こうする事でうまくエミュレーションレイヤとプログラムの実行機構を分割している模様。
netbsd32ではここにhookは登録していない。

	/*
	 * ...and finally, any other random fork hooks that subsystems
	 * might have registered.
	 */
	doforkhooks(p2, p1);

doforkhooks()はkern/kern_subr.cにあり、関数リストを一通り実行するだけの単純な仕組みになっている:

void
doforkhooks(struct proc *p2, struct proc *p1)
{
	struct hook_desc *hd;

	LIST_FOREACH(hd, &forkhook_list, hk_list) {
		((void (*)(struct proc *, struct proc *))*hd->hk_fn)
		    (p2, p1);
	}
}

hook関数はforkhook_establish()で登録できるようだ:

static hook_list_t forkhook_list;

void *
forkhook_establish(void (*fn)(struct proc *, struct proc *))
{
	return hook_establish(&forkhook_list, (void (*)(void *))fn, NULL);
}

grepで検索してみたが、kern/uipc_sem.cがksem_forkhookを登録しているのみのようだ。

	/*
	 * This begins the section where we must prevent the parent
	 * from being swapped.
	 */
	uvm_lwp_hold(l1);
	uvm_proc_fork(p1, p2, (flags & FORK_SHAREVM) ? true : false);

ここは一つづつ中の処理をみていくことにする。

void
uvm_lwp_hold(struct lwp *l)
{

	if (l == curlwp) {
		atomic_inc_uint(&l->l_holdcnt);
	} else {
		mutex_enter(&l->l_swaplock);
		if (atomic_inc_uint_nv(&l->l_holdcnt) == 1 &&
		    (l->l_flag & LW_INMEM) == 0)
			uvm_swapin(l);
		mutex_exit(&l->l_swaplock);
	}
}

uvm_lwp_holdは指定されたlwpがswapoutされていたらswapinする関数のようだ。

/*
 * uvm_proc_fork: fork a virtual address space
 *
 * - the address space is copied as per parent map's inherit values
 */
void
uvm_proc_fork(struct proc *p1, struct proc *p2, bool shared)
{

	if (shared == true) {
		p2->p_vmspace = NULL;
		uvmspace_share(p1, p2);
	} else {
		p2->p_vmspace = uvmspace_fork(p1->p_vmspace);
	}

	cpu_proc_fork(p1, p2);
}

こっちはprocのfork処理そのもののようだ。
sharedフラグが足っていたらuvmspace_share()でp1のメモリ空間をp2にも適用し、立ってなかったらuvmspace_fork()でp2に新しい空間を割り当ててメモリをコピーする、かな。

最後のcpu_proc_fork()はアーキテクチャ依存なfork処理。MIPSアーキテクチャではarch/mips/include/cpu.hで
#define cpu_proc_fork(p1, p2)
とかかれており、ようは何もしていない。

	/*
	 * Finish creating the child process.
	 * It will return through a different path later.
	 */
	lwp_create(l1, p2, uaddr, inmem, (flags & FORK_PPWAIT) ? LWP_VFORK : 0,
	    stack, stacksize, (func != NULL) ? func : child_return, arg, &l2,
	    l1->l_class);

新しいlwpを作っている。procとlwpと両方あってややこしい。
これも中をみてみよう。

	/*
	 * First off, reap any detached LWP waiting to be collected.
	 * We can re-use its LWP structure and turnstile.
	 */
	isfree = NULL;
	if (p2->p_zomblwp != NULL) {
		mutex_enter(p2->p_lock);
		if ((isfree = p2->p_zomblwp) != NULL) {
			p2->p_zomblwp = NULL;
			lwp_free(isfree, true, false);/* releases proc mutex */
		} else
			mutex_exit(p2->p_lock);
	}
	if (isfree == NULL) {
		l2 = pool_cache_get(lwp_cache, PR_WAITOK);
		memset(l2, 0, sizeof(*l2));
		l2->l_ts = pool_cache_get(turnstile_cache, PR_WAITOK);
		SLIST_INIT(&l2->l_pi_lenders);
	} else {
		l2 = isfree;
		ts = l2->l_ts;
		KASSERT(l2->l_inheritedprio == -1);
		KASSERT(SLIST_EMPTY(&l2->l_pi_lenders));
		memset(l2, 0, sizeof(*l2));
		l2->l_ts = ts;
	}

lwp構造体を確保している。
p2->p_zomblwpがある場合は使いまわそうとしているようだ。

	l2->l_stat = LSIDL;
	l2->l_proc = p2;
	l2->l_refcnt = 1;
	l2->l_class = sclass;

初期パラメータの代入。この辺は飛ばす。

	uvm_lwp_fork(l1, l2, stack, stacksize, func,
	    (arg != NULL) ? arg : l2);

中身を見てみよう:

void
uvm_lwp_fork(struct lwp *l1, struct lwp *l2, void *stack, size_t stacksize,
    void (*func)(void *), void *arg)
{
	int error;

	/*
	 * Wire down the U-area for the process, which contains the PCB
	 * and the kernel stack.  Wired state is stored in l->l_flag's
	 * L_INMEM bit rather than in the vm_map_entry's wired count
	 * to prevent kernel_map fragmentation.  If we reused a cached U-area,
	 * L_INMEM will already be set and we don't need to do anything.
	 *
	 * Note the kernel stack gets read/write accesses right off the bat.
	 */

	if ((l2->l_flag & LW_INMEM) == 0) {
		vaddr_t uarea = USER_TO_UAREA(l2->l_addr);

		if ((error = uarea_swapin(uarea)) != 0)
			panic("%s: uvm_fault_wire failed: %d", __func__, error);
#ifdef PMAP_UAREA
		/* Tell the pmap this is a u-area mapping */
		PMAP_UAREA(uarea);
#endif
		l2->l_flag |= LW_INMEM;
	}

#ifdef KSTACK_CHECK_MAGIC
	/*
	 * fill stack with magic number
	 */
	kstack_setup_magic(l2);
#endif

	/*
	 * cpu_lwp_fork() copy and update the pcb, and make the child ready
 	 * to run.  If this is a normal user fork, the child will exit
	 * directly to user mode via child_return() on its first time
	 * slice and will not return here.  If this is a kernel thread,
	 * the specified entry point will be executed.
	 */
	cpu_lwp_fork(l1, l2, stack, stacksize, func, arg);
}

l2->l_flagにLW_INMEMがなかったらswapinしている。
このswapinというのは文字通りswapinなのか、それとも実は未だページを確保していない領域で、swapinを呼ぶ事によってページが確保されているのか?
気になる所だが、今回はそこまで調べない事にする。
次はcpu_lwp_fork()だ。 MIPSアーキテクチャの場合はarch/mips/mips/vm_machdep.cにある:

	l2->l_md.md_ss_addr = 0;
	l2->l_md.md_ss_instr = 0;
	l2->l_md.md_astpending = 0;

ここは何の変数だったかちょっと覚えが無い。

	if ((l1->l_md.md_flags & MDP_FPUSED) && l1 == fpcurlwp)
		savefpregs(l1);

l1がfpを使ってたらレジスタをセーブ。

	/*
	 * Copy pcb from proc p1 to p2.
	 * Copy p1 trapframe atop on p2 stack space, so return to user mode
	 * will be to right address, with correct registers.
	 */
	memcpy(&l2->l_addr->u_pcb, &l1->l_addr->u_pcb, sizeof(struct pcb));
	f = (struct frame *)((char *)l2->l_addr + USPACE) - 1;
	memcpy(f, l1->l_md.md_regs, sizeof(struct frame));

コメントの通り、pcbをコピー。

	/*
	 * If specified, give the child a different stack.
	 */
	if (stack != NULL)
		f->f_regs[_R_SP] = (uintptr_t)stack + stacksize;

スタックがl1と別ならこれを設定。

	l2->l_md.md_regs = (void *)f;
	l2->l_md.md_flags = l1->l_md.md_flags & MDP_FPUSED;
	x = (MIPS_HAS_R4K_MMU) ?
	    (MIPS3_PG_G | MIPS3_PG_RO | MIPS3_PG_WIRED) :
	    MIPS1_PG_G;
	pte = kvtopte(l2->l_addr);
	for (i = 0; i < UPAGES; i++)
		l2->l_md.md_upte[i] = pte[i].pt_entry &~ x;

フレームの設定、フラグの設定、pteエントリに設定など。

	pcb = &l2->l_addr->u_pcb;
	pcb->pcb_context[0] = (intptr_t)func;		/* S0 */
	pcb->pcb_context[1] = (intptr_t)arg;		/* S1 */
	pcb->pcb_context[MIPS_CURLWP_CARD - 16] = (intptr_t)l2;/* S? */
	pcb->pcb_context[8] = (intptr_t)f;		/* SP */
	pcb->pcb_context[10] = (intptr_t)lwp_trampoline;/* RA */
#ifdef IPL_ICU_MASK
	pcb->pcb_ppl = 0;	/* machine dependent interrupt mask */
#endif

pcb_contextにそれぞれのレジスタに必要な値を書き込んでいる。
S0に関数ポインタ、S1に引数、SPにスタックポインタ、RAにlwp_trampolineのアドレスを設定。

話をlwp_create()へ戻す。(returnしたからね)

	if ((p2->p_flag & PK_SYSTEM) == 0) {
		/* Locking is needed, since LWP is in the list of all LWPs */
		lwp_lock(l2);
		/* Inherit a processor-set */
		l2->l_psid = l1->l_psid;
		/* Inherit an affinity */
		if (l1->l_affinity) {
			kcpuset_use(l1->l_affinity);
			l2->l_affinity = l1->l_affinity;
		}
		/* Look for a CPU to start */
		l2->l_cpu = sched_takecpu(l2);
		lwp_unlock_to(l2, l2->l_cpu->ci_schedstate.spc_mutex);
	}

flagにPK_SYSTEMがたっていなかったら、使用するプロセッサをスケジューラに聞きに行っている。

	SYSCALL_TIME_LWP_INIT(l2);

	if (p2->p_emul->e_lwp_fork)
		(*p2->p_emul->e_lwp_fork)(l1, l2);

SYSCALL_TIMEの初期化とemulation hookの呼び出し。

こんどはfork1()の続き。

	if (p2->p_sflag & PS_STOPFORK) {
		p2->p_nrlwps = 0;
		p2->p_stat = SSTOP;
		p2->p_waited = 0;
		p1->p_nstopchild++;
		l2->l_stat = LSSTOP;
		l2->l_flag |= tmp;
		lwp_unlock(l2);
	} else {
		p2->p_nrlwps = 1;
		p2->p_stat = SACTIVE;
		l2->l_stat = LSRUN;
		l2->l_flag |= tmp;
		sched_enqueue(l2, false);
		lwp_unlock(l2);
	}

だいぶ飛ばしたが、ここでflagをみてSTOP状態で開始するかランキューに積むかを選んでいる。

	/*
	 * Return child pid to parent process,
	 * marking us as parent via retval[1].
	 */
	if (retval != NULL) {
		retval[0] = p2->p_pid;
		retval[1] = 0;
	}

親プロセスへの帰り値としてpidを返している。
これでfork処理は完了した。

次は、forkされたプロセスを追っていく。