linux驱动移植-中断子系统整体框架


在之前的文章中,我们曾经学习过按键驱动的编写,其中我们就使用到了外部中断,我们是通过request_irq函数实现中断注册的。这一节我们将深入了解linux的中断实现机制。

一、中断介绍

1.1 什么是中断

中断是由软件或者硬件触发的一种事件,可以引起CPU的注意。

举个例子:当我们在键盘上按下一个按键时,接下来我们期望发生什么?操作系统和计算机应该做什么?为简单起见,假设每个外围设备都有一条连接CPU的中断线,设备可以通过它向 CPU 发出中断信号,但是,中断不会直接发送给 CPU。在中断信号到达CPU之前,都会首先经过一个叫做中断控制器(Interrupt Controller)的硬件,其作用是根据中断源(Interrupt Source)的优先级对中断进行派发。

在多核系统中,中断控制器还需要根据CPU预先设定的规则,将某个中断送入指定的CPU,以实现中断的负载均衡(irq balance)。

1.2 中断控制器

中断控制器负责手机所有中断源发起的中断,现有的中断控制器几乎都是可编程的,通过对中断控制器的编程,可以控制每个中断源的优先级,还可以打开和关闭某一个中断源等。在多核系统,甚至可以控制某个中断送入哪个CPU进行处理。

每种架构的中断控制器配置都不一样,比如:

  • x86带PIC:在单核x86 架构中,使用并配置了多个 8259 PIC充当中断控制器;
  • x86 与 APIC:在多核x86系统中,使用了一个叫做APIC(Advanced Programmable Interrupt Controller)的中断控制器,一个APIC由两个独立的设备组成:local APIC和I/O APIC;
  • ARM架构,使用较多的中断控制器是VIC(Vector Interrupt Controller),进入多核时代,使用较多的是GIC(Generic Interrupt Controller);

1.3 GIC

GIC版本包括V1~V4,每个 GIC 版本的主要特点:

  • GIC v1
    • 最多支持 8 个 PE;
    • 可处理多达 1020 个中断源;
    • 安全扩展支持;
    • 用于 ARM Cortex-A5、A9、R7 等;
  • GIC v2
    • 包括所有 v1 功能;
    • 添加虚拟化功能;
    • ARM Cortex-A7、A15、A53、A57、…… 等;
    • GIC v2m 是添加了 MSI(基于消息的中断)功能的模型;
  • GIC v3
    • 包括所有 v2 功能;
    • 最多支持 128 个 PE,每个集群 8 个 PE;
    • 能够处理超过 1020 个中断源;
    • 支持 MSI 方法以及信号(v1 和 v2 中使用的传统)方法;添加了高级安全功能,可根据非安全组和安全组分离中断处理;
      • MSI 在 SPI 和 LPI 中可用;
    • 除了 SPI、PPI 和 SGI 之外,还支持 LPI(局部特定外设中断);
    • 支持亲和路由功能;
    • ARM Cortex-A53、A57、A72、…… 等;
  • GICv4
    • 包括所有 v3 功能;
    • 直接注入虚拟中断;
    • ARM Cortex-A53、A57、A72、…… 等;

后面将会以GIC v2来展开介绍。

1.4 IRQ编号

系统中每一个注册的中断源,都会分配一个唯一的编号用于识别该中断,我们称之为IRQ编号。

IRQ编号贯穿在整个linux的中断子系统中。在移动设备中,每个中断源的IRQ编号都会在arch相关的一些头文件中,例如arch/arm/mach-s3c24xx/include/mach/irqs.h。

驱动程序在请求中断服务时,它会使用IRQ编号注册该中断,中断发生时,CPU通常会从中断控制器中获取相关信息,然后计算出相应的IRQ编号,然后把该IRQ编号传递到相应的驱动程序中。

二、中断注册

linux中断子系统向驱动程序提供了注册中断的API,下面我们将会一一介绍。

2.1 request_threaded_irq

request_threaded_irq函数:将中断线程化,中断将作为内核线程运行,可被赋予不同的实时优先级。在负载较高时,中断线程可以被挂起,以避免某些更高优先级的实时任务得不到及时响应。

int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
                     irq_handler_t thread_fn,
                     unsigned long flags, const char *name, void *dev);

参数如下:

  • irq:中断号,所申请的中断向量;
  • handler:中断处理服务函数,该函数工作在中断上下文中,如果不需要,可以传入NULL,但是不可以和thread_fn同时为NULL;
  • thread_fn:中断线程的回调函数,工作在内核进程上下文中,如果不需要,可以传入NULL,但是不可以和handler同时为NULL;
  • flags:指定中断属性、中断触发方式(一般用宏定义表示)等,定义在linux/interrupt.h中;
  • name:指定中断的名称;用命令cat /proc/interrupts可查看系统中断申请与使用情况;
  • dev:传入中断处理程序的参数,可以为NULL,但在注册共享中断时,此参数不能为NULL。该参数可作为共享中断时的中断区别参数,还可以把其传给一个结构体变量,用于保存一个设备的信息,使中断处理函数可以获得该设备的信息;

2.1  request_irq

如果不需要将中断线程化,一般使用以下函数即可:

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

之前我们在按键驱动程序中就是用了这个函数注册了几个外部中断。

三、中断子系统(Generic irq)的软件抽象

我们可以用下面的图示表示通用中断子系统的层次结构:

3.1 硬件封装层struct irq_chip

硬件封装层它包含了体系架构相关的所有代码,其使用struct irq_chip对中断控制器的接口抽象我们看看irq_chip的部分定义,定义在include/linux/irq.h文件中:

struct irq_chip {
    struct device    *parent_device;     //指向父设备
    const char    *name;      //  /proc/interrupts中显示的名字
    unsigned int    (*irq_startup)(struct irq_data *data);  //启动中断,如果设置成NULL,则默认为enable
    void        (*irq_shutdown)(struct irq_data *data);     //关闭中断,如果设置成NULL,则默认为disable
    void        (*irq_enable)(struct irq_data *data);   //中断使能,如果设置成NULL,则默认为chip->unmask
    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); //在SMP中设置CPU亲和力
    int        (*irq_retrigger)(struct irq_data *data);    //重新发送中断到CPU
    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_calc_mask)(struct irq_data *data);

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

    void        (*irq_compose_msi_msg)(struct irq_data *data, struct msi_msg *msg);
    void        (*irq_write_msi_msg)(struct irq_data *data, struct msi_msg *msg);

    int        (*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state);
    int        (*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state);

    int        (*irq_set_vcpu_affinity)(struct irq_data *data, void *vcpu_info);

    void        (*ipi_send_single)(struct irq_data *data, unsigned int cpu);
    void        (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest);

    unsigned long    flags;
};

因为不同芯片的中断控制器对其挂接的IRQ有不同的控制方法,因而这个结构体主要是由一组用于回调(callback),指向系统实际的中断控制器所使用的控制方法的函数指针构成。

我们只要对每个中断控制器实现以上接口(不必全部),并把它和相应的IRQ关联起来,上层的实现即可通过这些接口访问中断控制器。

3.2 中断通用逻辑层

中断通用逻辑层通过标准的封装接口(实际上就是struct irq_chip定义的接口)访问并控制中断控制器的行为,体系相关的中断入口函数在获取IRQ编号后,通过中断通用逻辑层提供的标准函数,把中断调用传递到中断流控层中。

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

int generic_handle_irq(unsigned int irq);
struct irq_desc *irq_to_desc(unsigned int irq);
int irq_set_chip(unsigned int irq, struct irq_chip *chip);
...

其中irq_to_desc函数可以通过IRQ编号获取到irq_desc结构的指针。

3.3 中断流控层

所谓中断流控是指合理并正确地处理连续发生的中断,比如一个中断在处理中,同一个中断再次到达时如何处理,何时应该屏蔽中断,何时打开中断,何时回应中断控制器等一系列的操作。

该层实现了与体系和硬件无关的中断流控处理操作,它针对不同的中断电气类型(level,edge......),实现了对应的标准中断流控处理函数,在这些处理函数中,最终会把中断控制权传递到驱动程序注册中断时传入的处理函数或者是中断线程中。目前内核提供了以下几个主要的中断流控函数的实现:

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

这些函数在include/linux/irqdesc.h中声明。

3.4 驱动程序API

该部分向驱动程序提供了一系列的API,用于向系统申请/释放中断,打开/关闭中断,设置中断类型和中断唤醒系统的特性等操作。驱动程序的开发者通常只会使用到这一层提供的这些API即可完成驱动程序的开发工作,其他的细节都由另外几个软件层较好地“隐藏”起来了,驱动程序开发者无需再关注底层的实现。其中的一些API如下:

void disable_irq_nosync(unsigned int irq);
bool disable_hardirq(unsigned int irq);
void disable_irq(unsigned int irq);
void disable_percpu_irq(unsigned int irq);
void enable_irq(unsigned int irq);
void enable_percpu_irq(unsigned int irq, unsigned int type);
bool irq_percpu_is_enabled(unsigned int irq);
void irq_wake_thread(unsigned int irq, void *dev_id);

void disable_nmi_nosync(unsigned int irq);
void disable_percpu_nmi(unsigned int irq);
void enable_nmi(unsigned int irq);
void enable_percpu_nmi(unsigned int irq, unsigned int type);
int prepare_percpu_nmi(unsigned int irq);
void teardown_percpu_nmi(unsigned int irq);

这些函数在include/linux/interrupt.h中声明。

四 IRQ描述

每一个IRQ中断在linux中断子系统中通过struct irq_desc描述,所有的irq_desc结构的组织方式有两种。

4.1 基于数组方式

平台相关板级代码事先根据系统中的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),
    }
};

4.2  基于基数树方式

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

4.3 struct irq_desc

include/linux/irqdesc.h中定义irq_desc结构:

/**
 * struct irq_desc - interrupt descriptor
 * @irq_common_data:    per irq and chip data passed down to chip functions
 * @kstat_irqs:         irq stats per cpu
 * @handle_irq:         highlevel irq-events handler
 * @preflow_handler:    handler called before the flow handler (currently used by sparc)
 * @action:             the irq action chain
 * @status:             status information
 * @core_internal_state__do_not_mess_with_it: core internal status information
 * @depth:              disable-depth, for nested irq_disable() calls
 * @wake_depth:         enable depth, for multiple irq_set_irq_wake() callers
 * @tot_count:          stats field for non-percpu irqs
 * @irq_count:          stats field to detect stalled irqs
 * @last_unhandled:     aging timer for unhandled count
 * @irqs_unhandled:     stats field for spurious unhandled interrupts
 * @threads_handled:    stats field for deferred spurious detection of threaded handlers
 * @threads_handled_last: comparator field for deferred spurious detection of theraded handlers
 * @lock:               locking for SMP
 * @affinity_hint:      hint to user space for preferred irq affinity
 * @affinity_notify:    context for notification of affinity changes
 * @pending_mask:       pending rebalanced interrupts
 * @threads_oneshot:    bitfield to handle shared oneshot threads
 * @threads_active:     number of irqaction threads currently running
 * @wait_for_threads:   wait queue for sync_irq to wait for threaded handlers
 * @nr_actions:         number of installed actions on this descriptor
 * @no_suspend_depth:   number of irqactions on a irq descriptor with
 *                      IRQF_NO_SUSPEND set
 * @force_resume_depth: number of irqactions on a irq descriptor with
 *                      IRQF_FORCE_RESUME set
 * @rcu:                rcu head for delayed free
 * @kobj:               kobject used to represent this struct in sysfs
 * @request_mutex:      mutex to protect request/free before locking desc->lock
 * @dir:                /proc/irq/ procfs entry
 * @debugfs_file:       dentry for the debugfs file
 * @name:               flow handler name for /proc/interrupts output
 */
struct irq_desc {
        struct irq_common_data  irq_common_data;
        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            core_internal_state__do_not_mess_with_it;
        unsigned int            depth;          /* nested irq disables */
        unsigned int            wake_depth;     /* nested wake enables */
        unsigned int            tot_count;
        unsigned int            irq_count;      /* For detecting broken IRQs */
        unsigned long           last_unhandled; /* Aging timer for unhandled count */
        unsigned int            irqs_unhandled;
        atomic_t                threads_handled;
        int                     threads_handled_last;
        raw_spinlock_t          lock;
        struct cpumask          *percpu_enabled;
        const struct cpumask    *percpu_affinity;
#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
        unsigned long           threads_oneshot;
        atomic_t                threads_active;
        wait_queue_head_t       wait_for_threads;
#ifdef CONFIG_PM_SLEEP
        unsigned int            nr_actions;
        unsigned int            no_suspend_depth;
        unsigned int            cond_suspend_depth;
        unsigned int            force_resume_depth;
#endif
#ifdef CONFIG_PROC_FS
        struct proc_dir_entry   *dir;
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
        struct dentry           *debugfs_file;
        const char              *dev_name;
#endif
#ifdef CONFIG_SPARSE_IRQ
        struct rcu_head         rcu;
        struct kobject          kobj;
#endif
        struct mutex            request_mutex;
        int                     parent_irq;
        struct module           *owner;
        const char              *name;
} ____cacheline_internodealigned_in_smp;

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

  • irq_data: 这个后面单独介绍。
  • 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申请中断服务,两个重要的参数是handler和thread_fn;
  • 当设备触发一个中断后,CPU会进入事先设定好的中断入口,它属于底层体系相关的代码,它通过中断控制器获得irq编号,在对irq_data结构中的某些字段进行处理后,会将控制权传递到中断流控层(通过irq_desc->handle_irq);
  • 中断流控处理代码在作出必要的流控处理后,通过irq_desc->action链表,取出驱动程序申请中断时注册的handler和thread_fn,根据它们的赋值情况,或者只是调用handler回调,或者启动一个线程执行thread_fn,又或者两者都执行;
  • 至此,中断最终由驱动程序进行了响应和处理;

参考文章:

[1]Linux的中断处理机制 [一] - 数据结构(1)

[2]Linux的中断处理机制 [二] - 数据结构(2)

[3]Linux的中断处理机制 [三] - hardirq

[4]Linux的中断处理机制 [四] - softirq(1)

[5]Linux的中断处理机制 [五] - softirq(2)

[6]Linux中的中断处理机制 [六] - 从tasklet到中断线程化

[7]ARM的中断处理 [一]

[8]ARM的中断处理 [二]

[9]

[10]

[11]The Linux Kernel documentation

[12]Linux 内部

[13]Interrupts in Linux

[14]Interrupts -1- (Interrupt Controller)

[15]Linux中断(interrupt)子系统之一:中断系统基本原理

相关