Linux/x86_64の割り込み処理 その2:Cからの復帰時の処理

do_IRQ()

ここからCコード

asmlinkage unsigned int do_IRQ(struct pt_regs *regs)
{	
	unsigned irq = ~regs->orig_rax;

	if (unlikely(irq >= NR_IRQS)) {
		(panic:略)
	}

	exit_idle();
	irq_enter();
	__do_IRQ(irq, regs);
	irq_exit();
	return 1;
}

3行目は、前回の IRQ0xXX_interrupt の先頭で push $0xXX-256 としている部分と呼応しており、irq に割り込みベクタ番号を取り出します。範囲を外れていれば、panic です。

9行目では、現在実行中のタスクがアイドルタスクであれば、アイドル状態から抜けたわけですので何かをするために exit_idle() を呼んでいるわけですが、EL5 では何もしないようです。

irq_enter() では、add_preempt_count(HARDIRQ_OFFSET); という処理などを行ないます (後述)。irq_exit() では逆に preempt_count を戻し、さらに softirq (後述) があれば、実行します。

カーネルモードでの割り込みの場合

	call	do_IRQ
ret_from_intr:
	cli
	decl	%gs:pda_irqcount
	leaveq
exit_intr:
	movq	%gs:pda_kernelstack,%rcx
	subq	(THREAD_SIZE-PDA_STACKOFFSET),%rcx
	testl	$3,CS-ARGOFFSET(%rsp)
	je	retint_kernel
	(略)
retint_kernel:
	cli
restore_args:
	(レジスタ復旧)
	iretq

コードは common_interrupts の続きです。1行目が Cコードの呼出しで、そこから返ってきたところから説明を続けます。
3行目で割り込みを禁止しています。Cコードで許可している場合があるためです。4行目は、do_IRQ 前に incl した値を戻しています。leaveq で ebp と esp を戻します。

7、8行目が分かりにくいところですが、鍵は struct thread_info の後ろに、そのタスクのカーネルモードスタックがある、というところにあります。PDA の kernelstack には、タスクスイッチ時に新しいタスクの thread_info + THREAD_SIZE-PDA_STACKOFFSET に設定されています。
THREAD_SIZE は 2ページ (8KB)、PDA_STACKOFFSET は 40バイトです。

9行目は、前回の common_interrupt の15行目と同じです。前回 ARGOFFSET は事前に引かれていました。CSレジスタの下位 3bit が 0 ならばカーネルモードでの割り込みだったため、そのまま帰って行きます。

ユーザモードでの割り込みの場合

上記リストで(略)となっている部分です。

	je          retint_kernel
retint_with_reschedule:
	movl	$_TIF_WORK_MASK,%edi
retint_check:
	movl	threadinfo_flags(%rcx),%edx
	andl	%edi,%edx
	jnz	retint_careful
retint_swapgs:
	cli
	swapgs
	jmp	restore_args
retint_careful:
	bt	$TIF_NEED_RESCHED,%rdx
	jnc	retint_signal
	sti
	pushq	%rdi
	call	schedule
	popq	%rdi
	movq	%gs:pda_kernelstack,%rcx
	subq	(THREAD_SIZE-PDA_STACKOFFSET),%rcx
	cli
	jmp	retint_check
retint_signal:
	testl$(_TIF_SIGPENDING|_TIF_NOTIFY_RESUME|_TIF_SINGLESTEP),%edx
	jz	retint_swapgs
	sti
	movq	$-1,ORIG_RAX(%rsp)
	xorl	%esi,%esi
	movq	%rsp,%rdi
	call	do_notify_resume	(regs,0)
	cli	
	movl	$_TIF_NEED_RESCHED,%rdi
	movq	%gs:pda_kernelstack,%rcx
	subq	(THREAD_SIZE-PDA_STACKOFFSET),%rcx
	jmp	retint_check

3行目 _TIF_WORK_MASK は、struct thread_info のメンバ flags のビットマスクで、TIF_SYSCALL_TRACE|TIF_SYSCALL_AUDIT|TIF_SINGLESTEP|TIF_SECCOMP です。
これら特殊な状態であるかどうかを6行目で判定し、特殊な状態であれば retint_careful にジャンプして文字通り注意して割り込みから帰る処理をします。

通常の状態であれば、GS レジスタをユーザーモードの値に戻し、他のレジスタも戻して戻ります (restore_args は「カーネルモードでの割り込み」で引用したリストにあります)。

retint_careful では、まず flags の TIF_NEED_RESCHED ビットを見ます。このビットは、タスクスケジューラを呼ぶべきときにセットされますので、1であれば割り込みを許可した上でスケジューラを呼んでいます。schedule() は引数を取りません。ここで他のタスクにコンテキストスイッチがなされる可能性があります。戻ってきたら、_TIF_WORK_MASK のチェックからやり直しです。

タスクスケジューラを呼ぶ必要がなければ、次はシグナルの確認です。これも thread_info の flags に示されているはずです。なお _TIF_NOTIFY_RESUME は EL5 では使われていないと思います。いずれにしても、do_notify_resume() という C のコードを呼んでいます。
第1引数 (%rdi) で、スタックに保存されたレジスタ値へのポインタを、第3引数 (%edx) で thread_info の flags を、それぞれ渡しています。

void do_notify_resume(struct pt_regs *regs, void *unused, __u32 thread_info_flags);

シグナルが配信されていれば、regs の中身を書き換えて、シグナルハンドラを呼び出すようにします。
do_notify_resume() から戻ってきたら、TIF_NEED_RESCHED のチェックからやり直しです。

preempt_count

  • Per-threadの変数 (struct thread_infoのメンバ)
  • CONFIG_PREEMPT時、0か否かでpreempt可能かどうかを決めている
  • 割り込み処理中はpreemptできないため、割り込み中か否かのカウンタを兼ねている
  • PDAのirqcountとはup/downのタイミングも異なる

その他トピック

多重割り込み

  • 割り込みゲートを用いているため、割り込み直後には割り込み禁止。
    – ハンドラが呼ばれる直前に割り込みが許可される。 ⇒ 多重割り込みがあり得る。 – ハンドラにIRQF_DISABLEDというフラグが立てられる。
  • irq_desc[]の中にIRQ_INPROGRESSのフラグがある。これがたっているとハンドラは呼ばれない。
    – 同一ベクタの割り込みハンドラは多重には呼ばれない。

softirq

  • ハードウェアの割り込みの延長の処理。割り込みより優先は低い。
  • 割り込み処理の後 (irq_exitの中で) 呼ばれる。
  • ただし、softirqハンドラを呼んでるうちにもどんどんsoftirqが発生するなどによりsoftirqの実行が遅れている場合、CPUごとのカーネルスレッドksoftirqdが実行。
    – カーネルスレッドで実行されるとはいえ、PDAのirqcountは上げられ、スタックも割り込みスタックが用いられる。

softirqの種類

以下のsoftirqがあります。汎用的には、taskletを用います。

enum
{
	HI_SOFTIRQ=0,
	TIMER_SOFTIRQ,
	NET_TX_SOFTIRQ,
	NET_RX_SOFTIRQ,
	BLOCK_SOFTIRQ,
	BLOCK_IOPOLL_SOFTIRQ,
	TASKLET_SOFTIRQ
};