タイマー割込みとプリエンプション

最初に読むならスケジューラ周りかなと、まずはタイマー割込みの扱いから追っかけることにする。

普通のマルチタスクOSはプロセスプリエンプションを提供している。つまり、10ミリ秒周期とかでタイマ割込みを発生させて、プロセスを切り替える(ためにスケジューラを呼び出す)。本物のx86であればi8253とかが割込み源になるが、9vxはユーザプロセスでありハードウェアを直接叩くわけにはいかないし、その必要もないので、シグナルで代用する。割込みコンテキストがシグナルコンテキストで実行されると考えればよい。

9vx/vx32.cのsetclock()を見ると、setitimer()関数を使っていることがわかる。ITIMER_VIRTUALなので、プロセスの仮想時間で25ミリ秒*1経過すると、SIGVTALRMが発生する。

static void
setclock(int start)
{
	struct itimerval itv;

	/* Ask for clock tick to interrupt execution after ClockMillis ms. */
	memset(&itv, 0, sizeof itv);
	if(start)
		itv.it_value.tv_usec = ClockMillis*1000;
	else
		itv.it_value.tv_usec = 0;
	setitimer(ITIMER_VIRTUAL, &itv, 0);
}

これを受けるシグナルハンドラはというと、libvx32/sig.cのsighandler()関数がそうだ。

static void sighandler(int signo, siginfo_t *si, void *ucontext)
{
	if (vx32_sighandler(signo, si, ucontext)){
		// Back into the fire.
		return;
	}
	
	// vx32_sighandler thought that vx32 wasn't
	// running or otherwise didn't know what to do.
	// TODO: If there was a handler registered before us, call it?
	// If the signal is not important, ignore it.
	if (signo == SIGVTALRM || signo == SIGALRM)
		return;

vx32_sighandler()関数はコンテキストのセーブ、リストアなどが必要なOS依存コードで、Mac OS Xであればdarwin.cで定義されている。SIGVTALRMだけに着目すると、仮想的なIRQ番号であるVXIRQ_TIMERを例外要因に設定している。

int vx32_sighandler(int signo, siginfo_t *si, void *v)
{
    :
	int newtrap;
	switch(signo){
    :
	case SIGVTALRM:
		newtrap = VXTRAP_IRQ + VXIRQ_TIMER;
		break;
    :
	emu->cpu_trap = newtrap;

	r = vxemu_sighandler(emu, ctx->ss.eip);

(この間は後で書く)

9vx/vx32.cのvxproc_run()呼出しから復帰する。戻り値はVXTRAP_IRQ + VXIRQ_TIMER。

void
touser(void *initsp)
{
    :
	/*
	 * User-mode execution loop.
	 */
	for(;;){
    :
		setsigsegv(1);
		setclock(1);
		rc = vxproc_run(vp);
		setclock(0);
		setsigsegv(0);
    :
		proc2ureg(vp, &u);
		u.trap = rc;
		trap(&u);
		ureg2proc(&u, vp);
	}
}

そして、9vx/trap.cのtrap()関数が呼ばれ、最終的にスケジューラであるsched()関数が呼ばれる。

void
trap(Ureg *ureg)
{
	char buf[ERRMAX];
	int vno;
	
	vno = ureg->trap;

	switch(vno){
    :
	case VXTRAP_IRQ+VXIRQ_TIMER:
		sched();
		break;

sched()関数がリターンするまでがシグナルコンテキストで動く?siglongjmp()使うのかと思ったけど違うな。

最初に戻ってsetclock()関数がどこから呼ばれるかというと、上で既出だがtouser()関数。これはカーネル空間からユーザ空間に戻るときに呼ばれる関数である*2。で、setitimer()はPlan9ユーザプロセスを実行中だけ設定するようにしている。

また、シグナルハンドラの登録は、9vx/main.cのsiginit()関数から呼ばれる、vx32_siginit()関数(libvx32/sig.c)で行われる。

*1:ClockMillisは25。

*2:x86であれば、この関数の最後はiret命令になる。関連して、initプロセス (カーネルコンテキスト)