Linux内核-中断机制
2024年9月 · 预计阅读时间: 14 分钟
#
1.中断子系统架构这里的用户层不是 APP 层,还是在驱动内核中
#
1.1 中断控制器GIC(Generic Interrupt Controller),控制中断的打开、关闭、优先级等等。
#
1.2 中断号IRQ number:CPU 需要为每一个外设中断编号,我们称之 IRQ Number。这个 IRQ number 是一个虚拟的 interrupt ID,和硬件无关,仅仅是被 CPU 用来标识一个外设中断。
HW interrupt ID:对于 GIC 中断控制器而言,它收集了多个外设的 interrupt request line 并向上传递,因此,GIC 中断控制器需要对外设中断进行编码。GIC 中断控制器用 HW interrupt ID 来标识外设的中断。
linux kernel 中的中断子系统需要提供一个将 HW interrupt ID 映射到 IRQ number 上来的机制,也就是 irq domain
#
1.3 中断类型RK3566 与 RK3568 使用的是 GIC-V3
GIC-V3 支持四种类型的中断,分别是 SGI、PPI、SPI 和 LPI,每个中断类型的介绍如下:
- SGI(Software Generated Interrupt,软件生成中断):SGI 是通过向 GIC 中的 SGI 寄存器写入来生成的中断。它通常用于处理器之间的通信,允许一个 PE 发送中断给一个或多个指定的 PE,中断号 ID0 - ID15 用于 SGI。
- PPI(Private Peripheral Interrupt,私有外设中断):针对特定 PE 的外设中断。不与其他 PE 共享,中断号 ID16 - ID31 用于 PPI,比如每个核上有个 tick 中断,用于进程调度使用。
- SPI(Shared Peripheral Interrupt,共享外设中断):全局外设中断,可以路由到指定的处理器核心(PE)或一组 PE,它允许多个 PE 接收同一个中断,此类中断时由外设触发的中断信号,比如按键、串口。中断号 ID32 - ID1019 用于 SPI。
- LPI(Locality-specific Peripheral Interrupt,特定局部外设中断):LPI 是 GICv3 中引入的一种中断类型,与其他类型的中断有几个不同之处。LPI 总是基于消息的中断,其配置存储在内存表中,而不是寄存器中。
#
2.内核中断的申请过程(中断上半部)#
2.1 request_irq 函数请求一个中断号(IRQ number)并将一个中断处理程序(irq_handler_t)与该中断关联起来;flags 是中断标志,例如上升沿触发;name 是名字;dev 是传递给中断处理函数的参数。
request_irq()
函数实际上是调用了 request_threaded_irq()
函数来完成中断申请的过程:
request_threaded_irq()
函数的主要作用是在系统中注册中断处理函数。
其中维护了两个结构体:
#
struct irqactionirqaction 结构体是 Linux 内核中用于描述中断行为的数据结构之一。它用于定义中断处理过程中的回调函数和相关属性。irqaction 结构体的主要功能是管理与特定中断相关的行为和处理函数
#
struct irq_desc在 Linux 内核中,每个外设的外部中断都是用的struct irq_desc
结构体进行描述的。
每个硬件中断都有一个对应的 irq_desc 实例,它用于记录与该中断相关的各种信息和状态。该结构体的主要功能是管理中断处理函数、中断行为以及与中断处理相关的其他数据。
#
struct irq_domainxlate 解析设备树,map 函数创建或者更新硬件中断号(hw 参数)和虚拟中断号(virq 参数)的映射关系,保存到 platform_device 中
#
2.2 (重点)内核的中断组织形式与处理流程示例:
硬件连接如下图:
处理流程如下图,
1)发生了 GPIO 中断;
2)首先通过 GIC 的中断处理 handle_irq 函数,读取 GPIO 的寄存器,发现是 GPIO 模块的中断,就调用下面的 GPIO 中断 B;
2.1)读取 GPIO 寄存器可以得到 hwirq,根据 hwirq 得到之前映射的 irq;
2.2)再根据映射的 irq 就可以从中断号“数组”里找到对应的 irq_desc;
3)然后在中断中断处理函数 handle_irq,会调用下面挂载的每一个 irqaction 链表的处理函数(这个是对应具体设备的处理函数);
3)在中断 B 的 irqaction 中会判断中断源,进行相应的中断处理(调用 handler,以及内核线程 thread_fn 等)。(irqaction 是我们注册一个中断时,内核创建的)。
#
3.中断下半部#
3.1 软件中断#
什么是软件中断?软件中断是实现中断下半部的方法之一,但是软中断的资源优先,对应的中断号不多,一般用在网络设备、块设备驱动中。
所有软件中断如下:
以上代码定义了一个枚举类型,用于标识软中断的不同类型或优先级,每个枚举常量对应一个特定的软中断类型。
内核开发者不希望我们直接添加软中断,而是使用tasklet
。
以下 API 用于软中断:
#
tasklet 的使用一种特殊的软中断机制,被广泛用于处理中断下文相关的任务。
一种常见且有效的方法,在多核处理系统上可以避免并发问题。
tasklet 绑定的函数在同一时间只能在一个 CPU 上运行,因此不会出现并发冲突。
tasklet 的 API 如下:
tasklet_disable
关闭 tasklet 后,即使调用 tasklet_schedule 函数触发 tasklet,tasklet 的处理函数也不会再被执行。这可以用于临时暂停或停止 tasklet 的执行,直到再次启用(通过调用 tasklet_enable 函数)。
需要注意的是,
tasklet_disable
关闭 tasklet 并不会销毁 tasklet 结构体,因此可以随时通过调用 tasklet_enable 函数重新启用 tasklet,或者调用 tasklet_kill 函数来销毁 tasklet;tasklet_enable
使能 tasklet 并不会自动触发 tasklet 的执行,而是通过调用tasklet_schedule
函数来触发;- 调度 tasklet 只是将 tasklet 标记为需要执行,并不会立即执行 tasklet 的处理函数。实际的执行时间取决于内核的调度和处理机制;
- 在销毁 tasklet 之前,应该确保该 tasklet 已经被停止(通过调用 tasklet_disable 函数)。否则,销毁一个正在执行的 tasklet 可能导致内核崩溃或其他错误。
#
tasklet 内核源码分析#
1)tasklet 如何初始化?软中断处理函数的定义内核源码 kernel/kernel/softirq.c 文件
在执行__init softirq_init
函数时,会触发 TASKLET_SOFTIRQ
,然后会调用 tasklet_action
函数,一路调用,最终是调用tasklet_action_common
函数
在上面的代码中,tasklet_action_common()函数对任务链表中的每个 tasklet 进行处理。
首先禁用本地中断,获取任务链表头指针,清空任务链表,并重新设置尾指针。
然后它循环遍历任务链表,对每个 tasklet 进行处理。
如果 tasklet 的锁获取成功,并且计数器为 0,它将执行 tasklet 的处理函数,并清除状态标志位。
如果锁获取失败或计数不为 0,它将 tasklet 添加到任务链表的尾部,并触发指定的软中断。
最后,它启用本地中断,完成任务处理过程。
#
2)那么 tasklet 在什么时候加到链表里面的呢?tasklet 是通__tasklet_schedule_common()
函数加入到链表中的。
#
3)tasklet 相比自己添加软中断有哪些优点和缺点呢?优点:
简化的接口和编程模型:Tasklet 提供了一个简单的接口和编程模型,使得在内核中处理延迟工作变得更加容易。相比自己添加软中断,Tasklet 提供了更高级的抽象。
低延迟:Tasklet 在软中断上下文中执行,避免了内核线程的上下文切换开销,因此具有较低的延迟。这对于需要快速响应的延迟敏感任务非常重要。
自适应调度:Tasklet 具有自适应调度的特性,当多个 Tasklet 处于等待状态时,内核会合并它们以减少不必要的上下文切换。这种调度机制可以提高系统的效率。
缺点:
无法处理长时间运行的任务:Tasklet 适用于短时间运行的延迟工作,如果需要处理长时间运行的任务,可能会阻塞其他任务的执行。对于较长的操作,可能需要使用工作队列或内核线程来处理。
缺乏灵活性:Tasklet 的执行受限于软中断的上下文,不适用于所有类型的延迟工作。某些情况下,可能需要更灵活的调度和执行机制,这时自定义软中断可能更加适合。
资源限制:Tasklet 的数量是有限的,系统中可用的 Tasklet 数量取决于架构和内核配置。如果需要大量的延迟工作处理,可能会受到 Tasklet 数量的限制。
综上所述,Tasklet 提供了一种简单且低延迟的延迟工作处理机制,适用于短时间运行的任务和对响应时间敏感的场景。
#
3.2 内核线程#
工作队列 work queue#
1)什么是工作队列工作队列是操作系统中管理和调度异步任务执行的一种机制,协调和分配待处理的任务给可用的工作线程或工作进程。
基本原理是
将需要执行的任务按顺序排列在队列中,并提供一组工作线程或者工作进程来处理队列中的任务。
当有新的任务到达时,它们会被添加到队列的末尾,工作线程或工作进程从队列的头部获取任务,并执行相应的处理操作。
工作队列将工作推后以后,会交给内核线程去执行。Linux 在启动过程中会创建一个工作者内核线程(worker thread),这个线程创建以后处于 sleep 状态。当有工作需要处理的时候,会唤醒这个线程去处理工作。
工作队列包括共享工作队列和自定义工作队列两种:
- 共享队列是由内核管理的全局工作队列,用于处理内核中一些系统级任务。共享工作队列是内核中一个默认工作队列,可以由多个内核组件和驱动程序共享使用。
- 自定义工作队列是由内核或驱动程序创建的特定工作队列,用于处理特定的任务。自定义工作队列通常与特定的内核模块或驱动程序相关联,用于执行该模块或驱动程序相关的任务。
#
2)共享工作队列在 Linux 内核中,使用 work_struct
结构体表示一个工作项
,这些工作组织成工作队列,工作队列
使用 workqueue_struct
结构体表示
工作队列相关接口函数:
示例:
#
3)自定义工作队列共享队列是由内核管理的全局工作队列,自定义工作队列是由内核或驱动程序创建的特定工作队列,用于处理特定的任务。
结构体 struct work_struct 描述的是要延迟执行的工作项,定义在 include/linux/workqueue.h 当中,如下所示
这些工作组织成工作队列,内核使用 struct workqueue_struct 结构体描述一个工作队列,定义在 include/linux/workqueue.h 当中:
工作队列相关接口函数:
当工作队列创建好之后,需要将要延迟执行的工作项放在工作队列上,调度工作队列,使用 queue_work_on
函数,函数原型如下所示:
该函数有三个参数,第一个参数是一个整数 cpu,第二个参数是一个指向 struct workqueue_struct 的指针 wq,第三个参数是一个指向 struct work_struct 的指针 work。
#
4)延迟工作延迟工作是一种将工作的执行延迟到稍后时间点进行处理的技术。任务可以在指定的延迟时间后执行,也可以根据优先级,任务类型或者其他条件进行排序和处理。
延迟工作在许多应用场景中都非常有用,尤其是在需要处理大量任务,提供系统性能和可靠性的情况下。以下是一些常用的应用场景:
1 延迟工作常用于处理那些需要花费较长时间的任务,比如发送电子邮件,处理图像等。通过将这些任务放入队列中并延迟执行,可以避免阻塞应用程序的主线程,提高系统的响应速度。
2 延迟工作可以用来执行定时任务,比如定时备份数据库,通过将任务设置为在未来的某个时间点执行,提高系统的可靠性和效率。
使用 struct delayed_work
来描述延迟工作,定义在 include/linux/workqueue.h
当中,原型定义如下所示:
示例:
#
5)工作队列传参之前软中断-tasklet 可以直接传入参数 data:
那么工作队列如何传入参数呢?这是工作队列的处理函数:传入的是结构体 work_struct。
我们可以使用 Linux 中常用的方法:定义一个私有数据结构体,把 work_struct 放入,然后可以通过 work_struct 获得包含它的数据结构。
创建工作队列时传入:
然后在中断处理函数里面调度:
最后,在工作队列处理函数中,通过container_of
函数获得结构体struct work_data
:
#
7)并发管理工作队列在使用工作队列时,首先定义一个work 结构体,然后将 work 添加到workqueue(工作队列)中,最后 worker thread 执行 workqueue。
当执行完结束的时候,worker thread 会睡眠,等到新的中断产生,work 再继续添加到工作队列,然后工作线程执行每个工作,周而复始。
多核系统中
- 存在多个工作队列,每个工作队列与一个工作线程(Worker Thread)绑定。
- 当有新的工作项产生时,系统需要决定将其分配给哪个工作队列:一种常见的策略是使用负载均衡算法,根据工作队列的负载情况来平衡分配工作项,以避免某个工作队列过载而导致性能下降。
- 在多核线程系统中,多个工作线程可以同时执行各自绑定的工作队列中的工作项,这样可以实现并行处理,提高系统的整体性能和响应速度。
普通工作队列的弊端:
- 排队等待:在工作项 w0 工作甚至是睡眠时,工作项 w1 w2 是排队等待的,在繁忙的系统中,工作队列可能会积累大量的待处理工作项,导致任务调度的延迟;
- 线程相互影响:如果工作项的处理时间差异很大,一些工作线程可能会一直忙于处理长时间的工作项,而其他工作线程则处于空闲状态,导致资源利用不均衡;
- 排队等待,无优先级:在某些场景下,可能需要根据工作项的重要性或紧急程度进行优先级调度,而工作队列本身无法提供这种级别的优先级控制;
- 当工作线程从工作队列中获取工作项并执行时,可能需要频繁地进行上下文切换,将处理器的执行上下文从一个线程切换到另一个线程。这种上下文切换开销可能会影响系统的性能和效率。
并发管理工作队列:一种并发编程模式,用于有效地管理和调度待执行的任务或工作项。
通过餐厅来类比:
你是一个餐厅的服务员,有很多顾客同时来到餐厅用餐。为了提高效率,你需要将顾客的点菜请求放到一个队列中,这就是工作队列。然后,你和其他服务员可以从队列中获取顾客的点菜请求,每个服务员独立地为顾客提供服务。通过这种方式,你们可以并发地处理多个顾客的点菜请求,而不需要等待上一个顾客点完菜再去处理下一个顾客的请求。每个服务员可以独立地从队列中获取任务,并根据需要执行相应的服务。这种独立获取任务的过程就是从工作队列中取出任务并执行的过程。
示例:
#
中断线程化中断线程化是实时 Linux 项目开发的一个新特性,目的是降低中断处理对系统实时延迟的影响。
将中断处理和主线程的工作分开,让它们可以并行执行。中断线程负责处理中断事件,而主线程负责执行主要的工作任务。这样一来,不仅可以减少切换的开销,还可以提高整个程序的响应速度和性能。
中断线程化的处理仍然可以看作是将原来的中断上半部分和中断下半部分。上半部分还是用来处理紧急的事情,下半部分也是出路比较耗时的操作,但是下半部分会交给一个专门的内核线程来处理。
这个内核线程只用于这个中断。当发生中断的时候,会唤醒这个内核线程,然后由这个内核线程来执行中断下半部分的函数。
接口:
#
4.设备树#
4.1 设备树里中断节点的语法设备树中,GIC 下面的节点需要三个 cells 表示。分别表示类、哪个、触发类型。
#
4.2 设备树里中断节点的示例#
4.3 在代码中获得中断#
5.内核驱动 gpio-keys.c 分析(GPIO/Input/中断/Timer)#
5.1 程序框架- 通过平台设备模型,在 probe 函数中注册 input_dev;
- 使用 GPIO 获得中断,注册中断,在中断里启动定时器,实现消抖;
- 在定时器回调函数里上报 input event.(EV_KEY 事件,根据设备树的
linux,code
参数上报 KEYBIT)
#
5.2 gpio_keys_probe 函数先解析设备树的信息
分配、构造 input_dev
接着,遍历所有的按键:注册中断(注意使用 GPIO 或使用中断,分别对应gpio_keys_gpio_isr
和gpio_keys_irq_isr
)
最后,注册中断处理函数
#
5.3 gpio_keys_gpio_isr 中断处理函数为了消抖,可以使用定时器或者推迟的工作队列。
#
5.4 gpio_keys_irq_isr 中断处理函数可以看出使用中断描述按键的方法,不能精确上报按键松开。所以需要使用 GPIO 方法。