linux kernel的中断子系统之(三):IRQ number和中断描述符

作者:linuxer 发布于:2014-8-26 17:03 分类:中断子系统

一、前言

本文主要围绕IRQ number和中断描述符(interrupt descriptor)这两个概念描述通用中断处理过程。第二章主要描述基本概念,包括什么是IRQ number,什么是中断描述符等。第三章描述中断描述符数据结构的各个成员。第四章描述了初始化中断描述符相关的接口API。第五章描述中断描述符相关的接口API。

二、基本概念

1、通用中断的代码处理示意图

一个关于通用中断处理的示意图如下:

zhongduan

在linux kernel中,对于每一个外设的IRQ都用struct irq_desc来描述,我们称之中断描述符(struct irq_desc)。linux kernel中会有一个数据结构保存了关于所有IRQ的中断描述符信息,我们称之中断描述符DB(上图中红色框图内)。当发生中断后,首先获取触发中断的HW interupt ID,然后通过irq domain翻译成IRQ nuber,然后通过IRQ number就可以获取对应的中断描述符。调用中断描述符中的highlevel irq-events handler来进行中断处理就OK了。而highlevel irq-events handler主要进行下面两个操作:

(1)调用中断描述符的底层irq chip driver进行mask,ack等callback函数,进行interrupt flow control。

(2)调用该中断描述符上的action list中的specific handler(我们用这个术语来区分具体中断handler和high level的handler)。这个步骤不一定会执行,这是和中断描述符的当前状态相关,实际上,interrupt flow control是软件(设定一些标志位,软件根据标志位进行处理)和硬件(mask或者unmask interrupt controller等)一起控制完成的。

 

2、中断的打开和关闭

我们再来看看整个通用中断处理过程中的开关中断情况,开关中断有两种:

(1)开关local CPU的中断。对于UP,关闭CPU中断就关闭了一切,永远不会被抢占。对于SMP,实际上,没有关全局中断这一说,只能关闭local CPU(代码运行的那个CPU)

(2)控制interrupt controller,关闭某个IRQ number对应的中断。更准确的术语是mask或者unmask一个 IRQ。

本节主要描述的是第一种,也就是控制CPU的中断。当进入high level handler的时候,CPU的中断是关闭的(硬件在进入IRQ processor mode的时候设定的)。

对于外设的specific handler,旧的内核(2.6.35版本之前)认为有两种:slow handler和fast handle。在request irq的时候,对于fast handler,需要传递IRQF_DISABLED的参数,确保其中断处理过程中是关闭CPU的中断,因为是fast handler,执行很快,即便是关闭CPU中断不会影响系统的性能。但是,并不是每一种外设中断的handler都是那么快(例如磁盘),因此就有slow handler的概念,说明其在中断处理过程中会耗时比较长。对于这种情况,如果在整个specific handler中关闭CPU中断,对系统的performance会有影响。因此,对于slow handler,在从high level handler转入specific handler中间会根据IRQF_DISABLED这个flag来决定是否打开中断,具体代码如下(来自2.6.23内核):

irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
{
    ……

    if (!(action->flags & IRQF_DISABLED))
        local_irq_enable_in_hardirq();

    ……
}

如果没有设定IRQF_DISABLED(slow handler),则打开本CPU的中断。然而,随着软硬件技术的发展:

(1)硬件方面,CPU越来越快,原来slow handler也可以很快执行完毕

(2)软件方面,linux kernel提供了更多更好的bottom half的机制

因此,在新的内核中,比如3.14,IRQF_DISABLED被废弃了。我们可以思考一下,为何要有slow handler?每一个handler不都是应该迅速执行完毕,返回中断现场吗?此外,任意中断可以打断slow handler执行,从而导致中断嵌套加深,对内核栈也是考验。因此,新的内核中在interrupt specific handler中是全程关闭CPU中断的。


3、IRQ number

从CPU的角度看,无论外部的Interrupt controller的结构是多么复杂,I do not care,我只关心发生了一个指定外设的中断,需要调用相应的外设中断的handler就OK了。更准确的说是通用中断处理模块不关心外部interrupt controller的组织细节(电源管理模块当然要关注具体的设备(interrupt controller也是设备)的拓扑结构)。一言以蔽之,通用中断处理模块可以用一个线性的table来管理一个个的外部中断,这个表的每个元素就是一个irq描述符,在kernel中定义如下:

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 request line)用一个中断描述符来描述,每一个外设的interrupt request line分配一个中断号(irq number),系统中有多少个中断线(或者叫做中断源)就有多少个中断描述符(struct irq_desc)。NR_IRQS定义了该硬件平台IRQ的最大数目。

总之,一个静态定义的表格,irq number作为index,每个描述符都是紧密的排在一起,一切看起来很美好,但是现实很残酷的。有些系统可能会定义一个很大的NR_IRQS,但是只是想用其中的若干个,换句话说,这个静态定义的表格不是每个entry都是有效的,有空洞,如果使用静态定义的表格就会导致了内存很大的浪费。为什么会有这种需求?我猜是和各个interrupt controller硬件的interrupt ID映射到irq number的算法有关。在这种情况下,静态表格不适合了,我们改用一个radix tree来保存中断描述符(HW interrupt作为索引)。这时候,每一个中断描述符都是动态分配,然后插入到radix tree中。如果你的系统采用这种策略,那么需要打开CONFIG_SPARSE_IRQ选项。上面的示意图描述的是静态表格的中断描述符DB,如果打开CONFIG_SPARSE_IRQ选项,系统使用Radix tree来保存中断描述符DB,不过概念和静态表格是类似的。

此外,需要注意的是,在旧内核中,IRQ number和硬件的连接有一定的关系,但是,在引入irq domain后,IRQ number已经变成一个单纯的number,和硬件没有任何关系。

 

三、中断描述符数据结构

1、底层irq chip相关的数据结构

中断描述符中应该会包括底层irq chip相关的数据结构,linux kernel中把这些数据组织在一起,形成struct irq_data,具体代码如下:

struct irq_data {
    u32            mask;----------TODO
    unsigned int        irq;--------IRQ number
    unsigned long        hwirq;-------HW interrupt ID
    unsigned int        node;-------NUMA node index
    unsigned int        state_use_accessors;--------底层状态,参考IRQD_xxxx
    struct irq_chip        *chip;----------该中断描述符对应的irq chip数据结构
    struct irq_domain    *domain;--------该中断描述符对应的irq domain数据结构
    void            *handler_data;--------和外设specific handler相关的私有数据
    void            *chip_data;---------和中断控制器相关的私有数据
    struct msi_desc        *msi_desc;
    cpumask_var_t        affinity;-------和irq affinity相关
};

中断有两种形态,一种就是直接通过signal相连,用电平或者边缘触发。另外一种是基于消息的,被称为MSI (Message Signaled Interrupts)。msi_desc是MSI类型的中断相关,这里不再描述。

node成员用来保存中断描述符的内存位于哪一个memory node上。 对于支持NUMA(Non Uniform Memory Access Architecture)的系统,其内存空间并不是均一的,而是被划分成不同的node,对于不同的memory node,CPU其访问速度是不一样的。如果一个IRQ大部分(或者固定)由某一个CPU处理,那么在动态分配中断描述符的时候,应该考虑将内存分配在该CPU访问速度比较快的memory node上。

 

2、irq chip数据结构

Interrupt controller描述符(struct irq_chip)包括了若干和具体Interrupt controller相关的callback函数,我们总结如下:

成员名字 描述
name 该中断控制器的名字,用于/proc/interrupts中的显示
irq_startup start up 指定的irq domain上的HW interrupt ID。如果不设定的话,default会被设定为enable函数
irq_shutdown shutdown 指定的irq domain上的HW interrupt ID。如果不设定的话,default会被设定为disable函数
irq_enable enable指定的irq domain上的HW interrupt ID。如果不设定的话,default会被设定为unmask函数
irq_disable disable指定的irq domain上的HW interrupt ID。
irq_ack 和具体的硬件相关,有些中断控制器必须在Ack之后(清除pending的状态)才能接受到新的中断。
irq_mask mask指定的irq domain上的HW interrupt ID
irq_mask_ack mask并ack指定的irq domain上的HW interrupt ID。
irq_unmask mask指定的irq domain上的HW interrupt ID
irq_eoi 有些interrupt controler(例如GIC)提供了这样的寄存器接口,让CPU可以通知interrupt controller,它已经处理完一个中断
irq_set_affinity 在SMP的情况下,可以通过该callback函数设定CPU affinity
irq_retrigger 重新触发一次中断,一般用在中断丢失的场景下。如果硬件不支持retrigger,可以使用软件的方法。
irq_set_type 设定指定的irq domain上的HW interrupt ID的触发方式,电平触发还是边缘触发
irq_set_wake 和电源管理相关,用来enable/disable指定的interrupt source作为唤醒的条件。
irq_bus_lock 有些interrupt controller是连接到慢速总线上(例如一个i2c接口的IO expander芯片),在访问这些芯片的时候需要lock住那个慢速bus(只能有一个client在使用I2C bus)
irq_bus_sync_unlock unlock慢速总线
irq_suspend
irq_resume
irq_pm_shutdown
电源管理相关的callback函数
irq_calc_mask TODO
irq_print_chip /proc/interrupts中的信息显示

 

3、中断描述符

在linux kernel中,使用struct irq_desc来描述一个外设的中断,我们称之中断描述符,具体代码如下:

struct irq_desc {
    struct irq_data        irq_data;
    unsigned int __percpu    *kstat_irqs;------IRQ的统计信息
    irq_flow_handler_t    handle_irq;--------(1)
    struct irqaction    *action; -----------(2)
    unsigned int        status_use_accessors;-----中断描述符的状态,参考IRQ_xxxx
    unsigned int        core_internal_state__do_not_mess_with_it;----(3)
    unsigned int        depth;----------(4)
    unsigned int        wake_depth;--------(5)
    unsigned int        irq_count;  ---------(6)
    unsigned long        last_unhandled;  
    unsigned int        irqs_unhandled;
    raw_spinlock_t        lock;-----------(7)
    struct cpumask        *percpu_enabled;-------(8)
#ifdef CONFIG_SMP
    const struct cpumask    *affinity_hint;----和irq affinity相关,后续单独文档描述
    struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
    cpumask_var_t        pending_mask;
#endif
#endif
    unsigned long        threads_oneshot; -----(9)
    atomic_t        threads_active;
    wait_queue_head_t       wait_for_threads;
#ifdef CONFIG_PROC_FS
    struct proc_dir_entry    *dir;--------该IRQ对应的proc接口
#endif
    int            parent_irq;
    struct module        *owner;
    const char        *name;
} ____cacheline_internodealigned_in_smp

(1)handle_irq就是highlevel irq-events handler,何谓high level?站在高处自然看不到细节。我认为high level是和specific相对,specific handler处理具体的事务,例如处理一个按键中断、处理一个磁盘中断。而high level则是对处理各种中断交互过程的一个抽象,根据下列硬件的不同:

(a)中断控制器

(b)IRQ trigger type

highlevel irq-events handler可以分成:

(a)处理电平触发类型的中断handler(handle_level_irq)

(b)处理边缘触发类型的中断handler(handle_edge_irq)

(c)处理简单类型的中断handler(handle_simple_irq)

(d)处理EOI类型的中断handler(handle_fasteoi_irq)

会另外有一份文档对high level handler进行更详细的描述。

(2)action指向一个struct irqaction的链表。如果一个interrupt request line允许共享,那么该链表中的成员可以是多个,否则,该链表只有一个节点。

(3)这个有着很长名字的符号core_internal_state__do_not_mess_with_it在具体使用的时候被被简化成istate,表示internal state。就像这个名字定义的那样,我们最好不要直接修改它。

#define istate core_internal_state__do_not_mess_with_it

(4)我们可以通过enable和disable一个指定的IRQ来控制内核的并发,从而保护临界区的数据。对一个IRQ进行enable和disable的操作可以嵌套(当然一定要成对使用),depth是描述嵌套深度的信息。

(5)wake_depth是和电源管理中的wake up source相关。通过irq_set_irq_wake接口可以enable或者disable一个IRQ中断是否可以把系统从suspend状态唤醒。同样的,对一个IRQ进行wakeup source的enable和disable的操作可以嵌套(当然一定要成对使用),wake_depth是描述嵌套深度的信息。

(6)irq_count、last_unhandled和irqs_unhandled用于处理broken IRQ 的处理。所谓broken IRQ就是由于种种原因(例如错误firmware),IRQ handler没有定向到指定的IRQ上,当一个IRQ没有被处理的时候,kernel可以为这个没有被处理的handler启动scan过程,让系统中所有的handler来认领该IRQ。

(7)保护该中断描述符的spin lock。

(8)一个中断描述符可能会有两种情况,一种是该IRQ是global,一旦disable了该irq,那么对于所有的CPU而言都是disable的。还有一种情况,就是该IRQ是per CPU的,也就是说,在某个CPU上disable了该irq只是disable了本CPU的IRQ而已,其他的CPU仍然是enable的。percpu_enabled是一个描述该IRQ在各个CPU上是否enable成员。

(9)threads_oneshot、threads_active和wait_for_threads是和IRQ thread相关,后续文档会专门描述。

 

四、初始化相关的中断描述符的接口

1、静态定义的中断描述符初始化

int __init early_irq_init(void)
{
    int count, i, node = first_online_node;
    struct irq_desc *desc;

    init_irq_default_affinity();

    desc = irq_desc;
    count = ARRAY_SIZE(irq_desc);

    for (i = 0; i < count; i++) {---遍历整个lookup table,对每一个entry进行初始化
        desc[i].kstat_irqs = alloc_percpu(unsigned int);---分配per cpu的irq统计信息需要的内存
        alloc_masks(&desc[i], GFP_KERNEL, node);---分配中断描述符中需要的cpu mask内存
        raw_spin_lock_init(&desc[i].lock);---初始化spin lock
        lockdep_set_class(&desc[i].lock, &irq_desc_lock_class);
        desc_set_defaults(i, &desc[i], node, NULL);---设定default值
    }
    return arch_early_irq_init();---调用arch相关的初始化函数
}

2、使用Radix tree的中断描述符初始化

int __init early_irq_init(void)
{……

    initcnt = arch_probe_nr_irqs();---体系结构相关的代码来决定预先分配的中断描述符的个数

    if (initcnt > nr_irqs)---initcnt是需要在初始化的时候预分配的IRQ的个数
        nr_irqs = initcnt; ---nr_irqs是当前系统中IRQ number的最大值

    for (i = 0; i < initcnt; i++) {--------预先分配initcnt个中断描述符
        desc = alloc_desc(i, node, NULL);---分配中断描述符
        set_bit(i, allocated_irqs);---设定已经alloc的flag
        irq_insert_desc(i, desc);-----插入radix tree
    }
  ……
}

即便是配置了CONFIG_SPARSE_IRQ选项,在中断描述符初始化的时候,也有机会预先分配一定数量的IRQ。这个数量由arch_probe_nr_irqs决定,对于ARM而言,其arch_probe_nr_irqs定义如下:

int __init arch_probe_nr_irqs(void)
{
    nr_irqs = machine_desc->nr_irqs ? machine_desc->nr_irqs : NR_IRQS;
    return nr_irqs;
}

3、分配和释放中断描述符

对于使用Radix tree来保存中断描述符DB的linux kernel,其中断描述符是动态分配的,可以使用irq_alloc_descs和irq_free_descs来分配和释放中断描述符。alloc_desc函数也会对中断描述符进行初始化,初始化的内容和静态定义的中断描述符初始化过程是一样的。最大可以分配的ID是IRQ_BITMAP_BITS,定义如下:

#ifdef CONFIG_SPARSE_IRQ
# define IRQ_BITMAP_BITS    (NR_IRQS + 8196)---对于Radix tree,除了预分配的,还可以动态
#else                                                                             分配8196个中断描述符
# define IRQ_BITMAP_BITS    NR_IRQS---对于静态定义的,IRQ最大值就是NR_IRQS
#endif

 

五、和中断控制器相关的中断描述符的接口

这部分的接口主要有两类,irq_desc_get_xxx和irq_set_xxx,由于get接口API非常简单,这里不再描述,主要描述set类别的接口API。此外,还有一些locked版本的set接口API,定义为__irq_set_xxx,这些API的调用者应该已经持有保护irq desc的spinlock,因此,这些locked版本的接口没有中断描述符的spin lock进行操作。这些接口有自己特定的使用场合,这里也不详细描述了。

1、接口调用时机

kernel提供了若干的接口API可以让内核其他模块可以操作指定IRQ number的描述符结构。中断描述符中有很多的成员是和底层的中断控制器相关,例如:

(1)该中断描述符对应的irq chip

(2)该中断描述符对应的irq trigger type

(3)high level handler

在过去,系统中各个IRQ number是固定分配的,各个IRQ对应的中断控制器、触发类型等也都是清楚的,因此,一般都是在machine driver初始化的时候一次性的进行设定。machine driver的初始化过程会包括中断系统的初始化,在machine driver的中断初始化函数中,会调用本文定义的这些接口对各个IRQ number对应的中断描述符进行irq chip、触发类型的设定。

在引入了device tree、动态分配IRQ number以及irq domain这些概念之后,这些接口的调用时机移到各个中断控制器的初始化以及各个具体硬件驱动初始化过程中,具体如下:

(1)各个中断控制器定义好自己的struct irq_domain_ops callback函数,主要是map和translate函数。

(2)在各个具体的硬件驱动初始化过程中,通过device tree系统可以知道自己的中断信息(连接到哪一个interrupt controler、使用该interrupt controller的那个HW interrupt ID,trigger type为何),调用对应的irq domain的translate进行翻译、解析。之后可以动态申请一个IRQ number并和该硬件外设的HW interrupt ID进行映射,调用irq domain对应的map函数。在map函数中,可以调用本节定义的接口进行中断描述符底层interrupt controller相关信息的设定。

 

2、irq_set_chip

这个接口函数用来设定中断描述符中desc->irq_data.chip成员,具体代码如下:

int irq_set_chip(unsigned int irq, struct irq_chip *chip)
{
    unsigned long flags;
    struct irq_desc *desc = irq_get_desc_lock(irq, &flags, 0); ----(1)

    desc->irq_data.chip = chip;
    irq_put_desc_unlock(desc, flags);---------------(2)

    irq_reserve_irq(irq);----------------------(3)
    return 0;
}

(1)获取irq number对应的中断描述符。这里用关闭中断+spin lock来保护中断描述符,flag中就是保存的关闭中断之前的状态flag,后面在(2)中会恢复中断flag。

(3)前面我们说过,irq number有静态表格定义的,也有radix tree类型的。对于静态定义的中断描述符,没有alloc的概念。但是,对于radix tree类型,需要首先irq_alloc_desc或者irq_alloc_descs来分配一个或者一组IRQ number,在这些alloc函数值,就会set那些那些已经分配的IRQ。对于静态表格而言,其IRQ没有分配,因此,这里通过irq_reserve_irq函数标识该IRQ已经分配,虽然对于CONFIG_SPARSE_IRQ(使用radix tree)的配置而言,这个操作重复了(在alloc的时候已经设定了)。

 

3、irq_set_irq_type

这个函数是用来设定该irq number的trigger type的。

int irq_set_irq_type(unsigned int irq, unsigned int type)
{
    unsigned long flags;
    struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, IRQ_GET_DESC_CHECK_GLOBAL);
    int ret = 0;

    type &= IRQ_TYPE_SENSE_MASK;
    ret = __irq_set_trigger(desc, irq, type);
    irq_put_desc_busunlock(desc, flags);
    return ret;
}

来到这个接口函数,第一个问题就是:为何irq_set_chip接口函数使用irq_get_desc_lock来获取中断描述符,而irq_set_irq_type这个函数却需要irq_get_desc_buslock呢?其实也很简单,irq_set_chip不需要访问底层的irq chip(也就是interrupt controller),但是irq_set_irq_type需要。设定一个IRQ的trigger type最终要调用desc->irq_data.chip->irq_set_type函数对底层的interrupt controller进行设定。这时候,问题来了,对于嵌入SOC内部的interrupt controller,当然没有问题,因为访问这些中断控制器的寄存器memory map到了CPU的地址空间,访问非常的快,因此,关闭中断+spin lock来保护中断描述符当然没有问题,但是,如果该interrupt controller是一个I2C接口的IO expander芯片(这类芯片是扩展的IO,也可以提供中断功能),这时,让其他CPU进行spin操作太浪费CPU时间了(bus操作太慢了,会spin很久的)。肿么办?当然只能是用其他方法lock住bus了(例如mutex,具体实现是和irq chip中的irq_bus_lock实现相关)。一旦lock住了slow bus,然后就可以关闭中断了(中断状态保存在flag中)。

解决了bus lock的疑问后,还有一个看起来奇奇怪怪的宏:IRQ_GET_DESC_CHECK_GLOBAL。为何在irq_set_chip函数中不设定检查(check的参数是0),而在irq_set_irq_type接口函数中要设定global的check,到底是什么意思呢?既然要检查,那么检查什么呢?和“global”对应的不是local而是“per CPU”,内核中的宏定义是:IRQ_GET_DESC_CHECK_PERCPU。SMP情况下,从系统角度看,中断有两种形态(或者叫mode):

(1)1-N mode。只有1个processor处理中断

(2)N-N mode。所有的processor都是独立的收到中断,如果有N个processor收到中断,那么就有N个处理器来处理该中断。

听起来有些抽象,我们还是用GIC作为例子来具体描述。在GIC中,SPI使用1-N模型,而PPI和SGI使用N-N模型。对于SPI,由于采用了1-N模型,系统(硬件加上软件)必须保证一个中断被一个CPU处理。对于GIC,一个SPI的中断可以trigger多个CPU的interrupt line(如果Distributor中的Interrupt Processor Targets Registers有多个bit被设定),但是,该interrupt source和CPU的接口寄存器(例如ack register)只有一套,也就是说,这些寄存器接口是全局的,是global的,一旦一个CPU ack(读Interrupt Acknowledge Register,获取interrupt ID)了该中断,那么其他的CPU看到的该interupt source的状态也是已经ack的状态。在这种情况下,如果第二个CPU ack该中断的时候,将获取一个spurious interrupt ID。

对于PPI或者SGI,使用N-N mode,其interrupt source的寄存器是per CPU的,也就是每个CPU都有自己的、针对该interrupt source的寄存器接口(这些寄存器叫做banked register)。一个CPU 清除了该interrupt source的pending状态,其他的CPU感知不到这个变化,它们仍然认为该中断是pending的。

对于irq_set_irq_type这个接口函数,它是for 1-N mode的interrupt source使用的。如果底层设定该interrupt是per CPU的,那么irq_set_irq_type要返回错误。

4、irq_set_chip_data

每个irq chip总有自己私有的数据,我们称之chip data。具体设定chip data的代码如下:

int irq_set_chip_data(unsigned int irq, void *data)
{
    unsigned long flags;
    struct irq_desc *desc = irq_get_desc_lock(irq, &flags, 0); 
    desc->irq_data.chip_data = data;
    irq_put_desc_unlock(desc, flags);
    return 0;
}

多么清晰、多么明了,需要文字继续描述吗?

5、设定high level handler

这是中断处理的核心内容,__irq_set_handler就是设定high level handler的接口函数,不过一般不会直接调用,而是通过irq_set_chip_and_handler_name或者irq_set_chip_and_handler来进行设定。具体代码如下:

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);

……
    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);
}

理解这个函数的关键是在is_chained这个参数。这个参数是用在interrupt级联的情况下。例如中断控制器B级联到中断控制器A的第x个interrupt source上。那么对于A上的x这个interrupt而言,在设定其IRQ handler参数的时候要设定is_chained参数等于1,由于这个interrupt source用于级联,因此不能probe、不能被request(已经被中断控制器B使用了),不能被threaded(具体中断线程化的概念在其他文档中描述)


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

标签: irq 中断子系统 中断描述符

评论:

loren
2017-02-15 10:45
博主,请教一个问题:
在不重新编译kernel,也不修改IDT表(安全性、稳定性考虑)的情况下,我想在driver中动态分配一个vector number用来做IPI的中断vector,有什么办法可以做到吗?
我有参考pci_enable_msi()的实现,但是由于里面用到的assingn_irq_vector()函数没有EXPORT出来,所以我没办法使用。我看assign_irq_vector()函数主要是将以irq为参数来分配对应的vector,并将两者的关系记录到vector_irq[]数组中。
kernel version为3.16.0.
非常感谢!
linuxer
2017-02-15 22:29
@loren:在驱动中使用IPI还是比较少见的,呵呵~~我对ARM平台比较熟悉,在ARM平台上,IPI都是专用的,不能分配。
Peter
2017-01-12 00:23
刚接触Interrupt子系统,就被该文章吸引了,超赞. 当前这边文章中提到了GIC中断描述符的创建及初始化( irq_set_irq_type函数中),那GPIO类型的中断描述符又是怎么创建和初始化的呢? 还望大神指点. 感谢.
hello-world
2017-01-12 10:06
@Peter:你可以看看_gpiochip_irqchip_add这个函数的实现就明白了
Peter
2017-01-13 14:50
@hello-world:@hello-world, 谢谢,在irq_create_mapping函数中看到了.
理查德1030
2016-12-02 15:30
请教:
err = request_irq(irqdesc->hwirq, irqdesc->handler, \
                  irqdesc->flags, irqdesc->name, irqdesc->dev_id);
err return -22;
疑问:为什么75没有占用,我还是注册不上,我需要继续修改哪些东西呢?
说明:
(1)hwirq是芯片手册看描述的gpio bank中断号75,flags=0,dev_id=(void *)0

(2)cat /proc/interrupts 看了一下,未发现中断号被占用。
如下是系统已有的中断被hisi sdk有些模块占用,没有源码,所以不知道SDK中做了什么。。
{
           CPU0       CPU1      
29:       3080        464       GIC  29  arch_timer
......
45:         52          0       GIC  45  himci
......
70:          0          0       GIC  70  JPEGU_0
71:          0          0       GIC  71  IVE
80:          0          0       GIC  80  FISHEYE
}
(3)我想自己实现一个gpio中断,irq_chip已初始化,irq_domain也做了初始化(可能初始化不对),irq_domian用的是radix tree,但hisi3519的中断子系统是几级GIC也不清楚。
(4)内核版本linux3.18.y, 芯片ARM-hisi3519v101.
理查德1030
2016-12-02 16:12
@理查德1030:/proc/irq/目录下已经有芯片手册所有终端号命名的文件,是不是表示irq_domain已经做了初始化?只要实现struct irq_chip和request_irq,。但为什么75就是返回-22,换个未被占用的号48返回成功,/proc/interrupts也会添加(虽然不起作用)。
joeychen
2016-07-05 21:34
请教: 设备驱动中断在哪里设置绑定的cpu?
hello-world
2016-07-06 10:30
@joeychen:通过irq_set_affinity接口可以设定啊
奔跑的蜗牛
2016-05-31 14:49
hi 各位大神,
看了你们的文件,收益匪浅,你写的文章,我其实已经看过好机会了,之前只是大概的看看,没有这么认真的研读。这次认真的研读了下,有去思考,所以,有些疑问。
1 highlevel irq-events handle(desc->handle_irq) interrupt domain抽象的handler,应该是执行这个interrupt controller相关的动作,那具体是哪些呢,能麻烦举个例子吗。
2 specifier handler这个是完成哪些任务呢?
3 用户注册的handler,这个就是用户想要完成的任务了,没什么好说的。
还有,这3个之间有的执行流程有什么关系吗?
奔跑的蜗牛
2016-05-31 16:29
@奔跑的蜗牛:不好意思,通过后面几篇的阅读,已经理解了。谢谢。
electrlife
2016-03-18 16:12
从驱动子系统一路看过来,对于中断子系统有点疑问,中断控制器一般也说也是一类设备,应该算是平台设备,所以我觉得中断子系统应该也是类似使用设备驱动模型的方式来实现,但这段时间学习下来,似乎没有看到设备驱动模型的身影,不知是否我理解有偏差。如其它的子系统,一般也都是基本设备的驱动模型来的,比如I2C,USB,SPI等,难道中断控制器不可以实现成设备驱动模型的结构吗?或者更进一步,设备驱动模块适应哪些方面的抽象,或是对于哪些场景有限制??
郭健
2016-03-18 17:08
@electrlife:我很喜欢这个问题,多谢!

我的理解是这样的,诚然,interrupt controller从实际的物理拓扑结构上看,的确是和其他的HW block(例如USB device controller什么的)没有什么不同,如果驱动USB device controller的软件使用了platform bus、platform device以及platform driver的模型,那么interrupt controller的驱动也应该使用类似的方法。但是,interrupt controller是很特殊的一个HW block,它应该在所有的设备驱动初始化之前ready,毕竟每一种driver都需要中断子系统提供的服务。因此,中断子系统的初始化(init_IRQ)必须先于驱动模型初始化(driver_init)执行,这样,首先初始化的中断驱动如何利用驱动模型呢?
electrlife
2016-03-18 18:38
@郭健:多谢回复,这样理解有点道理,比如clocksource clock_dev_event等也是如此,但是对regulator好像不适用,我觉得regulator应该是系统比较早启动的一类设备。
除非regulator是较后的启动!
吴晓鹏
2016-09-05 15:39
@electrlife:@郭健:郭健兄说的是一方面的原因,也存在另一方面的原因:历史遗留问题。
Linux早期的版本,跟arch相关的硬件操作都会放在arch里面,随着soc越来越多,arch/arm里面已经非常冗余和混乱了,通过关注新内核动向,arch/arm里面的硬件操作会逐渐由系统退化成驱动,这样显得更加清晰标准。例如从linux-3.10之后arch/arm64已经没有arch/arm里面拥有很多mach相关的操作,同样irq已经移到了driver下,后面的内核版本cpuidle也是逐渐退化成驱动了,在后面硬件驱动逐渐跟arch撇清关系,例如irq-gic-v3.c,用硬件的版本号来区分。不知道我的理解是否正确,欢迎大家吐槽。
linuxer
2016-09-06 09:08
@吴晓鹏:你说的非常的对,irq chip的驱动是一种普通的设备驱动,应该放到drivers目录下,不过不是electrlife要问的问题,他的疑问是irq chip为何不使用platform这样的设备模型的方式来实现。
callme_friend
2016-03-03 17:37
貌似不能再回复了,重启一层。
irq_create_mapping()确实不会重复建立映射,看的不仔细~~
(1)我按照大神思路去看了3.17内核的gpio(基于zynq)部分初始代码,确实在probe()函数里新建了gpio irq的domain。但是这个过程很不优雅:因为在dts的gpio部分,没有看到如下所示、包含interrupt-controller的内容:
gpf: gpf {
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
};
上面这个node是明确表明这是一个interrupt-controller,所以在内核init_IRQ时,应该就会建立domain吧?
而我的这个zynq的dts文件关于gpio的部分如下:
gpio0: gpio@e000a000 {
    compatible = "xlnx,zynq-gpio-1.0";
    #gpio-cells = <2>;
    clocks = <&clkc 42>;
    gpio-controller;
    interrupt-parent = <&intc>;
    interrupts = <0 20 4>;
    reg = <0xe000a000 0x1000>;
};
------------------------------------------------------
这里并没有看到interrupt-controller属性,但是probe函数里居然新建了irq domain,总感觉别扭?
(2)通过上面的dts差异,结合大神的《GPIO子系统系列》文章,我在该dts中没看到关于pin bank、pin ctrl的描述,是不是可以认定它没有完整的支持GPIO子系统?
郭健
2016-03-04 12:40
@callme_friend:支持GPIO type的中断控制器有两种方法:
一、irq chip driver的方法,需要:
1、device tree中对应的node有interrupt-controller的属性
2、在drivers/irqchip目录下增加对应的irq chip driver的代码(driver)。需要定义IRQCHIP_DECLARE,以便可以匹配device tree中的节点(device)

二、使用platform driver的方法,需要:
1、定义platform driver和of_match_table数据结构
2、device tree中的节点会变成platform中的device
3、利用驱动模型的机制,让device和driver匹配并在probe函数中进行中断控制器的初始化。

哪种好呢?我喜欢第一种,但是内核中大部分的平台都是使用了第二种方法,具体为什么可能需要再仔细考虑一下。
callme_friend
2016-03-04 15:39
@郭健:站在dts角度,第二种方法您觉得别扭吗?
明明没有指定interrupt-controller属性,却莫名其妙的建立irq domain,有点奇怪的感觉。
callme_friend
2016-03-04 15:40
@郭健:另外,大神的《GPIO子系统》还继续整不?
郭健
2016-03-04 18:10
@callme_friend:本来不想搞了,不过最近思路有些变化。原来总想搞一些高大上,什么调度器啦,RCU啦,多么cool,最近打算搞一些人民群众喜闻乐见的,和具体工作相关的各种硬件驱动的frame work。因此,GPIO应该会继续整的。

BTW,这位帅哥,如果你有兴趣,可以一起整啊
callme_friend
2016-03-05 18:06
@郭健:兴趣是有,就是能力不足,我离大神水平太远太远!
那些高大上的文章,我们其实也很喜闻乐见的,我都浏览过,只是还不完全理解,暂时不能和大神交流。
本人还很期待大神的关于VFS、网络子系统的神作。
系统对用户的接口是fs,通过vfs,再深入到设备驱动,应该很high;另外网络子系统也是我有兴趣的方向~!
大神的文章像大百科全书一样,而且都是基于新内核,再次膜拜!!!
wangyunqian
2019-03-09 11:58
@callme_friend:三年过去了,说下我的理解:定不定义interrupt-controller属性,跟在哪里创建 irq domain没有什么关系吧。像gic的irq domain也是在init的时候创建的呀。dts里的interrupt-controller只是表示本node是一个中断控制器的node。如果gpio的node不添加该关键字,使用gpio中断的设备没有办法利用device tree提供的标准机制,比如说就不能在设备的node里面定义interrupts关键字,就必须在自己的驱动里面调用gpio_to_irq()来获取中断号。以上是我的个人理解,还请帮忙指正!
callme_friend
2016-02-26 11:46
关于irq_desc->handle_irq,通过文章看是有:边沿触发、电平触发等类型。
但是它是在哪里被赋值呢?
(1)通过查看内核和GIC初始化,只看到分配了irq_desc,并将irq_desc->handle_irq初始化为handle_bad_irq,而不是文章里介绍的边沿触发、电平触发处理函数;
(2)在驱动程序申请中断时,调用request_threaded_irq();而这个函数也未看到设置irq_desc->handle_irq。

我搜源码,一般只看到gpio控制器有调用irq_set_handler(handle_edge_irq);而在uart设备上未看到这样的设置?当这个uart设备中断时,程序走到irq_desc->handle_irq()时,到底调用的是哪个函数?

W(b)    vector_irq
    vector_irq:
        __irq_usr
            irq_handler
                ldr    r1, =handle_arch_irq  //已初始化为gic_handle_irq
                ldr    pc, [r1]
                ||
                \/
                gic_handle_irq()
                    handle_IRQ(irqnr, regs)
                            desc->handle_irq(irq, desc);
郭健
2016-02-26 21:51
@callme_friend:基本上,边沿触发、电平触发这些属于硬件中断的属性,对于一个HW Interrupt而言,它附属于某一个interrupt controller(irq domain),有类型(SPI/PPI等),有ID,有属性(上升沿或者电平触发),而这些信息最好的方法就是通过device tree的方法传递给kernel。

你的问题是关于irq_desc->handle_irq的设定,而所谓irq_desc是和IRQ number息息相关的,IRQ number就是virtual interrupt ID。哪里设定handle_irq呢?固定设定当然不好了,最好的方法就是驱动初始化的时候根据device tree传递的信息来相应的进行初始化。

一般驱动初始化的过程如下(仅仅说中断相关的内容):
1、分配一个IRQ number
2、建立IRQ number到HW Interrupt ID之间的mapping(在某个irq domain上)
3、注册IRQ number对应的handler

irq_desc->handle_irq在上面所说的第二步设定,怎么进行IRQ number到HW Interrupt ID之间的mapping呢?是通过struct irq_domain数据结构中的struct irq_domain_ops操作函数集中的map成员函数来完成的。从名字就可以退出其功能,就是建立IRQ和HW interrupt ID映射关系的时候调用的。在这个函数中,知道IRQ number,可以找到中断描述符,知道HW interrupt的ID以及属性,因此也知道如何设定irq_desc->handle_irq。

对于GIC而言,这个函数是gic_irq_domain_map,你可以看看。

当然,我上面说的方法我认为是最好的方式,不过,内核中也有一些遗留的代码是固定设定的(在中断控制器驱动中一次性设定),最终应该会被慢慢淘汰掉。
callme_friend
2016-02-27 06:55
@郭健:针对你上面说的“一般驱动初始化的过程”:
1、分配一个IRQ number -----------这个应该是early_irq_init()和gic_init_bases()时建立irq_desc[]或radix树
2、建立IRQ number到HW Interrupt ID之间的mapping(在某个irq domain上)
       -------------这一步貌似是gic初始化的时候建立的吧?而且这里没有看到设置irq_desc->handle_irq呀?irq_desc->handle_irq在何处赋值是我要问的核心问题。
3、注册IRQ number对应的handler -------这一步应该是request_irq完成吧?
郭健
2016-02-28 23:15
@callme_friend:1、不是的,early_irq_init()是中断描述符初始化,gic_init_bases是具体的中断控制器驱动初始化,我们这里说的是一个特定的设备驱动的IRQ的初始化,在过去,都是静态定义的,现在推荐使用device tree的方法动态分配。你可以参考drivers/of/irq.c文件

2、gic_irq_domain_map--->irq_set_chip_and_handler中会设定irq_desc->handle_irq。

3、是的
callme_friend
2016-02-29 17:22
@郭健:1、嗯,大神的device tree也看了几遍了,还要继续看几遍。
2、还是关于irq_desc->handle_irq何时赋值的问题,我刚开始问的也是这个问题。
《gic_irq_domain_map--->irq_set_chip_and_handler中会设定irq_desc->handle_irq。》---------------gic_irq_domain_map这个函数是在哪里被调用呢?
您的上篇文章《Linux kernel的中断子系统之(二):IRQ Domain介绍》里提及是在irq_of_parse_and_map()里建立;但是我用si搜索3.17内核,调用这个函数的设备驱动不算多。irq_of_parse_and_map()的合适调用时机是哪里?放在驱动的probe函数里合适吗?
郭健
2016-02-29 23:32
@callme_friend:其实基本上驱动也不会直接调用这些借口函数,这些接口函数多半进行了封装。例如:对于platform bus上的设备,其驱动的probe函数可以使用下面的代码:
......
    adc->irq = platform_get_irq(pdev, 1);
    if (adc->irq <= 0) {
        dev_err(dev, "failed to get adc irq\n");
        return -ENOENT;
    }

    ret = devm_request_irq(dev, adc->irq, s3c_adc_irq, 0, dev_name(dev),
                adc);
    if (ret < 0) {
        dev_err(dev, "failed to attach adc irq\n");
        return ret;
    }
......

platform_get_irq这个函数可以看一看,然后你就明白了。
callme_friend
2016-03-01 16:09
@郭健:在platform_get_irq()里确实看到最终调用了map,初始化了irq_desc->handle_irq。
我又有个疑问:在of_platform_populate()里,把dts解析出的node转化为platform设备时,也会调用domain_ops的map,也会初始化irq_desc->handle_irq;这样是不是和驱动的probe函数里有重复执行map的可能?
我跟的代码是3.17版本:
zynq_init_machine()-->
    of_platform_populate()-->                                        //创建平台设备
        of_platform_bus_create()-->
            of_platform_device_create_pdata()-->
                of_device_alloc()-->
                    of_irq_to_resource_table()-->
                        of_irq_to_resource()-->
                            irq_of_parse_and_map()-->
                                irq_create_of_mapping()-->
                                    irq_create_mapping()-->
                                        irq_domain_associate()-->
                                            //即gic_irq_domain_map:里面初始化irq_desc->handle_irq
                                            domain->ops->map()    
我的dts如下:
    gpio0: gpio@e000a000 {
        compatible = "xlnx,zynq-gpio-1.0";
        #gpio-cells = <2>;
        clocks = <&clkc 42>;
        gpio-controller;
        interrupt-parent = <&intc>;
        interrupts = <0 20 4>;
        reg = <0xe000a000 0x1000>;
    };
对应的驱动代码:
static struct platform_driver zynq_gpio_driver = {
    .probe = zynq_gpio_probe,
zynq_gpio_probe()
    platform_get_irq()-->
        of_irq_get()-->
            irq_create_of_mapping()-->
                irq_create_mapping(domain,hwirq)-->
                    irq_domain_associate()-->
                        //即gic_irq_domain_map:里面初始化irq_desc->handle_irq
                        domain->ops->map()    //gic_irq_domain_map
-------------------------------------------------------------
上述gpio节点,会不会在of_platform_populate()和zynq_gpio_probe()时,被先后调用?那hw irq到virq的映射岂不是发生两次?
郭健
2016-03-01 23:01
@郭健:你可以再仔细看看irq_create_mapping的代码,虽然其名字是创建映射,但是该函数首先是通过HW interrupt ID来找virtual interrupt ID,如果存在,则不需要创建,直接返回virtual interrupt ID,因此,映射不会创建两次的。
heziq
2015-03-09 09:22
有过这样的需求request_irq的时候并不想中断函数被立即调用。所以我一般情况是,在request-irq之前,先disable-irq。请问这样可以吗?我看过源代码,没有发现可能引起异常的部分。
wowo
2015-03-09 16:18
@heziq:request IRQ和hardware enable,应该是两个逻辑,只有hardware enable了,才有可能产生中断。因此一般的做法是:在probe中request IRQ,在需要使用设备时,enable它,中断产生。
heziq
2015-03-09 16:34
@wowo:@wowo:难道在probe中,request IRQ后,必须后面在加个enable IRQ, 我注册的中断函数才会被执行吗?我看见所有的源代码都是probe完以后,就等irq发生。我的意思是:我reqest IRq 后,不想立即调用我的中断处理函数。 等我需要的时候在打开中断,响应中断。有什么方法不?
heziq
2015-03-09 16:36
@heziq:“我看见所有的源代码都是probe完以后,就等irq发生。”有误,源代码中request IRQ 以后
wowo
2015-03-09 18:37
@heziq:我想表达的是:
中断handler是否被调用,有两个因素决定:一是irq是否enable,这是由irq的接口决定的;二是产生中断的设备是否可以产生中断,例如uart,是否enable了UART的收发功能。
大多数时候,设备驱动关注的是后一种,通过控制设备的行为,控制是否产生中断。
linuxer
2015-03-09 18:03
@heziq:是否在request_irq的时候enable该irq是一个option,由其中断描述符的NOAUTOEN这个flag决定的,具体可以参考代码:

if (irq_settings_can_autoenable(desc))
    irq_startup(desc, true);

在request-irq之前调用disable irq也没有问题,前提是正确初始化了硬件和中断描述符。不过针对你的需求,我倒是觉得你可以调用set_irq_flags来设定IRQF_NOAUTOEN这个flag
heziq
2015-03-09 18:41
@linuxer:高实在是高!

static int sirfsoc_uart_startup(struct uart_port *port)
{
    struct sirfsoc_uart_port *sirfport    = to_sirfport(port);
    unsigned int index            = port->line;
    int ret;
    set_irq_flags(port->irq, IRQF_VALID | IRQF_NOAUTOEN);
    ret = request_irq(port->irq,
                sirfsoc_uart_isr,
                0,
                SIRFUART_PORT_NAME,
                sirfport);
    if (ret != 0) {
        dev_err(port->dev, "UART%d request IRQ line (%d) failed.\n",
                            index, port->irq);
        goto irq_err;
    }
    startup_uart_controller(port);
    enable_irq(port->irq);
irq_err:
    return ret;
}
buyit
2015-05-01 22:22
@heziq:你找的这段代码来自于CSR,(reviewer是出书的某位大牛)。
事实上如果你搜索kernel代码的话,会发现很少有人调用IRQF_NOAUTOEN这个flag,所有调用它的都是1级或者2级interrupt controller,唯一的例外就是CSR的这份uart代码,所以我觉得它没有参考价值。

      从这个函数来看,申请中断和开启中断中间只隔了一个uart controller的初始化函数。那么如果把startup_uart_controller(port)函数放到最前面,等这个函数把uart硬件初始化好了之后,再调用request_irq函数来注册中断,这样子就不需要IRQF_NOAUTOEN了。
      request_irq的注释里面已经说明了,这个函数返回之后,handler就有可能马上被调用。如果硬件还没有准备好,说明你注册中断时间点很有可能不合理。
wowo
2015-05-02 20:59
@buyit:非常赞同,如果有奇奇怪怪的代码存在,很有可能是流程上有问题。
linuxer
2015-05-03 22:22
@buyit:IRQF_NOAUTOEN这个flag绝非为“普通”设备而打造,也不是给那些“文艺”类型的设备使用,它是为那些“2B”设备而产生的。这些设备会产生spurious interrupt,撰写driver的时候需要:
1、申请中断的时候不要打开中断
2、当期待中真正中断要来的时候调用enable_irq
3、处理完成后disable之

普通的request_irq不能满足这类奇葩设备的要求
buyit
2015-05-04 10:26
@linuxer:理解。
heziq 找的例子貌似不属于这种2b设备,看起来更像是代码逻辑的问题。
heziq
2015-03-09 18:42
@linuxer:完全如你所料!
tigger
2015-03-04 14:26
最近处理一个共享中断的case,共享中断确实如楼主所说,来了中断要调用一遍所有的挂在这个中断号下面的,irq handler。也就是下面的描述
(发生中断的时候要逐个询问是不是你的中断,软件上就是遍历action list啦),因此外设的irq handler中最好是一开始就启动判断,看看是否是自己的中断,如果不是,返回IRQ_NONE,表示这个中断不归我管。

容易出问题的地方就是,最好在irq handler中最好是一开始就启动判断,如果这个判断设计有误,比如不是自己的中断,还会跑自己的后面的handler,那么就有可能引入诡异的问题。

发表评论:

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