原文地址:https://zhuanlan.zhihu.com/p/360683396


前言

软中断?软件中断?仿佛真假美猴王,在源码面前才能让他们现出原型!

本文阐述的两个概念分别是:

  1. 软中断(softIRQ),即中断下半部机制。ISR运行时间不易过长,linux将中断中的一部分逻辑推后执行,这就是softIRQ,它完全由软件实现;
  2. 软件中断(Software Interrupt),从软件中断指令而来。在32位x86中,为了实现linux用户态到内核态的切换,linux使用软中断指令“int 0x80”来触发异常,切换CPU特权级,实现系统调用。

实现系统调用的“软中断”和中断下半部的“软中断”并不是一回事!二者是完全不同的实现机制,只是翻译的时候同名导致混淆。下面会结合一些官方描述和linux kernel 源码来具体分析二者的区别,让同样被迷惑的读者认清真相!

传统的系统调用实现原理

调用系统调用的传统方法是使用汇编指令"int",0x80是中断向量号。在内核初始化期间调用的函数trap_init()中,在中断描述符表(IDT)上设置系统调用门:

void __init trap_init(void)
{
	… …
	idt_setup_traps();
	… …
}
void __init idt_setup_traps(void)
{
	idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);
}

static const __initconst struct idt_data def_idts[] = {
	… …
#if defined(CONFIG_IA32_EMULATION)
	SYSG(IA32_SYSCALL_VECTOR,	entry_INT80_compat),
#elif defined(CONFIG_X86_32)
	SYSG(IA32_SYSCALL_VECTOR,	entry_INT80_32),
#endif
};
/* System interrupt gate */
#define SYSG(_vector, _addr)				\
	G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)

#define IA32_SYSCALL_VECTOR		0x80

IA32_SYSCALL_VECTOR 值为 0x80。用户空间的lib库函数会调用软件中断指令"int 0x80"触发中断,然后硬件根据向量号"0x80"在 IDT 中找到对应的表项,即中断描述符,进行特权级检查,发现 DPL = CPL = 3 ,允许调用。然后硬件将切换到进程内核栈 (tss.ss0 : tss.esp0)。软件执行函数是“entry_INT80_32”,其定义在 arch/x86/entry/entry_32.S:

SYM_FUNC_START(entry_INT80_32)
	ASM_CLAC
	pushl	%eax			/* pt_regs->orig_ax */

	SAVE_ALL pt_regs_ax=$-ENOSYS switch_stacks=1	/* save rest */

	movl	%esp, %eax
    call	do_int80_syscall_32

1)将eax寄存器值入栈保存。寄存器eax中保存系统调用号,对应sys_call_table[]的下标,用于索引kernel中的系统调用函数;

2)SAVE_ALL 将其他寄存器的值压入栈中进行保存,系统调用涉及到的参数情况如下:

* Arguments:
 * eax  system call number
 * ebx  arg1
 * ecx  arg2
 * edx  arg3
 * esi  arg4
 * edi  arg5
 * ebp  arg6

3)将当前栈指针保存到 eax ,调用 do_int80_syscall_32函数,该函数在arch/x86/entry/common.c 中定义:

/* Handles int $0x80 */
__visible noinstr void do_int80_syscall_32(struct pt_regs *regs)
{
	unsigned int nr = syscall_32_enter(regs);

	/*
	 * Subtlety here: if ptrace pokes something larger than 2^32-1 into
	 * orig_ax, the unsigned int return value truncates it.  This may
	 * or may not be necessary, but it matches the old asm behavior.
	 */
	nr = (unsigned int)syscall_enter_from_user_mode(regs, nr);
	instrumentation_begin();

	do_syscall_32_irqs_on(regs, nr);

	instrumentation_end();
	syscall_exit_to_user_mode(regs);
}
/*
 * Invoke a 32-bit syscall.  Called with IRQs on in CONTEXT_KERNEL.
 */
static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs,
	  unsigned int nr)
{
	if (likely(nr < IA32_NR_syscalls)) {
		nr = array_index_nospec(nr, IA32_NR_syscalls);
		regs->ax = ia32_sys_call_table[nr](regs);
	}
}

可见最后是根据eax传入的syscall number,在sys_call_table 中调用对应位置处的sys_xxx。

基于软件中断指令"int"实现的传统系统调用方式,属于同步中断(异常),工作在进程上下文且可被中断。

下面看下软中断(softIRQ)的原理。

软中断(softIRQ)

由于ISR运行时间不易过长,且不能睡眠,linux将中断中的一部分逻辑推后执行,于是诞生了软中断机制。实际上出现在内核代码中的术语“软中断(softirq)”常常表示可延迟函数的所有种类,即tasklet、softirq、work queue都可以统称为“软中断”,为了不产生混淆,我们使用更加广泛的统称:中断下(底)半部。

软中断(softIRQ)自内核2.3引入,是最基本、最优先的中断下半部机制。

【1】软中断数据结构

struct softirq_action
{
	void	(*action)(struct softirq_action *);
};

softirq_action用于描述一个软中断,action是软中断的处理函数。softirq不能动态分配,都是静态定义的。内核中使用“softirq_vec”表示所有支持的软中断:

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
enum
{
	HI_SOFTIRQ=0,
	TIMER_SOFTIRQ,
	NET_TX_SOFTIRQ,
	NET_RX_SOFTIRQ,
	BLOCK_SOFTIRQ,
	IRQ_POLL_SOFTIRQ,
	TASKLET_SOFTIRQ,
	SCHED_SOFTIRQ,
	HRTIMER_SOFTIRQ,
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */
	NR_SOFTIRQS
};

【2】软中断的触发时机

  • irq_exit
    在硬中断退出时,会检查local_softirq_pending和preemt_count,如果都符合条件,则执行软中断。
  • local_bh_enable
    使用此函数开启软中断时,会检查local_softirq_pending,如果都符合条件,则执行软中断。
  • raise_softirq
    主动唤起一个软中断。内核提供了软中断处理线程“ksoftirq”,在适当时机会唤醒ksoftirq后台线程进行执行,处理软中断。

【3】软中断执行

软中断执行核心API是“__do_softirq”。

软中断类似硬件中断机制,是“异步中断”,在同一个CPU上资源不可重入,工作在中断上下文,因此不能睡眠。不过可以在多个处理器上同时执行。

总结

本文通过对两个“软中断”实现机制的分析,我们论证了系统调用的"软中断" 和中断下半部的“软中断”并不是一个概念。前者被翻译成“软件触发的中断”更加合理!

使用时需要注意的是软中断工作在中断上下文,需要注意原子操作。

参考

《深入理解linux内核》

Linux内核浅析-软中断

Linux syscall过程分析(万字长文)

注:所有kernel源码均来自linux 5.12-rc3