分享

中断 | Rock3的Linux博客

 写意人生 2014-06-26

原文:http:///blog/category/linux-os/kernel/interrupt-kernel/

如有侵权,请告知,谢谢.       

irq_desc[]数组是linux内核中用于维护IRQ资源的管理单元,它存储了某IRQ号对应的哪些处理函数,属于哪个PIC管理、来自哪个设备、IRQ自身的属性、资源等,是内核中断子系统的一个核心数组,习惯上称其为“irq数组”(个人爱好,下标就irq号)。本篇博客着重学习irq_desc[]数组的一些操作的过程和方法,如初始化、中断处理、中断号申请、中断线程等,而对于辅助性的8259A和APIC等设备的初始化过程,不详细讨论,对于某些图片或代码,也将其省略掉了。

    本文中出现的irq_desc->和desc->均表示具体的irq数组变量,称其中的一个个体为irq_desc[]数组元素,描述个体时也直接时用字符串desc。为了区别PIC的handle和driver的handle,将前者称为中断处理函数(对应desc->handle_irq,实际上对应handle_xxx_irq()),而将后者称为中断处理操作(对应desc->action)。本文中将以irq_descp[]数组为操作对象的层称为irq层。本文使用的内核代码版本为3.10.9。

    一篇好的博客应该是尽量多的说明,配少量的核心代码,这里偷懒了,很多部分实际是代码分析的过程,也没有省略掉。本篇博客耗时48小时。

一、irq_desc结构和irq_desc[]数组

    irq_desc[]数组,在kernel/irq/irqdesc.c中声明,用于内核管理中断请求,例如中断请求来自哪个设备,使用什么函数处理,同步资源等:

1
2
3
4
5
6
7
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
    [0 ... NR_IRQS-1] = {
        .handle_irq = handle_bad_irq,
        .depth      = 1,
        .lock       = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
    }  
};

    整体上,关于irq_desc结构体,如下图所示:irq_desc

 

    struct irq_desc结构体(以前的版本结构体的名字是irq_desc_t)定义如下所示(简化过,include/linux/irqdesc.h)。大部分成员都是辅助性的,关键的成员是irq_data、handle_irqs、action、depth、lock、istat,所谓irq_desc[]数组的初始化,看其主要成员的初始化的过程,在这里做简单的说明:

  • action指针指向具体的设备驱动提供的中断处理操作,就是所为的ISR,action本身是一个单向链表结构体,由next指针指向下一个操作,因此action实际上是一个操作链,可以用于共享IRQ线的情况。
  • handle_irq是irq_desc结构中与PIC相关的中断处理函数的接口,通常称作”hard irq handler“。此函数对应了PIC中的handle_xxx_irq()系列函数(xxx代表触发方式),do_IRQ()就会调用该函数,此函数最终会执行desc->action。
  • irq_data用于描述PIC方法使用的数据,irq_data下面有两个比较重要的结构:chip和state_use_accessors,前者表示此irq_desc[]元素时用的PIC芯片类型,其中包含对该芯片的基本操作方法的指针;后者表示该chip的状态和属性,其中有些用于判断irq_desc本身应该所处的状态。
  • lock用于SMP下不同core下的同步。
  • depth表示中断嵌套深度,也即一个中断打断了几个其他中断。
  • istate表示该desc目前的状态,将在“六、istate状态”中描述。
1
2
3
4
5
6
7
8
9
10
11
12
13
struct irq_desc {
    struct irq_data     irq_data;
    irq_flow_handler_t  handle_irq;
    ...
    struct irqaction    *action;    /* IRQ action list */
    unsigned int        status_use_accessors;
    unsigned int        core_internal_state__do_not_mess_with_it;
    unsigned int        depth;      /* nested irq disables */
    raw_spinlock_t      lock;
    ...
    struct module       *owner;
    const char      *name;
} ____cacheline_internodealigned_in_smp;

   这里还是看一下irqaction结构体,action的handler是具体的中断服务程序,next指针用于指向同一个链上的后一个的irqaction,thread_fn用于描述软中断处理函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef irqreturn_t (*irq_handler_t)(int, void *);
struct irqaction {
    irq_handler_t       handler;
    void            *dev_id;
    void __percpu       *percpu_dev_id;
    struct irqaction    *next;
    irq_handler_t       thread_fn;
    struct task_struct  *thread;
    unsigned int        irq;
    unsigned int        flags;
    unsigned long       thread_flags;
    unsigned long       thread_mask;
    const char      *name;
    struct proc_dir_entry   *dir;
} ____cacheline_internodealigned_in_smp;

    这意味着所有的驱动在写中断处理函数时,必须以irqreturn_t为类型:

1
2
3
4
5
6
7
8
// intel e1000
static irqreturn_t e1000_intr(int irq, void *data);
// acpi
static irqreturn_t acpi_irq(int irq, void *dev_id)
// hd
static irqreturn_t hd_interrupt(int irq, void *dev_id)
// ac97
static irqreturn_t atmel_ac97c_interrupt(int irq, void *dev)

    在这里,很容易产生一个问题,就是驱动程序处理的数据在哪?总要有些数据要处理,是从void参数吗?那么这个数据怎么获取的?handle_irq_event_percpu()函数里有具体的action的调用方式:

1
res = action->handler(irq, action->dev_id);

    那么,void *参数来自action->dev_id,而dev_id是驱动程序注册时,调用request_irq()函数传递给内核的。而这个dev_id通常指向一个device设备,驱动程序就通过该device设备将需要的数据接收上来,并进行处理。

二、irq_desc[]的初始化——8259A

   irq_desc[]数组是内核维护中断请求资源的核心数组,它必须在合适的时机予以初始化。内核起动后,有步骤的初始化内核各个子系统,init_IRQ()函数主要负责完成内核中断子系统的主要初始化。irq_desc[]数组伴随着init_IRQ()函数的执行而完成其一部分的初始化。
   init_IRQ()函数的调用路径为main()->...->start_kernel()->init_IRQ()->native_init_IRQ()。init_IRQ()函数与irq_desc[]数组初始化或者IDT、interrupt[]数组的设置有关的函数或过程,关于init_IRQ的内部调用关系,如下图所示:

init_IRQ

    下面是具体的代码分析过程:

    从init_IRQ()函数开始分析,init_IRQ在arch/x86/kernel/irqinit.c中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void __init init_IRQ(void)
{
    int i;
    /* 
     * We probably need a better place for this, but it works for
     * now ...
     */
    x86_add_irq_domains();
    /* 
     * On cpu 0, Assign IRQ0_VECTOR..IRQ15_VECTOR's to IRQ 0..15.
     * If these IRQ's are handled by legacy interrupt-controllers like PIC,
     * then this configuration will likely be static after the boot. If
     * these IRQ's are handled by more mordern controllers like IO-APIC,
     * then this vector space can be freed and re-used dynamically as the
     * irq's migrate etc.
     */
    for (i = 0; i < legacy_pic->nr_legacy_irqs; i++)
        per_cpu(vector_irq, 0)[IRQ0_VECTOR + i] = i;
    x86_init.irqs.intr_init();
}

    x86_add_irq_domains()直接略过。这里的注释还时很有用的,这里说开始时使用8259A注册这些中断向量号,如果系统使用IO APIC,将覆盖这些中断向量号,并且能够动态的重新使用。vector_irq为在arch/x86/include/asm/hw_irq.h中定义的per_cpu整形数组,长度为256,用于描述每个CPU的中断向量号,即vector_irq[](vector_irq[]元素初始化时被赋值为-1)中存储着系统可以使用的中断向量号。这里需要注意,vector_irq[]数组时PER_CPU的。

    legacy_pic字面意思为“遗留的PIC”,就是指8259A,legacy_pic定义在arch/x86/kernel/i8259.c,其中NR_IRQS_LEGACY为16:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct legacy_pic default_legacy_pic = {
    .nr_legacy_irqs = NR_IRQS_LEGACY,
    .chip  = &i8259A_chip,
    .mask = mask_8259A_irq,
    .unmask = unmask_8259A_irq,
    .mask_all = mask_8259A,
    .restore_mask = unmask_8259A,
    .init = init_8259A,
    .irq_pending = i8259A_irq_pending,
    .make_irq = make_8259A_irq,
};
struct legacy_pic *legacy_pic = &default_legacy_pic;

    a) native_inti_IRQ()

    init_IRQ()将vector_irq[]逐个赋值(就赋值了16个,从0x30到0x39)。x86_init为x86架构初始化时的一个全局变量,记录了各个子系统(irq,paging,timer,iommu,pci等)初始化使用的具体函数。而实际的x86_init.irqs.intr_init指针指向native_init_IRQ()函数(arch/x86/kernel/irqinit.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void __init native_init_IRQ(void)
{
    int i;
    /* Execute any quirks before the call gates are initialised: */
    x86_init.irqs.pre_vector_init();
    apic_intr_init();
    /* 
     * Cover the whole vector space, no vector can escape
     * us. (some of these will be overridden and become
     * 'special' SMP interrupts)
     */
    i = FIRST_EXTERNAL_VECTOR;
    for_each_clear_bit_from(i, used_vectors, NR_VECTORS) {
        /* IA32_SYSCALL_VECTOR could be used in trap_init already. */
        set_intr_gate(i, interrupt[i - FIRST_EXTERNAL_VECTOR]);
    }  
    if (!acpi_ioapic && !of_ioapic)
        setup_irq(2, &irq2);
#ifdef CONFIG_X86_32
    irq_ctx_init(smp_processor_id());
#endif
}
   x86_init.irqs.pre_vector_init指针指向init_ISA_irqs()函数,主要完成8259A/Local APIC的初始化,apic_intr_init()函数主要完成apic相关的中断的初始化。接着,native_init_IRQ()函数将调用set_intr_gate()函数设置中断门,将interrupt[]数组设置的地址设置到相应的中断门。注意,这里只是对没有used_vectors进行set_intr_gate()的赋值,并不是从FIRST_EXTERNAL_VECTOR到NR_VECTORS全部赋值,因为有些特殊情况会预留(关于used_vectors和vector_irq的关系,详见“七、中断向量、锁和CPU”)。余下的两个接口处理了一些特殊情况,这里不展开了。

    实际上init_IRQ()主要调用了native_init_IRQ(),除了使用set_intr_gate()来初始化Interrupt describptor外,后者主要干了两件事:init_ISA_irqs()和apic_intr_init()。先从简单的看起,apic_intr_init()函数实际上是一系列的set_intr_gate,但不通过interrupt[]数组,也不通过irq_desc[](这就是native_init_IRQ()函数中所为的“特殊情况”,属于used_vectors的范围):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void __init apic_intr_init(void)
{
    smp_intr_init();
#ifdef CONFIG_X86_THERMAL_VECTOR
    alloc_intr_gate(THERMAL_APIC_VECTOR, thermal_interrupt);
#endif
    ...
#ifdef CONFIG_HAVE_KVM
    /* IPI for KVM to deliver posted interrupt */
    alloc_intr_gate(POSTED_INTR_VECTOR, kvm_posted_intr_ipi);
#endif
    ...
}

    而smp_intr_init()函数如下执行apic_intr_intr()函数类似的操作,也通过set_intr_gate()函数设置了一些中断门。

    这些中断门没有通过interrupt数组,也没有irq_desc数组,而是直接使用set_intr_gate()接口将其IDT中的中断门描述符初始化。而这些中断在/proc/interrupt中显示比较特殊,并不以中断向量号的形式显示,而是以名字的形式,比如NMI,本身也不连接任何的PIC(截取一部分):

1
2
3
4
5
6
7
8
9
10
[rock3@e4310 linux-stable]$ cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3      
...
 44:         66         80         77         72   PCI-MSI-edge      snd_hda_intel
 45:   14948296          0          0          0   PCI-MSI-edge      iwlwifi
NMI:       1539      19912      17314      17232   Non-maskable interrupts
LOC:   45133746   42836772   33584448   33666542   Local timer interrupts
SPU:          0          0          0          0   Spurious interrupts
PMI:       1539      19912      17314      17232   Performance monitoring interrupts
IWI:     641572     409182     330064     302186   IRQ work interrupts

    然后看比较复杂的init_ISA_irqs()函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void __init init_ISA_irqs(void)
{
    struct irq_chip *chip = legacy_pic->chip;
    const char *name = chip->name;
    int i;
#if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)
    init_bsp_APIC();
#endif
    legacy_pic->init(0);
    for (i = 0; i < legacy_pic->nr_legacy_irqs; i++)
        irq_set_chip_and_handler_name(i, chip, handle_level_irq, name);
}

    legacy_pic->init指针指向init_8259A()函数,因此init_ISA_irqs执行了init_8259A(0)。irq_set_chip_and_handler_name()函数用于设置irq_desc[]数组的handle_irq、name、chip等成员。因此init_ISA_irqs()函数做了三件事:init_bsp_APIC()、init_8259A()、irq_set_chip_and_handler_name()。此时legacy_pic->nr_legacy_irqs为16。

    init_bsp_APIC()为对Local APIC的某种初始化操作,与irq_desc[]数组初始化无关,不讨论了。

   init_8259A(0)为对8259A的某种初始化操作,与Irq_desc[]数组的初始化无关,不讨论了。

  irq_set_chip_and_handler_name()函数如下(kernel/irq/chip.c):

1
2
3
4
5
6
7
void
irq_set_chip_and_handler_name(unsigned int irq, struct irq_chip *chip,
                  irq_flow_handler_t handle, const char *name)
{
    irq_set_chip(irq, chip);
    __irq_set_handler(irq, handle, 0, name);
}

    irq_set_chip()将irq_descp[]数组的*action的chip成员,主要是__irq_set_handler()函数(kernel/irq/chip.c),看下__irq_set_handler()函数都设置了irq_desc[]数组的什么成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void
__irq_set_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained,
          const char *name)
{
    unsigned long flags;
    struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, 0);
    if (!desc)
        return;
    if (!handle) {
        handle = handle_bad_irq;
    } else {
        if (WARN_ON(desc->irq_data.chip == &no_irq_chip))
            goto out;
    }
    /* Uninstall? */
    if (handle == handle_bad_irq) {
        if (desc->irq_data.chip != &no_irq_chip)
            mask_ack_irq(desc);
        irq_state_set_disabled(desc);
        desc->depth = 1;
    }
    desc->handle_irq = handle;
    desc->name = name;
    if (handle != handle_bad_irq && is_chained) {
        irq_settings_set_noprobe(desc);
        irq_settings_set_norequest(desc);
        irq_settings_set_nothread(desc);
        irq_startup(desc, true);
    }
out:
    irq_put_desc_busunlock(desc, flags);
}

    主要就设置了两个成员:handle_irq全部设置为handle_level_irq,name设置为“XT-PIC”(8259A)。而irq_desc[]数组中的handle_irq成员在do_IRQ()中被调用来执行具体的ISA。这个位置使用了buslock,也即desc->irq_data.chip->irq_bus_lock,而不是desc->lock。buslock用于中断控制器的操作,desc->lock用于IRQ中断处理函数的操作。

三、irq_desc[]的初始化——IO APIC

   IO APIC的handle_irq通过setup_IO_APIC_irqs()函数初始化。调用过程是start_kernel()->rest_init()->kernel_thread(kernel_init)->kernel_init_freeable()->smp_init()->APIC_init_uniprocessor()->setup_IO_APIC()->setup_IO_APIC_irqs()->__io_apic_setup_irqs()->io_apic_setup_irq_pin()->setup_ioapic_irq()->ioapic_register_intr()->irq_set_chip_and_handler_name()。中间经过了太多的过程,其中主要的入口有setup_IO_APIC()用于初始化IO APIC,在初始化IO APIC的过程中完成了对IO APIC的irq_desc[]数组的初始化(setup_IO_APIC_irqs()),最终调用了irq_set_chip_and_handler_name()函数,完成了对irq_desc[]数组的初始化,其初始化desc->handle默认为handle_fasteoi_irq()或handle_edge_irq(),desc->name分别对应fasteoi或edge。

   当然,这个过程在8259A调用irq_set_chip_and_handler_name()之后,那么根据__irq_set_handler()的实现,handle_irq可以更新,因此后注册的IO APIC替代了先前的8259A。

   这里还有个IRQ个数的问题,8259A初始化了16个irq_desc[]数组(0x30到0x39),而APIC应该时224个,但是实际上在setup_IO_APIC_irqs()函数执行时,轮询了系统侦测到的所有的IO APIC,对每个IO APIC在__io_apic_setup_irqs()函数中,又轮询该IO APIC上注册的所有的设备,对于每个注册者,执行io_apic_setup_irq_pin()->setup_ioapic_irq()->ioapic_register_intr()->irq_set_chip_and_handler_name()的过程,而对于每个IO APIC上的每个注册者,对应的irq号,就通过pin_2_irq()接口确认。这意味着,IO APIC要将哪些irq_desc[]数组初始化。

   IO APIC的hanle_irq有两种,分别是handle_fasteoi_irq()和handle_edge_irq(),最终也都调用了handle_irq_event()->handle_irq_event_percpu(),在irq_desc[]初始化上与8259A一致,在“四、desc->handle_irq”部分会详细分析。

   这样就存在一个问题,因为IO APIC并非每个irq_desc[]数组元素都去初始化,而是只初始化那些连接有设备的,那么如何能保证这些irq号就是驱动申请的irq号那?

四、desc->handle_irq

    经过“irq_desc[]的初始化”部分的描述desc->handle_irq已经初始化完毕,而desc->handle_irq接口实际上可以挂接几个函数(8259A和IO APIC):

  • void handle_level_irq(unsigned int irq, struct irq_desc *desc);(8259A)
  • void handle_fasteoi_irq(unsigned int irq, struct irq_desc *desc);(IO APIC)
  • void handle_edge_irq(unsigned int irq, struct irq_desc *desc);(IO APIC)

?    字面意思说明三种处理函数分别代表电平触发、xxx触发、边沿触发,他们之间各有不同但最终均调用了handle_irq_event()。本文中将以以上函数称为handle_xxx_irq()系列函数(还有其他几个handle_xxx_irq()函数,也属于此系列,但是不是x86平台时用或者不是8259A或IO APIC时用)。他们三者之间在何时屏蔽IRQ线、是否要响应发出中断信号的硬件等处有微小的区别。

    下面以handle_level_irq()函数为例,看下它具体干了什么事情,其他handle_xxx_irq()函数做了基本相同的工作:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void
handle_level_irq(unsigned int irq, struct irq_desc *desc)
{
    raw_spin_lock(&desc->lock);
    mask_ack_irq(desc);
    if (unlikely(irqd_irq_inprogress(&desc->irq_data)))
        if (!irq_check_poll(desc))
            goto out_unlock;
    desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
    kstat_incr_irqs_this_cpu(irq, desc);
    /*
     * If its disabled or no action available
     * keep it masked and get out of here
     */
    if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
        desc->istate |= IRQS_PENDING;
        goto out_unlock;
    }
    handle_irq_event(desc);
    cond_unmask_irq(desc);
out_unlock:
    raw_spin_unlock(&desc->lock);
}

    首先,raw_spin_lock先获取锁(关于desc->lock将在“七、中断向量、锁和CPU“部分详细介绍),退出前释放锁,然后按照下列次序进行处理:

  • 用mask_irq_ack()函数向产生中断的硬件发出ACK响应,并暂时屏蔽该中断线(handle_level_irq()独有操作)。
  • 用irqd_irq_inprogress()判断中断是否处于inprogress阶段,如果处于inprogress阶段,则校验并等待“伪中断”轮询完毕(关闭“伪中断”轮询,详见“六、istate状态”)。
  • 去掉desc->istate中的IRQS_REPLAY和IRQS_WAITING标志(详见“六、istate状态”)。
  • kstat_incr_irqs_this_cpu()函数更新desc关于cpu的统计数据。
  • 如果desc->action为空或者desc->irq_data处于DISABLE状态,则将该irq_desc[]元素挂起(desc->istate置位IRQS_PENDING)并返回。
  • 执行handle_irq_event(desc),循环调用desc->action。
  • 用cond_unmask_irq()函数恢复IRQ线。

   handle_irq_event()代码如下(kernel/irq/handle.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
    struct irqaction *action = desc->action;
    irqreturn_t ret;
     
    desc->istate &= ~IRQS_PENDING;
    irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
    raw_spin_unlock(&desc->lock);
    ret = handle_irq_event_percpu(desc, action);
    raw_spin_lock(&desc->lock);
    irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
    return ret;
}

   handle_irq_event()函数首先将irq_desc[]数组的istate清除IRQ_PENDING标志,然后设置desc->irq_data->state_use_accessors增加IRQD_IRQ_INPROGRESS,然后执行handle_irq_event_percpu()函数,逐个cpu执行action,执行完毕后,清除desc->irq_data->state_use_accessors的IRQD_IRQ_INPROGRESS标志,说明IRQD_IRQ_INPROGRESS标志表示正在执行某个具体中断处理操作,也即正在执行action。注意此处锁的位置,更新desc->irq_data->state_use_accessors的标志时,锁,执行action的时候不锁,进入handle_irq_event()时,就已经锁住了。下面来看handle_irq_event_percpu()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
irqreturn_t
handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
{
    irqreturn_t retval = IRQ_NONE;
    unsigned int flags = 0, irq = desc->irq_data.irq;
    do {
        irqreturn_t res;
        trace_irq_handler_entry(irq, action);
        res = action->handler(irq, action->dev_id);
        trace_irq_handler_exit(irq, action, res);
        if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n",
                  irq, action->handler))
            local_irq_disable();
        switch (res) {
        case IRQ_WAKE_THREAD:
            /*
             * Catch drivers which return WAKE_THREAD but
             * did not set up a thread function
             */
            if (unlikely(!action->thread_fn)) {
                warn_no_thread(irq, action);
                break;
            }
            irq_wake_thread(desc, action);
            /* Fall through to add to randomness */
        case IRQ_HANDLED:
            flags |= action->flags;
            break;
        default:
            break;
        }
        retval |= res;
        action = action->next;
    } while (action);
    add_interrupt_randomness(irq, flags);
    if (!noirqdebug)
        note_interrupt(irq, desc, retval);
    return retval;
}

   trace_irq_handler_entry()函数和trace_irq_handler_entry()函数用于找出action中那些不合法的(网上有人这么说,没找到,具体不详)并强行disble他们。具体中断处理调用了action->handler(),反复执行将整个chain上的所有action都执行一遍,所有的返回值或起来作为总的返回值。针对IRQ_WAKE_THREAD,说明驱动程序使用了软断的方式(设置了thread_fn),那么就要调用irq_wake_thread(),尝试在调度器中激活action->thread_fn。关于irq_wake_thread(),详见“八、中断线程”部分。最后,在默认情况下,通过note_interrupt()接口更新desc->action的结果,并决定是否触发“伪中断”轮询函数poll_spurious_irq()。

    从上述分析不难看处,Linux内核在实现内核中断处理函数函数时层次分明:

  • desc->handle_irq,负责与PIC触发方式相关的操作,并作desc->istate相关校验、置位;
  • handle_irq_event(),负责设置desc->irq_data以及解锁避免其他CPU忙等。
  • handle_irq_event_percpu(),负责具体的在CPU上运行desc->action以及转向软中断、转向“伪中断”轮询的过程。

五、中断申请函数request_irq()

    从上述描述可以看出,irq_desc[]数组的name,handle_irq,irq_data->chip伴随着8259A和IO APIC的初始化而初始化,而irq_desc[]数组的其他部分,如actions还没有注册,actions是驱动程序通过request_irq()函数项内核的IRQ系统申请一个IRQ号,并初始化该号的irq_desc[]数组元素中的action(include/linux/interrupt.h):

1
2
3
4
5
6
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
        const char *name, void *dev)
{
    return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

    request_threaded_irq()函数代码真不少,删除参数校验和调试的部分,主要完成了申请action内存,并初始化之,然后挂接action和irq_desc[]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
             irq_handler_t thread_fn, unsigned long irqflags,
             const char *devname, void *dev_id)
{
    ...
    action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
    if (!action)
        return -ENOMEM;
    action->handler = handler;
    action->thread_fn = thread_fn;
    action->flags = irqflags;
    action->name = devname;
    action->dev_id = dev_id;
    chip_bus_lock(desc);
    retval = __setup_irq(irq, desc, action);
    chip_bus_sync_unlock(desc);
    if (retval)
        kfree(action);
    ...
    return retval;
}

    自此,irq_desc[]数组的初始化基本完成。

    request_irq()函数需要携带irq号这个变量用于注册,但是驱动程序本身并清楚哪个中断号是可用的,也不清楚自己的IRQ线对应的哪个irq_desc[]元素,怎么解决这个问题?一方面,驱动程序一般都有自己的默认中断向量号,但不止一个,调用request_irq()时,会调用irq_settings_can_request()函数(给省略掉了)来检测该desc是否可以被申请,如果不能被申请,则返回-EINVAL,驱动程序会再重新申请;另一方面,内核提供了一种irq号侦测机制,auto probe,详见“六、istate状态”。

六、istate状态

    Linux内核中的中断子系统有四种状态、特性,分别是:

  • 用于描述irq_desc的istate。
  • 用于描述irq_desc->action->handler的flags。
  • 用于描述irq_desc->action->thread_fn的thread_flags。
  • 用于描述irq_desc->irq_data的state_use_accessors。

    此处重点介绍istate,但首先还是irq_desc->action->flags。include/linux/interrupt.h文件中定义了handling routines的一些标志(并没有完全列出,还有一些IRQF_TRIGGER_的表示触发方式的标志):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
 * These flags used only by the kernel as part of the
 * irq handling routines.
 *
 * IRQF_DISABLED - keep irqs disabled when calling the action handler.
 *                 DEPRECATED. This flag is a NOOP and scheduled to be removed
 * IRQF_SHARED - allow sharing the irq among several devices
 * IRQF_PROBE_SHARED - set by callers when they expect sharing mismatches to occur
 * IRQF_TIMER - Flag to mark this interrupt as timer interrupt
 * IRQF_PERCPU - Interrupt is per cpu
 * IRQF_NOBALANCING - Flag to exclude this interrupt from irq balancing
 * IRQF_IRQPOLL - Interrupt is used for polling (only the interrupt that is
 *                registered first in an shared interrupt is considered for
 *                performance reasons)
 * IRQF_ONESHOT - Interrupt is not reenabled after the hardirq handler finished.
 *                Used by threaded interrupts which need to keep the
 *                irq line disabled until the threaded handler has been run.
 * IRQF_NO_SUSPEND - Do not disable this IRQ during suspend
 * IRQF_FORCE_RESUME - Force enable it on resume even if IRQF_NO_SUSPEND is set
 * IRQF_NO_THREAD - Interrupt cannot be threaded
 * IRQF_EARLY_RESUME - Resume IRQ early during syscore instead of at device
 *                resume time.
 */

    以上标志如果非得从“状态”和“特性”中选择一个的话,应该是“特性”,其前缀为IRQF_,表示IRQ Flags。在request_irq()函数执行时,需要填写flags变量,也就是这些标志,这些标志用于说明你申请的handler的特性,request_irq的flags总是带有IRQF_DISABLED的标志,其他特性让其他IRQ操作函数按不同的路径执行。如IRQ_SHARED标志表示可以共享IRQ号,IRQF_NO_THREAD标志表示中断处理函数不能被线程化。

    然后是istate状态,kernel/irq/internals.h中定义了这些“状态”,使用IRQS_前缀,他们可以同时存在,而并不一定相互转化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
 * Bit masks for desc->state
 *
 * IRQS_AUTODETECT      - autodetection in progress
 * IRQS_SPURIOUS_DISABLED   - was disabled due to spurious interrupt
 *                detection
 * IRQS_POLL_INPROGRESS     - polling in progress
 * IRQS_ONESHOT         - irq is not unmasked in primary handler
 * IRQS_REPLAY          - irq is replayed
 * IRQS_WAITING         - irq is waiting
 * IRQS_PENDING         - irq is pending and replayed later
 * IRQS_SUSPENDED       - irq is suspended
 */
enum {
    IRQS_AUTODETECT     = 0x00000001,
    IRQS_SPURIOUS_DISABLED  = 0x00000002,
    IRQS_POLL_INPROGRESS    = 0x00000008,
    IRQS_ONESHOT        = 0x00000020,
    IRQS_REPLAY     = 0x00000040,
    IRQS_WAITING        = 0x00000080,
    IRQS_PENDING        = 0x00000200,
    IRQS_SUSPENDED      = 0x00000800,
};

    这些状态存储在irq_desc->istate,也就是core_internal_state__do_not_mess_with_it。从字面意思看,有些状态不好理解,通过他们在linux内核中的调用关系搞清楚来龙去脉:

     IRQS_AUTODETECT状态和IRQS_WAITING状态

?    AUTODETECT状态在函数probe_irq_on()中被开启,在probe_ifq_off()函数或probe_ifq_mask()函数中被关闭。而probe_irq_on()和probe_irq_off()是驱动与内核申请可用的irq号的调用函数,一个驱动程序可以通过request_irq()向内核申请注册中断处理函数到desc->action,此时需要携带irq号作为参数,而irq号可以自是预先设定(Intel Enternet drvier),也可以通过一种叫自动侦测(auto probe)的过程来完成:

  • 首先,其驱动程序调用probe_irq_on()接口,开始auto probe的过程。probe_irq_on()函数将返回一个32bits的irq_desc是否可用的掩码mask(此处因为返回值类型为int,所以只能返回32位的掩码)。probe_irq_on()接口首先选出所有可以被侦测(desc->irq_data->status_use_accessors是否包含IRQD_IRQ_NOPROBE标志)、并且没有action函数的irq_desc[],并将这些irq_desc->istate设置IRQS_AUTODETECT标志和IRQS_WAITING标志;然后过滤掉所有的伪中断irq_desc[],是否可用通过判断desc->istate包含IRQS_WAITING状态(伪中断经过handle_xxx_irq()处理将清除IRQS_WAITING状态)。上面的工作对于auto probe都是辅助性的,但最重要的是probe_irq_on()函数在开始时就调用了async_synchronize_full()接口,来同步内核中所有异步调用的函数(这样就不存在发生了中断,但没有被记录的情况)。
  • 其次,驱动程序收到mask掩码后(当然也可以利用mask做一些事情,但并非所有的驱动都这样做),主动产生一次硬件中断。
  • 然后,驱动程序调用probe_irq_off(mask),后者侦测所有的irq_desc[],查看到底是哪些irq_desc[]元素发生了硬件中断,如果只发生了一次硬件中断,那么就返该irq_desc[]的下标,也即irq号;如果没有侦测到中断发生,则返回0;如果侦测到多次中断发生,则返回一个负数(中断发生次数的相反数)。probe_irq_off()函数通过desc->istate是否包含IRQS_WAITING标志来判断是否发生了一次硬件中断(包含就没发生,不包含就发生了)。
  • 最后,驱动程序通过probe_irq_off()的返回值,来判断是否是一个可用的irq号,如果可用,则通过request_irq()来申请注册中断处理函数。

?    如果再扣的细一点,还有两个问题:

  • 问题1,async_synchronize_full()函数能够使所有的irq_desc[]的istate都去掉IRQS_WAITING标志,这是为什么?
  • 问题2,probe_irq_off()函数为什么可以使用带有IRQS_WAITING标志来判断哪个irq描述符发生了硬件中断?

    对问题1,涉及到IRQS_WAITING标志的含义,IRQS_WAITING标志在probe_irq_on()接口中被设置,在执行irq_desc->handle_irq,也即具体的handle_xxx_irq()接口时被清除,猜测async_synchronize_full()接口可能会等待所有的中断处理函数执行完毕吧(此处还有问题,如何等待源源不断的中断处理执行完成?那得看async_synchronize_full的细节了。)

   对问题2,在init_8259A()函数或者setup_IO_APIC_irqs()函数中,都调用了irq_set_chip_and_handler_name()函数,后者将desc->irq_data.chip和desc->handle_irq以及desc->name给予初始化,这意味着与irq_desc[]关联的PIC已经完全驱动起来,并且与irq_desc[]建立了关联关系,当硬件产生中断信号时,PIC可以接收到该信号,并通知CPU,CPU查找到IDT里的中断处理程序后,就执行了do_IRQ()函数,然后调用了handle_IRQ_event()接口,然后到具体的irq_desc->handle_irq,如handle_level_irq(),而在具体的handle_xxx_irq()接口中,就会清除掉IRQS_WAITING标志。

    总结一下,IRQS_AUTODETECT表示某个irq_desc[]变量处于自动侦测状态,通过probe_irq_on()函数设置此状态,通过probe_irq_off()清除此状态。IRQS_WAITING表示某个irq_desc[]变量处于等待状态,也即等待被处理,等待irq_desc->handle_irq的执行,此状态通过probe_irq_on()设置,通过irq_desc->handle_irq,也即handle_xxx_irq()系列函数清除。

    IRQS_SPURIOUS_DISABLED状态

    前面说IRQS_AUTODETECT状态时,提到“伪中断”,这个IRQS_SPURIOUS_DISABLED就是指这种情况。根据wiki上的说法:“一类不希望被产生的硬件中断。发生的原因有很多种,如中断线路上电气信号异常,或是中断请求设备本身有问题。”,猜测是哪些PIC上确实侦听到了中断信号,但实际上没有发生中断,或者没有找到中断处理函数的情况。Linux内核将某个irq_desc[]元素发生了10万次中断,却有9.9万次没有处理的情况,视为“伪中断”,会将其置位,意味着因为“伪中断”而被禁用(紧接着会执行irq_disable(desc))。中断发生次数统计通过desc->irq_count,中断发生却没有处理的统计通过desc->irq_unhandled。内核在note_interrupt()函数中处理此情况。

    setup_irq()函数用于驱动程序将具体的中断处理函数挂接到desc->action下,该函数执行时,将清除IRQS_SPURIOUS_DISABLED标志。

    对于“伪中断”,内核并不是扔掉不管,而是有一套自己的处理方法,有人还对其做过优化。目前,内核采用poll_spurious_irqs()的方法来处理被IRQS_SPURIOUS_DISABLED的desc。poll_spurious_irqs()函数将轮询所有的“伪中断”,并尝试在本地core上执行一次(通过try_one_irq()函数)。try_one_irq()函数有选择的执行handle_irq_event(),(有些情况的伪中断不予执行,比如PER_CPU的、嵌套的等等)。poll_spurious_irqs()函数并不清除IRQS_SPURIOUS_DISABLED标志,而是尝试轮询并执行他们一次。

?    总结一下,IRQS_SPURIOUS_DISABLED标志意味着某irq_desc[]元素被视为“伪中断”,并被禁用。该标志被note_interrupt()函数设置,被setup_irq()函数清除。对于哪些伪中断,系统尝试时用poll_spurious_irqs()函数在本地CPU上轮询并执行他们一次(在中断线被PIC禁用时,仍可以执行中断处理函数,即是中断处理函数执行完毕,也不清除此标志)。

    IRQS_POLL_INPROGRESS状态

?    in progress表示正处于过程当中,poll in progress字面意思就是中断处理函数目前正在以poll的方式执行。硬件中断处理函数通常是立即执行,而软中断才留在后面执行。

    前面提到的try_one_irq()函数,在其执行handle_irq_event()函数前,将设置此标志表示中断处理函数正在以poll的方式被执行,在其执行完毕handle_irq_event()后清除此标志,表示中断处理函数执行poll完毕。而调用try_one_irq()函数的还由misrouted_irq()函数(用于尝试执行一次可能时misrouted的情况,硬件产生了中断信号,内核却将其对应到错误的irq_desc[]元素上的情况)函数中被调用。这是对于“伪中断”的轮询,但对正常的中断处理,并没有采用poll的方法(NAPI采用了,另说),而是在具体的handle_xxx_irq()函数中需要执行irq_check_poll()方法,等待中断处理函数poll完毕,因为驱动实现的中断处理函数未必是可重入的。

   总结一下,IRQS_POLL_INPROGRESS,表示一个irq_desc[]元素正处于轮询调用action链的阶段。此标志只在try_one_irq()函数(被poll_spurious_irqs()函数和misrouted_irq()函数调用)调用handle_irq_event()前被设置,在其调用handle_irq_event()后被清除。由于具体的中断处理函数(desc->action)的设计未必是可重入的,因此desc->handle_irq,如handle_xxx_irq()需要等待其上的轮询完毕后才能执行。这意味着,仅仅“伪中断”才会被轮询,并且一个中断处理函数可以同时被“伪中断”轮询执行,也可以正常执行,但必须排队执行。

   这个地方还是不理解,既然被定为“伪中断”,那么就会被irq_disable()——从硬件上屏蔽该中断线,怎么还会接收到中断、并执行中断处理函数那?这可能就是“伪中断”神奇的地方。

    IRQS_ONESHOT状态?

    开一枪状态?应该是个不太重要的状态。该状态表示irq_desc[]元素的主处理函数不是非屏蔽的(直接说mask不就完了,难道除了mask,unmask还有半mask?)。在handle_fasteoi_irq()函数(其他handle_xxx_irq()中没有,fasteoi一定是一种比较特殊的情况)中,如果该标志设置,就需要mask_irq()执行一下,然后就是硬件操作了。此处的mask应该是这样的,mask意味着屏蔽中断,也就是在中断处理函数执行的时后,从CPU的角度短暂的禁止该中断(应该是中断向量,通过设置CPU的中断屏蔽寄存器IMR,来完成此过程)。而是否需要mask中断,是中断处理函数的本身的需要,因此,也就是应该是desc->action->flags里的设置(IRQF_ONESHOT标志),而IRQS_ONESHOT状态应该是源于IRQF_ONESHOT标志的,在setup_irq()函数被执行用来为desc添加action的时候,在非共享的方式下,如果发现action->flags中设置了IRQF_ONESHOT标志,则为desc->istate设置此状态。

    在irq_finalize_oneshot()函数中,将会执行unmask_irq(),并清除该标志。irq_finalize_oneshot()函数在one shot中断处理函数运行完毕(action->thread_fn)时被调用。

    总结,one shot,字面意思开一枪,这枪肯定是早期的步枪,没能实现自动填装弹药。IRQS_ONESHOT更像一种属性,而非状态(在其被设置时,并不是中断被屏蔽的时刻,而时表示此action是one shot类型的),在setup_irq()的时后被条件设置,在handle_fasteoi_irq()执行mask_irq()操作,在irq_finalize_oneshot()中执行unmask_irq()操作,并清除此标志。

    IRQS_REPLAY状态?

    从字面意思上讲,应该是重新发生一次中断的意思。该标志在handle_xxx_irq()函数中的开始部分就被清除(避免多个core同时执行),在check_irq_resend()函数中,若判断desc->istate包含IRQS_PENDING标志,则设置该状态。IRQS_REPLAY状态仅在check_irq_resend()函数中被设置,check_irq_resend()函数通过重新激发中断控制器上的中断信号来完成此过程(中断硬件将不发送中断信号,仅仅由PIC重新向CPU发送),对handle_level_irq()函数触发的中断,不予重新发送(不知道原因)。而check_irq_resend()函数又被__enable_irq()和irq_startup()所调用,最终被irq_set_chip_and_handler_name()和irq_set_handler()调用,他们的调用关系如下图所示:

   resend

    总结,IRQS_REPLAY状态表示需要重新激发一次中断信号(正在重新发送IRQ信号),它在desc->handle_irq被初始化最后时刻被设置(被irq_set_chip_and_handler_name()函数设置):完成desc->handle_irq的挂接后要由PIC自动产生一次中断,利用重新注册action的机会,对“挂起”(IRQS_PENDING)状态的中断再触发一遍,然后由handle_xxx_irq来查看中断处理函数是否正常,该标志在handle_xxx_irq()时被清除。

    IRQS_PENDING状态

    字面意思“正在挂起”,为什么要挂起?在什么情况下挂起?在handle_xxx_irq()函数(handle_ege_irq()函数和handle_edge_eoi_irq()函数比较特殊)中,如果发现以下情况中的一种,则挂起,挂起后直接释放锁并推出,并不执行handle_irq_event()函数:

  • desc->action = NULL,也即无具体的中断处理函数;
  • desc->irq_data->state_use_accessors包含IRQD_IRQ_DISABLED,也即中断控制器被禁用;

    还有一种情况会“挂起”中断处理,在probe_irq_on()时,对于所有的没有分配action并且可以被PROBE的desc,需要重新irq_startup()一下,清掉此前mask it self的longstanding interrupt。在irq_startup()函数中,硬件会尝试挂起该中断向量,如果成功的话,desc->istate也需要置位IRQS_PENDING。

   另一种需要“挂起”的情况是try_one_irq()函数中,如果desc->irq_data->state_use_accessors包含IRQD_IRQ_INPROGRESS标志(表示IRQ不处于),则为desc->istate置挂起标志并推出。

    以下是清除IRQS_PENDING的场景:

  • handle_irq_event()函数开始处即清除IRQS_PENDING标志;
  • check_irq_resend()函数,会在PIC重新触发、向CPU产生中断信号前,清除掉IRQS_PENDING标志;

?    以下是校验IRQS_PENDING的场景:

  • try_one_irq()函数中,如果desc->irq_data->state_use_accessors并不包含IRQD_IRQ_INPROGRESS标志,但是desc->istate却包含IRQS_PENDING标志,并且desc->action不为空,则返回执行action链上的函数。
  • check_wakeup_irq()函数中,对所有的desc,如果desc->depth==1并且置位IRQS_PENDING标志,则返回-EBUSY(这个地方一定有特殊含义,就不细研究了)。

?    总结,IRQS_PENDING表示中断处理被挂起,通常是因为没有中断处理函数或者中断控制器被禁用。通常由handle_xxx_irq()设置,由handle_irq_event()清除。

    IRQS_SUSPENDED状态

    字面意思为“暂停”,下面是置位IRQS_SUSPENDED状态的场景:

  • 在__disable_irq()函数中,如果参数suspend设置为true,则置位IRQS_SUSPENDED状态。suspend_device_irqs()函数调用__disable_irq()函数时,会设置此标志。

?    清除IRQS_SUSPENDED状态场景:

  • 在__enable_irq()函数中,如果参数resume为ture,则清除IRQS_SUSPENDED状态。resume_device_irqs()函数调用__enable_irq()函数时,清除此标志。

    校验IRQS_SUSPENDED状态的场景:

  • 在__enable_irq()函数中,如果desc->depth=1,则校验IRQS_SUSPENDED状态,如果置位,则退出,不执行irq_enable()。
  • 在check_wakeup_irqs()函数中,具备IRQS_SUSPENDED状态成为是否执行mask_irq()的一个条件。
  • 在suspend_device_irqs()函数,对所有具备IRQS_SUSPENDED状态的desc执行 synchronize_irq()。

?    总结,IRQS_SUSPENDED标志表示所有中断线均被disable,通过suspend_device_irqs()函数置位,通过resume_device_irqs()函数清除。

    总体看来这些状态代表着irq_desc[]的一些操作阶段,但是他们之间是可以共存的,下图粗略的描述了irq_desc->istate的各种状态之间的关系,状态的输入箭头表示状态置位操作,状态的输出箭头表示状态的清除操作,函数的输入箭头表示被上游函数调用,函数的输出箭头表示调用了下游函数,虚线表示mask_irq()操作实际上是handle_xxx_irq的一个特例handle_fasteoi_irq()中的操作,直线没箭头表示对该状态ONESHOT的直接操作,关于ONESHOT状态,实际上是desc->action->thread_fn的一个属性,将会在“八、中断线程中详细讨论”:

istate

七、中断向量、锁和CPU

    中断向量这个词经常说,那么到底什么是中断向量那?内核中有两个概念,一个叫vector,另一个叫irq。vector指的就是“中断向量”,也就是PIC向CPU传递的用于表示发生中断的IRQ线的一种编号。而irq,也就是常说的irq号,是内核维护的中断请求的数组下标。以下说法或变量是等价的(x86架构):

  • 中断向量号
  • PIC向CPU传递的关于中断向量号
  • IDT表的索引号
  • interrupt[]数组的下标+0x20
  • irq_desc[]数组的下标+0x20
  • irq_desc->action->irq+0x20
  • vector_irq[]数组的下标
  • irq号+0x20

    硬件维护的中断向量号,在x86平台上,本质上是PIC向CPU传递的用于表示发生中断的IRQ线的一种向量编号,CPU通过该向量找到IDT表中的对应的中断门描述符。由于Intel保留了前0x20个中断、异常号,所以内核就没必要再维护这块了,所以内核中的数组下标从0开始,就对应了中断向量的0x20号。但是vector_irq[]数组比较特殊,它的下标代表vector,而内容代表irq,实际上就是irq到vector的一个影射:

    vector_irq[]数组在arch/x86/include/asm/hw_irq.h文件中定义:

1
2
typedef int vector_irq_t[NR_VECTORS];
DECLARE_PER_CPU(vector_irq_t, vector_irq);

    通过setup_vector_irq()函数初始化:

1
2
3
4
5
6
7
8
9
10
11
void setup_vector_irq(int cpu)
{
#ifndef CONFIG_X86_IO_APIC
    int irq;
    ...
    for (irq = 0; irq < legacy_pic->nr_legacy_irqs; irq++)
        per_cpu(vector_irq, cpu)[IRQ0_VECTOR + irq] = irq;
#endif
    __setup_vector_irq(cpu);
}

    前文中提到在init_IRQ()函数中,会首先初始化vector_irq[]数组,而且vector_irq[]数组是PER_CPU的。vector_irq[]在声明时被全部初始化为-1,-1表示未被使用。在init_IRQ()函数中,16个(0x30~0x3f,为什么是0x30开始,就不讨论了)被初始化为0到15。

    used_vectors按照内核注释“used_vectors is BITMAP for irq is not managed by percpu vector_irq”的说法,used_vectors是vector_irq[]不管的中断向量。used_vectors通过alloc_intr_gate()函数置位,alloc_intr_gate()函数的主要功能是set_intr_gate()。used_vectors[]在trap_init()时,前0x20个被置位。系统调用0x80随后也被used_vectors置位,从名字上看,used_vectors应该时叫system vectors,就时IO设备中断以外的中断和异常。实际上used_vectors标明了锁有的中断、异常哪些是IO device中断,哪些是非IO device中断,这些非IO device中断中包含了Intel预留的前0x20个中断、异常,也包含Linux内核选取的0x80系统调用软件中断,当然还包括一些其他的中断和异常,但并不做具体的区分。

    因此,used_vectors和vector_irq[]的关系就清晰了,下面是关于并发的操作。

    ULK上说:”简而言之,当硬件设备产生了一个中断信号时,多APIC系统就选择其中的一个CPU,并把信号传递给相应的本地APIC,本地APIC又依次中断它的CPU。这个时间不通报给其他所有的CPU“。我想这个地方所谓的”多APIC系统“,是指多IO APIC系统,而IO APIC选择一个CPU,将信号传递给其Local APIC,然后接收到信号的Local APIC再选择一个本地的Core通知其中断发生,那么就意味着在多IO APIC系统中,选择哪个CPU的哪个Core来处理IO设备中断,是由IO APIC决定的,而且从硬件上就决定了其不会同时由两个以上的Core收到相同的中断信号。那么,Linux内核应该无需担心两个core同时处理一个中断信号引起的desc->handle_irq的情况了,但是会发生如下情况:

  • 某CPU在处理某中断,此刻发生了相同的中断,要相同的CPU来处理;丢中断;
  • 某CPU在处理某中断,此刻发生了相同的中断,要不同的CPU来处理;避免这种情况;
  • 某CPU在处理某中断,此刻发生了不同的中断,要相同的CPU来处理;中断嵌套;
  • 某CPU在处理某中断,此刻发生了不同的中断,要不同的CPU来处理;正常处理;

    对于第一种情况,起码要有一种机制保证正常的丢失中断,而不是总被自己打断,而无法完成正常的中断处理。而对第二种情况,应该尽量避免,因为中断处理函数不一定是可重入的,因此必须顺序执行,一次没处理完,不能并发处理下一次。Linux内核通过自旋锁来完成这个工作,也即在handle_xxx_irq()系列函数开始时,获取锁,退出时,释放锁。Linux内核中断子系统中至少使用到了4种锁用于同步不同的资源,分别是:

  • desc->irq_data.chip->irq_bus_lock
  • desc->lock
  • irq_domain_mutex
  • gc_lock

    前面说的锁就是desc->lock的用处。irq_desc->lock类型为raw_spinlock_t,是一个自选锁。首先,得看一下自旋锁的特点和操作。自旋锁有以下特点:

  • 自旋,同时只能被一个进程持有,只能由持有该锁的进程解锁;
  • 忙等,如果其他进程想要获取锁,则会一致处于等待状态而无法做其他事情。
  • 递归死锁,以上两条说明会有这个现象。

    自旋锁可以时用下列方法去操作:

  • spin_lock()和spin_unlock()
  • raw_spin_lock()和raw_spin_unlock()
  • raw_spin_lock_irq()和raw_spin_unlock_irq()
  • raw_spin_lock_irqsave()和raw_spin_unlock_irqrestore()

        内核在desc->handle_irq到handle_irq_event()接口的过程中,通常这样处理:

handle_xxx_irq

 

    为什么要着要么做?避免同时对调用handle_irq_event(),奇怪的是在handle_irq_event()里,在irqd_set(IRQD_IRQ_INPROGRESS)后,又解锁,这是为什么?显然,内核并不是害怕两个CPU同时执行了desc->action(除了handle_xxx_irq()系列函数,内核还有其他位置调用了desc->action),而是害怕在锁与解锁之间的操作被交叉操作,这些操作包括(以handle_level_irq()为例,上图中没有标出):

  • mask_ack_irq(desc),向发出中断的硬件ACK,并且暂时屏蔽该中断;
  • 如果irqd_irq_inprogress(),则执行irq_check_poll(),这表示该中断可能正在“伪中断”轮询函数轮询。(不知道有没有这种情况,伪中断轮询函数没有轮询该中断,desc->irq_data状态却不是IN_PROGRESS)
  • desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
  • kstat_incr_irqs_this_cpu(irq, desc)更新cpu相关的中断统计;
  • 如果desc->action为空,或者desc->irq_data处于DISABLE的状态,执行desc->istate |= IRQS_PENDING;

    看样子,desc->lock只保护desc->istate状态变化的操作,硬件中断处理中PIC对中断线的一些操作,并不保护desc->action。这意味着

八、中断线程

    Linux内核的硬中断处理程序(desc->handle_irq)将处理中断信号发生后需要立即完成的事情,如网卡数据包的拷贝,响应中断等,而将不太紧急的工作留给软中断去完成。在“六、istate状态”一节中有提到一种ONESHOT状态,就与中断线程相关。使用ps aux | grep ksoftirqd命令,会发现系统中正运行着几个(core总个数)ksoftirq进程:

1
2
3
4
5
[rock3@e4310 linux-stable]$ ps aux | grep ksoftirqd
root         3  0.0  0.0      0     0 ?        S    11月12   0:37 [ksoftirqd/0]
root        13  0.0  0.0      0     0 ?        S    11月12   0:34 [ksoftirqd/1]
root        18  0.0  0.0      0     0 ?        S    11月12   0:23 [ksoftirqd/2]
root        23  0.0  0.0      0     0 ?        S    11月12   0:22 [ksoftirqd/3]

    这些进程就是内核启动的中断线程,他是一种内核线程,每个core都有一个,用于处理不太紧急的中断事务,本节就讨论从硬中断处理转向软中断处理的过程,这得从setup_irq()函数(用于设置desc->action)说起。先看下调用关系:softirq

    实际上干事的是__setup_irq()函数,__setup_irq(irq,desc,new)将new加入到desc->action链表中。该函数判断如果加入的new->thread_fn不为空,则使用kthread_create()函数创建一个内核线程并将该线程加入到内核线程kthread_create_list链表中,kthread_create()函数返回task_struct结构体指针,该指针被赋值为new->thread,在函数退出__setup_irq()函数返回前,调用wake_up_process()函数来唤醒该new->thread。

   当然创建的线程是irq_thread,不过irq_thread()函数将new->thread_fn又包裹了一层:irq_thread()通过irq_thread_fn()函数或者irq_forced_thread_fn()函数来调用new->thread_fn,而irq_thread_fn/irq_forced_thread_fn的补充操作为irq_finalize_oneshot(),即根据ONESHOT状态来执行unmaks_irq()。irq_thread()函数还有不少其他操作,这里就不分析了。

    通过kthread_create()创建内核线程,到wake_up_process()唤醒它,中断子系统进入了软中断(softirq)的阶段,该阶段以内核线程的方式来处理中断,被调度器调度。软中断通常有tasklet、工作队列等具体方式。具体原理请详见“软中断原理”一文。

九、总结

    本篇博客实际上是逆向学习Linux中断子系统的过程,本意为看下irq_desc[]数组初始化的过程,结果有很多不明白的地方,就一路跟踪过来,虽然也学到了一些内核中断子系统的东西,但整体上对框架和细节的把握并不到位,估计以后还需要重新系统学习:

  • 带着问题学,逆向思维很重要。
  • 逆向学习的过程应该是先纵向、再横向,但本篇博客中很多地方是先纵横不分,类似篮球里的“盯球不盯人”,不好。
  • 假设一定要验证,“我以为”是失败之母;
  • 用20%的时间学会整体框架,而不是用80%的时间了解细节。
  • 从别人那学,会快,但不深刻。
  • 不要学究。

十、遗留问题

1、关于锁的问题,还是没想明白,从找到interrupt[i]的内容,执行do_IRQ()函数,到handle_xxx_irq(),再到handle_irq_event(),在handle_xxx_irq()开始处加锁,而在desc->action处解锁,那么说ISR并不是临界区,可以在两个CPU上执行,而desc->lock只保护desc->istate变化以及PIC的一些操作,而不保护ISR。这个问题很重要,涉及到SMP架构下对中断的处理,涉及到如何与软中断配合。

    网上查找了一下,说ISR is not a critical section,但是do_IRQ()是critical section。不明白。desc->action不一定是可重入的,两个CPU同时执行它的时后,就有可能出现错误。

2、转向软中断的一些细节。

 

参考资料:

1、Understanding Linux Kernel

2、Professional Linux Kernel Architecture

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多