Linux中断子系统(二)-通用框架处理

背景

  • 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. 概述

【原创】Linux中断子系统(一)-中断控制器及驱动分析讲到了底层硬件GIC驱动,以及Arch-Specific的中断代码,本文将研究下通用的中断处理的过程,属于硬件无关层。当然,我还是建议你看一下上篇文章。

这篇文章会解答两个问题:

  1. 用户是怎么使用中断的(中断注册)?
  2. 外设触发中断信号时,最终是怎么调用到中断handler的(中断处理)?

2. 数据结构分析

先来看一下总的数据结构,核心是围绕着struct irq_desc来展开:

  • Linux内核的中断处理,围绕着中断描述符结构struct irq_desc展开,内核提供了两种中断描述符组织形式:

    1. 打开CONFIG_SPARSE_IRQ宏(中断编号不连续),中断描述符以radix-tree来组织,用户在初始化时进行动态分配,然后再插入radix-tree中;
    2. 关闭CONFIG_SPARSE_IRQ宏(中断编号连续),中断描述符以数组的形式组织,并且已经分配好;
    3. 不管哪种形式,最终都可以通过linux irq号来找到对应的中断描述符;
  • 图的左侧灰色部分,主要在中断控制器驱动中进行初始化设置,包括各个结构中函数指针的指向等,其中struct irq_chip用于对中断控制器的硬件操作,struct irq_domain与中断控制器对应,完成的工作是硬件中断号到Linux irq的映射;

  • 图的上侧灰色部分,中断描述符的创建(这里指CONFIG_SPARSE_IRQ),主要在获取设备中断信息的过程中完成的,从而让设备树中的中断能与具体的中断描述符irq_desc匹配;

  • 图中剩余部分,在设备申请注册中断的过程中进行设置,比如struct irqactionhandler的设置,这个用于指向我们设备驱动程序中的中断处理函数了;

中断的处理主要有以下几个功能模块:

  1. 硬件中断号到Linux irq中断号的映射,并创建好irq_desc中断描述符;
  2. 中断注册时,先获取设备的中断号,根据中断号找到对应的irq_desc,并将设备的中断处理函数添加到irq_desc中;
  3. 设备触发中断信号时,根据硬件中断号得到Linux irq中断号,找到对应的irq_desc,最终调用到设备的中断处理函数;

上述的描述比较简单,更详细的过程,往下看吧。

3. 流程分析

3.1 中断注册

这一次,让我们以问题的方式来展开:

先来让我们回答第一个问题:用户是怎么使用中断的?

  1. 熟悉设备驱动的同学应该都清楚,经常会在驱动程序中调用request_irq()接口或者request_threaded_irq()接口来注册设备的中断处理函数;
  2. request_irq()/request_threaded_irq接口中,都需要用到irq,也就是中断号,那么这个中断号是从哪里来的呢?它是Linux irq,它又是如何映射到具体的硬件设备的中断号的呢?

先来看第二个问题:设备硬件中断号到Linux irq中断号的映射

  • 硬件设备的中断信息都在设备树device tree中进行了描述,在系统启动过程中,这些信息都已经加载到内存中并得到了解析;

  • 驱动中通常会使用platform_get_irqirq_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完成两个工作:

    1. 针对linux irq中断号创建一个irq_desc中断描述符;
    2. 调用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

上述函数执行完成后,完成了两大工作:

  1. 硬件中断号与Linux中断号完成映射,并为Linux中断号创建了irq_desc中断描述符;
  2. 数据结构的绑定及初始化,关键的地方是设置了中断处理往上执行的入口;

再看第一个问题:中断是怎么来注册的?

设备驱动中,获取到了irq中断号后,通常就会采用request_irq/request_threaded_irq来注册中断,其中request_irq用于注册普通处理的中断,request_threaded_irq用于注册线程化处理的中断;

在讲具体的注册流程前,先看一下主要的中断标志位:

arduino 复制代码
#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用于完成中断的相关设置,包括中断线程化的处理:

    1. 中断线程化用于减少系统关中断的时间,增强系统的实时性;
    2. ARM64默认开启了CONFIG_IRQ_FORCED_THREADING,引导参数传入threadirqs时,则除了IRQF_NO_THREAD外的中断,其他的都将强制线程化处理;
    3. 中断线程化会为每个中断都创建一个内核线程,如果中断进行共享,对应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_irqhandle_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 总结

中断的处理,总体来说可以分为两部分来看:

  1. 从上到下:围绕irq_desc中断描述符建立好连接关系,这个过程就包括:中断源信息的解析(设备树),硬件中断号到Linux中断号的映射关系、irq_desc结构的分配及初始化(内部各个结构的组织关系)、中断的注册(填充irq_desc结构,包括handler处理函数)等,总而言之,就是完成静态关系创建,为中断处理做好准备;
  2. 从下到上,当外设触发中断信号时,中断控制器接收到信号并发送到处理器,此时处理器进行异常模式切换,并逐步从处理器架构相关代码逐级回调。如果涉及到中断线程化,则还需要进行中断内核线程的唤醒操作,最终完成中断处理函数的执行。
相关推荐
文心快码BaiduComate18 小时前
Comate 搭载GLM-5.2:百万上下文,稳定支撑长程任务
前端·程序员·开源
程序员cxuan18 小时前
分享一下我最近常用的 10 个 Codex 小技巧。
人工智能·后端·程序员
Moonbit19 小时前
MoonBit ×CCF开源创新大赛 倒计时24天!快来提交你的作品
程序员·编程语言
zzzzzz31019 小时前
假如我是掘金管理员,我先给评论区装个'代码审查'系统
python·程序员·机器人
demo007x1 天前
Docling 文档转换以及技术架构分析
前端·后端·程序员
保持当下1 天前
分享一些程序员很棘手但是却又简单的工具
程序员·免费·js·工具
Hilaku2 天前
AI 写代码越快,为什么 Code Review 越不能省?
前端·javascript·程序员
程序员cxuan2 天前
LobsterAI 快把职业门槛打没了
人工智能·程序员
Coffeeee2 天前
Codachi — 藏在 Claude Code 状态栏里的电子宠物
人工智能·程序员·claude