1. はじめに

本稿は、NetBSD のvmlocking ブランチにおけるソフト割り込み処理について、実際のコードを追いながら説明します。

対象とする読者

本稿は、読者にNetBSD-current 特にport-i386 についての充分な知識があることを前提とし、vmlockingブランチ特有でない事柄については、既知のものとして扱います。

ソースコードのバージョン

本稿中に引用したソースコードは、特記なき場合、2007年8月26日頃のvmlocking ブランチのものです。また、マシン依存部のコードは、特記なき場合、port-i386 のものです。

ソフト割り込み処理の概要

vmlocking ブランチにおけるソフト割り込み処理は、割り込みスレッドのコンテキストで実行されます。
割り込みスレッドは、adaptive mutex でブロックすることが出来ます。割り込みスレッドは、割り込まれたスレッドのコンテキストを借用して実行される為、通常のカーネルスレッドと比較して小さいオーバーヘッドで実行可能です。また、割り込みスレッドは、カーネルスレッドを含む他のスレッドをプリエンプトする為、通常のカーネルスレッドと比較して低遅延で実行可能です。

2. ソフト割り込みの開始

Xsoftintr 関数

【ソースコード1】に、vmlocking ブランチのソフト割り込みのための割り込みハンドラ、Xsoftintr 関数の前半部分を引用します。

【ソースコード1】:Xsoftintr 関数(前半)

/*
 * softintr()
 *
 * Switch to the LWP assigned to handle interrupts from the given
 * source.  We borrow the VM context from the interrupted LWP.
 *
 * On entry:
 *
 *	%eax		intrsource
 *	%esi		address to return to
 */
IDTVEC(softintr)
	pushl	$_C_LABEL(softintr_ret)	/* set up struct switchframe */
	pushl	%ebx
	pushl	%esi
	pushl	%edi
	movl	$IPL_HIGH,CPUVAR(ILEVEL)
	movl	CPUVAR(CURLWP),%esi
	movl	IS_LWP(%eax),%edi	/* switch to handler LWP */
	movl	%edi,CPUVAR(CURLWP)
	movl	L_ADDR(%edi),%edx
	movl	L_ADDR(%esi),%ecx
	movl	%esp,PCB_ESP(%ecx)
	movl	%ebp,PCB_EBP(%ecx)
	movl	PCB_ESP0(%edx),%esp	/* onto new stack */
	sti
	pushl	IS_MAXLEVEL(%eax)	/* ipl to run at */
	pushl	%esi
	call	_C_LABEL(softint_dispatch)/* run handlers */
	addl	$8,%esp

Xsoftintr 関数は、trunk のXsoftnet 関数等と同様、Xdoreti 関数、Xspllower 関数から、それぞれIS_RESUME 、IS_RECURSE ベクタ経由で呼ばれます。同関数の処理内容は、以下のようになります。

13-16行目

ebx、esi、edi レジスタの値をスタック上に保存します。ここで、スタック上の各レジスタの配置がswitchframe と同様になるようにし、switchframe 内のeip はsoftintr_ret 関数となるようにします。この理由は後述します。

17行目

この行はsplhigh() と等価です。

18-20行目

curlwp を割り込みスレッドに切り換えます。

22-24行目

スタックポインタの値を割り込まれたスレッドのPCB に保存します。

25行目

スタックを割り込みスレッドのものに切り換えます。PCB_ESP ではなくPCB_ESP0 を使用していることに注意します。この時点で、割り込みスレッドへのコンテキストスイッチは終了です。カーネルスタック以外の、VM 等のコンテキストは割り込まれたスレッドのものをそのまま借用し、またスケジューラを使用することもないため、非常に軽量・低遅延にコンテキストスイッチが行えることがわかります。
また、割り込まれたスレッドの種別や状態にかかわらずプリエンプトしていることに注意します。割り込まれたスレッドが他の割り込みスレッドである場合もあります。

26行目

割り込みを許可します。

27-30行目

適切な引数をスタックにプッシュし、softint_dispatch 関数を呼び出します。この時点での割り込みレベルはIPL_HIGH であることに注意します。同関数の処理内容について、次節で説明します。また、【図1】に、この時点でのスタックフレームを示します。

softint_dispatch 関数

【ソースコード2】に、softint_dispatch 関数の前半部分を引用します。

【ソースコード2】:softint_dispatch 関数(前半)

/*
 * softint_dispatch:
 *
 *	Entry point from machine-dependent code.
 */
void
softint_dispatch(lwp_t *pinned, int s)
{
    struct timeval now;
    softint_t *si;
 	u_int timing;
 	lwp_t *l;
 
 	l = curlwp;
 	si = l->l_private;
 
 	/*
 	 * Note the interrupted LWP, and mark the current LWP as running
 	 * before proceeding.  Although this must as a rule be done with
 	 * the LWP locked, at this point no external agents will want to
 	 * modify the interrupt LWP's state.
 	 */
 	timing = (softint_timing ? LW_TIMEINTR : 0);
 	l->l_switchto = pinned;
 	l->l_stat = LSONPROC;
 	l->l_flag |= (LW_RUNNING | timing);
 
 	/*
 	 * Dispatch the interrupt.  If softints are being timed, charge
	 * for it.
 	 */
 	if (timing)
 		microtime(&l->l_stime);
 	softint_execute(si, l, s);
 	if (timing) {
 		microtime(&now);
 		updatertime(l, &now);
 		l->l_flag &= ~LW_TIMEINTR;
 	}

7行目

softint_dispatch 関数の引数は次のとおりです。

pinned

割り込まれたスレッドのlwp_t へのポインタ。割り込みスレッドの処理がブロックするか、または終了するまでこのスレッドが実行されることはありません。

s

splx 関数の引数として使用する、保存された割り込みマスク。i386の場合は、IPL_SOFTxxx 等の値となります。ソフト割り込みハンドラは、この値で表わされる割り込みマスクで実行されます。

24行目

割り込みスレッドのlwp_t のl_switchto メンバに、割り込まれたスレッドのlwp_t へのポインタ(pinned)を設定します。

34行目

softint_execute 関数を呼び出します。この関数が、実際のソフト割り込みハンドラを呼び出します。ソフト割り込みハンドラがadaptive mutex を使用する場合、softint_execute 関数の延長でブロックする可能性があります。

3. 割り込みスレッドのブロック時の処理(前編)

mi_switch 関数

割り込みスレッドがadaptive mutex でブロックすると、通常どおりmi_switch 関数が呼び出されます。【ソースコード3】に、mi_switch 関数の前半部分を引用します。

【ソースコード3】:mi_switch 関数(前半)

 /*
  * The machine independent parts of context switch.
  *
  * Returns 1 if another LWP was actually run.
  */
 int
 mi_switch(lwp_t *l)
 {
struct schedstate_percpu *spc;
struct lwp *newl;
int retval, oldspl;
struct timeval tv;
bool returning;

KASSERT(lwp_locked(l, NULL));
LOCKDEBUG_BARRIER(l->l_mutex, 1);

 #ifdef KSTACK_CHECK_MAGIC
kstack_check_magic(l);
 #endif

microtime(&tv);

/*
 * It's safe to read the per CPU schedstate unlocked here, as all we
 * are after is the run time and that's guarenteed to have been last
 * updated by this CPU.
 */
KDASSERT(l->l_cpu == curcpu());

/*
 * Process is about to yield the CPU; clear the appropriate
 * scheduling flags.
 */
spc = &l->l_cpu->ci_schedstate;
returning = false;
newl = NULL;

/*
 * If we have been asked to switch to a specific LWP, then there
 * is no need to inspect the run queues.  If a soft interrupt is
 * blocking, then return to the interrupted thread without adjusting
 * VM context or its start time: neither have been changed in order
 * to take the interrupt.
 */
if (l->l_switchto != NULL) {
	if ((l->l_flag & LW_INTR) != 0) {
		returning = true;
		softint_block.ev_count++;
		if ((l->l_flag & LW_TIMEINTR) != 0)
			updatertime(l, &tv);
	}
	newl = l->l_switchto;
	l->l_switchto = NULL;
}

if (!returning) {
	/* Count time spent in current system call */
	SYSCALL_TIME_SLEEP(l);

	/*
	 * XXXSMP If we are using h/w performance counters,
	 * save context.
	 */
 #if PERFCTRS
	if (PMC_ENABLED(l->l_proc)) {
		pmc_save_context(l->l_proc);
	}
 #endif
	updatertime(l, &tv);
}

/*
 * If on the CPU and we have gotten this far, then we must yield.
 */
mutex_spin_enter(spc->spc_mutex);
KASSERT(l->l_stat != LSRUN);
if (l->l_stat == LSONPROC) {
	KASSERT(lwp_locked(l, &spc->spc_lwplock));
	if ((l->l_flag & LW_IDLE) == 0) {
		l->l_stat = LSRUN;
		lwp_setlock(l, spc->spc_mutex);
		sched_enqueue(l, true);
	} else
		l->l_stat = LSIDL;
}

/*
 * Let sched_nextlwp() select the LWP to run the CPU next. 
 * If no LWP is runnable, switch to the idle LWP.
 */
if (newl == NULL) {
	newl = sched_nextlwp();
	if (newl != NULL) {
		sched_dequeue(newl);
		KASSERT(lwp_locked(newl, spc->spc_mutex));
		newl->l_stat = LSONPROC;
		newl->l_cpu = l->l_cpu;
		newl->l_flag |= LW_RUNNING;
		lwp_setlock(newl, &spc->spc_lwplock);
	} else {
		newl = l->l_cpu->ci_data.cpu_idlelwp;
		newl->l_stat = LSONPROC;
		newl->l_flag |= LW_RUNNING;
	}
	spc->spc_curpriority = newl->l_usrpri;
	newl->l_priority = newl->l_usrpri;
	cpu_did_resched();
	spc->spc_flags &= ~SPCF_SWITCHCLEAR;
}

/* Update the new LWP's start time while it is still locked. */
if (!returning)
	newl->l_stime = tv;

if (l != newl) {
	struct lwp *prevlwp;

	/*
	 * If the old LWP has been moved to a run queue above,
	 * drop the general purpose LWP lock: it's now locked
	 * by the scheduler lock.
	 *
	 * Otherwise, drop the scheduler lock.  We're done with
	 * the run queues for now.
	 */
	if (l->l_mutex == spc->spc_mutex) {
		mutex_spin_exit(&spc->spc_lwplock);
	} else {
		mutex_spin_exit(spc->spc_mutex);
	}

	/* Unlocked, but for statistics only. */
	uvmexp.swtch++;

	/*
	 * Save old VM context, unless a soft interrupt
	 * handler is blocking.
	 */
	if (!returning)
		pmap_deactivate(l);

	/* Switch to the new LWP.. */
	l->l_ncsw++;
	l->l_flag &= ~LW_RUNNING;
	oldspl = MUTEX_SPIN_OLDSPL(l->l_cpu);
	prevlwp = cpu_switchto(l, newl, returning);

ソフト割り込みスレッドのブロック時、mi_switch 関数は割り込みスレッドに割り込まれたスレッドに対してコンテキストスイッチを行ないます。このとき、VM のスイッチ等の処理はスキップされます。割り込みスレッドは割り込まれたスレッドのコンテキストを借用して動作しているためです。具体的には次のような処理が行われます。

46-55行目

softint_dispatch 関数】の節で説明しましたように、l_switchto メンバは割り込まれたスレッドのlwp_t を指しています。また、LW_INTR フラグは割り込みスレッドであることを示します。
従って、ここではreturning 変数はtrue、newl 変数は割り込まれたスレッドのlwp_t へのポインタとなります。
最後にl_switchto メンバをNULL に設定します。これにより、次回のブロックは通常の処理となります。

92-110行目

通常のコンテキストスイッチであればここでスケジューラを呼び出し、次に実行するLWPを選択する処理を行いますが、ここでは既にスイッチ先のスレッドが決まっている(newl != NULL)ためこの処理は行いません。

140-141行目

割り込まれたスレッドのVM コンテキストをそのまま借用しているため、pmap_deactivate を呼び出す必要はありません。

147行目

cpu_switchto 関数を呼び出し、割り込まれたスレッドへコンテキストスイッチを行います。同関数の処理内容については、次節で説明します。

4. 割り込みスレッドのブロック時の処理(後編)

cpu_switchto 関数

【ソースコード4】に、cpu_switchto 関数を引用します。

【ソースコード4】:cpu_switchto 関数

 /*
  * struct lwp *cpu_switchto(struct lwp *oldlwp, struct newlwp,
  *			    bool returning)
  *
  *	1. if (oldlwp != NULL), save its context.
  *	2. then, restore context of newlwp.
  *
  * Note that the stack frame layout is known to "struct switchframe" in
  * <machine/frame.h> and to the code in cpu_lwp_fork() which initializes
  * it for a new lwp.
  */
 ENTRY(cpu_switchto)
pushl	%ebx
pushl	%esi
pushl	%edi

movl	16(%esp),%esi		# oldlwp
movl	20(%esp),%edi		# newlwp
movl	24(%esp),%edx		# returning
testl	%esi,%esi
jz	1f

/* Save old context. */
movl	L_ADDR(%esi),%eax
movl	%esp,PCB_ESP(%eax)
movl	%ebp,PCB_EBP(%eax)

/* Switch to newlwp's stack. */
 1:	movl	L_ADDR(%edi),%ebx
movl	PCB_EBP(%ebx),%ebp
movl	PCB_ESP(%ebx),%esp

/* Set curlwp. */
movl	%edi,CPUVAR(CURLWP)

/* Skip the rest if returning to a pinned LWP. */
testl	%edx,%edx
jnz	4f

/* Switch TSS.  Reset "task busy" flag before loading. */
movl	%cr3,%eax
movl	%eax,PCB_CR3(%ebx)	# for TSS gates
movl	CPUVAR(GDT),%eax
movl	L_MD_TSS_SEL(%edi),%edx
andl	$~0x0200,4(%eax,%edx, 1)
ltr	%dx

/* Don't bother with the rest if switching to a system process. */
testl	$LW_SYSTEM,L_FLAG(%edi)
jnz	4f

/* Is this process using RAS (restartable atomic sequences)? */
movl	L_PROC(%edi),%eax
cmpl	$0,P_RASLIST(%eax)
jne	5f

/*
 * Restore cr0 (including FPU state).  Raise the IPL to IPL_IPI.
 * FPU IPIs can alter the LWP's saved cr0.  Dropping the priority
 * is deferred until mi_switch(), when cpu_switchto() returns.
 */
 2:	movl	$IPL_IPI,CPUVAR(ILEVEL)
movl	PCB_CR0(%ebx),%ecx
movl	%cr0,%edx

/*
 * If our floating point registers are on a different CPU,
 * set CR0_TS so we'll trap rather than reuse bogus state.
 */
movl	PCB_FPCPU(%ebx),%eax
cmpl	CPUVAR(SELF),%eax
je	3f
orl	$CR0_TS,%ecx

/* Reloading CR0 is very expensive - avoid if possible. */
 3:	cmpl	%edx,%ecx
je	4f
movl	%ecx,%cr0

/* Return to the new LWP, returning 'oldlwp' in %eax. */
 4:	movl	%esi,%eax
popl	%edi
popl	%esi
popl	%ebx
ret

/* Check for restartable atomic sequences (RAS). */
 5:	movl	L_MD_REGS(%edi),%ecx
pushl	TF_EIP(%ecx)
pushl	%eax
call	_C_LABEL(ras_lookup)
addl	$8,%esp
cmpl	$-1,%eax
je	2b
movl	L_MD_REGS(%edi),%ecx
movl	%eax,TF_EIP(%ecx)
jmp	2b

13-15行目

switchframe を構築します。

24-26行目

コンテキストスイッチ元のLWP(今回の場合、割り込みスレッド)のスタックポインタをPCB に保存します。

29-31行目

コンテキストスイッチ先のLWP(今回の場合、割り込みスレッドに割り込まれたスレッド)のスタックポインタをPCB から復元します。

34行目

curlwp を切り換えます。

37-38行目

cpu_switchto 関数のreturning 引数はvmlocking ブランチで新設されたものです。
returning 引数がtrue の場合、cpu_switchto 関数はカーネルスタックのスイッチとcurlwp の切り換え以外のほとんどの処理をスキップします。これはXsoftintr 関数が最低限のコンテキストスイッチ処理しか行なわないことと対応します。

81-85行目

このswitchframe はXsoftintr 関数が作成したものであることに注意してください。
このret 命令は、通常cpu_switchto 関数の呼び出し元への復帰ですが、この場合のリターンアドレスはsoftintr_ret 関数です。同関数の処理内容について、次節で説明します。

softintr_ret 関数

【ソースコード5】に、softintr_ret 関数を引用します。

【ソースコード5】:softintr_ret 関数

  /*
  * softintr_ret()
  *
  * Trampoline function that gets returned to by cpu_switchto() when
  * an interrupt handler blocks.  On entry:
  *
  *	%eax		prevlwp from cpu_switchto()
  */
 NENTRY(softintr_ret)
movl	CPUVAR(ILEVEL),%edx
pushl	L_MUTEX(%eax)		/* %eax from cpu_switchto */
movl	%edx,CPUVAR(MTX_OLDSPL)	/* preserve priority until return */
call	_C_LABEL(mutex_spin_exit)/* unlock the old LWP */
addl	$4,%esp
cli
jmp	*%esi			/* back to splx/doreti */

10-14行目

コンテキストスイッチ元のLWP をアンロックします。これは通常のコンテキストスイッチの際にmi_switch 関数が行なっていることと同様です。

15-16行目

割り込みを禁止し、Xdoreti 関数、またはXspllower 関数に復帰します。

5. 割り込みスレッドのアンブロック時の処理

割り込みスレッドのアンブロック時の処理は、通常のLWP と同様です。
通常のLWP と同様、アンブロック時にランキューに乗り、スケジューラによって選択され、実行されます。割り込みスレッドはCPU にバインドされており、他のCPU でスケジュールされることはありません。

mi_switch 関数

【ソースコード6】に、mi_switch 関数の後半部分を引用します。

【ソースコード6】:mi_switch 関数(後半)

	prevlwp = cpu_switchto(l, newl, returning);

	/*
	 * .. we have switched away and are now back so we must
	 * be the new curlwp.  prevlwp is who we replaced.
	 */
	curlwp = l;
	if (prevlwp != NULL) {
		curcpu()->ci_mtx_oldspl = oldspl;
		lwp_unlock(prevlwp);
	} else {
		splx(oldspl);
	}

	/* Restore VM context. */
	pmap_activate(l);
	retval = 1;
} else {
	/* Nothing to do - just unlock and return. */
	mutex_spin_exit(spc->spc_mutex);
	lwp_unlock(l);
	retval = 0;
}

KASSERT(l == curlwp);
KASSERT(l->l_stat == LSONPROC);
KASSERT(l->l_cpu == curcpu());

/*
 * XXXSMP If we are using h/w performance counters, restore context.
 */
 #if PERFCTRS
if (PMC_ENABLED(l->l_proc)) {
	pmc_restore_context(l->l_proc);
}
 #endif

SYSCALL_TIME_WAKEUP(l);
LOCKDEBUG_BARRIER(NULL, 1);

return retval;
 }

特に割り込みスレッド特有の処理を行なってはいないことがわかります。

16行目

割り込みスレッドであっても、通常どおりpmap_activate 関数の呼び出しを行います。

6. ソフト割り込みの終了

softint_dispatch 関数

【ソースコード7】に、softint_dispatch 関数の後半部分を引用します。

【ソースコード7】:softint_dispatch 関数(後半)

if (timing)
	microtime(&l->l_stime);
softint_execute(si, l, s);
if (timing) {
	microtime(&now);
	updatertime(l, &now);
	l->l_flag &= ~LW_TIMEINTR;
}

	/*
 * If we blocked while handling the interrupt, the pinned LWP is
 * gone so switch to the idle LWP.  It will select a new LWP to
 * run.
 *
 * We must drop the priority level as switching at IPL_HIGH could
 * deadlock the system.  We have already set si->si_active = 0,
 * which means another interrupt at this level can be triggered. 
 * That's not be a problem: we are lowering to level 's' which will
 * prevent softint_dispatch() from being reentered at level 's',
 * until the priority is finally dropped to IPL_NONE on entry to
 * the idle loop.
 */
l->l_stat = LSIDL;
if (l->l_switchto == NULL) {
	splx(s);
	lwp_exit_switchaway(l);
	/* NOTREACHED */
}
l->l_switchto = NULL;
l->l_flag &= ~LW_RUNNING;
 }

24-28行目

softint_execute 関数が呼び出すハンドラが一度でもブロックした場合の処理です。
先述の【mi_switch 関数(前半)】で見たように、割り込みスレッドが最初にブロックした際、lwp_t のl_switchto メンバはmi_switch 関数によってNULL に設定されます。この場合、softint_dispatch 関数は、単にlwp_exit_switchaway 関数を呼び出してIdle LWP にスイッチします。もしスケジュール可能なLWP があれば、Idle LWP は直ちにmi_switch 関数を呼び出し、他のLWP にスイッチします。lwp_exit_switchaway 関数は復帰しません。次にこの割り込みスレッドが実行される時は、先述の【Xsoftintr 関数(前半)】で見たように、Xsoftintr 関数によってスタックが巻き戻されます。
なお、割り込みスレッドのアンブロック時にmi_switch 関数がpmap_activate 処理を行なっているので、ここでも対応するpmap_deactivate の呼び出しが必要です。(引用したコードには呼び出しがありませんが、それはバグです。)

29-30行目

一度もブロックしなかった場合の処理です。この場合、l_switchto メンバは割り込まれたスレッドのlwp_t を指したままです。

31行目

呼び出し元であるXsoftintr 関数へ復帰します。同関数の処理内容について、次節で説明します。

Xsoftintr 関数

【ソースコード8】に、Xsoftintr 関数の後半部分を引用します。

【ソースコード8】:Xsoftintr 関数(後半)

pushl	IS_MAXLEVEL(%eax)	/* ipl to run at */
pushl	%esi
call	_C_LABEL(softint_dispatch)/* run handlers */
addl	$8,%esp
movl	L_ADDR(%esi),%ecx
movl	PCB_ESP(%ecx),%esp
movl	%esi,CPUVAR(CURLWP)
popl	%edi			/* unwind switchframe */
popl	%esi
addl	$8,%esp
cli
jmp	*%esi			/* back to splx/doreti */

8-10行目

switchframe を巻き戻し、レジスタの値を復元します。ebx レジスタは特に変更していないので、復元の必要はありません。

11-12行目

割り込みを禁止し、Xdoreti 関数、またはXspllower 関数に復帰します。

まとめ

割り込みスレッドの実行は非常に軽量・低遅延ですが、ブロックした場合、アンブロック時の処理には通常のカーネルスレッドと同程度のオーバーヘッドがあります。
割り込みスレッドは滅多にブロックしないよう排他設計すべきであり、逆に言えば、もし割り込みスレッドがあまり頻繁にブロックするようなら、それは排他設計に問題があると言えます。