Linux学习|Gentoo/Arch/FreeBSDLinux

Linux中断一网打尽(2) - IDT及中断处理的实现

2020-02-11  本文已影响0人  扫帚的影子

Linux中断一网打尽(1) - IDT及中断处理的实现

通过阅读本文您可以了解到:

如何设置IDT
IDT 中断描述符表定义

中断描述符表简单来说说是定义了发生中断/异常时,CPU按这张表中定义的行为来处理对应的中断/异常。

#define IDT_ENTRIES         256
gate_desc idt_table[IDT_ENTRIES] __page_aligned_bss;

从上面我们可以知道,其包含了256项,它是一个gate_desc的数据,其下标0-256就表示中断向量,gate_desc我们在下面马上介绍。

中断描述符项定义
IDT 中断描述符表本身的存储

IDT 中断描述符表的物理地址存储在IDTR寄存器中,这个寄存器存储了IDT的基地址和长度。查询时,从 IDTR 拿到 base address ,加上向量号 * IDT entry size,即可以定位到对应的表项(gate)。

idt1.jpg
设置IDT
传统系统调用的实现

这里所说的传统系统调用主要指旧的32位系统使用 int 0x80软件中断来进入内核态,实现的系统调用。因为这种传统系统调用方式需要进入内核后作权限验证,还要切换内核栈后作大量压栈方式,调用结束后清理栈作恢复,两个字太慢,后来CPU从硬件上支持快速系统调用sysenter/sysexit, 再后来又发展到syscall/sysret, 这两种都不需要通过中断方式进入内核态,而是直接转换到内核态,速度快了很多。

传统系统调用相关 IDT 的设置

​ 上面的SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32)就是设置系统调用的异常中断处理程序,其中 #define IA32_SYSCALL_VECTOR 0x80

再看一下SYSG的定义:

#define SYSG(_vector, _addr)                \
    G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)

它初始化一个中断门,权限是DPL3, 因此从用户态是允许发起系统调用的。

static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
  {
    struct thread_info *ti = current_thread_info();
    unsigned int nr = (unsigned int)regs->orig_ax;
  
  #ifdef CONFIG_IA32_EMULATION
    ti->status |= TS_COMPAT;
  #endif
  
    if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY) {
        nr = syscall_trace_enter(regs);
    }
  
    if (likely(nr < IA32_NR_syscalls)) {
        nr = array_index_nospec(nr, IA32_NR_syscalls);
  #ifdef CONFIG_IA32_EMULATION
        regs->ax = ia32_sys_call_table[nr](regs);
  #else
        regs->ax = ia32_sys_call_table[nr](
            (unsigned int)regs->bx, (unsigned int)regs->cx,
            (unsigned int)regs->dx, (unsigned int)regs->si,
            (unsigned int)regs->di, (unsigned int)regs->bp);
  #endif /* CONFIG_IA32_EMULATION */
    }
  
    syscall_return_slowpath(regs);
  }

通过中断向量号nria32_sys_call_table中断向量表中索引到具体的中断处理函数然后调用之,其结果最终合存入%eax寄存器。

一图以蔽之
idt3.jpg
硬件中断的实现
硬件中断的IDT初始化和调用流程

这里我们不讲解具体的代码细节,只关注流程 。

硬件中断相关IDT的初始化也是在Linux启动时完成,在start_kernel中通过调用init_IRQ完成,我们来看一下:

void __init init_IRQ(void)
{
    int i;
    for (i = 0; i < nr_legacy_irqs(); i++)
        per_cpu(vector_irq, 0)[ISA_IRQ_VECTOR(i)] = irq_to_desc(i);

    BUG_ON(irq_init_percpu_irqstack(smp_processor_id()));

    x86_init.irqs.intr_init(); // 即调用  native_init_IRQ
}

void __init native_init_IRQ(void)
{
    /* Execute any quirks before the call gates are initialised: */
    x86_init.irqs.pre_vector_init();

    idt_setup_apic_and_irq_gates();
    lapic_assign_system_vectors();

    if (!acpi_ioapic && !of_ioapic && nr_legacy_irqs())
        setup_irq(2, &irq2);
}

重点在于idt_setup_apic_and_irq_gates:

 */
void __init idt_setup_apic_and_irq_gates(void)
{
    int i = FIRST_EXTERNAL_VECTOR;
    void *entry;

    idt_setup_from_table(idt_table, apic_idts, ARRAY_SIZE(apic_idts), true);

    for_each_clear_bit_from(i, system_vectors, FIRST_SYSTEM_VECTOR) {
        entry = irq_entries_start + 8 * (i - FIRST_EXTERNAL_VECTOR);
        set_intr_gate(i, entry);
    }
}

其中的set_intr_gate用来初始化硬件相关的调用门,其对应的中断门处理函数在irq_entries_start中定义,它位于arch/x86/entry/entry_64.S中:

    .align 8
ENTRY(irq_entries_start)
    vector=FIRST_EXTERNAL_VECTOR
    .rept (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)
    UNWIND_HINT_IRET_REGS
    pushq   $(~vector+0x80)         /* Note: always in signed byte range */
    jmp common_interrupt
    .align  8
    vector=vector+1
    .endr
END(irq_entries_start)

这段汇编实现对不大熟悉汇编的同学可能看起来有点晕,其实很简单它相当于填充一个中断处理函数的数组,填充多少次呢? (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)这就是次数,数组的每一项都是一个函数:

    UNWIND_HINT_IRET_REGS
    pushq   $(~vector+0x80)         /* Note: always in signed byte range */
    jmp common_interrupt

即先将中断号压栈,然后跳转到common_interrupt执行,可以看到这个common_interrupt是硬件中断的通用处理函数,它里面最主要的就是调用do_IRQ:

__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
    struct pt_regs *old_regs = set_irq_regs(regs);
    struct irq_desc * desc;
    /* high bit used in ret_from_ code  */
    unsigned vector = ~regs->orig_ax;

    entering_irq();

    /* entering_irq() tells RCU that we're not quiescent.  Check it. */
    RCU_LOCKDEP_WARN(!rcu_is_watching(), "IRQ failed to wake up RCU");

    desc = __this_cpu_read(vector_irq[vector]);
    if (likely(!IS_ERR_OR_NULL(desc))) {
        if (IS_ENABLED(CONFIG_X86_32))
            handle_irq(desc, regs);
        else
            generic_handle_irq_desc(desc);
    } else {
        ack_APIC_irq();

        if (desc == VECTOR_UNUSED) {
            pr_emerg_ratelimited("%s: %d.%d No irq handler for vector\n",
                         __func__, smp_processor_id(),
                         vector);
        } else {
            __this_cpu_write(vector_irq[vector], VECTOR_UNUSED);
        }
    }

    exiting_irq();

    set_irq_regs(old_regs);
    return 1;
}

首先根据中断向量号获取到对应的中断描述符irq_desc, 然后调用generic_handle_irq来处理:

static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
    desc->handle_irq(desc);
}

这里最终会调用到中断描述符的handle_irq,因此另一个重点就是这个中断描述符的设置了,它可以单开一篇文章来讲,我们暂不详述了。

上一篇下一篇

猜你喜欢

热点阅读