linux kernel的中断子系统之(四):High level irq event handler

作者:linuxer 发布于:2014-8-28 20:00 分类:中断子系统

一、前言

当外设触发一次中断后,一个大概的处理过程是:

1、具体CPU architecture相关的模块会进行现场保护,然后调用machine driver对应的中断处理handler

2、machine driver对应的中断处理handler中会根据硬件的信息获取HW interrupt ID,并且通过irq domain模块翻译成IRQ number

3、调用该IRQ number对应的high level irq event handler,在这个high level的handler中,会通过和interupt controller交互,进行中断处理的flow control(处理中断的嵌套、抢占等),当然最终会遍历该中断描述符的IRQ action list,调用外设的specific handler来处理该中断

4、具体CPU architecture相关的模块会进行现场恢复。

上面的1、4这两个步骤在linux kernel的中断子系统之(六):ARM中断处理过程中已经有了较为细致的描述,步骤2在linux kernel的中断子系统之(二):irq domain介绍中介绍,本文主要描述步骤3,也就是linux中断子系统的high level irq event handler。

 

注:这份文档充满了猜测和空想,很多地方描述可能是有问题的,不过我还是把它发出来,抛砖引玉,希望可以引发大家讨论。

 

一、如何进入high level irq event handler

1、从具体CPU architecture的中断处理到machine相关的处理模块

说到具体的CPU,我们还是用ARM为例好了。对于ARM,我们在ARM中断处理文档中已经有了较为细致的描述。这里我们看看如何从从具体CPU的中断处理到machine相关的处理模块 ,其具体代码如下:

    .macro    irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
    ldr    r1, =handle_arch_irq
    mov    r0, sp
    adr    lr, BSYM(9997f)
    ldr    pc, [r1]
#else
    arch_irq_handler_default
#endif
9997:
    .endm

其实,直接从CPU的中断处理跳转到通用中断处理模块是不可能的,中断处理不可能越过interrupt controller这个层次。一般而言,通用中断处理模块会提供一些通用的中断代码处理库,然后由interrupt controller这个层次的代码调用这些通用中断处理的完成整个的中断处理过程。“interrupt controller这个层次的代码”是和硬件中断系统设计相关的,例如:系统中有多少个interrupt contrller,每个interrupt controller是如何控制的?它们是如何级联的?我们称这些相关的驱动模块为machine interrupt driver。

在上面的代码中,如果配置了MULTI_IRQ_HANDLER的话,ARM中断处理则直接跳转到一个叫做handle_arch_irq函数,如果系统中只有一个类型的interrupt controller(可能是多个interrupt controller,例如使用两个级联的GIC),那么handle_arch_irq可以在interrupt controller初始化的时候设定。代码如下:

……

if (gic_nr == 0) {
        set_handle_irq(gic_handle_irq);
}

……

gic_nr是GIC的编号,linux kernel初始化过程中,每发现一个GIC,都是会指向GIC driver的初始化函数的,不过对于第一个GIC,gic_nr等于0,对于第二个GIC,gic_nr等于1。当然handle_arch_irq这个函数指针不是per CPU的变量,是全部CPU共享的,因此,初始化一次就OK了。

当使用多种类型的interrupt controller的时候(例如HW 系统使用了S3C2451这样的SOC,这时候,系统有两种interrupt controller,一种是GPIO type,另外一种是SOC上的interrupt controller),则不适合在interrupt controller中进行设定,这时候,可以考虑在machine driver中设定。在这种情况下,handle_arch_irq 这个函数是在setup_arch函数中根据machine driver设定,具体如下:

handle_arch_irq = mdesc->handle_irq;

关于MULTI_IRQ_HANDLER这个配置项,我们可以再多说几句。当然,其实这个配置项的名字已经出卖它了。multi irq handler就是说系统中有多个irq handler,可以在run time的时候指定。为何要run time的时候,从多个handler中选择一个呢?HW interrupt block难道不是固定的吗?我的理解(猜想)是:一个kernel的image支持多个HW platform,对于不同的HW platform,在运行时检查HW platform的类型,设定不同的irq handler。

2、interrupt controller相关的代码

我们还是以2个级联的GIC为例来描述interrupt controller相关的代码。代码如下:

static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
    u32 irqstat, irqnr;
    struct gic_chip_data *gic = &gic_data[0];-----获取root GIC的硬件描述符
    void __iomem *cpu_base = gic_data_cpu_base(gic); 获取root GIC mapping到CPU地址空间的信息

    do {
        irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);---获取HW interrupt ID
        irqnr = irqstat & ~0x1c00;

        if (likely(irqnr > 15 && irqnr < 1021)) {----SPI和PPI的处理
            irqnr = irq_find_mapping(gic->domain, irqnr);---将HW interrupt ID转成IRQ number
            handle_IRQ(irqnr, regs);----处理该IRQ number
            continue;
        }
        if (irqnr < 16) {-----IPI类型的中断处理
            writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
#ifdef CONFIG_SMP
            handle_IPI(irqnr, regs);
#endif
            continue;
        }
        break;
    } while (1);
}

更多关于GIC相关的信息,请参考linux kernel的中断子系统之(七):GIC代码分析。对于ARM处理器,handle_IRQ代码如下:

void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{

……
        generic_handle_irq(irq);

……
}

3、调用high level handler

调用high level handler的代码逻辑非常简单,如下:

int generic_handle_irq(unsigned int irq)
{
    struct irq_desc *desc = irq_to_desc(irq); ---通过IRQ number获取该irq的描述符

    if (!desc)
        return -EINVAL;
    generic_handle_irq_desc(irq, desc);----调用high level的irq handler来处理该IRQ
    return 0;
}

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

 

二、理解high level irq event handler需要的知识准备

1、自动探测IRQ

一个硬件驱动可以通过下面的方法进行自动探测它使用的IRQ:

unsigned long irqs;
int irq;

irqs = probe_irq_on();--------启动IRQ自动探测
驱动那个打算自动探测IRQ的硬件产生中断
irq = probe_irq_off(irqs);-------结束IRQ自动探测

如果能够自动探测到IRQ,上面程序中的irq(probe_irq_off的返回值)就是自动探测的结果。后续程序可以通过request_threaded_irq申请该IRQ。probe_irq_on函数主要的目的是返回一个32 bit的掩码,通过该掩码可以知道可能使用的IRQ number有哪些,具体代码如下:

unsigned long probe_irq_on(void)
{

……
    for_each_irq_desc_reverse(i, desc) { ----scan 从nr_irqs-1 到0 的中断描述符
        raw_spin_lock_irq(&desc->lock);
        if (!desc->action && irq_settings_can_probe(desc)) {--------(1)
            desc->istate |= IRQS_AUTODETECT | IRQS_WAITING;-----(2)
            if (irq_startup(desc, false))
                desc->istate |= IRQS_PENDING;
        }
        raw_spin_unlock_irq(&desc->lock);
    } 
    msleep(100); --------------------------(3)


    for_each_irq_desc(i, desc) {
        raw_spin_lock_irq(&desc->lock);

        if (desc->istate & IRQS_AUTODETECT) {------------(4)
            if (!(desc->istate & IRQS_WAITING)) {
                desc->istate &= ~IRQS_AUTODETECT;
                irq_shutdown(desc);
            } else
                if (i < 32)------------------------(5)
                    mask |= 1 << i;
        }
        raw_spin_unlock_irq(&desc->lock);
    }

    return mask;
}

(1)那些能自动探测IRQ的中断描述符需要具体两个条件:

a、该中断描述符还没有通过request_threaded_irq或者其他方式申请该IRQ的specific handler(也就是irqaction数据结构)

b、该中断描述符允许自动探测(不能设定IRQ_NOPROBE)

(2)如果满足上面的条件,那么该中断描述符属于备选描述符。设定其internal state为IRQS_AUTODETECT | IRQS_WAITING。IRQS_AUTODETECT表示本IRQ正处于自动探测中。

(3)在等待过程中,系统仍然允许,各种中断依然会触发。在各种high level irq event handler中,总会有如下的代码:

desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);

这里会清除IRQS_WAITING状态。

(4)这时候,我们还没有控制那个想要自动探测IRQ的硬件产生中断,因此处于自动探测中,并且IRQS_WAITING并清除的一定不是我们期待的IRQ(可能是spurious interrupts导致的),这时候,clear IRQS_AUTODETECT,shutdown该IRQ。

(5)最大探测的IRQ是31(mask是一个32 bit的value),mask返回的是可能的irq掩码。

我们再来看看probe_irq_off的代码:

int probe_irq_off(unsigned long val)
{
    int i, irq_found = 0, nr_of_irqs = 0;
    struct irq_desc *desc;

    for_each_irq_desc(i, desc) {
        raw_spin_lock_irq(&desc->lock);

        if (desc->istate & IRQS_AUTODETECT) {----只有处于IRQ自动探测中的描述符才会被处理
            if (!(desc->istate & IRQS_WAITING)) {----找到一个潜在的中断描述符
                if (!nr_of_irqs)
                    irq_found = i;
                nr_of_irqs++;
            }
            desc->istate &= ~IRQS_AUTODETECT; ----IRQS_WAITING没有被清除,说明该描述符
            irq_shutdown(desc);                                     不是自动探测的那个,shutdown之
        }
        raw_spin_unlock_irq(&desc->lock);
    }
    mutex_unlock(&probing_active);

    if (nr_of_irqs > 1) ------如果找到多于1个的IRQ,说明探测失败,返回负的IRQ个数信息
        irq_found = -irq_found;

    return irq_found;
}

因为在调用probe_irq_off已经触发了自动探测IRQ的那个硬件中断,因此在该中断的high level handler的执行过程中,该硬件对应的中断描述符的IRQS_WAITING标致应该已经被清除,因此probe_irq_off函数scan中断描述符DB,找到处于auto probe中,而且IRQS_WAITING标致被清除的那个IRQ。如果找到一个,那么探测OK,返回该IRQ number,如果找到多个,说明探测失败,返回负的IRQ个数信息,没有找到的话,返回0。


2、resend一个中断

一个ARM SOC总是有很多的GPIO,有些GPIO可以提供中断功能,这些GPIO的中断可以配置成level trigger或者edge trigger。一般而言,大家都更喜欢用level trigger的中断。有的SOC只能是有限个数的GPIO可以配置成电平中断,因此,在项目初期进行pin define的时候,大家都在争抢电平触发的GPIO。

电平触发的中断有什么好处呢?电平触发的中断很简单、直接,只要硬件检测到硬件事件(例如有数据到来),其assert指定的电平信号,CPU ack该中断后,电平信号消失。但是对于边缘触发的中断,它是用一个上升沿或者下降沿告知硬件的状态,这个状态不是一个持续的状态,如果软件处理不好,容易丢失中断。

什么时候会resend一个中断呢?我们考虑一个简单的例子:

(1)CPU A上正在处理x外设的中断

(2)x外设的中断再次到来(CPU A已经ack该IRQ,因此x外设的中断可以再次触发),这时候其他CPU会处理它(mask and ack),并设置该中断描述符是pending状态,并委托CPU A处理该pending状态的中断。需要注意的是CPU已经ack了该中断,因此该中断的硬件状态已经不是pending状态,无法触发中断了,这里的pending状态是指中断描述符的软件状态。

(3)CPU B上由于同步的需求,disable了x外设的IRQ,这时候,CPU A没有处理pending状态的x外设中断就离开了中断处理过程。

(4)当enable x外设的IRQ的时候,需要检测pending状态以便resend该中断,否则,该中断会丢失的

具体代码如下:

void check_irq_resend(struct irq_desc *desc, unsigned int irq)
{

    if (irq_settings_is_level(desc)) {-------电平中断不存在resend的问题
        desc->istate &= ~IRQS_PENDING;
        return;
    }
    if (desc->istate & IRQS_REPLAY)----如果已经设定resend的flag,退出就OK了,这个应该
        return;                                                和irq的enable disable能多层嵌套相关
    if (desc->istate & IRQS_PENDING) {-------如果有pending的flag则进行处理
        desc->istate &= ~IRQS_PENDING;
        desc->istate |= IRQS_REPLAY; ------设置retrigger标志

        if (!desc->irq_data.chip->irq_retrigger ||
            !desc->irq_data.chip->irq_retrigger(&desc->irq_data)) {----调用底层irq chip的callback
#ifdef CONFIG_HARDIRQS_SW_RESEND
也可以使用软件手段来完成resend一个中断,具体代码省略,有兴趣大家可以自己看看
#endif
        }
    }
}

在各种high level irq event handler中,总会有如下的代码:

desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);

这里会清除IRQS_REPLAY状态,表示该中断已经被retrigger,一次resend interrupt的过程结束。

 

3、unhandled interrupt和spurious interrupt

在中断处理的最后,总会有一段代码如下:

irqreturn_t
handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
{

……

    if (!noirqdebug)
        note_interrupt(irq, desc, retval);
    return retval;
}

note_interrupt就是进行unhandled interrupt和spurious interrupt处理的。对于这类中断,linux kernel有一套复杂的机制来处理,你可以通过command line参数(noirqdebug)来控制开关该功能。

当发生了一个中断,但是没有被处理(有两种可能,一种是根本没有注册的specific handler,第二种是有handler,但是handler否认是自己对应的设备触发的中断),怎么办?毫无疑问这是一个异常状况,那么kernel是否要立刻采取措施将该IRQ disable呢?也不太合适,毕竟interrupt request信号线是允许共享的,直接disable该IRQ有可能会下手太狠,kernel采取了这样的策略:如果该IRQ触发了100,000次,但是99,900次没有处理,在这种条件下,我们就是disable这个interrupt request line。多么有情有义的策略啊!相关的控制数据在中断描述符中,如下:

struct irq_desc {
……
    unsigned int        irq_count;--------记录发生的中断的次数,每100,000则回滚
    unsigned long        last_unhandled;-----上一次没有处理的IRQ的时间点
    unsigned int        irqs_unhandled;------没有处理的次数
……
}

irq_count和irqs_unhandled都是比较直观的,为何要记录unhandled interrupt发生的时间呢?我们来看具体的代码。具体的相关代码位于note_interrupt中,如下:

void note_interrupt(unsigned int irq, struct irq_desc *desc,  irqreturn_t action_ret)
{
    if (desc->istate & IRQS_POLL_INPROGRESS ||  irq_settings_is_polled(desc))
        return;


    if (action_ret == IRQ_WAKE_THREAD)----handler返回IRQ_WAKE_THREAD是正常情况
        return;

    if (bad_action_ret(action_ret)) {-----报告错误,这些是由于specific handler的返回错误导致的
        report_bad_irq(irq, desc, action_ret);
        return;
    }

    if (unlikely(action_ret == IRQ_NONE)) {-------是unhandled interrupt
        if (time_after(jiffies, desc->last_unhandled + HZ/10))---(1)
            desc->irqs_unhandled = 1;---重新开始计数
        else
            desc->irqs_unhandled++;---判定为unhandled interrupt,计数加一
        desc->last_unhandled = jiffies;-------保存本次unhandled interrupt对应的jiffies时间
    }

if (unlikely(try_misrouted_irq(irq, desc, action_ret))) {---是否启动Misrouted IRQ fixup
    int ok = misrouted_irq(irq);
    if (action_ret == IRQ_NONE)
        desc->irqs_unhandled -= ok;
}

    desc->irq_count++;
    if (likely(desc->irq_count < 100000))-----------(2)
        return;

    desc->irq_count = 0;
    if (unlikely(desc->irqs_unhandled > 99900)) {--------(3)

        __report_bad_irq(irq, desc, action_ret);---报告错误

        desc->istate |= IRQS_SPURIOUS_DISABLED;
        desc->depth++;
        irq_disable(desc);

        mod_timer(&poll_spurious_irq_timer,----------(4)
              jiffies + POLL_SPURIOUS_IRQ_INTERVAL);
    }
    desc->irqs_unhandled = 0;
}

(1)是否是一次有效的unhandled interrupt还要根据时间来判断。一般而言,当硬件处于异常状态的时候往往是非常短的时间触发非常多次的中断,如果距离上次unhandled interrupt的时间超过了10个jiffies(如果HZ=100,那么时间就是100ms),那么我们要把irqs_unhandled重新计数。如果不这么处理的话,随着时间的累计,最终irqs_unhandled可能会达到99900次的,从而把这个IRQ错误的推上了审判台。

(2)irq_count每次都会加一,记录IRQ被触发的次数。但只要大于100000才启动 step (3)中的检查。一旦启动检查,irq_count会清零,irqs_unhandled也会清零,进入下一个检查周期。

(3)如果满足条件(IRQ触发了100,000次,但是99,900次没有处理),disable该IRQ。

(4)启动timer,轮询整个系统中的handler来处理这个中断(轮询啊,绝对是真爱啊)。这个timer的callback函数定义如下:

static void poll_spurious_irqs(unsigned long dummy)
{
    struct irq_desc *desc;
    int i;

    if (atomic_inc_return(&irq_poll_active) != 1)----确保系统中只有一个excuting thread进入临界区
        goto out;
    irq_poll_cpu = smp_processor_id(); ----记录当前正在polling的CPU

    for_each_irq_desc(i, desc) {------遍历所有的中断描述符
        unsigned int state;

        if (!i)-------------越过0号中断描述符。对于X86,这是timer的中断
             continue;

        /* Racy but it doesn't matter */
        state = desc->istate;
        barrier();
        if (!(state & IRQS_SPURIOUS_DISABLED))----名花有主的那些就不必考虑了
            continue;

        local_irq_disable();
        try_one_irq(i, desc, true);---------OK,尝试一下是不是可以处理
        local_irq_enable();
    }
out:
    atomic_dec(&irq_poll_active);
    mod_timer(&poll_spurious_irq_timer,--------一旦触发了该timer,就停不下来
          jiffies + POLL_SPURIOUS_IRQ_INTERVAL);
}

 

三、和high level irq event handler相关的硬件描述

1、CPU layer和Interrupt controller之间的接口

从逻辑层面上看,CPU和interrupt controller之间的接口包括:

(1)触发中断的signal。一般而言,这个(些)信号是电平触发的。对于ARM CPU,它是nIRQ和nFIQ信号线,对于X86,它是INT和NMI信号线,对于PowerPC,这些信号线包括MC(machine check)、CRIT(critical interrupt)和NON-CRIT(Non critical interrupt)。对于linux kernel的中断子系统,我们只使用其中一个信号线(例如对于ARM而言,我们只使用nIRQ这个信号线)。这样,从CPU层面看,其逻辑动作非常的简单,不区分优先级,触发中断的那个信号线一旦assert,并且CPU没有mask中断,那么软件就会转到一个异常向量执行,完毕后返回现场。

(2)Ack中断的signal。这个signal可能是物理上的一个连接CPU和interrupt controller的铜线,也可能不是。对于X86+8259这样的结构,Ack中断的signal就是nINTA信号线,对于ARM+GIC而言,这个信号就是总线上的一次访问(读Interrupt Acknowledge Register寄存器)。CPU ack中断标识cpu开启启动中断服务程序(specific handler)去处理该中断。对于X86而言,ack中断可以让8259将interrupt vector数据送到数据总线上,从而让CPU获取了足够的处理该中断的信息。对于ARM而言,ack中断的同时也就是获取了发生中断的HW interrupt ID,总而言之,ack中断后,CPU获取了足够开启执行中断处理的信息。

(3)结束中断(EOI,end of interrupt)的signal。这个signal用来标识CPU已经完成了对该中断的处理(specific handler或者ISR,interrupt serivce routine执行完毕)。实际的物理形态这里就不描述了,和ack中断signal是类似的。

(4)控制总线和数据总线接口。通过这些接口,CPU可以访问(读写)interrupt controller的寄存器。

 

2、Interrupt controller和Peripheral device之间的接口

所有的系统中,Interrupt controller和Peripheral device之间的接口都是一个Interrupt Request信号线。外设通过这个信号线上的电平或者边缘向CPU(实际上是通过interrupt controller)申请中断服务。

 

四、几种典型的high level irq event handler

本章主要介绍几种典型的high level irq event handler,在进行high level irq event handler的设定的时候需要注意,不是外设使用电平触发就选用handle_level_irq,选用什么样的high level irq event handler是和Interrupt controller的行为以及外设电平触发方式决定的。介绍每个典型的handler之前,我会简单的描述该handler要求的硬件行为,如果该外设的中断系统符合这个硬件行为,那么可以选择该handler为该中断的high level irq event handler。

1、边缘触发的handler。

使用handle_edge_irq这个handler的硬件中断系统行为如下:

xyz

我们以上升沿为例描述边缘中断的处理过程(下降沿的触发是类似的)。当interrupt controller检测到了上升沿信号,会将该上升沿状态(pending)锁存在寄存器中,并通过中断的signal向CPU触发中断。需要注意:这时候,外设和interrupt controller之间的interrupt request信号线会保持高电平,这也就意味着interrupt controller不可能检测到新的中断信号(本身是高电平,无法形成上升沿)。这个高电平信号会一直保持到软件ack该中断(调用irq chip的irq_ack callback函数)。ack之后,中断控制器才有可能继续探测上升沿,触发下一次中断。

ARM+GIC组成的系统不符合这个类型。虽然GIC提供了IAR(Interrupt Acknowledge Register)寄存器来让ARM来ack中断,但是,在调用high level handler之前,中断处理程序需要通过读取IAR寄存器获得HW interrpt ID并转换成IRQ number,因此实际上,对于GIC的irq chip,它是无法提供本场景中的irq_ack函数的。很多GPIO type的interrupt controller符合上面的条件,它们会提供pending状态寄存器,读可以获取pending状态,而向pending状态寄存器写1可以ack该中断,让interrupt controller可以继续触发下一次中断。

handle_edge_irq代码如下:

void handle_edge_irq(unsigned int irq, struct irq_desc *desc)
{
    raw_spin_lock(&desc->lock); -----------------(0)

    desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);----参考上一章的描述

    if (unlikely(irqd_irq_disabled(&desc->irq_data) ||-----------(1)
             irqd_irq_inprogress(&desc->irq_data) || !desc->action)) {
        if (!irq_check_poll(desc)) {
            desc->istate |= IRQS_PENDING;
            mask_ack_irq(desc);
            goto out_unlock;
        }
    }
    kstat_incr_irqs_this_cpu(irq, desc); ---更新该IRQ统计信息


    desc->irq_data.chip->irq_ack(&desc->irq_data); ---------(2)

    do {
        if (unlikely(!desc->action)) { -----------------(3)
            mask_irq(desc);
            goto out_unlock;
        }


        if (unlikely(desc->istate & IRQS_PENDING)) { ---------(4)
            if (!irqd_irq_disabled(&desc->irq_data) &&
                irqd_irq_masked(&desc->irq_data))
                unmask_irq(desc);
        }

        handle_irq_event(desc); -------------------(5)

    } while ((desc->istate & IRQS_PENDING) &&
         !irqd_irq_disabled(&desc->irq_data)); -------------(6)

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

(0) 这时候,中断仍然是关闭的,因此不会有来自本CPU的并发,使用raw spin lock就防止其他CPU上对该IRQ的中断描述符的访问。针对该spin lock,我们直观的感觉是raw_spin_lock和(7)中的raw_spin_unlock是成对的,实际上并不是,handle_irq_event中的代码是这样的:

irqreturn_t handle_irq_event(struct irq_desc *desc)
{

    raw_spin_unlock(&desc->lock); -------和上面的(0)对应

    处理具体的action list

    raw_spin_lock(&desc->lock);--------和上面的(7)对应

}

实际上,由于在handle_irq_event中处理action list的耗时还是比较长的,因此处理具体的action list的时候并没有持有中断描述符的spin lock。在如果那样的话,其他CPU在对中断描述符进行操作的时候需要spin的时间会很长的。

(1)判断是否需要执行下面的action list的处理。这里分成几种情况:

a、该中断事件已经被其他的CPU处理了

b、该中断被其他的CPU disable了

c、该中断描述符没有注册specific handler。这个比较简单,如果没有irqaction,根本没有必要调用action list的处理

如果该中断事件已经被其他的CPU处理了,那么我们仅仅是设定pending状态(为了委托正在处理的该中断的那个CPU进行处理),mask_ack_irq该中断并退出就OK了,并不做具体的处理。另外正在处理该中断的CPU会检查pending状态,并进行处理的。同样的,如果该中断被其他的CPU disable了,本就不应该继续执行该中断的specific handler,我们也是设定pending状态,mask and ack中断就退出了。当其他CPU的代码离开临界区,enable 该中断的时候,软件会检测pending状态并resend该中断。

这里的irq_check_poll代码如下:

static bool irq_check_poll(struct irq_desc *desc)
{
    if (!(desc->istate & IRQS_POLL_INPROGRESS))
        return false;
    return irq_wait_for_poll(desc);
}

IRQS_POLL_INPROGRESS标识了该IRQ正在被polling(上一章有描述),如果没有被轮询,那么返回false,进行正常的设定pending标记、mask and ack中断。如果正在被轮询,那么需要等待poll结束。

(2)ack该中断。对于中断控制器,一旦被ack,表示该外设的中断被enable,硬件上已经准备好触发下一次中断了。再次触发的中断会被调度到其他的CPU上。现在,我们可以再次回到步骤(1)中,为什么这里用mask and ack而不是单纯的ack呢?如果单纯的ack则意味着后续中断还是会触发,这时候怎么处理?在pending+in progress的情况下,我们要怎么处理?记录pending的次数,有意义吗?由于中断是完全异步的,也有可能pending的标记可能在另外的CPU上已经修改为replay的标记,这时候怎么办?当事情变得复杂的时候,那一定是本来方向就错了,因此,mask and ack就是最好的策略,我已经记录了pending状态,不再考虑pending嵌套的情况。

(3)在调用specific handler处理具体的中断的时候,由于不持有中断描述符的spin lock,因此其他CPU上有可能会注销其specific handler,因此do while循环之后,desc->action有可能是NULL,如果是这样,那么mask irq,然后退出就OK了

(4)如果中断描述符处于pending状态,那么一定是其他CPU上又触发了该interrupt source的中断,并设定了pending状态,“委托”本CPU进行处理,这时候,需要把之前mask住的中断进行unmask的操作。一旦unmask了该interrupt source,后续的中断可以继续触发,由其他的CPU处理(仍然是设定中断描述符的pending状态,委托当前正在处理该中断请求的那个CPU进行处理)。

(5)处理该中断请求事件

irqreturn_t handle_irq_event(struct irq_desc *desc)
{
    struct irqaction *action = desc->action;
    irqreturn_t ret;

    desc->istate &= ~IRQS_PENDING;----CPU已经准备处理该中断了,因此,清除pending状态
    irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);--设定INPROGRESS的flag
    raw_spin_unlock(&desc->lock);

    ret = handle_irq_event_percpu(desc, action); ---遍历action list,调用specific handler

    raw_spin_lock(&desc->lock);
    irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);---处理完成,清除INPROGRESS标记
    return ret;
}

(6)只要有pending标记,就说明该中断还在pending状态,需要继续处理。当然,如果有其他的CPU disable了该interrupt source,那么本次中断结束处理。

 

2、电平触发的handler

使用handle_level_irq这个handler的硬件中断系统行为如下:

level

我们以高电平触发为例。当interrupt controller检测到了高电平信号,并通过中断的signal向CPU触发中断。这时候,对中断控制器进行ack并不能改变interrupt request signal上的电平状态,一直要等到执行具体的中断服务程序(specific handler),对外设进行ack的时候,电平信号才会恢复成低电平。在对外设ack之前,中断状态一直是pending的,如果没有mask中断,那么中断控制器就会assert CPU。

handle_level_irq的代码如下:

void handle_level_irq(unsigned int irq, struct irq_desc *desc)
{
    raw_spin_lock(&desc->lock);
    mask_ack_irq(desc); ---------------------(1)

    if (unlikely(irqd_irq_inprogress(&desc->irq_data)))---------(2)
        if (!irq_check_poll(desc))
            goto out_unlock;

    desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);--和retrigger中断以及自动探测IRQ相关
    kstat_incr_irqs_this_cpu(irq, desc);


    if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {-----(3)
        desc->istate |= IRQS_PENDING;
        goto out_unlock;
    }

    handle_irq_event(desc);

    cond_unmask_irq(desc); --------------(4)

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

(1)考虑CPU<------>interrupt controller<------>device这样的连接方式中,我们认为high level handler主要是和interrupt controller交互,而specific handler(request_irq注册的那个)是和device进行交互。Level类型的中断的特点就是只要外设interrupt request line的电平状态是有效状态,对于interrupt controller,该外设的interrupt总是active的。由于外设检测到了事件(比如数据到来了),因此assert了指定的电平信号,这个电平信号会一直保持,直到软件清除了外设的状态寄存器。但是,high level irq event handler这个层面只能操作Interrupt controller,不能操作具体外设的寄存器(那应该属于具体外设的specific interrupt handler处理内容,该handler会挂入中断描述符中的IRQ action list)。直到在具体的中断服务程序(specific handler中)操作具体外设的寄存器,才能让这个asserted电平信号消息。

正是因为level trigger的这个特点,因此,在high level handler中首先mask并ack该IRQ。这一点和边缘触发的high level handler有显著的不同,在handle_edge_irq中,我们仅仅是ack了中断,并没有mask,因为边缘触发的中断稍纵即逝,一旦mask了该中断,容易造成中断丢失。而对于电平中断,我们不得不mask住该中断,如果不mask住,只要CPU ack中断,中断控制器将持续的assert CPU中断(因为有效电平状态一直保持)。如果我们mask住该中断,中断控制器将不再转发该interrupt source来的中断,因此,所有的CPU都不会感知到该中断,直到软件unmask。这里的ack是针对interrupt controller的ack,本身ack就是为了clear interrupt controller对该IRQ的状态寄存器,不过由于外部的电平仍然是有效信号,其实未必能清除interrupt controller的中断状态,不过这是和中断控制器硬件实现相关的。

(2)对于电平触发的high level handler,我们一开始就mask并ack了中断,因此后续specific handler因该是串行化执行的,为何要判断in progress标记呢?不要忘记spurious interrupt,那里会直接调用handler来处理spurious interrupt。

(3)这里有两个场景

a、没有注册specific handler。如果没有注册handler,那么保持mask并设定pending标记(这个pending标记有什么作用还没有想明白)。

b、该中断被其他的CPU disable了。如果该中断被其他的CPU disable了,本就不应该继续执行该中断的specific handler,我们也是设定pending状态,mask and ack中断就退出了。当其他CPU的代码离开临界区,enable 该中断的时候,软件会检测pending状态并resend该中断。

(4)为何是有条件的unmask该IRQ?正常的话当然是umask就OK了,不过有些threaded interrupt(这个概念在下一份文档中描述)要求是one shot的(首次中断,specific handler中开了一枪,wakeup了irq handler thread,如果允许中断嵌套,那么在specific handler会多次开枪,这也就不是one shot了,有些IRQ的handler thread要求是one shot,也就是不能嵌套specific handler)。

 

3、支持EOI的handler

TODO

原创文章,转发请注明出处。蜗窝科技。http://www.wowotech.net/linux_kenrel/High_level_irq_event_handler.html

标签: 中断处理

评论:

linuxer_fy
2017-11-30 10:45
@linuxer
针对handle_edge_irq的疑问 (版本3.4.x)
进入中断时CPU自动关闭本地中断
handle_edge_irq{
  raw_spin_lock(&desc->lock);
  
  do -while

  raw_spin_unlock(&desc->lock);
}
由于当前CPU关闭中断,所以本CPU上不会再次进入handle_edge_irq,又由于有spin的限制,其他CPU也不会再次进入handle_edge_irq,如此一来,什么情况下会产生设定pending状态的case??

    if (unlikely(irqd_irq_disabled(&desc->irq_data) ||
             irqd_irq_inprogress(&desc->irq_data) || !desc->action)) {
        if (!irq_check_poll(desc)) {
            desc->istate |= IRQS_PENDING;
            mask_ack_irq(desc);
            goto out_unlock;
        }
    }


另外linuxer在讨论这一块内容时,是不是假定HW interrupt ID与IRQ number一一对应?
如果情况是在同一个irq domain中,两个HW interrupt ID对应一个IRQ number呢?
当需要执行mask时,mask_ack_irq(desc);的结果是否是正确mask到某个HW interrupt ID呢?
我看 struct irq_desc -> struct irq_data -> hwirq这个成员是单的
如果某个 irq_desc是被多个HW interrupt共用的情况下,mask的动作是什么样的?
linuxer本文中提到的mask,比较详细的说法应该是:mask该中断控制器上的某个HW interrupt ID吧。
而没有mask IRQ number一说。
zgh
2020-11-30 16:34
@linuxer_fy:不存在同一个irq domain中,两个HW interrupt ID对应一个IRQ number
linuxer_fy
2017-10-30 10:16
"什么时候会resend一个中断呢?我们考虑一个简单的例子:
(1)CPU A上正在处理x外设的中断
(2)x外设的中断再次到来(CPU A已经ack该IRQ,因此x外设的中断可以再次触发),这时候其他CPU会处理它(mask and ack),并设置该中断描述符是pending状态,并委托CPU A处理该pending状态的中断。需要注意的是CPU已经ack了该中断,因此该中断的硬件状态已经不是pending状态,无法触发中断了,这里的pending状态是指中断描述符的软件状态。

针对(2)。之前你提到过同一个IRQ上的中断是不会嵌套的。
那和这里的描述是否有冲突?
在(1)中CPU A刚开始处理x外设中断时,不应该也是mask and ack吗。如此一来x外设应该不会再产生中断才是。
linuxer_fy
2017-10-30 10:58
@linuxer_fy:追加:根据handle_edge_irq的代码来看

A. 产生x外设的中断,如果是”首次“,则只是调用ack成员函数(让外设可以重新产生中断)
B. 再产生x外设的中断,由于A中某CPU正在处理,所以B产生的中断只是设置pending,同时调用mask_and_ack(让外设禁止产生中断)

如此看来,同一个IRQ的中断还是可以嵌套的。这和linuxer之前的说法:在旧的内核中允许中断嵌套,但是嵌套永远都是发生在不同的irq之间,同一种irq,无论过去还是现在都不会嵌套。"有出入了吧。

注:有些ack成员函数做的内容其实是mask+ack(比如X86上的8259A芯片),如果是这种case,则同一个IRQ上是不会产生中断嵌套的。
Hisenberg
2017-09-21 14:26
感谢linuxer大神的分享。关于probe_irq_on/off有个疑问。我自己的理解,这两个API是为了让驱动程序在不知道自己的硬件使用的中断线的情况下,找到对应的IRQ number,随后进行specific handler的注册。但是在probe过程中,需要该IRQ number已经分配了irq_desc,并且注册了high-level irq handler。我的问题是:1. 对于ARM平台,现在基本上都是用DTS来告知系统,每个HW module使用的HW Interrupt ID信息,这是否意味着,这类设备都是无需进行irq probe的?2. 如果需要probe,那是不是要求对所有的IRQ number都预先分配了irq_desc并注册了high-level irq handler?对于那些动态分配的方式是不可以进行探测的吗?理解的不深刻,如有谬误,还望指正。谢谢!
每天一小步
2020-07-21 11:58
@Hisenberg:同样有这些疑惑,现在你搞懂了吗?linuxer能解答一下不?谢谢
cade
2017-03-21 20:09
@linuxer:
向大神请教下
disable_irq_nosync(data->irq);
enable_irq(data->irq);
== > 是操作 interrupt controller还是操作cpu?
linuxer
2017-03-21 22:19
@cade:操作interrupt controller
理查德1030
2016-12-07 12:10
hi,linuxer:
    request_irq()用irq:176,但在中断触发的时候确变成了184(下一个GPIO bank),导致irq_flow_handler_t获取寄存器地址到184对应的bank,两个gpio bank用同一硬件中断号75.

               |--(76)---pl061 A
cpu----GIC-----|                    |-----bank 1, [176,+8]
               |--(75)---pl061 B----|
                                    |-----bank 2, [184,+8]
不知是在那个环节出问题了???

1、注册分配过程:
[pl061_probe:315] irq:75, irq_base:0
irq: irq_create_mapping(0xc8dd2c00, 0x0)
irq: -> using domain @c8dd2c00
[__irq_alloc_descs:444] irq:-1, from:1, cnt:1, node:-1
[__irq_alloc_descs:477] start:176, from:1, cnt:1, nr_irqs:177
irq: irq 0 on domain /soc/amba/gpio@12146000 mapped to virtual irq 176

2、中断请求过程:
[request_threaded_irq:1637] irq:176, retval:0
[pl061_irq_type;146] offset:0, trigger:8, chip.base:0xfea46000
[pl061_irq_unmask;241] irqd_to_hwirq:1, mask:1

3、中断低电平触发
[generic_handle_irq:353] hwirq:75, irq:75
[pl061_irq_handler:197] irq_base:184, gpio_base:448
[pl061_irq_handler:200] irq:75, GPIOMIS:0, GPIOIC:0
[pl061_irq_handler:201] chip->base:fea47000, GPIOIS:0, GPIOIE:0
[pl061_irq_handler:202] offset:0, GPIOIEV:0, GPIORIS:253
[pl061_irq_handler;211] irqnumber:184, offset:0, pending:0
/*pending手动置1后的进入high level handler */
[generic_handle_irq:352] irq:184, parent_irq:75
[generic_handle_irq:353] hwirq:0, irq:184
[handle_simple_irq;393] irq:184, desc->action:  (null)
4、中断注册状态
~ # cat /proc/interrupts
           CPU0       CPU1      
29:       3425       1009       GIC  29  arch_timer
......
96:          0      43018       GIC  96  timer
176:          0          0     pl061   0  GPIO
IPI0:          0          0  CPU wakeup interrupts
xie1230
2016-06-01 11:26
hi,linuxer!
我把这一个系列看完了,回头第2遍看这一篇文章了,发现一个问题不解。GIC驱动一篇中介绍说GIC是可以配置中断类型为电平触发,或者边缘触发的。
但是这一篇文章中只介绍的两个high level应该是只支持其中的某一种触发方式的场景。
1.GIC如何做到同时支持电平和边缘触发两种情景的呢?
2.本文中还提到说边缘触发场景不适合GIC,那么如果电平触发适合GIC,GIC又是怎么处理边缘中断的?
3.我看到GIC驱动里面设置的是handle_fasteoi_irq这个handler,和上面介绍的两种有什么区别?
求大神解惑
linuxer
2016-06-01 19:04
@xie1230:GIC如何做到同时支持电平和边缘触发两种情景的呢?
------------------------
GIC可以支持电平和边缘触发,但是不可能同时

本文中还提到说边缘触发场景不适合GIC,那么如果电平触发适合GIC,GIC又是怎么处理边缘中断的?
------------------------
请帮忙把原文贴出来,这份文档有些久远,我有些忘记了

我看到GIC驱动里面设置的是handle_fasteoi_irq这个handler,和上面介绍的两种有什么区别?
------------------------
这个需要硬件支持EOI,GIC是支持EOI的,因此其high level handler就是handle_fasteoi_irq
xie1230
2016-06-02 08:54
@linuxer:原文在边缘中断的介绍里面,有如下这么一段:
“ARM+GIC组成的系统不符合这个类型。虽然GIC提供了IAR(Interrupt Acknowledge Register)寄存器来让ARM来ack中断,但是,在调用high level handler之前,中断处理程序需要通过读取IAR寄存器获得HW interrpt ID并转换成IRQ number,因此实际上,对于GIC的irq chip,它是无法提供本场景中的irq_ack函数的。很多GPIO type的interrupt controller符合上面的条件,它们会提供pending状态寄存器,读可以获取pending状态,而向pending状态寄存器写1可以ack该中断,让interrupt controller可以继续触发下一次中断。 ”
可以看出边缘中断的high level handler是无法给GIC使用的,那么GIC又是如何处理这种类型的中断呢?在handle_fasteoi_irq里面是怎么处理的呢?也许这个就是TODO部分没有介绍的内容吧
linuxer
2016-06-03 09:01
@xie1230:在中断子系统中定义了很多high level handler,例如handle_edge_irq、handle_level_irq、handle_fasteoi_irq,具体选择哪一个是和该interrupt controller的type相关的,对于GIC,我们选择了handle_fasteoi_irq
xie1230
2016-05-19 11:35
linuxer君写的真是太好了
madang
2015-11-13 15:52
问个问题:
在arm 平台上,针对spi 和 ppi有不同的处理函数
spi:
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;
}
这里有个设置和清除 IRQD_IRQ_INPROGRESS 标记的代码,但是对于ppi的处理函数
handle_percpu_devid_irq(),这里却没有类似的处理函数, 为什么? 还有一个问题,我看了一下这个handle_percpu_devid_irq()的解释, 貌似percou的中断,这里不能加锁 ,为什么?
linuxer
2015-11-14 00:10
@madang:SPI和PPI有显著的不同,SPI可以被分发到系统中的任何一个cpu上去处理,而PPI是送达指定的CPU,你只要思考一下中断处理代码的并发情况就知道内核为何如此处理了。

percou的中断不是不能加锁,而是不需要加锁。
ezword
2015-10-11 23:43
time_after(jiffies, desc->last_unhandled + HZ/10),
这里应该是1/10 s,既100ms吧,而不是您上面所说的10s吧。
linuxer
2015-10-12 10:52
@ezword:多谢指正,您说的是对的。
buyit
2015-04-30 20:22
对level 类型中断的irqs_pending的解读如下:

Level irq的IRQS_PENDING状态来自于下面这个patch:
commit d4dc0f90d243fb54cfbca6601c9a7c5a758e437f
Author: Thomas Gleixner <tglx@linutronix.de>
Date:   Wed Apr 25 12:54:54 2012 +0200

    genirq: Allow check_wakeup_irqs to notice level-triggered interrupts
    
    Level triggered interrupts do not cause IRQS_PENDING to be set when
    they fire while "disabled" as the 'pending' state is always present in
    the level - they automatically refire where re-enabled.
    
    However the IRQS_PENDING flag is also used to abort a suspend cycle -
    if any 'is_wakeup_set' interrupt is PENDING, check_wakeup_irqs() will
    cause suspend to abort. Without IRQS_PENDING, suspend won't abort.
    
    Consequently, level-triggered interrupts that fire during the 'noirq'
    phase of suspend do not currently abort suspend.
    
    So set IRQS_PENDING even for level triggered interrupts, and make sure
    to clear the flag in check_irq_resend.

从注释里面可以看出来:
•    这个patch是为了解决一个level irq suspend状态错误的问题。
•    如果这是一个edge irq,假设在中断上半部disable_irq(),然后启动workqueue处理下半部,假设这个时候来了第二个同样的edge irq,系统设置IRQS_PENDING状态之后退出来,假设workqueue运行到一半还没有结束的时候系统进入suspend流程,那么在函数check_wakeup_irqs()会判断这个irq,如果这个irq是一个可以唤醒系统的中断,系统就不会suspend,而是直接退出suspend流程。
•    如果这是一个level irq,kernel之前的处理办法是先mask当前正在处理的这个irq,如果有相同的irq第二次进来,假设当前irq已经被disable了,那么就什么也不做直接退出。这样的结果是如果系统接下来进入suspend流程,假设这个irq是可以唤醒系统的,它的状态不是pending,所以系统真正进入suspend。这样子从逻辑上面来讲就是错误的,因为一个可以唤醒系统的中断源已经有效了,系统不应该进入suspend。
•    为了解决这个bug,在level irq重入的时候,如果状态已经是disable了,就设置IRQS_PENDING,然后在check_wakeup_irqs函数里面一旦检测到这种状态之后就会退出suspend流程。当最后enable_irq()函数被呼叫的时候,通过呼叫check_irq_resend()来去掉level irq的pending位。
linuxer
2015-05-03 22:43
@buyit:先标记一下,陪孩子玩了一天,累死了,脑袋转不动了,明天再理解。anyway,多谢分享!

发表评论:

Copyright @ 2013-2015 蜗窝科技 All rights reserved. Powered by emlog