Linux中断(interrupt)子系统

中断系统基本原理

这个中断系列文章主要针对移动设备中的Linux进行讨论,文中的例子基本都是基于ARM这一体系架构,其他架构的原理其实也差不多,区别只是其中的硬件抽象层。内核版本基于3.3。虽然内核的版本不断地提升,不过自从上一次变更到当前的通用中断子系统后,大的框架性的东西并没有太大的改变。

1. 设备、中断控制器和CPU

一个完整的设备中,与中断相关的硬件可以划分为3类,它们分别是:设备、中断控制器和CPU本身,下图展示了一个smp系统中的中断硬件的组成结构:
Linux中断(interrupt)子系统_第1张图片
设备 设备是发起中断的源,当设备需要请求某种服务的时候,它会发起一个硬件中断信号,通常,该信号会连接至中断控制器,由中断控制器做进一步的处理。在现代的移动设备中,发起中断的设备可以位于soc(system-on-chip)芯片的外部,也可以位于soc的内部,因为目前大多数soc都集成了大量的硬件IP,例如I2C、SPI、Display Controller等等。

    中断控制器  中断控制器负责收集所有中断源发起的中断,现有的中断控制器几乎都是可编程的,通过对中断控制器的编程,我们可以控制每个中断源的优先级、中断的电器类型,还可以打开和关闭某一个中断源,在smp系统中,甚至可以控制某个中断源发往哪一个CPU进行处理。对于ARM架构的soc,使用较多的中断控制器是VIC(Vector Interrupt Controller),进入多核时代以后,GIC(General Interrupt Controller)的应用也开始逐渐变多。

    CPU  cpu是最终响应中断的部件,它通过对可编程中断控制器的编程操作,控制和管理者系统中的每个中断,当中断控制器最终判定一个中断可以被处理时,他会根据事先的设定,通知其中一个或者是某几个cpu对该中断进行处理,虽然中断控制器可以同时通知数个cpu对某一个中断进行处理,实际上,最后只会有一个cpu相应这个中断请求,但具体是哪个cpu进行响应是可能是随机的,中断控制器在硬件上对这一特性进行了保证,不过这也依赖于操作系统对中断系统的软件实现。在smp系统中,cpu之间也通过IPI(inter processor interrupt)中断进行通信。

2. IRQ编号

系统中每一个注册的中断源,都会分配一个唯一的编号用于识别该中断,我们称之为IRQ编号。IRQ编号贯穿在整个Linux的通用中断子系统中。在移动设备中,每个中断源的IRQ编号都会在arch相关的一些头文件中,例如arch/xxx/mach-xxx/include/irqs.h。驱动程序在请求中断服务时,它会使用IRQ编号注册该中断,中断发生时,cpu通常会从中断控制器中获取相关信息,然后计算出相应的IRQ编号,然后把该IRQ编号传递到相应的驱动程序中。

3. 在驱动程序中申请中断

Linux中断子系统向驱动程序提供了一系列的API,其中的一个用于向系统申请中断:

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)

其中,

  • irq是要申请的IRQ编号,
  • handler是中断处理服务函数,该函数工作在中断上下文中,如果不需要,可以传入NULL,但是不可以和thread_fn同时为NULL;
  • thread_fn是中断线程的回调函数,工作在内核进程上下文中,如果不需要,可以传入NULL,但是不可以和handler同时为NULL;
  • irqflags 是该中断的一些标志,可以指定该中断的电气类型,是否共享等信息;
  • devname指定该中断的名称;
  • dev_id 用于共享中断时的cookie data,通常用于区分共享中断具体由哪个设备发起;
    关于该API的详细工作机理我们后面再讨论。

4. 通用中断子系统(Generic irq)的软件抽象

在通用中断子系统(generic irq)出现之前,内核使用__do_IRQ处理所有的中断,这意味着__do_IRQ中要处理各种类型的中断,这会导致软件的复杂性增加,层次不分明,而且代码的可重用性也不好。事实上,到了内核版本2.6.38,__do_IRQ这种方式已经彻底在内核的代码中消失了。通用中断子系统的原型最初出现于ARM体系中,一开始内核的开发者们把3种中断类型区分出来,他们是:

  • 电平触发中断(level type)
  • 边缘触发中断(edge type)
  • 简易的中断(simple type)

后来又针对某些需要回应eoi(end of interrupt)的中断控制器,加入了fast eoi type,针对smp加入了per cpu type。把这些不同的中断类型抽象出来后,成为了中断子系统的流控层。要使所有的体系架构都可以重用这部分的代码,中断控制器也被进一步地封装起来,形成了中断子系统中的硬件封装层。我们可以用下面的图示表示通用中断子系统的层次结构:

Linux中断(interrupt)子系统_第2张图片

硬件封装层 它包含了体系架构相关的所有代码,包括中断控制器的抽象封装,arch相关的中断初始化,以及各个IRQ的相关数据结构的初始化工作,cpu的中断入口也会在arch相关的代码中实现。中断通用逻辑层通过标准的封装接口(实际上就是struct irq_chip定义的接口)访问并控制中断控制器的行为,体系相关的中断入口函数在获取IRQ编号后,通过中断通用逻辑层提供的标准函数,把中断调用传递到中断流控层中。我们看看irq_chip的部分定义:

struct irq_chip {
    const char  *name;
    unsigned int    (*irq_startup)(struct irq_data *data);
    void        (*irq_shutdown)(struct irq_data *data);
    void        (*irq_enable)(struct irq_data *data);
    void        (*irq_disable)(struct irq_data *data);

    void        (*irq_ack)(struct irq_data *data);
    void        (*irq_mask)(struct irq_data *data);
    void        (*irq_mask_ack)(struct irq_data *data);
    void        (*irq_unmask)(struct irq_data *data);
    void        (*irq_eoi)(struct irq_data *data);

    int     (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
    int     (*irq_retrigger)(struct irq_data *data);
    int     (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
    int     (*irq_set_wake)(struct irq_data *data, unsigned int on);
        ......
};

看到上面的结构定义,很明显,它实际上就是对中断控制器的接口抽象,我们只要对每个中断控制器实现以上接口(不必全部),并把它和相应的irq关联起来,上层的实现即可通过这些接口访问中断控制器。而且,同一个中断控制器的代码可以方便地被不同的平台所重用。

中断流控层 所谓中断流控是指合理并正确地处理连续发生的中断,比如一个中断在处理中,同一个中断再次到达时如何处理,何时应该屏蔽中断,何时打开中断,何时回应中断控制器等一系列的操作。该层实现了与体系和硬件无关的中断流控处理操作,它针对不同的中断电气类型(level,edge……),实现了对应的标准中断流控处理函数,在这些处理函数中,最终会把中断控制权传递到驱动程序注册中断时传入的处理函数或者是中断线程中。目前内核提供了以下几个主要的中断流控函数的实现(只列出部分):

  • handle_simple_irq();
  • handle_level_irq(); 电平中断流控处理程序
  • handle_edge_irq(); 边沿触发中断流控处理程序
  • handle_fasteoi_irq(); 需要eoi的中断处理器使用的中断流控处理程序
  • handle_percpu_irq(); 该irq只有单个cpu响应时使用的流控处理程序

中断通用逻辑层 该层实现了对中断系统几个重要数据的管理,并提供了一系列的辅助管理函数。同时,该层还实现了中断线程的实现和管理,共享中断和嵌套中断的实现和管理,另外它还提供了一些接口函数,它们将作为硬件封装层和中断流控层以及驱动程序API层之间的桥梁,例如以下API:

  • generic_handle_irq();
  • irq_to_desc();
  • irq_set_chip();
  • irq_set_chained_handler();

驱动程序API 该部分向驱动程序提供了一系列的API,用于向系统申请/释放中断,打开/关闭中断,设置中断类型和中断唤醒系统的特性等操作。驱动程序的开发者通常只会使用到这一层提供的这些API即可完成驱动程序的开发工作,其他的细节都由另外几个软件层较好地“隐藏”起来了,驱动程序开发者无需再关注底层的实现,这看起来确实是一件美妙的事情,不过我认为,要想写出好的中断代码,还是花点时间了解一下其他几层的实现吧。其中的一些API如下:

  • enable_irq();
  • disable_irq();
  • disable_irq_nosync();
  • request_threaded_irq();
  • irq_set_affinity();

这里不再对每一层做详细的介绍,我将会在本系列的其他几篇文章中做深入的探讨。

5. irq描述结构:struct irq_desc

整个通用中断子系统几乎都是围绕着irq_desc结构进行,系统中每一个irq都对应着一个irq_desc结构,所有的irq_desc结构的组织方式有两种:

基于数组方式 平台相关板级代码事先根据系统中的IRQ数量,定义常量:NR_IRQS,在kernel/irq/irqdesc.c中使用该常量定义irq_desc结构数组:

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

基于基数树方式 当内核的配置项CONFIG_SPARSE_IRQ被选中时,内核使用基数树(radix tree)来管理irq_desc结构,这一方式可以动态地分配irq_desc结构,对于那些具备大量IRQ数量或者IRQ编号不连续的系统,使用该方式管理irq_desc对内存的节省有好处,而且对那些自带中断控制器管理设备自身多个中断源的外部设备,它们可以在驱动程序中动态地申请这些中断源所对应的irq_desc结构,而不必在系统的编译阶段保留irq_desc结构所需的内存。

下面我们看一看irq_desc的部分定义:

struct irq_data {
    unsigned int        irq;
    unsigned long       hwirq;
    unsigned int        node;
    unsigned int        state_use_accessors;
    struct irq_chip     *chip;
    struct irq_domain   *domain;
    void            *handler_data;
    void            *chip_data;
    struct msi_desc     *msi_desc;
#ifdef CONFIG_SMP
    cpumask_var_t       affinity;
#endif
};
struct irq_desc {
    struct irq_data     irq_data;
    unsigned int __percpu   *kstat_irqs;
    irq_flow_handler_t  handle_irq;
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
    irq_preflow_handler_t   preflow_handler;
#endif
    struct irqaction    *action;    /* IRQ action list */
    unsigned int        status_use_accessors;
    unsigned int        depth;      /* nested irq disables */
    unsigned int        wake_depth; /* nested wake enables */
    unsigned int        irq_count;  /* For detecting broken IRQs */

    raw_spinlock_t      lock;
    struct cpumask      *percpu_enabled;
#ifdef CONFIG_SMP
    const struct cpumask    *affinity_hint;
    struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
    cpumask_var_t       pending_mask;
#endif
#endif
    wait_queue_head_t       wait_for_threads;
    const char      *name;
} ____cacheline_internodealigned_in_smp;

对于irq_desc中的主要字段做一个解释:

    irq_data  这个内嵌结构在2.6.37版本引入,之前的内核版本的做法是直接把这个结构中的字段直接放置在irq_desc结构体中,然后在调用硬件封装层的chip->xxx()回调中传入IRQ编号作为参数,但是底层的函数经常需要访问->handler_data,->chip_data,->msi_desc等字段,这需要利用irq_to_desc(irq)来获得irq_desc结构的指针,然后才能访问上述字段,这带来了性能的降低,尤其在配置为sparse irq的系统中更是如此,因为这意味着基数树的搜索操作。为了解决这一问题,内核开发者把几个低层函数需要使用的字段单独封装为一个结构,调用时的参数则改为传入该结构的指针。实现同样的目的,那为什么不直接传入irq_desc结构指针?因为这会破坏层次的封装性,我们不希望低层代码可以看到不应该看到的部分,仅此而已。

    kstat_irqs  用于irq的一些统计信息,这些统计信息可以从proc文件系统中查询。

    action  中断响应链表,当一个irq被触发时,内核会遍历该链表,调用action结构中的回调handler或者激活其中的中断线程,之所以实现为一个链表,是为了实现中断的共享,多个设备共享同一个irq,这在外围设备中是普遍存在的。

    status_use_accessors  记录该irq的状态信息,内核提供了一系列irq_settings_xxx的辅助函数访问该字段,详细请查看kernel/irq/settings.h

    depth  用于管理enable_irq()/disable_irq()这两个API的嵌套深度管理,每次enable_irq时该值减去1,每次disable_irq时该值加1,只有depth==0时才真正向硬件封装层发出关闭irq的调用,只有depth==1时才会向硬件封装层发出打开irq的调用。disable的嵌套次数可以比enable的次数多,此时depth的值大于1,随着enable的不断调用,当depth的值为1时,在向硬件封装层发出打开irq的调用后,depth减去1后,此时depth为0,此时处于一个平衡状态,我们只能调用disable_irq,如果此时enable_irq被调用,内核会报告一个irq失衡的警告,提醒驱动程序的开发人员检查自己的代码。

    lock  用于保护irq_desc结构本身的自旋锁。
    affinity_hit  用于提示用户空间,作为优化irq和cpu之间的亲缘关系的依据。
    pending_mask  用于调整irq在各个cpu之间的平衡。
    wait_for_threads  用于synchronize_irq(),等待该irq所有线程完成。

irq_data结构中的各字段:

    irq  该结构所对应的IRQ编号。
    hwirq  硬件irq编号,它不同于上面的irq;
    node  通常用于hwirq和irq之间的映射操作;
    state_use_accessors  硬件封装层需要使用的状态信息,不要直接访问该字段,内核定义了一组函数用于访问该字段:irqd_xxxx(),参见include/linux/irq.h。
    chip  指向该irq所属的中断控制器的irq_chip结构指针
    handler_data  每个irq的私有数据指针,该字段由硬件封转层使用,例如用作底层硬件的多路复用中断。
    chip_data  中断控制器的私有数据,该字段由硬件封转层使用。
    msi_desc  用于PCIe总线的MSI或MSI-X中断机制。
    affinity  记录该irq与cpu之间的亲缘关系,它其实是一个bit-mask,每一个bit代表一个cpu,置位后代表该cpu可能处理该irq。

这是通用中断子系统系列文章的第一篇,这里不会详细介绍各个软件层次的实现原理,但是有必要对整个架构做简要的介绍:

  • 系统启动阶段,取决于内核的配置,内核会通过数组或基数树分配好足够多的irq_desc结构;
  • 根据不同的体系结构,初始化中断相关的硬件,尤其是中断控制器;
  • 为每个必要irq的irq_desc结构填充默认的字段,例如irq编号,irq_chip指针,根据不同的中断类型配置流控handler;
  • 设备驱动程序在初始化阶段,利用request_threaded_irq() api申请中断服务,两个重要的参数是handler和thread_fn;
  • 当设备触发一个中断后,cpu会进入事先设定好的中断入口,它属于底层体系相关的代码,它通过中断控制器获得irq编号,在对irq_data结构中的某些字段进行处理后,会将控制权传递到中断流控层(通过irq_desc->handle_irq);
  • 中断流控处理代码在作出必要的流控处理后,通过irq_desc->action链表,取出驱动程序申请中断时注册的handler和thread_fn,根据它们的赋值情况,或者只是调用handler回调,或者启动一个线程执行thread_fn,又或者两者都执行;
  • 至此,中断最终由驱动程序进行了响应和处理。

6. 中断子系统的proc文件接口

在/proc目录下面,有两个与中断子系统相关的文件和子目录,它们是:
- /proc/interrupts:文件
- /proc/irq:子目录
读取interrupts会依次显示irq编号,每个cpu对该irq的处理次数,中断控制器的名字,irq的名字,以及驱动程序注册该irq时使用的名字,以下是一个例子:
Linux中断(interrupt)子系统_第3张图片
/proc/irq目录下面会为每个注册的irq创建一个以irq编号为名字的子目录,每个子目录下分别有以下条目:

  • smp_affinity irq和cpu之间的亲缘绑定关系;
  • smp_affinity_hint 只读条目,用于用户空间做irq平衡只用;
  • spurious 可以获得该irq被处理和未被处理的次数的统计信息;
  • handler_name 驱动程序注册该irq时传入的处理程序的名字;
    根据irq的不同,以上条目不一定会全部都出现,以下是某个设备的例子:
# cd /proc/irq
# ls
ls
332
248
......
......
12
11
default_smp_affinity

# ls 332
bcmsdh_sdmmc
spurious
node
affinity_hint
smp_affinity

# cat 332/smp_affinity
3

可见,以上设备是一个使用双核cpu的设备,因为smp_affinity的值是3,系统默认每个中断可以由两个cpu进行处理。

本章内容结束。接下来的计划:

Linux中断(interrupt)子系统之二:arch相关的硬件封装层
Linux中断(interrupt)子系统之三:中断流控处理层
Linux中断(interrupt)子系统之四:驱动程序接口层
Linux中断(interrupt)子系统之五:软件中断(softirq)

arch相关的硬件封装层

Linux的通用中断子系统的一个设计原则就是把底层的硬件实现尽可能地隐藏起来,使得驱动程序的开发人员不用关注底层的实现,要实现这个目标,内核的开发者们必须把硬件相关的内容剥离出来,然后定义一些列标准的接口供上层访问,上层的开发人员只要知道这些接口即可完成对中断的进一步处理和控制。对底层的封装主要包括两部分:

  • 实现不同体系结构中断入口,这部分代码通常用asm实现;
  • 中断控制器进行封装和实现;
    本文的内容正是要讨论硬件封装层的实现细节。我将以ARM体系进行介绍,大部分的代码位于内核代码树的arch/arm/目录内。

1. CPU的中断入口

我们知道,arm的异常和复位向量表有两种选择,一种是低端向量,向量地址位于0x00000000,另一种是高端向量,向量地址位于0xffff0000,Linux选择使用高端向量模式,也就是说,当异常发生时,CPU会把PC指针自动跳转到始于0xffff0000开始的某一个地址上:
ARM的异常向量表

地址 异常种类
FFFF0000 复位
FFFF0004 未定义指令
FFFF0008 软中断(swi)
FFFF000C Prefetch abort
FFFF0010 Data abort
FFFF0014 保留
FFFF0018 IRQ
FFFF001C FIQ

中断向量表在arch/arm/kernel/entry_armv.S中定义,为了方便讨论,下面只列出部分关键的代码:

    .globl  __stubs_start
__stubs_start:

    vector_stub irq, IRQ_MODE, 4

    .long   __irq_usr           @  0  (USR_26 / USR_32)
    .long   __irq_invalid           @  1  (FIQ_26 / FIQ_32)
    .long   __irq_invalid           @  2  (IRQ_26 / IRQ_32)
    .long   __irq_svc           @  3  (SVC_26 / SVC_32)

    vector_stub dabt, ABT_MODE, 8

    .long   __dabt_usr          @  0  (USR_26 / USR_32)
    .long   __dabt_invalid          @  1  (FIQ_26 / FIQ_32)
    .long   __dabt_invalid          @  2  (IRQ_26 / IRQ_32)
    .long   __dabt_svc          @  3  (SVC_26 / SVC_32)

vector_fiq:
    disable_fiq
    subs    pc, lr, #4
    ......
    .globl  __stubs_end
__stubs_end:



    .equ    stubs_offset, __vectors_start + 0x200 - __stubs_start
    .globl  __vectors_start
__vectors_start:
 ARM(   swi SYS_ERROR0  )
 THUMB( svc #0 )
 THUMB( nop         )
    W(b)    vector_und + stubs_offset
    W(ldr)  pc, .LCvswi + stubs_offset
    W(b)    vector_pabt + stubs_offset
    W(b)    vector_dabt + stubs_offset
    W(b)    vector_addrexcptn + stubs_offset
    W(b)    vector_irq + stubs_offset
    W(b)    vector_fiq + stubs_offset

    .globl  __vectors_end
__vectors_end:

代码被分为两部分:

  • 第一部分是真正的向量跳转表,位于__vectors_start和__vectors_end之间;
  • 第二部分是处理跳转的部分,位于__stubs_start和__stubs_end之间;
    vector_stub irq, IRQ_MODE, 4  

以上这一句把宏展开后实际上就是定义了vector_irq,根据进入中断前的cpu模式,分别跳转到__irq_usr或__irq_svc。

系统启动阶段,位于arch/arm/kernel/traps.c中的early_trap_init()被调用:

void __init early_trap_init(void)
{
    ......
    /*
     * Copy the vectors, stubs and kuser helpers (in entry-armv.S)
     * into the vector page, mapped at 0xffff0000, and ensure these
     * are visible to the instruction stream.
     */
    memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
    memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
    ......
}

以上两个memcpy会把__vectors_start开始的代码拷贝到0xffff0000处,把__stubs_start开始的代码拷贝到0xFFFF0000+0x200处,这样,异常中断到来时,CPU就可以正确地跳转到相应中断向量入口并执行他们。
Linux中断(interrupt)子系统_第4张图片

对于系统的外部设备来说,通常都是使用IRQ中断,所以我们只关注__irq_usr和__irq_svc,两者的区别是进入和退出中断时是否进行用户栈和内核栈之间的切换,还有进程调度和抢占的处理等,这些细节不在这里讨论。两个函数最终都会进入irq_handler这个宏:

    .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

如果选择了MULTI_IRQ_HANDLER配置项,则意味着允许平台的代码可以动态设置irq处理程序,平台代码可以修改全局变量:handle_arch_irq,从而可以修改irq的处理程序。这里我们讨论默认的实现:arch_irq_handler_default,它位于arch/arm/include/asm/entry_macro_multi.S中:

    .macro  arch_irq_handler_default
    get_irqnr_preamble r6, lr
1:  get_irqnr_and_base r0, r2, r6, lr
    movne   r1, sp
    @
    @ routine called with r0 = irq number, r1 = struct pt_regs *
    @
    adrne   lr, BSYM(1b)
    bne asm_do_IRQ
    ......

get_irqnr_preamble和get_irqnr_and_base两个宏由machine级的代码定义,目的就是从中断控制器中获得IRQ编号,紧接着就调用asm_do_IRQ,从这个函数开始,中断程序进入C代码中,传入的参数是IRQ编号和寄存器结构指针,这个函数在arch/arm/kernel/irq.c中实现:

/* * asm_do_IRQ is the interface to be used from assembly code. */
asmlinkage void __exception_irq_entry
asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
    handle_IRQ(irq, regs);
}

到这里,中断程序完成了从asm代码到C代码的传递,并且获得了引起中断的IRQ编号。

2. 初始化

与通用中断子系统相关的初始化由start_kernel()函数发起,调用流程如下图所视:
Linux中断(interrupt)子系统_第5张图片
图2.1 通用中断子系统的初始化

  • 首先,在setup_arch函数中,early_trap_init被调用,其中完成了第1节所说的中断向量的拷贝和重定位工作。
  • 然后,start_kernel发出early_irq_init调用,early_irq_init属于与硬件和平台无关的通用逻辑层,它完成irq_desc结构的内存申请,为它们其中某些字段填充默认值,完成后调用体系相关的arch_early_irq_init函数完成进一步的初始化工作,不过ARM体系没有实现arch_early_irq_init。
  • 接着,start_kernel发出init_IRQ调用,它会直接调用所属板子machine_desc结构体中的init_irq回调。machine_desc通常在板子的特定代码中,使用MACHINE_START和MACHINE_END宏进行定义。
  • machine_desc->init_irq()完成对中断控制器的初始化,为每个irq_desc结构安装合适的流控handler,为每个irq_desc结构安装irq_chip指针,使他指向正确的中断控制器所对应的irq_chip结构的实例,同时,如果该平台中的中断线有多路复用(多个中断公用一个irq中断线)的情况,还应该初始化irq_desc中相应的字段和标志,以便实现中断控制器的级联。

3. 中断控制器的软件抽象:struct irq_chip

正如上一篇文章Linux中断(interrupt)子系统之一:中断系统基本原理所述,所有的硬件中断在到达CPU之前,都要先经过中断控制器进行汇集,合乎要求的中断请求才会通知cpu进行处理,中断控制器主要完成以下这些功能:

  • 对各个irq的优先级进行控制;
  • 向CPU发出中断请求后,提供某种机制让CPU获得实际的中断源(irq编号);
  • 控制各个irq的电气触发条件,例如边缘触发或者是电平触发;
  • 使能(enable)或者屏蔽(mask)某一个irq;
  • 提供嵌套中断请求的能力;
  • 提供清除中断请求的机制(ack);
  • 有些控制器还需要CPU在处理完irq后对控制器发出eoi指令(end of interrupt);
  • 在smp系统中,控制各个irq与cpu之间的亲缘关系(affinity);

通用中断子系统把中断控制器抽象为一个数据结构:struct irq_chip,其中定义了一系列的操作函数,大部分多对应于上面所列的某个功能:

struct irq_chip {
    const char  *name;
    unsigned int    (*irq_startup)(struct irq_data *data);
    void        (*irq_shutdown)(struct irq_data *data);
    void        (*irq_enable)(struct irq_data *data);
    void        (*irq_disable)(struct irq_data *data);

    void        (*irq_ack)(struct irq_data *data);
    void        (*irq_mask)(struct irq_data *data);
    void        (*irq_mask_ack)(struct irq_data *data);
    void        (*irq_unmask)(struct irq_data *data);
    void        (*irq_eoi)(struct irq_data *data);

    int     (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
    int     (*irq_retrigger)(struct irq_data *data);
    int     (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
    int     (*irq_set_wake)(struct irq_data *data, unsigned int on);

    void        (*irq_bus_lock)(struct irq_data *data);
    void        (*irq_bus_sync_unlock)(struct irq_data *data);

    void        (*irq_cpu_online)(struct irq_data *data);
    void        (*irq_cpu_offline)(struct irq_data *data);

    void        (*irq_suspend)(struct irq_data *data);
    void        (*irq_resume)(struct irq_data *data);
    void        (*irq_pm_shutdown)(struct irq_data *data);

    void        (*irq_print_chip)(struct irq_data *data, struct seq_file *p);

    unsigned long   flags;

    /* Currently used only by UML, might disappear one day.*/
#ifdef CONFIG_IRQ_RELEASE_METHOD
    void        (*release)(unsigned int irq, void *dev_id);
#endif
};

各个字段解释如下:

name  中断控制器的名字,会出现在 /proc/interrupts中。
irq_startup  第一次开启一个irq时使用。
irq_shutdown  与irq_starup相对应。
irq_enable  使能该irq,通常是直接调用irq_unmask()。
irq_disable  禁止该irq,通常是直接调用irq_mask,严格意义上,他俩其实代表不同的意义,disable表示中断控制器根本就不响应该irq,而mask时,中断控制器可能响应该irq,只是不通知CPU,这时,该irq处于pending状态。类似的区别也适用于enable和unmask。

irq_ack  用于CPU对该irq的回应,通常表示cpu希望要清除该irq的pending状态,准备接受下一个irq请求。
irq_mask  屏蔽该irq。
irq_unmask  取消屏蔽该irq。
irq_mask_ack  相当于irq_mask + irq_ack。
irq_eoi  有些中断控制器需要在cpu处理完该irq后发出eoi信号,该回调就是用于这个目的。
irq_set_affinity  用于设置该irq和cpu之间的亲缘关系,就是通知中断控制器,该irq发生时,那些cpu有权响应该irq。当然,中断控制器会在软件的配合下,最终只会让一个cpu处理本次请求。
irq_set_type  设置irq的电气触发条件,例如IRQ_TYPE_LEVEL_HIGH或IRQ_TYPE_EDGE_RISING。
irq_set_wake  通知电源管理子系统,该irq是否可以用作系统的唤醒源。 

以上大部分的函数接口的参数都是irq_data结构指针,irq_data结构的由来在上一篇文章已经说过,这里仅贴出它的定义,各字段的意义请参考注释:

/**
 * struct irq_data - per irq and irq chip data passed down to chip functions
 * @irq:        interrupt number
 * @hwirq:      hardware interrupt number, local to the interrupt domain
 * @node:       node index useful for balancing
 * @state_use_accessors: status information for irq chip functions.
 *          Use accessor functions to deal with it
 * @chip:       low level interrupt hardware access
 * @domain:     Interrupt translation domain; responsible for mapping
 *          between hwirq number and linux irq number.
 * @handler_data:   per-IRQ data for the irq_chip methods
 * @chip_data:      platform-specific per-chip private data for the chip
 *          methods, to allow shared chip implementations
 * @msi_desc:       MSI descriptor
 * @affinity:       IRQ affinity on SMP
 *
 * The fields here need to overlay the ones in irq_desc until we
 * cleaned up the direct references and switched everything over to
 * irq_data.
 */
struct irq_data {
    unsigned int        irq;
    unsigned long       hwirq;
    unsigned int        node;
    unsigned int        state_use_accessors;
    struct irq_chip     *chip;
    struct irq_domain   *domain;
    void            *handler_data;
    void            *chip_data;
    struct msi_desc     *msi_desc;
#ifdef CONFIG_SMP
    cpumask_var_t       affinity;
#endif
};

根据设备使用的中断控制器的类型,体系架构的底层的开发只要实现上述接口中的各个回调函数,然后把它们填充到irq_chip结构的实例中,最终把该irq_chip实例注册到irq_desc.irq_data.chip字段中,这样各个irq和中断控制器就进行了关联,只要知道irq编号,即可得到对应到irq_desc结构,进而可以通过chip指针访问中断控制器。

4. 进入流控处理层

进入C代码的第一个函数是asm_do_IRQ,在ARM体系中,这个函数只是简单地调用handle_IRQ:

/* * asm_do_IRQ is the interface to be used from assembly code. */
asmlinkage void __exception_irq_entry
asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
    handle_IRQ(irq, regs);
}

handle_IRQ本身也不是很复杂:

void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
    struct pt_regs *old_regs = set_irq_regs(regs);

    irq_enter();

    /* * Some hardware gives randomly wrong interrupts. Rather * than crashing, do something sensible. */
    if (unlikely(irq >= nr_irqs)) {
        if (printk_ratelimit())
            printk(KERN_WARNING "Bad IRQ%u\n", irq);
        ack_bad_irq(irq);
    } else {
        generic_handle_irq(irq);
    }

    /* AT91 specific workaround */
    irq_finish(irq);

    irq_exit();
    set_irq_regs(old_regs);
}

irq_enter主要是更新一些系统的统计信息,同时在__irq_enter宏中禁止了进程的抢占:

#define __irq_enter() \
    do {                        \
        account_system_vtime(current);      \
        add_preempt_count(HARDIRQ_OFFSET);  \
        trace_hardirq_enter();          \
    } while (0)

CPU一旦响应IRQ中断后,ARM会自动把CPSR中的I位置位,表明禁止新的IRQ请求,直到中断控制转到相应的流控层后才通过local_irq_enable()打开。你可能会奇怪,既然此时的irq中断都是都是被禁止的,为何还要禁止抢占?这是因为要考虑中断嵌套的问题,一旦流控层或驱动程序主动通过local_irq_enable打开了IRQ,而此时该中断还没处理完成,新的irq请求到达,这时代码会再次进入irq_enter,在本次嵌套中断返回时,内核不希望进行抢占调度,而是要等到最外层的中断处理完成后才做出调度动作,所以才有了禁止抢占这一处理。
下一步,generic_handle_irq被调用,generic_handle_irq是通用逻辑层提供的API,通过该API,中断的控制被传递到了与体系结构无关的中断流控层:

int generic_handle_irq(unsigned int irq)
{
    struct irq_desc *desc = irq_to_desc(irq);

    if (!desc)
        return -EINVAL;
    generic_handle_irq_desc(irq, desc);
    return 0;
}

最终会进入该irq注册的流控处理回调中:

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

5. 中断控制器的级联

在实际的设备中,经常存在多个中断控制器,有时多个中断控制器还会进行所谓的级联。为了方便讨论,我们把直接和CPU相连的中断控制器叫做根控制器,另外一些和跟控制器相连的叫子控制器。根据子控制器的位置,我们把它们分为两种类型:

  • 机器级别的级联 子控制器位于SOC内部,或者子控制器在SOC的外部,但是是某个板子系列的标准配置,如图5.1的左边所示;

  • 设备级别的级联 子控制器位于某个外部设备中,用于汇集该设备发出的多个中断,如图5.1的右边所示;

Linux中断(interrupt)子系统_第6张图片
图5.1 中断控制器的级联类型

对于机器级别的级联,级联的初始化代码理所当然地位于板子的初始化代码中(arch/xxx/mach-xxx),因为只要是使用这个板子或SOC的设备,必然要使用这个子控制器。而对于设备级别的级联,因为该设备并不一定是系统的标配设备,所以中断控制器的级联操作应该在该设备的驱动程序中实现。机器设备的级联,因为得益于事先已经知道子控制器的硬件连接信息,内核可以方便地为子控制器保留相应的irq_desc结构和irq编号,处理起来相对简单。设备级别的级联则不一样,驱动程序必须动态地决定组合设备中各个子设备的irq编号和irq_desc结构。本章我只讨论机器级别的级联,设备级别的关联可以使用同样的原理,也可以实现为共享中断,我会在本系列接下来的文章中讨论。

要实现中断控制器的级联,要使用以下几个的关键数据结构字段和通用中断逻辑层的API:

    irq_desc.handle_irq  irq的流控处理回调函数,子控制器在把多个irq汇集起来后,输出端连接到根控制器的其中一个irq中断线输入脚,这意味着,每个子控制器的中断发生时,CPU一开始只会得到根控制器的irq编号,然后进入该irq编号对应的irq_desc.handle_irq回调,该回调我们不能使用流控层定义好的几个流控函数,而是要自己实现一个函数,该函数负责从子控制器中获得irq的中断源,并计算出对应新的irq编号,然后调用新irq所对应的irq_desc.handle_irq回调,这个回调使用流控层的标准实现。

    irq_set_chained_handler()  该API用于设置根控制器与子控制器相连的irq所对应的irq_desc.handle_irq回调函数,并且设置IRQ_NOPROBE和IRQ_NOTHREAD以及IRQ_NOREQUEST标志,这几个标志保证驱动程序不会错误地申请该irq,因为该irq已经被作为级联irq使用。

    irq_set_chip_and_handler()  该API同时设置irq_desc中的handle_irq回调和irq_chip指针。

以下例子代码位于:/arch/arm/plat-s5p/irq-eint.c:

int __init s5p_init_irq_eint(void)
{
    int irq;

    for (irq = IRQ_EINT(0); irq <= IRQ_EINT(15); irq++)
        irq_set_chip(irq, &s5p_irq_vic_eint);

    for (irq = IRQ_EINT(16); irq <= IRQ_EINT(31); irq++) {
        irq_set_chip_and_handler(irq, &s5p_irq_eint, handle_level_irq);
        set_irq_flags(irq, IRQF_VALID);
    }

    irq_set_chained_handler(IRQ_EINT16_31, s5p_irq_demux_eint16_31);
    return 0;
}

该SOC芯片的外部中断:IRQ_EINT(0)到IRQ_EINT(15),每个引脚对应一个根控制器的irq中断线,它们是正常的irq,无需级联。IRQ_EINT(16)到IRQ_EINT(31)经过子控制器汇集后,统一连接到根控制器编号为IRQ_EINT16_31这个中断线上。可以看到,子控制器对应的irq_chip是s5p_irq_eint,子控制器的irq默认设置为电平中断的流控处理函数handle_level_irq,它们通过API:irq_set_chained_handler进行设置。如果根控制器有128个中断线,IRQ_EINT0–IRQ_EINT15通常占据128内的某段连续范围,这取决于实际的物理连接。IRQ_EINT16_31因为也属于跟控制器,所以它的值也会位于128以内,但是IRQ_EINT16–IRQ_EINT31通常会在128以外的某段范围,这时,代表irq数量的常量NR_IRQS,必须考虑这种情况,定义出超过128的某个足够的数值。级联的实现主要依靠编号为IRQ_EINT16_31的流控处理程序:s5p_irq_demux_eint16_31,它的最终实现类似于以下代码:

static inline void s5p_irq_demux_eint(unsigned int start)
{
    u32 status = __raw_readl(S5P_EINT_PEND(EINT_REG_NR(start)));
    u32 mask = __raw_readl(S5P_EINT_MASK(EINT_REG_NR(start)));
    unsigned int irq;

    status &= ~mask;
    status &= 0xff;

    while (status) {
        irq = fls(status) - 1;
        generic_handle_irq(irq + start);
        status &= ~(1 << irq);
    }
}

在获得新的irq编号后,它的最关键的一句是调用了通用中断逻辑层的API:generic_handle_irq,这时它才真正地把中断控制权传递到中断流控层中来。

你可能感兴趣的:(Linux中断(interrupt)子系统)