简介

1 - 中断综述 - 图1
1 - 中断综述 - 图2

首先我们可以使用下面的命令查看中断号的映射

  1. cat /proc/interrupts

1 - 中断综述 - 图3
中断控制器还需要根据CPU预先设定的规则,将某个中断送入指定的CPU(有点像路由器),以实现中断的负载均衡(irq balance)。x86架构的中断控制器被称为APIC(Advanced Programmable Interrupt Controller),ARM架构的中断控制器则被称为GIC(Generic Interrupt Controller),它们都是适用于多核系统的。

数据结构

内核的目录结构

  1. Linux/
  2. |
  3. +-->include/
  4. | |
  5. | |-->irqchip/ 中断控制器相关API定义
  6. | |-->linux/ Linux硬件无关中断处理层API定义
  7. | |
  8. | +-->irqdesc.h
  9. | |-->irqhandler.h
  10. | |-->irqdomain.h
  11. | |-->irqflags.h
  12. | +-->irq.h
  13. +-->kernel/
  14. | |
  15. | +-->irq/ Linux硬件无关中断处理层实现
  16. | |
  17. | +-->irqdesc.c/irqdomain.c/.../etc.
  18. +-->driver/
  19. |
  20. +-->irqchip/ 中断控制器的相关实现

struct irq_chip

内核中断控制器的结构体是struct irq_chip,其中,”name”是中断控制器的名称,就是我们在”/proc/interupts”中看到的那个中断控制器的名称。

  1. struct irq_chip {
  2. const char *name;
  3. void (*irq_enable)(struct irq_data *data);
  4. void (*irq_disable)(struct irq_data *data);
  5. void (*irq_mask)(struct irq_data *data);
  6. void (*irq_unmask)(struct irq_data *data);
  7. void (*ipi_send_single)(struct irq_data *data, unsigned int cpu);
  8. void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest);
  9. ...
  10. };

struct irq_desc

Linux内核中描述中断的结构体为.其中”name”是这个IRQ的名称,同样可以在”/proc/interupts”中查看的中断设备相关的名称。

  1. struct irq_desc {
  2. const char *name;
  3. unsigned int depth;
  4. # 中断处理函数(ISR - Interrupt Service Routine),
  5. struct irqaction *action;
  6. struct irq_data irq_data;
  7. struct cpumask *percpu_enabled;
  8. ...
  9. };

需要注意的是为什么要使用struct irqaction 这样一个结构体。而不是直接使用一个回调函数?
这是因为在早期的一些处理器中,硬件中断的数目很少,而且有些中断编号已经永久性地分配给了标准的系统组件(比如keyboard和timer),为了服务于更多的设备,只能对有限的IRQ中断号进行共享。这样,同一个中断号就会对应多个不同的处理函数,这些处理函数通过一个单向链表串联在一起的。

struct irqaction

  1. struct irqaction {
  2. irq_handler_t handler;
  3. void *dev_id;
  4. struct irqaction *next;
  5. ...
  6. }

当一个中断发生的时候,其对应IRQ链表上的所有”irqaction”的”handler”都将被依次执行,以判断是否是自己的设备产生的中断,这主要靠读取自己设备的中断状态寄存器来完成。因此共享中断时,即便不是你的设备产生的中断,你的”handler”也会被调用到。为了避免无谓的消耗,需要一进”handler”就立刻进行判断,如果不是,就尽快的退出。
1 - 中断综述 - 图4
1 - 中断综述 - 图5当一个设备从挂接的IRQ线上卸载时,设备对应的”irqaction”也应该相应地从IRQ链表中移除,此时需要一个表示挂接在同一个IRQ上的不同设备的标识,这个标识就是”dev_id”。内核通过比对”dev_id”,来找到那个应该移除的”irqaction”。

struct irq_data

这个结构体是包含在struct irq_desc中的结构体。

  1. struct irq_data {
  2. struct irq_chip *chip;
  3. struct irq_domain *domain;
  4. unsigned int irq;
  5. unsigned long hwirq;
  6. ...
  7. };

其中,”chip”就指向了这个IRQ所挂接的中断控制器,两者的绑定是通过irq_set_chip()函数完成的。

  1. int irq_set_chip(unsigned int irq, struct irq_chip *chip)
  2. {
  3. struct irq_desc *desc = irq_get_desc_lock(irq, &flags, 0);
  4. desc->irq_data.chip = chip;
  5. ...
  6. }

1 - 中断综述 - 图6
绑定之后就可以利用”irq_chip”提供的各种处理函数了,比如内核提供的用于禁止一个IRQ的irq_disable(),它就是通过该IRQ对应的”irq_desc”的”irq_data”域,找到对应的”irq_chip”,进而回调”irq_chip”中的”irq_disable”函数。

中断域

早期的系统,只有一个中断控制器,接入中断控制器的物理中断号都是不同的。但是随着计算机系统的发展,系统中可以挂接更多的中断控制器。特别是嵌入式系统的出现,类似GPIO这种也可以视作一种中断控制器。每个中断控制器都有自己中断线的物理编号,且这些物理编号会有重复。此时,Linux Kernel发展出了IRQ Domain的概念,来区分这些相同的物理中断编号。
在Linux中,我们可以使用两个ID来标识外设的中断,

  1. IRQ number。CPU需要为每一个外设中断编号,我们称之IRQ Number。这个IRQ number是一个虚拟的interrupt ID,和硬件无关,仅仅是被CPU用来标识一个外设中断。
  2. HW interrupt ID。对于interrupt controller而言,它收集了多个外设的interrupt request line并向上传递,因此,interrupt controller需要对外设中断进行编码。Interrupt controller用HW interrupt ID来标识外设的中断。在interrupt controller级联的情况下,仅仅用HW interrupt ID已经不能唯一标识一个外设中断,还需要知道该HW interrupt ID所属的interrupt controller(HW interrupt ID在不同的Interrupt controller上是会重复编码的)。
    1. 比如有3个中断控制器A,B,C,每一个中断控制器外接了8个外设。那么对于中断控制器A来说外设HW interrupt ID的范围0~7,同时B,C也是相同的。这样就造成了重复编码。

这样,CPU和interrupt controller在标识中断上就有了一些不同的概念,但是,对于驱动工程师而言,我们和CPU视角是一样的,我们只希望得到一个IRQ number,而不关系具体是那个interrupt controller上的那个HW interrupt ID。这样一个好处是在中断相关的硬件发生变化的时候,驱动软件不需要修改。因此,linux kernel中的中断子系统需要提供一个将HW interrupt ID映射到IRQ number上来的机制,这就是中断域的产生了。
Notes:两种视角的中断号:IRQ Number:从驱动软件来看,CPU对每个中断进行编号。HW interrupt ID:从中断控制起来看,每个中断控制器上的中断都有一个编号。这两种不同视角就导致了,从硬件到软件的一个转换。

数据结构

struct irq_domain

  1. include/linux/irqdomain.h:110:
  2. struct irq_domain {
  3. struct list_head link; //Linux全局irq_domain链表的成员
  4. const char *name; //中断控制域的名称
  5. const struct irq_domain_ops *ops; //中断控制域的操作集合
  6. void *host_data; //该中断控制域的私有数据指针
  7. unsigned int flags; //该中断控制域的标识
  8. struct fwnode_handle *fwnode; //待补充
  9. enum irq_domain_bus_token bus_token;//中断域的总线类型,见irq_domain_bus_token的定义
  10. struct irq_domain_chip_generic *gc; //IRQ Domain使用的中断芯片数据结构
  11. struct irq_domain *parent; //如果支持中断控制域层次结构,指向该控制域的父级
  12. irq_hw_number_t hwirq_max; //该域中的最大物理中断号
  13. unsigned int revmap_direct_max_irq; //直接映射的最大中断号
  14. unsigned int revmap_size; //虚拟/物理线性映射表的大小
  15. struct radix_tree_root revmap_tree; //虚拟/物理映射基树(当该IRQ Domain使用基树方式映射时)
  16. unsigned int linear_revmap[]; //虚拟/物理线性映射表
  17. };
  18. enum irq_domain_bus_token {
  19. DOMAIN_BUS_ANY = 0,
  20. DOMAIN_BUS_PCI_MSI,
  21. DOMAIN_BUS_PLATFORM_MSI,
  22. DOMAIN_BUS_NEXUS,
  23. };

API

向系统注册irq domain

注册中断域可以分为3种类型。

  1. 线性映射
  2. Radix Tree map
  3. no map

    线性映射

    线性映射。其实就是一个lookup table,HW interrupt ID作为index,通过查表可以获取对应的IRQ number。对于Linear map而言,interrupt controller对其HW interrupt ID进行编码的时候要满足一定的条件:hw ID不能过大,而且ID排列最好是紧密的。对于线性映射,其接口API如下:
  1. static inline struct irq_domain *irq_domain_add_linear(struct device_node *of_node,
  2. unsigned int size,---------该interrupt domain支持多少IRQ
  3. const struct irq_domain_ops *ops,---callback函数
  4. void *host_data)-----driver私有数据
  5. {
  6. return __irq_domain_add(of_node, size, size, 0, ops, host_data);
  7. }


为irq domain创建映射

上面我们展示的是系统注册一个irq domain,但是我们前文提到的 HW interrupt ID和 IRQ number的映射关系还需要我们来实现。创建映射有四个接口函数:

  1. Linux-4.9.88/include/linux/irqdomain.h
  2. extern unsigned int irq_create_mapping(struct irq_domain *host,
  3. irq_hw_number_t hwirq);
  4. extern unsigned int irq_create_fwspec_mapping(struct irq_fwspec *fwspec);
  5. extern unsigned int irq_create_direct_mapping(struct irq_domain *host);
  6. extern int irq_create_strict_mappings(struct irq_domain *domain,
  7. unsigned int irq_base,
  8. irq_hw_number_t hwirq_base, int count);

irq_create_mapping

  1. kernel/irq/irqdomain.c:391
  2. /**
  3. * irq_create_mapping() - Map a hardware interrupt into linux irq space
  4. * @domain: domain owning this hardware interrupt or NULL for default domain
  5. * @hwirq: hardware irq number in that domain space
  6. *
  7. * Only one mapping per hardware interrupt is permitted. Returns a linux
  8. * irq number.
  9. * If the sense/trigger is to be specified, set_irq_type() should be called
  10. * on the number returned from that call.
  11. */
  12. unsigned int irq_create_mapping(struct irq_domain *domain,
  13. irq_hw_number_t hwirq)

该接口函数以irq domain和HW interrupt ID为参数,返回IRQ number(这个IRQ number是动态分配的)。驱动调用该函数的时候必须提供HW interrupt ID,也就是意味着driver知道自己使用的HW interrupt ID,而一般情况下,HW interrupt ID其实对具体的driver应该是不可见的,不过有些场景比较特殊,例如GPIO类型的中断,它的HW interrupt ID和GPIO有着特定的关系,driver知道自己使用那个GPIO,也就是知道使用哪一个HW interrupt ID了。

irq_create_strict_mappings

  1. kernel/irq/irqdomain.c:452:
  2. /**
  3. * irq_create_strict_mappings() - Map a range of hw irqs to fixed linux irqs
  4. * @domain: domain owning the interrupt range
  5. * @irq_base: beginning of linux IRQ range
  6. * @hwirq_base: beginning of hardware IRQ range
  7. * @count: Number of interrupts to map
  8. *
  9. * This routine is used for allocating and mapping a range of hardware
  10. * irqs to linux irqs where the linux irq numbers are at pre-defined
  11. * locations. For use by controllers that already have static mappings
  12. * to insert in to the domain.
  13. *
  14. * Non-linear users can use irq_create_identity_mapping() for IRQ-at-a-time
  15. * domain insertion.
  16. *
  17. * 0 is returned upon success, while any failure to establish a static
  18. * mapping is treated as an error.
  19. */
  20. int irq_create_strict_mappings(struct irq_domain *domain, unsigned int irq_base,
  21. irq_hw_number_t hwirq_base, int count)

给一组HW interrupt ID映射为IRQ number。

irq_create_of_mapping

  1. unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)

这个接口是利用device tree进行映射关系的建立。通常,一个普通设备的device tree node已经描述了足够的中断信息,在这种情况下,该设备的驱动在初始化的时候可以调用irq_of_parse_and_map这个接口函数进行该device node中和中断相关的内容(interrupts和interrupt-parent属性)进行分析,并建立映射关系,具体代码如下

  1. unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
  2. {
  3. struct of_phandle_args oirq;
  4. if (of_irq_parse_one(dev, index, &oirq))----分析device node中的interrupt相关属性
  5. return 0;
  6. return irq_create_of_mapping(&oirq);-----创建映射,并返回对应的IRQ number
  7. }

推荐使用这种做法,在设备树里面指定好。

常用中断的下半部分处理

中断处理的下半部分有四种。

  1. Workqueue
  2. Threaded IRQs
  3. Softirq
  4. Tasklets

    参考资料

    Linux kernel的中断子系统之(一):综述
    linux中断—中断上下文&进程上下文
    Linux的中断处理机制(一) - 数据结构(1)
    Linux中断(1)
    Linux kernel的中断子系统之(二):IRQ Domain介绍
    漫画-Linux中断子系统综述
    Linux中断(interrupt)子系统之一:中断系统基本原理
    PlantUML例程