背景

  • Read the fucking source code! --By 鲁迅
  • A picture is worth a thousand words. --By 高尔基

说明:

  1. Kernel版本:4.14
  2. ARM64处理器,Contex-A53,双核
  3. 使用工具:Source Insight 3.5, Visio

1. 概述

从这篇文章开始,来聊一聊中断子系统。

中断是处理器用于异步处理外围设备请求的一种机制,可以说中断处理是操作系统管理外围设备的基石,此外系统调度、核间交互等都离不开中断,它的重要性不言而喻。

来一张概要的分层图:

  • 硬件层:最下层为硬件连接层,对应的是具体的外设与SoC的物理连接,中断信号是从外设到中断控制器,由中断控制器统一管理,再路由到处理器上;
  • 硬件相关层:这个层包括两部分代码,一部分是架构相关的,比如ARM64处理器处理中断相关,另一部分是中断控制器的驱动代码;
  • 通用层:这部分也可以认为是框架层,是硬件无关层,这部分代码在所有硬件平台上是通用的;
  • 用户层:这部分也就是中断的使用者了,主要是各类设备驱动,通过中断相关接口来进行申请和注册,最终在外设触发中断时,进行相应的回调处理;

中断子系统系列文章,会包括硬件相关、中断框架层、上半部与下半部、Softirq、Workqueue等机制的介绍,本文会先介绍硬件相关的原理及驱动,前戏结束,直奔主题。

2. GIC硬件原理

  • ARM公司提供了一个通用的中断控制器GIC(Generic Interrupt Controller)GIC的版本包括V1 ~ V4,由于本人使用的SoC中的中断控制器是V2版本,本文将围绕GIC-V2来展开介绍;

来一张功能版的框图:

  • GIC-V2从功能上说,除了常用的中断使能、中断屏蔽、优先级管理等功能外,还支持安全扩展、虚拟化等;
  • GIC-V2从组成上说,主要分为DistributorCPU Interface两个模块,Distributor主要负责中断源的管理,包括优先级的处理,屏蔽、抢占等,并将最高优先级的中断分发给CPU InterfaceCPU Interface主要用于连接处理器,与处理器进行交互;
  • Virtual DistributorVirtual CPU Interface都与虚拟化相关,本文不深入分析;

再来一张细节图看看DistributorCPU Interface的功能:

  • GIC-V2支持三种类型的中断:

    1. SGI(software-generated interrupts):软件产生的中断,主要用于核间交互,内核中的IPI:inter-processor interrupts就是基于SGI,中断号ID0 - ID15用于SGI
    2. PPI(Private Peripheral Interrupt):私有外设中断,每个CPU都有自己的私有中断,典型的应用有local timer,中断号ID16 - ID31用于PPI
    3. SPI(Shared Peripheral Interrupt):共享外设中断,中断产生后,可以分发到某一个CPU上,中断号ID32 - ID1019用于SPIID1020 - ID1023保留用于特殊用途;
  • Distributor功能:

    1. 全局开关控制Distributor分发到CPU Interface
    2. 打开或关闭每个中断;
    3. 设置每个中断的优先级;
    4. 设置每个中断将路由的CPU列表;
    5. 设置每个外设中断的触发方式:电平触发、边缘触发;
    6. 设置每个中断的Group:Group0或Group1,其中Group0用于安全中断,支持FIQ和IRQ,Group1用于非安全中断,只支持IRQ;
    7. SGI中断分发到目标CPU上;
    8. 每个中断的状态可见;
    9. 提供软件机制来设置和清除外设中断的pending状态;
  • CPU Interface功能:

    1. 使能中断请求信号到CPU上;
    2. 中断的确认;
    3. 标识中断处理的完成;
    4. 为处理器设置中断优先级掩码;
    5. 设置处理器的中断抢占策略;
    6. 确定处理器的最高优先级pending中断;

中断处理的状态机如下图:

  • Inactive:无中断状态;
  • Pending:硬件或软件触发了中断,但尚未传递到目标CPU,在电平触发模式下,产生中断的同时保持pending状态;
  • Active:发生了中断并将其传递给目标CPU,并且目标CPU可以处理该中断;
  • Active and pending:发生了中断并将其传递给目标CPU,同时发生了相同的中断并且该中断正在等待处理;

GIC检测中断流程如下:

  1. GIC捕获中断信号,中断信号assert,标记为pending状态;
  2. Distributor确定好目标CPU后,将中断信号发送到目标CPU上,同时,对于每个CPU,Distributor会从pending信号中选择最高优先级中断发送至CPU Interface
  3. CPU Interface来决定是否将中断信号发送至目标CPU;
  4. CPU完成中断处理后,发送一个完成信号EOI(End of Interrupt)给GIC;

3. GIC驱动分析

3.1 设备信息添加

ARM平台的设备信息,都是通过Device Tree设备树来添加,设备树信息放置在arch/arm64/boot/dts/

下图就是一个中断控制器的设备树信息:

  • compatible字段:用于与具体的驱动来进行匹配,比如图片中arm, gic-400,可以根据这个名字去匹配对应的驱动程序;
  • interrupt-cells字段:用于指定编码一个中断源所需要的单元个数,这个值为3。比如在外设在设备树中添加中断信号时,通常能看到类似interrupts = <0 23 4>;的信息,第一个单元0,表示的是中断类型(1:PPI,0:SPI),第二个单元23表示的是中断号,第三个单元4表示的是中断触发的类型;
  • reg字段:描述中断控制器的地址信息以及地址范围,比如图片中分别制定了GIC Distributor(GICD)GIC CPU Interface(GICC)的地址信息;
  • interrupt-controller字段:表示该设备是一个中断控制器,外设可以连接在该中断控制器上;
  • 关于设备数的各个字段含义,详细可以参考Documentation/devicetree/bindings下的对应信息;

设备树的信息,是怎么添加到系统中的呢?Device Tree最终会编译成dtb文件,并通过Uboot传递给内核,在内核启动后会将dtb文件解析成device_node结构。关于设备树的相关知识,本文先不展开,后续再找机会补充。来一张图,先简要介绍下关键路径:

  • 设备树的节点信息,最终会变成device_node结构,在内存中维持一个树状结构;
  • 设备与驱动,会根据compatible字段进行匹配;

3.2 驱动流程分析

GIC驱动的执行流程如下图所示:

  • 首先需要了解一下链接脚本vmlinux.lds,脚本中定义了一个__irqchip_of_table段,该段用于存放中断控制器信息,用于最终来匹配设备;
  • 在GIC驱动程序中,使用IRQCHIP_DECLARE宏来声明结构信息,包括compatible字段和回调函数,该宏会将这个结构放置到__irqchip_of_table字段中;
  • 在内核启动初始化中断的函数中,of_irq_init函数会去查找设备节点信息,该函数的传入参数就是__irqchip_of_table段,由于IRQCHIP_DECLARE已经将信息填充好了,of_irq_init函数会根据arm,gic-400去查找对应的设备节点,并获取设备的信息。中断控制器也存在级联的情况,of_irq_init函数中也处理了这种情况;
  • or_irq_init函数中,最终会回调IRQCHIP_DECLARE声明的回调函数,也就是gic_of_init,而这个函数就是GIC驱动的初始化入口函数了;
  • GIC的工作,本质上是由中断信号来驱动,因此驱动本身的工作就是完成各类信息的初始化,注册好相应的回调函数,以便能在信号到来之时去执行;
  • set_smp_process_call设置__smp_cross_call函数指向gic_raise_softirq,本质上就是通过软件来触发GIC的SGI中断,用于核间交互;
  • cpuhp_setup_state_nocalls函数,设置好CPU进行热插拔时GIC的回调函数,以便在CPU热插拔时做相应处理;
  • set_handle_irq函数的设置很关键,它将全局函数指针handle_arch_irq指向了gic_handle_irq,而处理器在进入中断异常时,会跳转到handle_arch_irq执行,所以,可以认为它就是中断处理的入口函数了;
  • 驱动中完成了各类函数的注册,此外还完成了irq_chip, irq_domain等结构体的初始化,这些结构在下文会进一步分析;
  • 最后,完成GIC硬件模块的初始化设置,以及电源管理相关的注册等工作;

3.3 数据结构分析

先来张图:

  • GIC驱动中,使用struct gic_chip_data结构体来描述GIC控制器的信息,整个驱动都是围绕着该结构体的初始化,驱动中将函数指针都初始化好,实际的工作是由中断信号触发,也就是在中断来临的时候去进行回调;
  • struct irq_chip结构,描述的是中断控制器的底层操作函数集,这些函数集最终完成对控制器硬件的操作;
  • struct irq_domain结构,用于硬件中断号和Linux IRQ中断号(virq,虚拟中断号)之间的映射;

还是上一下具体的数据结构代码吧,关键注释如下:

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;
}; struct irq_domain {
struct list_head link; //用于添加到全局链表irq_domain_list中
const char *name; //IRQ domain的名字
const struct irq_domain_ops *ops; //IRQ domain映射操作函数集
void *host_data; //在GIC驱动中,指向了irq_gic_data
unsigned int flags;
unsigned int mapcount; //映射中断的个数 /* Optional data */
struct fwnode_handle *fwnode;
enum irq_domain_bus_token bus_token;
struct irq_domain_chip_generic *gc;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_domain *parent; //支持级联的话,指向父设备
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
struct dentry *debugfs_file;
#endif /* reverse map data. The linear map gets appended to the irq_domain */
irq_hw_number_t hwirq_max; //IRQ domain支持中断数量的最大值
unsigned int revmap_direct_max_irq;
unsigned int revmap_size; //线性映射的大小
struct radix_tree_root revmap_tree; //Radix Tree映射的根节点
unsigned int linear_revmap[]; //线性映射用到的查找表
}; struct irq_domain_ops {
int (*match)(struct irq_domain *d, struct device_node *node,
enum irq_domain_bus_token bus_token); // 用于中断控制器设备与IRQ domain的匹配
int (*select)(struct irq_domain *d, struct irq_fwspec *fwspec,
enum irq_domain_bus_token bus_token);
int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw); //用于硬件中断号与Linux中断号的映射
void (*unmap)(struct irq_domain *d, unsigned int virq);
int (*xlate)(struct irq_domain *d, struct device_node *node,
const u32 *intspec, unsigned int intsize,
unsigned long *out_hwirq, unsigned int *out_type); //通过device_node,解析硬件中断号和触发方式 #ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
/* extended V2 interfaces to support hierarchy irq_domains */
int (*alloc)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs, void *arg);
void (*free)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs);
void (*activate)(struct irq_domain *d, struct irq_data *irq_data);
void (*deactivate)(struct irq_domain *d, struct irq_data *irq_data);
int (*translate)(struct irq_domain *d, struct irq_fwspec *fwspec,
unsigned long *out_hwirq, unsigned int *out_type);
#endif
};

3.3.1 IRQ domain

IRQ domain用于将硬件的中断号,转换成Linux系统中的中断号(virtual irq, virq),来张图:

  • 每个中断控制器都对应一个IRQ Domain;
  • 中断控制器驱动通过irq_domain_add_*()接口来创建IRQ Domain;
  • IRQ Domain支持三种映射方式:linear map(线性映射),tree map(树映射),no map(不映射);
    1. linear map:维护固定大小的表,索引是硬件中断号,如果硬件中断最大数量固定,并且数值不大,可以选择线性映射;
    2. tree map:硬件中断号可能很大,可以选择树映射;
    3. no map:硬件中断号直接就是Linux的中断号;

三种映射的方式如下图:

  • 图中描述了三个中断控制器,对应到三种不同的映射方式;
  • 各个控制器的硬件中断号可以一样,最终在Linux内核中映射的中断号是唯一的;

4. Arch-speicific代码分析

  • 中断也是异常模式的一种,当外设触发中断时,处理器会切换到特定的异常模式进行处理,而这部分代码都是架构相关的;ARM64的代码位于arch/arm64/kernel/entry.S
  • ARM64处理器有四个异常级别Exception Level:0~3,EL0级对应用户态程序,EL1级对应操作系统内核态,EL2级对应Hypervisor,EL3级对应Secure Monitor;
  • 异常触发时,处理器进行切换,并且跳转到异常向量表开始执行,针对中断异常,最终会跳转到irq_handler中;

代码比较简单,如下:

/*
* Interrupt handling.
*/
.macro irq_handler
ldr_l x1, handle_arch_irq
mov x0, sp
irq_stack_entry
blr x1
irq_stack_exit
.endm

来张图:

  • 中断触发,处理器去异常向量表找到对应的入口,比如EL0的中断跳转到el0_irq处,EL1则跳转到el1_irq处;
  • 在GIC驱动中,会调用set_handle_irq接口来设置handle_arch_irq的函数指针,让它指向gic_handle_irq,因此中断触发的时候会跳转到gic_handle_irq处执行;
  • gic_handle_irq函数处理时,分为两种情况,一种是外设触发的中断,硬件中断号在16 ~ 1020之间,一种是软件触发的中断,用于处理器之间的交互,硬件中断号在16以内;
  • 外设触发中断后,根据irq domain去查找对应的Linux IRQ中断号,进而得到中断描述符irq_desc,最终也就能调用到外设的中断处理函数了;

GIC和Arch相关的介绍就此打住,下一篇文章会接着介绍通用的中断处理框架,敬请期待。

参考

ARM Generic Interrupt Controller Architecture version 2.0

欢迎关注公众号,不定期更新Linux内核机制相关文章,谢谢。

【原创】Linux中断子系统(一)-中断控制器及驱动分析的更多相关文章

  1. linux中断子系统:中断号的映射与维护初始化mmap过程

    本文均属自己阅读源代码的点滴总结.转账请注明出处谢谢. 欢迎和大家交流.qq:1037701636 email:gzzaigcn2009@163.com 写在前沿: 好久好久没有静下心来整理一些东西了 ...

  2. Linux时间子系统(十七) ARM generic timer驱动代码分析

    一.前言 关注ARM平台上timer driver(clocksource chip driver和clockevent chip driver)的驱动工程师应该会注意到timer硬件的演化过程.在单 ...

  3. linux 输入子系统之电阻式触摸屏驱动

    一.输入子系统情景回忆ING...... 在Linux中,输入子系统是由输入子系统设备驱动层.输入子系统核心层(Input Core)和输入子系统事件处理层(Event Handler)组成.其中设备 ...

  4. linux 输入子系统(2)----简单实例分析系统结构(input_dev层)

    实例代码如下: #include <linux/input.h> #include <linux/module.h> #include <linux/init.h> ...

  5. 【原创】Linux中断子系统(二)-通用框架处理

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...

  6. Linux输入子系统框架分析(1)

    在Linux下的输入设备键盘.触摸屏.鼠标等都能够用输入子系统来实现驱动.输入子系统分为三层,核心层和设备驱动层.事件层.核心层和事件层由Linux输入子系统本身实现,设备驱动层由我们实现.我们在设备 ...

  7. 从需求的角度去理解Linux系列:总线、设备和驱动

    笔者成为博客专家后整理以前原创的嵌入式Linux系列博文,现推出以让更多的读者受益. <从需求的角度去理解linux系列:总线.设备和驱动>是一篇有关如何学习嵌入式Linux系统的方法论文 ...

  8. Linux中断子系统:级联中断控制器驱动

    Linux中断子系统 Linux中断子系统是个很大的话题,如下面的思维导图所示,包含硬件.驱动.中断上半部.中断下半部等等.本文着眼于中断控制器(PIC),特别是级联中断控制器驱动部分,对驱动的设计和 ...

  9. 【原创】Linux中断子系统(三)-softirq和tasklet

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...

随机推荐

  1. 数位dp (2)

    今天继续写几个数位dp F - Balanced Number 题目大意:给你一个区间,让你求这个区间之中满足条件的数字有多少. 这个条件:可以选数的一个位为轴,左右到轴的长度乘上那个数字本身相等的数 ...

  2. Spring Boot在Controllder中常用注解

    1.@RestController @RestController 相当于@Controller+@ResponseBody 注解如果使用@RestController 注解Controller 中的 ...

  3. 学习RxJava+Retrofit+OkHttp+MVP的网络请求使用

    公司的大佬用的是这一套,那我这个菜鸟肯定要学习使用了. 我在网上找了很多文章,写的都很详细,比如 https://www.jianshu.com/u/5fd2523645da https://www. ...

  4. Coursera课程笔记----计算导论与C语言基础----Week 5

    从现实问题到计算机程序(Week 5) 总结回顾 计算机只能按照程序去执行,不可能自己"想出"一个解决问题的方法 面对一个问题,你必须自己找到解决方案,才有可能做出相应的程序 所以 ...

  5. PrintStream:打印流

    package com.itheima.demo05.PrintStream; import java.io.FileNotFoundException; import java.io.PrintSt ...

  6. Navicat15 for Mysql激活教程

    1.下载Navicat Premium Navicat15链接:http://www.navicat.com.cn/download/navicat-premium,选择相应版本,这里选择window ...

  7. 搞懂js中小数运算精度问题原因及解决办法

    js小数运算会出现精度问题 js number类型 JS 数字类型只有number类型,number类型相当于其他强类型语言中的double类型(双精度浮点型),不区分浮点型和整数型. number类 ...

  8. ASP.NET Core依赖注入(DI)

    ASP.NET Core允许我们指定注册服务的生存期.服务实例将根据指定的生存时间自动处理.因此,我们无需担心清理此依赖关系,他将由ASP.NET Core框架处理.有如下三种类型的生命周期. 关于依 ...

  9. 笨办法学习python-ex51自我理解笔记

    本章节主要讲的是web的工作原理,先大概熟悉记录一下,为以后写Django web框架打下基础. web工作原理: 1.用户从浏览器输入网址----->browser通过电脑中的网络设备(网卡) ...

  10. 【雕爷学编程】Arduino动手做(46)---电阻应变片传感器

    37款传感器与模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的.鉴于本人手头积累了一些传感器和模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里 ...