FreeBSD-current/mipsのSMP実装#初期化編

AsiaBSDConのニュース記事などを読んでいて気がついたが、どうもFreeBSD-current/mipsにはSMP対応コードが既に入っているらしい。
が、MP対応なチップ向けのコードが入っておらず、しかもgenericなコードの中にしばしば#if defined(TARGET_OCTEON)という文字が現れたりする。
mlでOCTEONはきちんと動いてるけどCaviumのライセンスに問題があってコードを公開出来ない、などと言っている人が居たが、もしかしたらその公開できないFreeBSD-current/OCTEONはSMPで動いているのかもしれない。

まずはentrypointから。

	/*
	 * The following needs to be done differently for each platform and
	 * there needs to be a good way to plug this in.
	 */
#if defined(SMP) && defined(CPU_XLR)
/*
 * Block all the slave CPUs
 */
	/*
	 * Read the cpu id from the cp0 config register
	 * cpuid[9:4], thrid[3: 0]
	 */
	mfc0	a0, COP_0_CONFIG, 7
	srl	a1, a0, 4
	andi	a1, a1, 0x3f
	andi	a0, a0, 0xf

	/* calculate linear cpuid */
	sll     t0, a1, 2
	addu    a2, t0, a0
/* Initially, disable all hardware threads on each core except thread0 */
	li	t1, VCPU_ID_0
	li	t2, XLR_THREAD_ENABLE_IND
	mtcr	t1, t2
#endif


#if defined(TARGET_OCTEON) /* Maybe this is mips32/64 generic? */
	.set push
	.set mips32r2
	rdhwr	t0, $0
	.set pop
#else
	move	t0, zero
#endif

	/* Stage the secondary cpu start until later */
	bne	t0, zero, start_secondary
	nop

#ifdef SMP
	la	t0, _C_LABEL(__pcpu)
	SET_CPU_PCPU(t0)
	/* If not master cpu, jump... */
/*XXX this assumes the above #if 0'd code runs */
	bne    a2, zero, start_secondary
	nop
#endif

色々グダグダと書いてあって一部分からない所もあるが、結局の所セカンダリCPUはstart_secondaryへ飛ばしている。

#ifdef SMP
start_secondary:
	move	a0, a1
2:
	addiu	t0, PCPU_SIZE
	subu	a1, 1
	bne	a1, zero, 2b
	nop
	SET_CPU_PCPU(t0)
smp_wait:
	lw	sp, PC_BOOT_STACK(t0)
	beqz	sp, smp_wait
	nop
	jal	_C_LABEL(smp_init_secondary)
	nop
#else
start_secondary:
	b	start_secondary
	nop
#endif

entrypointから飛んできたセカンダリCPUがPC_BOOT_STACK(t0)が0の間ビジーループしてメインCPUが上がってくるのを待ち、値が取れてからsmp_init_secondary()をコールしている。

void
smp_init_secondary(u_int32_t cpuid)
{

	if (cpuid >=  MAXCPU)
		panic ("cpu id exceeds MAXCPU\n");

	/* tlb init */
	R4K_SetWIRED(0);
	R4K_TLBFlush(num_tlbentries);
	R4K_SetWIRED(VMWIRED_ENTRIES);
	MachSetPID(0);

	Mips_SyncCache();

	mips_cp0_status_write(0);
	while (!aps_ready)
		;

	mips_sync(); mips_sync();
	/* Initialize curthread. */
	KASSERT(PCPU_GET(idlethread) != NULL, ("no idle thread"));
	PCPU_SET(curthread, PCPU_GET(idlethread));

	mtx_lock_spin(&ap_boot_mtx);

	smp_cpus++;

	CTR1(KTR_SMP, "SMP: AP CPU #%d Launched", PCPU_GET(cpuid));

	/* Build our map of 'other' CPUs. */
	PCPU_SET(other_cpus, all_cpus & ~PCPU_GET(cpumask));

	printf("SMP: AP CPU #%d Launched!\n", PCPU_GET(cpuid));

	if (smp_cpus == mp_ncpus) {
		smp_started = 1;
		smp_active = 1;
	}

	mtx_unlock_spin(&ap_boot_mtx);

	while (smp_started == 0)
		; /* nothing */
	/* Enable Interrupt */
	mips_cp0_status_write(SR_INT_ENAB);
	/* ok, now grab sched_lock and enter the scheduler */
	mtx_lock_spin(&sched_lock);

	/*
	 * Correct spinlock nesting.  The idle thread context that we are
	 * borrowing was created so that it would start out with a single
	 * spin lock (sched_lock) held in fork_trampoline().  Since we've
	 * explicitly acquired locks in this function, the nesting count
	 * is now 2 rather than 1.  Since we are nested, calling
	 * spinlock_exit() will simply adjust the counts without allowing
	 * spin lock using code to interrupt us.
	 */
	spinlock_exit();
	KASSERT(curthread->td_md.md_spinlock_count == 1, ("invalid count"));

	binuptime(PCPU_PTR(switchtime));
	PCPU_SET(switchticks, ticks);

	/* kick off the clock on this cpu */
	mips_start_timer();
	cpu_throw(NULL, choosethread());	/* doesn't return */

	panic("scheduler returned us to %s", __func__);
}

TLBを設定、cpumaskを設定し割り込みを有効化、タイマを開始してスケジューラにCPUを明け渡している。

メインCPU側からの初期化もみていこう。

#ifdef SMP
	la	t0, _C_LABEL(__pcpu)
	SET_CPU_PCPU(t0)
	/* If not master cpu, jump... */
/*XXX this assumes the above #if 0'd code runs */
	bne    a2, zero, start_secondary
	nop
#endif

	/* Call the platform-specific startup code. */
	jal	_C_LABEL(platform_start)
	sw	zero, START_FRAME - 8(sp)	# Zero out old fp for debugger

	la      sp, _C_LABEL(thread0)
	lw      a0, TD_PCB(sp)
	li	t0, ~7
	and	a0, a0, t0
	subu    sp, a0, START_FRAME

	jal	_C_LABEL(mi_startup)		# mi_startup(frame)
	sw	zero, START_FRAME - 8(sp)	# Zero out old fp for debugger

	PANIC("Startup failed!")

セカンダリCPUではなかった場合は、start_secondaryへ飛ばずにplatform_start()を呼び、それが終わったらmi_startup()を呼ぶ。

platform_start()を一通り読んでみたが、ここでは未だメインCPUのみの初期化に止まっているようだ。
mi_startup()へ進むと、SYSINIT()マクロによってカーネルに登録されている初期化関数群が指定された順序で実行されていく。
このうち、kern/subr_smp.cのcpu_mpがSMPの初期化に関係あるようだ(まぁ、名前のまんまだし)。

/*
 * Call the MD SMP initialization code.
 */
static void
mp_start(void *dummy)
{

	/* Probe for MP hardware. */
	if (smp_disabled != 0 || cpu_mp_probe() == 0) {
		mp_ncpus = 1;
		all_cpus = PCPU_GET(cpumask);
		return;
	}

	mtx_init(&smp_ipi_mtx, "smp rendezvous", NULL, MTX_SPIN);
	cpu_mp_start();
	printf("FreeBSD/SMP: Multiprocessor System Detected: %d CPUs\n",
	    mp_ncpus);
	cpu_mp_announce();
}
SYSINIT(cpu_mp, SI_SUB_CPU, SI_ORDER_THIRD, mp_start, NULL);

SYSINIT()マクロによってmi_startup()からmp_start()が実行され、cpu_mp_start()とcpu_mp_announce()が実行される。

void
cpu_mp_start(void)
{
	int i, cpuid;

	mtx_init(&ap_boot_mtx, "ap boot", NULL, MTX_SPIN);

	cpuid = 1;
	for (i = 0; i < MAXCPU; i++) {

		if (i == boot_cpu_id)
			continue;
		if (smp_start_secondary(i)) {
			all_cpus |= (1 << cpuid);
			mp_ncpus++;
		cpuid++;
		}
	}
	idle_mask |= CR_INT_IPI;
	PCPU_SET(other_cpus, all_cpus & ~PCPU_GET(cpumask));
}

cpu_mp_start()はsmp_start_secondary()をCPUの数だけ実行する。

static int
smp_start_secondary(int cpuid)
{
	struct pcpu *pcpu;
	int i;

	if (bootverbose)
		printf("smp_start_secondary: starting cpu %d\n", cpuid);

	pcpu_init(&__pcpu[cpuid], cpuid, sizeof(struct pcpu));

	if (bootverbose)
		printf("smp_start_secondary: cpu %d started\n", cpuid);

	return 1;
}

smp_start_secondary()はpcpu_init()を実行する。

/*
 * Initialize the MI portions of a struct pcpu.
 */
void
pcpu_init(struct pcpu *pcpu, int cpuid, size_t size)
{

	bzero(pcpu, size);
	KASSERT(cpuid >= 0 && cpuid < MAXCPU,
	    ("pcpu_init: invalid cpuid %d", cpuid));
	pcpu->pc_cpuid = cpuid;
	pcpu->pc_cpumask = 1 << cpuid;
	cpuid_to_pcpu[cpuid] = pcpu;
	SLIST_INSERT_HEAD(&cpuhead, pcpu, pc_allcpu);
	cpu_pcpu_init(pcpu, cpuid, size);
	pcpu->pc_rm_queue.rmq_next = &pcpu->pc_rm_queue;
	pcpu->pc_rm_queue.rmq_prev = &pcpu->pc_rm_queue;

}

pcpu_init()によりstruct pcpuが初期化され、cpu_pcpu_init()が呼び出される。

/*
 * Initialise a struct pcpu.
 */
void
cpu_pcpu_init(struct pcpu *pcpu, int cpuid, size_t size)
{
#ifdef SMP
	if (cpuid != 0)
		pcpu->pc_boot_stack = (void *)(pcpu_boot_stack + cpuid *
		    (KSTACK_PAGES * PAGE_SIZE));
#endif
	pcpu->pc_next_asid = 1;
	pcpu->pc_asid_generation = 1;
}

ここでセカンダリCPUがstart_secondaryで待っているboot_stackのアドレスが設定され、smp_init_secondary()に進むようになる。