异常与中断的概念及处理流程

异常向量表

_start: b   reset
    ldr pc, _undefined_instruction
    ldr pc, _software_interrupt
    ldr pc, _prefetch_abort
    ldr pc, _data_abort
    ldr pc, _not_used
    ldr pc, _irq //发生中断时,CPU跳到这个地址执行该指令 **假设地址为0x18**
    ldr pc, _fiq

进程线程中断的核心_栈

CPU内部的寄存器很重要,如果要暂停一个程序,中断一个程序,就需要把这些寄存器的值保存下来:这就称为保存现场。 保存在内存中,这块内存就称之为栈。程序要继续执行,就先从栈中恢复那些CPU内部寄存器的值。

并不局限于中断,包括:

  • 函数调用
  • 中断处理
  • 进程切换

Linux系统对中断处理的演进

Linux对中断的扩展:硬件中断、软件中断

  • Linux系统把中断的意义扩展了,对于按键中断等硬件产生的中断,称之为“硬件中断”(hard irq)。每个硬件中断都有对应的处理函数,比如按键中断、网卡中断的处理函数肯定不一样。
  • 相对的,还可以人为地制造中断:软件中断(soft irq)

    • 有哪些软件中断?查内核源码include/linux/interrupt.h
      
      enum
      {
      HI_SOFTIRQ=0,
      TIMER_SOFTIRQ,
      NET_TX_SOFTIRQ,
      NET_RX_SOFTIRQ,
      BLOCK_SOFTIRQ,
      IRQ_POLL_SOFTIRQ,
      TASKLET_SOFTIRQ,
      SCHED_SOFTIRQ,
      HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
              numbering. Sigh! */
      RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

    NR_SOFTIRQS };

    
    + 怎么触发软件中断?最核心的函数是`raise_softirq`,简单地理解就是设置`softirq_veq[nr]`的标记位 `extern void raise_softirq(unsigned int nr);`
    + 怎么设置软件中断的处理函数:`extern void open_softirq(int nr, void (*action)(struct softirq_action *));`
    + 后面讲到的中断下半部tasklet就是使用软件中断实现的

中断处理原则1:不能嵌套

如果中断嵌套突然暴发,那么栈将越来越大,栈终将耗尽。

中断处理原则2:越快越好

  • 在单芯片系统中,假设中断处理很慢,那应用程序在这段时间内就无法执行:系统显得很迟顿。
  • 在SMP系统中,假设中断处理很慢,那么正在处理这个中断的CPU上的其他线程也无法执行。
  • 在中断的处理过程中,该CPU是不能进行进程调度的,所以中断的处理要越快越好,尽早让其他中断能被处理──进程调度靠定时器中断来实现。

在Linux系统中使用中断是挺简单的,为某个中断irq注册中断处理函数handler,可以使用request_irq函数, 在handler函数中,代码尽可能高效。 static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)

要处理的事情实在太多,拆分为:上半部、下半部

在handler函数里只做紧急的事,然后就重新开中断,让系统得以正常运行;那些不紧急的事,以后再处理,处理时是开中断的。

中断下半部的实现有很多种方法,讲2种主要的:tasklet(小任务)、work queue(工作队列)。

下半部要做的事情耗时不是太长:tasklet

当下半部比较耗时但是能忍受,并且它的处理比较简单时,可以用tasklet来处理下半部。tasklet是使用软件中断来实现。

写字太多,不如贴代码,代码一目了然:

使用流程图简化一下: 简单说:

  • 下半部,开中断的时候,可以被中断,但是被中断前,count >= 1
  • 再次中断上半部结束的时候,会检查 count 的值,正常情况 count 是0,但是如果是再下半部中被中断的,那么 count 就不是0,就会直接退出结束本次中断
  • cpu 会回到上次下半部被中断的地方继续执行下去。

注意:从上面的流程可以看出,上半部和下半部的关系是 N:1. 所以,下半部处理的时候要完整一点。 比如按键处理,那就要处理所有按键。

下半部要做的事情太多并且很复杂:工作队列

如果中断要做的事情实在太耗时,那就不能用软件中断来做,而应该用内核线程来做:在中断上半部唤醒内核线程。内核线程和APP都一样竞争执行,APP有机会执行,系统不会卡顿。

这个内核线程是系统帮我们创建的,一般是kworker线程,内核中有很多这样的线程 ps -A | grep kworker, kworker线程要去“工作队列”(work queue)上取出一个一个“工作”(work),来执行它里面的函数。

  • 创建work:#define DECLARE_WORK(n, f) struct work_struct n = __WORK_INITIALIZER(n, f) 先写出一个函数,然后用这个宏填充一个work结构体。
  • 要执行这个函数时,把work提交给work queue就可以了 static inline bool schedule_work(struct work_struct *work) 上述函数会把work提供给系统默认的work queue:system_wq,它是一个队列。
  • 谁来执行work中的函数?schedule_work函数不仅仅是把work放入队列,还会把kworker线程唤醒。此线程抢到时间运行时,它就会从队列中取出work,执行里面的函数。
  • 谁把work提交给work queue?在中断场景中,可以在中断上半部调用schedule_work函数。

新技术:threaded irq

新技术threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个CPU上执行,这提高了效率。

你可以只提供thread_fn,系统会为这个函数创建一个内核线程。发生中断时,内核线程就会执行这个函数。 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)

下半部总结

为什么要有这么多种下半部处理方法?

  • tasklet: 比上半部处理的好处是,可以被中断,用于处理稍微需要点时间的事务。 本质上还是在中断处理。
  • work_queue: 比 tasklet 的好处是,work_queue 运行在进程上下文,可以进行休眠、阻塞、发生调度等。在内核线程中处理。
  • threaded_irq: 比 work_queue 的好处是,在多核处理器上运行,效率高,因为每个 threaded_irq 创建一个内核线程,所以可以被多核分别调度。 而 work_queue 只能挤在一个核上面。

Linux中断系统中的重要数据结构

最核心的结构体是irq_desc,之前为了易于理解,我们说在Linux内核中有一个中断数组,对于每一个硬件中断,都有一个数组项,这个数组就是irq_desc数组。

注意:如果内核配置了CONFIG_SPARSE_IRQ,那么它就会用基数树(radix tree)来代替irq_desc数组。SPARSE的意思是“稀疏”,假设大小为1000的数组中只用到2个数组项,那不是浪费嘛?所以在中断比较“稀疏”的情况下可以用基数树来代替数组。

irq_desc数组

irq_desc结构体在include/linux/irqdesc.h 中定义,主要内容如下图, 每一个irq_desc数组项中都有一个函数:handle_irq,还有一个action链表。

struct irq_desc {
    ...
    struct irq_data     irq_data;
    irq_flow_handler_t  handle_irq;
    struct irqaction    *action;    /* IRQ action list */
    const char      *name;
    ...
} ____cacheline_internodealigned_in_smp;

外部设备1、外部设备n共享一个GPIO中断B,多个GPIO中断汇聚到GIC(通用中断控制器)的A号中断,GIC再去中断CPU。那么软件处理时就是反过来,先读取GIC获得中断号A,再细分出GPIO中断B,最后判断是哪一个外部芯片发生了中断。

中断的处理函数来源有三:

GIC的处理函数

假设irq_desc[A].handle_irq是XXX_gpio_irq_handler(XXX指厂家),这个函数需要读取芯片的GPIO控制器,细分发生的是哪一个GPIO中断(假设是B),再去调用irq_desc[B]. handle_irq。

注意:irq_desc[A].handle_irq细分出中断后B,调用对应的irq_desc[B].handle_irq。显然中断A是CPU感受到的顶层的中断,GIC中断CPU时,CPU读取GIC状态得到中断A。

模块的中断处理函数

比如对于GPIO模块向GIC发出的中断B,它的处理函数是irq_desc[B].handle_irq。 BSP开发人员会设置对应的处理函数,一般是handle_level_irq或handle_edge_irq,从名字上看是用来处理电平触发的中断、边沿触发的中断。

注意:导致GPIO中断B发生的原因很多,可能是外部设备1,可能是外部设备n,可能只是某一个设备,也可能是多个设备。所以irq_desc[B].handle_irq会调用某个链表里的函数,这些函数由外部设备提供。这些函数自行判断该中断是否自己产生,若是则处理。

外部设备提供的处理函数:

这里说的“外部设备”可能是芯片,也可能总是简单的按键。它们的处理函数由自己驱动程序提供,这是最熟悉这个设备的“人”:它知道如何判断设备是否发生了中断,如何处理中断。

对于共享中断,比如GPIO中断B,它的中断来源可能有多个,每个中断源对应一个中断处理函数。所以irq_desc[B]中应该有一个链表,存放着多个中断源的处理函数。

一旦程序确定发生了GPIO中断B,那么就会从链表里把那些函数取出来,一一执行。这个链表就是action链表。

irqaction结构体

irqaction结构体在 include/linux/interrupt.h 中定义,主要内容如下图:

struct irqaction {
    irq_handler_t       handler;
    void            *dev_id;
    struct irqaction    *next;
    irq_handler_t       thread_fn;
    struct task_struct  *thread;
    struct irqaction    *secondary;
    unsigned int        irq;
    unsigned int        flags;
    const char      *name;
    ...
} ____cacheline_internodealigned_in_smp;
  • 当调用request_irq、request_threaded_irq注册中断处理函数时,内核就会构造一个irqaction结构体。在里面保存name、dev_id等,最重要的是handler、thread_fn、thread。
  • handler是中断处理的上半部函数,用来处理紧急的事情。
  • thread_fn对应一个内核线程thread,当handler执行完毕,Linux内核会唤醒对应的内核线程。在内核线程里,会调用 thread_fn函数。
  • 可以提供handler而不提供thread_fn,就退化为一般的request_irq函数。
  • 可以不提供handler只提供thread_fn,完全由内核线程来处理中断。
  • 也可以既提供handler也提供thread_fn,这就是中断上半部、下半部。
  • 在reqeust_irq时可以传入dev_id,为何需要dev_id?作用如下,在共享中断中必须提供dev_id,非共享中断可以不提供。
    • 中断处理函数执行时,可以使用dev_id
    • 卸载中断时要传入dev_id,这样才能在action链表中根据dev_id找到对应项

irq_data结构体

irq_data结构体在 include/linux/irq.h 中定义,主要内容如下:

struct irq_data {
    u32         mask;
    unsigned int        irq;
    unsigned long       hwirq;
    struct irq_common_data  *common;
    struct irq_chip     *chip;
    struct irq_domain   *domain;
#ifdef  CONFIG_IRQ_DOMAIN_HIERARCHY
    struct irq_data     *parent_data;
#endif
    void            *chip_data;
};
  • irq_data 就是个中转站,里面有irq_chip指针 irq_domain指针,都是指向别的结构体。
  • 有意思的是irq、hwirq,irq是软件中断号,hwirq是硬件中断号。比如上面我们举的例子,在GPIO中断B是软件中断号,可以找到irq_desc[B]这个数组项;GPIO里的第x号中断,这就是hwirq。
  • 谁来建立irq、hwirq之间的联系呢?由irq_domain来建立。irq_domain会把本地的hwirq映射为全局的irq,什么意思?比如GPIO控制器里有第1号中断,UART模块里也有第1号中断,这两个“第1号中断”是不一样的,它们属于不同的“域”──irq_domain。

irq_domain结构体

irq_domain结构体在 include/linux/irqdomain.h 中定义,主要内容如下:

struct irq_domain {
    const struct irq_domain_ops *ops;
    irq_hw_number_t hwirq_max;
    unsigned int revmap_size;
    unsigned int linear_revmap[];
    ...
};

如何在设备树中指定中断,设备树的中断如何被转换为irq时,irq_domain将会起到极大的作为。

设备树中

interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;

它表示要使用 gpio1 里的第5号中断,hwirq就是 5。但是我们在驱动中会使用 request_irq(irq, handler) 这样的函数来注册中断,irq是什么?它是软件中断号,它应该从“gpio1的第5号中断”转换得来。

谁把hwirq转换为irq?由gpio1的相关数据结构,就是gpio1对应的irq_domain结构体。irq_domain结构体中有一个irq_domain_ops结构体,里面有各种操作函数,主要是:

  • xlate 用来解析设备树的中断属性,提取出hwirq、type等信息。
  • map 把hwirq转换为irq。

irq_chip结构体

irq_chip结构体在 include/linux/irq.h 中定义,主要内容如下

struct irq_chip {
    // irq_enable: enable the interrupt (defaults to chip->unmask if NULL)
    void        (*irq_enable)(struct irq_data *data);  
    // irq_disable: disable the interrupt
    void        (*irq_disable)(struct irq_data *data);
    // irq_ack: start of a new interrupt
    void        (*irq_ack)(struct irq_data *data);
    // irq_mask: mask an interrupt source
    void        (*irq_mask)(struct irq_data *data);
    // irq_mask_ack: ack and mask an interrupt source
    void        (*irq_mask_ack)(struct irq_data *data);
    ...
};
  • 我们在request_irq后,并不需要手工去使能中断,原因就是系统调用对应的irq_chip里的函数帮我们使能了中断。
  • 我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用irq_chip中的相关函数。
  • 但是对于外部设备相关的清中断操作,还是需要我们自己做的。外设备千变万化,内核里可没有对应的清除中断操作

在设备树中指定中断_在代码中获得中断

发表评论