说明:Kernel版本:4.14 ARM64处理器,Contex-A53,双核 使用工具:Source Insight 3.5, Visio 1. 概述 深入分析Linux中断子系统之中断控制器及驱动讲到了底层硬件GIC驱动,以及Arch-Specific的中断代码,本文将研究下通用的中断处理的过程,属于硬件无关层。当然,我还是建议你看一下上篇文章。 这篇文章会解答两个问题: 用户是怎么使用中断的( 中断注册 )?外设触发中断信号时,最终是怎么调用到中断handler的( 中断处理 )?2. 数据结构分析 先来看一下总的数据结构,核心是围绕着 struct irq_desc 来展开: Linux内核的中断处理,围绕着中断描述符结构 struct irq_desc 展开,内核提供了两种中断描述符组织形式:打开 CONFIG_SPARSE_IRQ 宏(中断编号不连续),中断描述符以radix-tree 来组织,用户在初始化时进行动态分配,然后再插入radix-tree 中;关闭 CONFIG_SPARSE_IRQ 宏(中断编号连续),中断描述符以数组的形式组织,并且已经分配好;不管哪种形式,都可以通过 linux irq 号来找到对应的中断描述符;图的左侧灰色部分,主要在中断控制器驱动中进行初始化设置,包括各个结构中函数指针的指向等,其中 struct irq_chip 用于对中断控制器的硬件操作,struct irq_domain 与中断控制器对应,完成的工作是硬件中断号到Linux irq 的映射;图的上侧灰色部分,中断描述符的创建(这里指 CONFIG_SPARSE_IRQ ),主要在获取设备中断信息的过程中完成的,从而让设备树中的中断能与具体的中断描述符irq_desc 匹配;图中剩余部分,在设备申请注册中断的过程中进行设置,比如 struct irqaction 中handler 的设置,这个用于指向我们设备驱动程序中的中断处理函数了; 中断的处理主要有以下几个功能模块: 硬件中断号到 Linux irq 中断号的映射,并创建好irq_desc 中断描述符;中断注册时,先获取设备的中断号,根据中断号找到对应的 irq_desc ,并将设备的中断处理函数添加到irq_desc 中;设备触发中断信号时,根据硬件中断号得到 Linux irq 中断号,找到对应的irq_desc ,最终调用到设备的中断处理函数; 上述的描述比较简单,更详细的过程,往下看吧。 更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取. 3. 流程分析3.1 中断注册 这一次,让我们以问题的方式来展开:先来让我们回答第一个问题:用户是怎么使用中断的? 熟悉设备驱动的同学应该都清楚,经常会在驱动程序中调用 request_irq() 接口或者request_threaded_irq() 接口来注册设备的中断处理函数;request_irq()/request_threaded_irq 接口中,都需要用到irq ,也就是中断号,那么这个中断号是从哪里来的呢?它是Linux irq ,它又是如何映射到具体的硬件设备的中断号的呢? 先来看第二个问题:设备硬件中断号到 Linux irq 中断号的映射 硬件设备的中断信息都在设备树 device tree 中进行了描述,在系统启动过程中,这些信息都已经加载到内存中并得到了解析;驱动中通常会使用 platform_get_irq 或irq_of_parse_and_map 接口,去根据设备树的信息去创建映射关系(硬件中断号到linux irq 中断号映射);深入分析Linux中断子系统之中断控制器及驱动提到过struct irq_domain 用于完成映射工作,因此在irq_create_fwspec_mapping 接口中,会先去找到匹配的irq domain ,再去回调该irq domain 中的函数集,通常irq domain 都是在中断控制器驱动中初始化的,以ARM GICv2 为例,最终回调到gic_irq_domain_hierarchy_ops 中的函数;如果已经创建好了映射,那么可以直接进行返回 linux irq 中断号了,否则的话需要irq_domain_alloc_irqs 来创建映射关系;irq_domain_alloc_irqs 完成两个工作:针对 linux irq 中断号创建一个irq_desc 中断描述符;调用 domain->ops->alloc 函数来完成映射,在ARM GICv2 驱动中对应gic_irq_domain_alloc 函数,这个函数很关键,所以下文介绍一下; gic_irq_domain_alloc 函数如下: gic_irq_domain_translate :负责解析出设备树中描述的中断号和中断触发类型(边缘触发、电平触发等);gic_irq_domain_map :将硬件中断号和linux中断号绑定到一个结构中,也就完成了映射,此外还绑定了irq_desc 结构中的其他字段,最重要的是设置了irq_desc->handle_irq 的函数指针,这个最终是中断响应时往上执行的入口,这个是关键,下文讲述中断处理过程时还会提到;根据硬件中断号的范围设置 irq_desc->handle_irq 的指针,共享中断入口为handle_fasteoi_irq ,私有中断入口为handle_percpu_devid_irq ; 上述函数执行完成后,完成了两大工作: 硬件中断号与Linux中断号完成映射,并为Linux中断号创建了 irq_desc 中断描述符;数据结构的绑定及初始化,关键的地方是设置了中断处理往上执行的入口; 再看第一个问题:中断是怎么来注册的? 设备驱动中,获取到了 irq 中断号后,通常就会采用request_irq/request_threaded_irq 来注册中断,其中request_irq 用于注册普通处理的中断,request_threaded_irq 用于注册线程化处理的中断; 在讲具体的注册流程前,先看一下主要的中断标志位: #define IRQF_SHARED 0x00000080 //多个设备共享一个中断号,需要外设硬件支持 #define IRQF_PROBE_SHARED 0x00000100 //中断处理程序允许sharing mismatch发生 #define __IRQF_TIMER 0x00000200 //时钟中断 #define IRQF_PERCPU 0x00000400 //属于特定CPU的中断 #define IRQF_NOBALANCING 0x00000800 //禁止在CPU之间进行中断均衡处理 #define IRQF_IRQPOLL 0x00001000 //中断被用作轮训 #define IRQF_ONESHOT 0x00002000 //一次性触发的中断,不能嵌套,1)在硬件中断处理完成后才能打开中断;2)在中断线程化中保持关闭状态,直到该中断源上的所有thread_fn函数都执行完 #define IRQF_NO_SUSPEND 0x00004000 //系统休眠唤醒操作中,不关闭该中断 #define IRQF_FORCE_RESUME 0x00008000 //系统唤醒过程中必须强制打开该中断 #define IRQF_NO_THREAD 0x00010000 //禁止中断线程化 #define IRQF_EARLY_RESUME 0x00020000 //系统唤醒过程中在syscore阶段resume,而不用等到设备resume阶段 #define IRQF_COND_SUSPEND 0x00040000 //与NO_SUSPEND的用户共享中断时,执行本设备的中断处理函数 request_irq 也是调用request_threaded_irq ,只是在传参的时候,线程处理函数thread_fn 函数设置成NULL;由于在硬件中断号和Linux中断号完成映射后, irq_desc 已经创建好,可以通过irq_to_desc 接口去获取对应的irq_desc ;创建 irqaction ,并初始化该结构体中的各个字段,其中包括传入的中断处理函数赋值给对应的字段;__setup_irq 用于完成中断的相关设置,包括中断线程化的处理:中断线程化用于减少系统关中断的时间,增强系统的实时性; ARM64默认开启了 CONFIG_IRQ_FORCED_THREADING ,引导参数传入threadirqs 时,则除了IRQF_NO_THREAD 外的中断,其他的都将强制线程化处理;中断线程化会为每个中断都创建一个内核线程,如果中断进行共享,对应 irqaction 将连接成链表,每个irqaction 都有thread_mask 位图字段,当所有共享中断都处理完成后才能unmask 中断,解除中断屏蔽;3.2 中断处理 当完成中断的注册后,所有结构的组织关系都已经建立好,剩下的工作就是当信号来临时,进行中断的处理工作。 来回顾一下深入分析Linux中断子系统之中断控制器及驱动 中的Arch-specific 处理流程: 中断收到之后,首先会跳转到异常向量表的入口处,进而逐级进行回调处理,最终调用到 generic_handle_irq 来进行中断处理。 generic_handle_irq 处理如下图: generic_handle_irq 函数最终会调用到desc->handle_irq() ,这个也就是对应到上文中在建立映射关系的过程中,调用irq_domain_set_info 函数,设置好了函数指针,也就是handle_fasteoi_irq 和handle_percpu_devid_irq ;handle_fasteoi_irq :处理共享中断,并且遍历irqaction 链表,逐个调用action->handler() 函数,这个函数正是设备驱动程序调用request_irq/request_threaded_irq 接口注册的中断处理函数,此外如果中断线程化处理的话,还会调用__irq_wake_thread() 唤醒内核线程;handle_percpu_devid_irq :处理per-CPU中断处理,在这个过程中会分别调用中断控制器的处理函数进行硬件操作,该函数调用action->handler() 来进行中断处理; 来看看中断线程化处理后的唤醒流程吧 __handle_irq_event_percpu->__irq_wake_thread : __handle_irq_event_percpu->__irq_wake_thread 将唤醒irq_thread 中断内核线程;irq_thread 内核线程,将根据是否为强制中断线程化对函数指针handler_fn 进行初始化,以便后续进行调用;irq_thread 内核线程将while(!irq_wait_for_interrupt) 循环进行中断的处理,当满足条件时,执行handler_fn ,在该函数中最终调用action->thread_fn ,也就是完成了中断的处理;irq_wait_for_interrupt 函数,将会判断中断线程的唤醒条件,如果满足了,则将当前任务设置成TASK_RUNNING 状态,并返回0,这样就能执行中断的处理,否则就调用schedule() 进行调度,让出CPU,并将任务设置成TASK_INTERRUPTIBLE 可中断睡眠状态;3.3 总结 中断的处理,总体来说可以分为两部分来看: 从上到下:围绕 irq_desc 中断描述符建立好连接关系,这个过程就包括:中断源信息的解析(设备树),硬件中断号到Linux中断号的映射关系、irq_desc 结构的分配及初始化(内部各个结构的组织关系)、中断的注册(填充irq_desc 结构,包括handler处理函数)等,总而言之,就是完成静态关系创建,为中断处理做好准备;从下到上,当外设触发中断信号时,中断控制器接收到信号并发送到处理器,此时处理器进行异常模式切换,并逐步从处理器架构相关代码逐级回调。如果涉及到中断线程化,则还需要进行中断内核线程的唤醒操作,最终完成中断处理函数的执行。 首页 - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛 转载地址:深入分析Linux中断子系统之通用框架处理 - 圈点 - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛