ARM的中断处理[二]

adtxl
2023-04-27 / 0 评论 / 443 阅读 / 正在检测是否收录...

转载在https://zhuanlan.zhihu.com/p/90074320 作者:兰新宇

中断源的状态

GIC对一个中断源的处理过程包含Inactive, Pending, Active和Active and Pending四种状态。

image489df71fd253e0e7.png

中断源没有被assert(触发)的时候,处于初始的"Inactive"状态。如果某个中断源被触发,GIC会将IAR寄存器(Interrupt Acklowlege Register)中该中断源对应的bit置1,然后通知CPU core(PE)。在CPU尚未做出应答之前,该中断源处于"Pending"(待处理)状态。这里IAR可理解为中断标志寄存器。

在Pending状态中,GIC会关闭对该中断源的响应,在此期间,如果该中断源上有新的中断到来,所有连接GIC的CPU都无法收到。因为GIC是中断源和CPU之间的桥梁,GIC已经在桥的这一头挡住了中断源,在桥的另一头的CPU自然是没办法接收的。

image2d0400ae54b84965.png

接下来CPU会读取IAR寄存器中置1的bit,读取后硬件自动将其清零,同时回复一个硬件的ACK信号给GIC,表示CPU已经开始处理,此时中断源进入"Active"(正在处理)状态。

image4a2beb4ca4a53bcd.png

这时GIC会解除对该中断源的屏蔽,也就是说,如果之后该中断源上有第二个中断到来,那么CPU是可以接收到的。从新来的中断的角度,该中断源应该处于Pending状态,而从上一个还没处理完的中断的角度,该中断源又应该处于Active状态,所以这个特殊时期的状态被叫做Active and Pending(以下简称A&P)。

image1dcff07c6db01be5.png

之后,CPU完成了对该中断源的处理(从Linux的角度就是执行完了hardirq部分),就会填写EOI(End Of Interrupt)寄存器,打开中断,中断源此时又回到了Inactive状态。

Linux的GIC实现

来看一下Linux对GIC中断流程的实现:

void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
        //ACK(pending --> active)
    u32 irqnr = gic_read_iar();

        // PPI, SPI or LPI
    if (likely(irqnr > 15 && irqnr < 1020) || irqnr >= 8192) {
        gic_write_eoir(irqnr);
        handle_domain_irq(gic_data.domain, irqnr, regs);
    }

        // SGI
    if (irqnr < 16) {
        gic_write_eoir(irqnr);
        #ifdef CONFIG_SMP
            handle_IPI(irqnr, regs);
            #else
            WARN_ONCE(true, "Unexpected SGI received!\n");
        #endif
        }
}

GIC作为一个具体的中断控制器,从Linux的角度来看,相当于是一个外设,因而其对GIC的功能实现是放在"/drivers/irqchip/irq-gic-xx.c"系列文件中的。这里gic_handle_irq()就是GIC中断处理的入口了,它首先通过gic_read_iar()读取IAR寄存器做出ACK应答,而后判断中断源的类型。

如果中断号小于16,说明是SGI,那么就按照IPI核间中断的方式进行处理,当然前提是这是一个SMP的系统。如果中断号介于16和1020之间,或者大于8192,说明是PPI, SPI或者LPI,那么就按照普通流程处理。普通流程的入口函数是handle_domain_irq(),它的作用等同于前面文章讲的do_IRQ()。

do_IRQ()是一个经典的API,但它存在不适合架构解耦的问题,因此虽然现在do_IRQ()依然存在于Linux的代码中,但新的架构是不建议采用的,关于这个问题更详尽的说明请参考这篇文章。ARM作为一个新的处理器架构,响应号召,选择了handle_domain_irq()这种新型的处理方式。

比较奇怪的是,写EOI寄存器的gic_write_eoir()函数居然放在了中断处理函数handle_domain_irq()的前面,不是应该中断处理完了之后再填下EOI么?其实,这里的gic_write_eoir()并不是CPU真的向GIC告知自己的中断处理已结束,更详细的解释请参考这篇文章。

中断源的触发

前面的文章已经提到,中断源的触发方式分为电平触发和边沿触发,由于这部分内容涉及到中断源的状态变化,这和具体的中断控制器有关,所以当时不便于展开讨论,就放在这里讲解。

image744054e407e846ea.png

边沿触发

对于边沿触发,可以是上升沿触发,可以是下降沿触发,亦或上升沿下降沿都触发。触发后,中断源自动从assert的电平位置(假设为高电平)回落到deassert的电平位置(对应为低电平),但GIC会保持和CPU之间连线的高电平状态(相当于琐存),直到CPU做出ACK应答。

CPU应答后,GIC和CPU的连线也回落到deassert的电平位置。在CPU处理完毕之前(EOI),如果该中断源上有第二个中断发生,则进入A&P状态,没有则在处理完后回到Inactive状态。

image17e04bb8e38fa5b5.png

按照GIC的设计,在A&P阶段,正在处理该中断源的上一个中断的CPU是可以对第二个中断做出应答的,但是按照Linux的中断处理机制,hardirq部分是不允许嵌套的,不光是当前正在处理这个中断源的CPU(设为CPU A)不可以,其他CPU也不可以。

不过GIC已经琐存住了第二个中断的pending信息,让CPU稍后应答也不是什么问题,这第二个中断并不会丢掉。可是Linux作为一个通用的操作系统,是面向多种处理器架构和多种中断控制器的,而并不是所有的中断控制器都提供和GIC一样的功能,所以Linux从软件层面实现了类似的机制。

当处于Active状态的中断源产生第二个中断的时候(进入A&P状态),Linux会让第二个CPU(设为CPU B)去ACK应答这个中断,然后保存住这个pending信息(相当于软件琐存),这样该中断源又是Active状态了,CPU B也可以去干自己的事了。

CPU B在应答之后,还会让中断控制器屏蔽掉这个中断源,也就是告诉中断控制器,我们CPU这边已经有pending的了,不再接受该中断源上新的中断了。这个跟GIC在pending了中断源上的一个中断之后,屏蔽该中断源,不再接收这个中断源产生的新的中断,简直一模一样啊,可以把CPU B的行为视作对这一功能的软件模拟。

image831141c7adf61778.png

CPU B让GIC屏蔽掉的,只是GIC和CPU之间的这个通路,既然中断源现在是Active状态,那么如果来了第三个中断,那么GIC还可以再从硬件层面琐存住这个中断的pending信息,然后再次进入A&P状态。如果再来第四个中断,那么就只能被中断控制器丢弃了。

image23dd7c8ceef0bb63.png

本来呢,对同一中断源,处在pending状态的中断最多只能有一个,但在ARM+Linux的系统中,由于GIC和Linux分别从硬件层面和软件层面提供了pending功能,所以至多有两个。也就是说,中断是没有排队机制的,这就好像前面文章介绍的不可靠信号一样。

不可靠信号之所以没有排队机制是当时的软件设计使然,而中断没有排队机制一方面是由于硬件资源的限制,一方面是因为本来hardirq的执行时间就是很短的,如果在执行一个hardirq的时间里来了两个及以上的中断,那就算排队了,后面也是处理不过来的。

来看下Linux的代码是如何实现上述过程的吧,这部分的核心函数是handle_edge_irq()(源代码位于kernel/irq/chip.c)。

void handle_edge_irq(struct irq_desc *desc)
{
    raw_spin_lock(&desc->lock);

    /* CPU B */
    //当前有irq在运行,或者被禁止,或没有注册处理函数
    if (!irq_may_run(desc) || irqd_irq_disabled(&desc->irq_data) || !desc->action)) {
        desc->istate |= IRQS_PENDING;
        //mask该中断源
    mask_irq(desc);
    goto out_unlock;
    }

    /* CPU A */
    do {
        // 有pending待处理的中断
        if (unlikely(desc->istate & IRQS_PENDING)) {
            //被其他CPU在之前mask掉了
        if (!irqd_irq_disabled(&desc->irq_data) && irqd_irq_masked(&desc->irq_data))
            unmask_irq(desc);
    }
        //处理中断
    handle_irq_event(desc);
    } while ((desc->istate & IRQS_PENDING));
    goto out_unlock;

out_unlock:
    raw_spin_unlock(&desc->lock);
}

CPU B在ACK应答后进入这个函数,它需要知道当前有无其他CPU正在处理这个中断源(通过IRQD_IRQ_INPROGRESS标志),如果有(设为CPU A),那么CPU B将转化为软件琐存的角色,设置IRQS_PENDING标志位,并屏蔽(mask)该中断源。

如果当前没有其他CPU在处理,CPU B还需要看下这个中断源上,驱动有没有注册第二级的处理函数(desc->action),没有的话也是没法往下走的,也是只能pending并mask。

imagea8e38aa49c669811.png

屏蔽的操作对不同的中断控制器来说有所不同,而中断控制器在Linux中被抽象成了irq_chip结构体,GIC对应的也由一个irq_chip的实例"gic_chip"表示。

mask_irq()实际指向不同中断控制器各自注册的处理函数,在GIC的中断处理中,最终是由gic_mask_irq()来实现的中断屏蔽。在这里,"irq_chip"中那些函数指针才指向了具体的实现,有了明确的意义。

static struct irq_chip gic_chip = {
    .name            = "GICv3",
    .irq_mask        = gic_mask_irq,
    .irq_unmask        = gic_unmask_irq,
    .irq_eoi        = gic_eoi_irq,
        ...
}

需要注意的是,这里的mask针对的是正在处理的中断源,而不是整个中断控制器,所以此时除了CPU A以外,其他CPU是可以处理其他中断源上的中断的,不同的中断源对应不同的hardirq,这并不会存在嵌套性的问题。

等CPU A把上一个中断处理完,它就要去看下在此期间,这个中断源上有没有其他CPU留下的pending信息,如果有,那么首先unmask解除对该中断源的屏蔽(因为CPU这头马上就没有pending的了,又可以接收新的中断了),然后调用handle_irq_event(),开始处理这第二个中断,如此循环往复,直到不再有pending的中断。

image6e7f5b9ea787aa73.png

你有没有发现,在函数的开头有一个spinlock,既然CPU A在进这个函数的时候上了锁,那CPU B是怎么获得锁往下走的呢?

其实啊,这里的raw_spin_lock()和raw_spin_unlock()虽然在同一个函数,且分别镇守函数的入口和出口,但他两并不是一对。来看下handle_irq_event()的实现你就明白了。

irqreturn_t handle_irq_event(struct irq_desc *desc)
{
        //解除pending
    desc->istate &= ~IRQS_PENDING;
        //设置IRQD_IRQ_INPROGRESS表示自己正在处理
    irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
        //暂时不用锁了
    raw_spin_unlock(&desc->lock);

        //调用第二级处理函数
    irqreturn_t ret = handle_irq_event_percpu(desc);

        //再次上锁
    raw_spin_lock(&desc->lock);
        //处理完了
    irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
    return ret;
}

这里的raw_spin_unlock()和handle_edge_irq()函数中的raw_spin_lock()才是一对,而这里的raw_spin_lock()和handle_edge_irq()函数中的raw_spin_unlock()又是一对。在真正的处理函数handle_irq_event_percpu()执行期间,锁是打开的,这个spinlock保护的是对设置"IRQS_PENDING"和"IRQS_PENDING"这些关乎CPU之间同步的标志位的排他性。

因为Linux的这种机制已经保证了不同CPU对同一中断源对应的hardirq是不会嵌套的,所以在真正执行hardirq的时候,是不需要spinlock保护的。如果在整个handle_edge_irq()执行期间都上锁,那其他CPU就没法处理其他中断源上的中断了。

电平触发

和边沿触发不同的是,对于电平触发,在CPU应答后,此时GIC和CPU之间的连线已经回落到deassert的电平位置(低电平),按理此时应该进入Active状态。但中断源还是处在assert的电平位置(高电平),依然会触发中断,所以中断源会从Pending状态直接进入A&P状态。直到CPU将中断源的电平拉低,才会进入Active状态。

imagefe47676018e9f9a8.png

Linux中处理电平触发的核心函数是handle_level_irq():

void handle_level_irq(struct irq_desc *desc)
{
    raw_spin_lock(&desc->lock);
    /* CPU A & CPU B */
    mask_irq(desc);

    /* CPU B */
    //当前有irq在运行
    if (!irq_may_run(desc))
    goto out_unlock;

    /* CPU A */
    //没有注册处理函数 ,或irq被禁止 
    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);
}

因为CPU在应答中断控制器后,中断源还会触发中断,所以一进handle_level_irq(),就需要立刻mask掉这个中断源,直到CPU处理完毕,再unmask打开这个中断源。

根据电平触发的这一特性,中断天然地就不会形成嵌套,可是接下来还是用irq_may_run()判断了一下当前有无其他CPU正在处理该中断源上的中断。既然一进函数都已经mask了,第二个CPU怎么还可能收到这个中断源上的中断呢?

其实这是一个防错的设计,因为handle_irq_event()会调用中断的第二级处理函数,而这部分函数代码是由驱动工程师编写的,有可能在函数执行完毕后,中断源被意外的unmask了,这样CPU B就可能收到中断,这时CPU B要做的就是再次mask掉这个中断源。

由于CPU B收到的这个中断不是新产生的中断,所以不用设置IRQS_PENDING标志位,只有中断源被禁止或者中断源上没有注册第二级处理函数(desc->action)的时候,才需要pending待之后处理。

image1748bb78fd773711.png

0

评论 (0)

取消