技術情報

技術情報

NetBSD vmlockingブランチにおける割り込みスレッドの実装

2007年9月30日
エンタープライズOS事業ユニット 山本 高志

技術文書トップへ

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

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

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 関数に復帰します。

ページトップ

まとめ

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

関連記事

Linux
2014年6月5日
カーネルにおけるタイマー事情
Linux
2013年9月2日
Linux / x86_64の割り込み処理
ネットワーク
2014年10月7日
OpenFlow の概要
お気軽にお問い合わせください

VA Linuxでは、「受託開発サービス」をご提供しております。
サービスに関する詳細は、「受託開発サービス」の説明ページをご覧ください。

お問い合わせフォームはこちら
ページトップ