Linux 中断机制详解

前言

在日常的Linux驱动程序开发过程中,我们经常需要与中断机制打交道。这篇文章将从中断相关概念开始,并介绍Linux中的中断机制,最后以实现一个简单的按键中断驱动程序结束,话不多说,马上开始。

异常与中断

中断

中断是一种事件,它改变程序的正常执行流,可以由硬件设备甚至CPU本身生成。当中断发生时,当前的执行流将被暂停,中断处理程序运行。中断处理程序运行完毕后,先前的执行流将恢复。例如,每当你按下键盘上的一个键时,CPU会被中断,从而使计算机可以从键盘读取用户输入。这发生得非常快,所以你不会注意到任何变化或损害用户体验。

这里有一个类比加深对中断的理解:

假设你正在家里专心地写作(相当于CPU在执行当前的程序)。这时,电话铃声响起(相当于硬件中断信号),你需要暂停写作去接电话。

DALL·E 2024-06-27 22.52.18 - A cozy home office setting where a person is seated at a desk, engrossed in writing. The desk is cluttered with papers, a laptop, and a coffee mug. Su.webp

  • 你在写作(CPU正在执行当前的指令流)。
  • 电话铃声响起(硬件设备触发中断信号)。
  • 你暂时停下写作(CPU停止当前指令的执行)。
  • 你记住你在写作的内容和位置(CPU保存当前执行状态和上下文)。
  • 你去接电话(CPU调用中断服务程序ISR处理中断)。
  • 你处理完电话上的事情(ISR完成中断处理)。
  • 你挂断电话(中断处理结束)。
  • 你回到书桌继续写作(CPU恢复之前保存的执行状态和上下文,继续执行被中断的程序)。

在了解异常之前,我想先讲下广义和狭义的中断概念,因为这个概念混淆了我很久,只有区分清楚,才能知道如何正确看待中断与异常的关系。

  • 广义的中断:在广义上,中断指的是任何打断正常程序执行流程的事件。这包括由外部设备触发的硬件中断,也包括由CPU内部检测到的错误或特殊情况触发的异常。因此,从这个角度来看,异常可以被视为中断的一个子类。
  • 狭义的中断:在狭义上,中断专指由外部硬件设备触发的事件,与异常区分开来。在这种定义下,中断和异常是并列的两类事件,分别处理不同的情况。

这篇文章接下来将以广义的角度讲解中断,但是我们要知道一点,真实的异常与中断的处理程序是不一样的,中断使用中断处理程序(Interrupt Service Routine, ISR),ISR通常由设备驱动程序定义,每种中断类型(例如键盘中断、网络中断)有专门的ISR。异常使用异常处理程序(Exception Handler),通常由操作系统内核定义,每种异常类型(例如除零异常、页面错误)有专门的处理程序。

异常

异常通常与中断一起讨论。与中断不同,异常是相对于处理器时钟同步发生的;它们通常被称为同步中断。异常是处理器在执行指令时产生的,可能是由于编程错误(如除零)或必须由内核处理的异常情况(如页面错误)。由于许多处理器架构以类似于中断的方式处理异常,因此内核处理两者的基础设施相似。

中断类型

中断类型有三种,分别是:

  • 硬件中断(Hardware Interrupts)
  • 软件中断(Software Interrupts)
  • 异常(Exceptions)

硬件中断

硬件中断由外部硬件设备触发,用于通知CPU处理外部事件或设备请求。

  • 外部设备中断:由外围设备(如键盘、鼠标、硬盘、网络接口卡等)触发。例如,当键盘按键被按下时,会产生键盘中断。
  • 定时器中断:由系统定时器触发,用于实现定时任务或系统时钟的更新。
  • 电源管理中断:由电源管理设备触发,用于处理电源状态变化,如电池电量低、电源插入或拔出等。

软件中断

软件中断由程序指令触发,用于实现系统调用或用户态与内核态的切换。

  • 系统调用中断:程序通过特定指令(如x86架构中的INT指令)触发,进入内核态执行系统调用。
  • 用户定义中断:由用户程序显式触发,用于实现特定的功能或异常处理。

异常

异常是由CPU在执行指令过程中检测到的错误或特殊情况触发的,用于处理程序错误或特殊事件。

异常分为三种:陷阱、故障和终止。

  • 陷阱(Traps)
    • 来源:由指令执行过程中预期的事件触发,如系统调用。
    • 处理结果:通常处理后继续执行下一条指令。
    • 示例:系统调用陷阱。
  • 故障(Faults)
    • 来源:由指令执行过程中检测到的可恢复错误触发。
    • 处理结果:处理后通常可以恢复并重新执行导致故障的指令。
    • 示例:页面错误(Page Fault)、除零错误(Divide-by-zero Fault)。
  • 终止(Aborts)
    • 来源:由指令执行过程中检测到的不可恢复错误触发。
    • 处理结果:通常无法恢复,进程终止。
    • 示例:硬件故障、内存校验错误(Machine Check Abort)。

中断控制器

中断控制器(Interrupt Controller)是一个硬件模块,它负责接收多个外部和内部中断源的中断请求,并按照优先级和配置将这些中断请求发送给处理器。对于复杂系统,尤其是多处理器系统,中断控制器是不可或缺的组件,因为它能够高效地处理和调度大量的中断请求。

CPU每执行完一条指令后,就会检查中断控制器(如Arm架构的GIC)是否有中断请求。此时,CPU可以决定是否中断当前程序的执行(如果设置了屏蔽中断),跳转到中断处理程序。

image.png

这里还是用一个类比来加深对中断控制器的理解:想象一个繁忙的办公室,前台接待员负责接收并处理来自不同访客和电话的请求。办公室里的员工(处理器)正在处理各种各样的工作(指令执行)。

DALL·E 2024-06-27 22.53.52 - A busy office setting with a receptionist at the front desk handling requests from various visitors and phone calls. The receptionist is multitasking,.webp

  • 接收请求:接待员(中断控制器)接收并记录所有访客和电话的请求(中断信号)。
  • 判断优先级:接待员根据请求的紧急程度决定处理顺序(中断控制器判断中断优先级)。
  • 分配请求:接待员将高优先级请求分配给相应的员工(中断控制器将中断请求发送到目标处理器)。
  • 处理请求:员工暂停当前工作,处理访客或电话(处理器暂停当前指令,执行中断服务程序)。
  • 完成请求:员工处理完访客或电话后,返回继续工作(处理器处理完中断后,恢复执行被中断的指令)。

以上介绍的知识与操作系统无相关联,它属于计算机体系结构层面的概念,所以不管是裸机、RTOS还是Linux,都同样适用。

Linux 系统对中断的处理

进程上下文和中断上下文

内核使用进程上下文和中断上下文的组合方式来完成各种任务。

什么是上下文

上下文是指在某个特定时刻,CPU的状态和执行环境所需的信息集合。它包括了执行程序所需的所有寄存器、程序计数器、堆栈指针,以及与执行状态相关的数据。

因此我们可以得知,进程上下文和中断上下文拥有不同的执行环境和状态信息。

进程上下文

进程上下文是指内核代码在代表用户进程执行时的状态。服务于用户应用程序发出的系统调用的内核代码是在进程上下文中运行的。

它拥有如下特性:

  • 代表用户进程执行:在进程上下文中,内核代码代表某个具体的用户进程执行,因此拥有该进程的上下文(如进程控制块、内存地址空间)。
  • 可以睡眠和阻塞:在进程上下文中,内核代码可以调用可能导致睡眠或阻塞的函数,因为进程可以被调度器调度并在适当的时候重新唤醒。
  • 特权级别:内核代码在进程上下文中执行时具有内核态特权,可以访问内核数据结构和资源。

继续通过一个类比来加深理解:员工(代表一个进程)在办公室里工作,处理日常任务和项目。

DALL·E 2024-06-28 07.49.37 - An office setting where employees are working, handling daily tasks and projects. Some are typing on computers, others are discussing in small groups,.webp

  1. 日常工作:员工按照日常安排和优先级处理手头的任务(系统调用、用户进程的请求)。
  2. 可以打断和等待
    • 员工可以被打断去开会(阻塞),会后继续工作。
    • 如果需要某个同事的帮助,员工可以等待同事的回复(睡眠),在等待期间做其他事情或休息(调度其他进程)。
  3. 上下文信息:员工的办公桌上有他们的工作文件和工具(进程控制块、内存地址空间等),确保他们可以有效地完成工作。
  4. 权限和资源:员工可以访问公司资源(内核数据结构和资源),但需要遵循公司的规章制度(内核态特权)。

中断上下文

中断上下文是指内核代码在处理中断请求时的状态。中断处理程序(ISR)在中断上下文中异步运行。

它拥有如下特性:

  • 无特定进程关联:中断上下文不代表任何具体的用户进程,因此没有与特定进程相关的上下文。
  • 不能睡眠或阻塞:在中断上下文中,内核代码不能调用可能导致睡眠或阻塞的函数,因为中断处理程序需要尽快完成以恢复正常执行。
  • 高优先级:中断处理程序通常具有高优先级,以便迅速响应硬件事件。

中断上下文一旦运行就不能终止,它必须尽可能快的完成,这个过程中不能被其他中断信号中断。中断上下文中的代码不能执行以下操作:

  1. 进入睡眠状态或阻塞
  2. 获取互斥锁
  3. 执行耗时任务
  4. 访问用户空间虚拟内存

接下来通过一个类比加深理解:办公室里有一个专门的紧急任务响应人员(中断服务程序ISR),它负责处理突发事件(中断请求)。

DALL·E 2024-06-28 08.54.56 - An office setting where a dedicated emergency response staff member is handling sudden incidents. The responder is moving through the office, without .webp

  1. 紧急任务:当发生紧急事件(如火警报警、重要客户来电),响应人员立即停止当前工作,去处理突发事件(中断处理)。
  2. 快速响应:响应人员必须快速处理紧急任务(ISR必须尽快完成),确保紧急情况得到及时解决。
  3. 不允许等待和阻塞
    • 响应人员不能在处理紧急任务时等待其他人(不能睡眠或阻塞)。
    • 必须尽快处理完紧急任务并返回继续监控(完成中断处理)。
  4. 没有具体办公桌:响应人员没有固定的办公桌(无进程上下文),他们不依赖于具体的文件和工具,只需处理紧急情况。
  5. 快速通知他人:如果紧急任务需要进一步处理,响应人员可以通知其他同事(调度下半部,如软中断、任务队列),这些同事可以在日常工作中完成后续处理。

如果在接收到中断时需要完成大量工作,该怎么办?这就是问题所在。如果处理时间过长,会发生以下情况:

  1. 在最高优先级ISR运行时,它不会让其他中断运行。
  2. 同类型的中断将被遗漏(中断信号不能中断ISR)。

为了解决这个问题,在Linux系统中,中断的处理被分为两部分,上半部和下半部。为了便于理解,我们可以把上半部理解成硬件中断,也就是由硬件中断信号来触发的,它必须尽快的完成,且不能被其他中断信号中断,而下半部是软件中断,它是由软件来手动触发的,它负责完成处理时间长的任务,它可以被其他中断信号中断。

949e3b293c264d955173259e403015e29525d448aa51b85baf02c1ebdab39944.png

上半部

上半部是中断发生时立即执行的部分,它在中断上下文中执行,其主要任务是快速处理中断源,并尽可能快速地恢复正常操作。

特点

  • 快速执行:上半部必须尽快执行完毕,以减少对系统响应时间的影响。
  • 不允许阻塞:上半部不能调用可能导致睡眠或阻塞的函数。
  • 有限的工作量:上半部只执行必要的工作,复杂的处理推迟到下半部执行。

api

  • request_irq()
  • free_irq()

request_irq() 用于注册一个中断服务程序(ISR)。它的原型如下:

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);

参数

  • irq:中断号。
  • handler:中断处理函数。
  • flags:中断处理标志,常见的有 IRQF_SHARED(允许中断共享)。
  • name:中断处理程序的名字,用于 /proc/interrupts 显示。
  • dev:设备相关数据,通常是设备结构体的指针。

返回值

  • 成功时返回 0,失败时返回负数错误代码。

free_irq() 用于释放之前注册的中断处理程序。它的原型如下:

void free_irq(unsigned int irq, void *dev_id);

参数

  • irq:中断号。
  • dev_id:与 request_irq 中注册时的 dev 参数相同。

下半部

下半部负责处理较为耗时的任务,可以稍后执行,可以被其他优先级更高的中断信号中断。它由上半部手动触发,下半部主要包括以下机制:

  • 软中断(Softirqs)
  • 任务队列(Tasklets)
  • 工作队列(Workqueues)

软中断(Softirqs)

软中断(Softirqs)用于延迟执行一些时间要求不太严格但仍需要较快处理的任务。软中断通常由硬件中断处理程序或其他内核代码触发,用于处理需要快速响应但不适合在中断处理程序中完成的任务,如网络包处理和定时器管理。

优点
  1. 高效处理:软中断机制允许快速处理紧急任务,减轻了中断处理程序的负担,从而提高系统响应能力。
  2. 并发执行:同一类型的软中断可以在多个CPU上并发执行,充分利用多核处理器的优势,提升系统整体性能。
  3. 固定优先级:软中断具有固定的优先级管理,确保高优先级任务能得到及时处理。
  4. 灵活性:软中断可以由多种事件触发,包括硬件中断和内核其他事件,适用范围广泛。
缺点
  1. 不能阻塞:软中断在软中断上下文中执行,不能调用可能导致睡眠或阻塞的函数。
  2. 资源竞争:在多核系统中,同一类型的软中断并发执行可能导致资源竞争和锁争用问题,需要小心处理。
api
  • open_softirq()
  • raise_softirq()
  • raise_softirq_irqoff()

open_softirq()用于注册一个软中断处理函数。当软中断被触发时,内核会调用注册的处理函数来处理该软中断。它的原型如下:

void open_softirq(int nr, void (*action)(struct softirq_action *));

参数

  • nr:软中断号,用于标识具体的软中断类型。这个值在内核中是固定的,通常由内核代码或驱动程序指定。
  • action:软中断处理函数的指针,当软中断被触发时将调用此函数。该函数的原型如下:

raise_softirq() 用于触发指定的软中断。该函数通知内核有一个软中断需要处理,并将其加入软中断队列,等待处理。它的原型如下:

void raise_softirq(unsigned int nr);

参数

  • nr:软中断号,标识要触发的软中断类型。

raise_softirq_irqoff() 类似于 raise_softirq(),但它在调用时不会检查和修改中断状态。这通常用于在中断处理程序内部触发软中断,因为在中断处理程序中中断已经被禁用。

void raise_softirq_irqoff(unsigned int nr);

参数

  • nr:软中断号,标识要触发的软中断类型。

任务队列(Tasklets)

任务队列(Tasklets)是Linux内核中的一种基于软中断(Softirqs)实现的机制,用于延迟执行较为简单且快速的任务。Tasklets提供了比直接使用软中断更简便的接口,允许开发者在硬件中断处理程序中调度这些任务在稍后执行。

优点
  1. 简化接口:Tasklets提供了更简单的接口,比直接使用软中断更容易使用和管理。
  2. 上下文独立:Tasklets在软中断上下文中执行,不与特定进程绑定,因此可以在任意时刻被调度执行。
  3. 自动调度:同一类型的Tasklet在系统中只会有一个实例在同一时间执行,避免了并发执行带来的资源竞争问题。
  4. 灵活性:可以将复杂或耗时的处理从硬中断处理程序中分离出来,减少中断处理时间,提高系统响应能力。
缺点
  1. 不能阻塞:由于Tasklets在软中断上下文中执行,不能调用可能导致阻塞或睡眠的函数。
  2. 共享资源:多个Tasklets共享系统资源,需要注意同步和保护,避免竞态条件和资源争用。
  3. 优先级固定:Tasklets的优先级是固定的,不能灵活调整优先级,可能导致高优先级任务的延迟。
Api
  • DECLARE_TASKLET()
  • DECLARE_TASKLET_DISABLED()
  • tasklet_init()
  • tasklet_schedule()
  • tasklet_kill()

DECLARE_TASKLET() 用于静态声明并初始化一个任务队列(Tasklet)对象。Tasklet 是基于软中断的一种延迟执行机制,用于在硬件中断处理程序中调度一些简单且快速的任务在稍后执行。

#define DECLARE_TASKLET(name, func, data)

参数

  • name:Tasklet 对象的名称。
  • func:Tasklet 处理函数,当 Tasklet 被调度执行时调用此函数。
  • data:传递给 Tasklet 处理函数的参数。

DECLARE_TASKLET_DISABLED() 用于静态声明并初始化一个禁用状态的任务队列(Tasklet)对象。与 DECLARE_TASKLET() 不同,声明的 Tasklet 初始状态为禁用,需要显式启用后才能调度执行。

#define DECLARE_TASKLET_DISABLED(name, func, data)

参数

  • name:Tasklet 对象的名称。
  • func:Tasklet 处理函数,当 Tasklet 被调度执行时调用此函数。
  • data:传递给 Tasklet 处理函数的参数。

tasklet_init() 用于动态初始化一个任务队列(Tasklet)对象。与 DECLARE_TASKLET() 不同,tasklet_init() 在运行时进行初始化。

void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);

参数

  • t:指向 tasklet_struct 结构的指针,表示要初始化的 Tasklet 对象。
  • func:Tasklet 处理函数,当 Tasklet 被调度执行时调用此函数。
  • data:传递给 Tasklet 处理函数的参数。

tasklet_schedule() 用于调度一个任务队列(Tasklet)对象执行。被调度的 Tasklet 将在稍后执行(通常是在软中断上下文中)。

void tasklet_schedule(struct tasklet_struct *t);

参数

  • t:指向 tasklet_struct 结构的指针,表示要调度的 Tasklet 对象。

tasklet_kill() 用于销毁一个任务队列(Tasklet)对象,并确保在函数返回时,Tasklet 已经完成执行且不会再被调度。

void tasklet_kill(struct tasklet_struct *t);

参数

  • t:指向 tasklet_struct 结构的指针,表示要销毁的 Tasklet 对象。

下面给出一个代码示例:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>

// Tasklet 处理函数
void my_tasklet_handler(unsigned long data) {
    pr_info("Tasklet handler executed with data: %lu\n", data);
}

// 静态声明并初始化 Tasklet
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 42);

// 模块初始化函数
static int __init my_module_init(void) {
    pr_info("Loading tasklet example module\n");
    
    // 动态初始化 Tasklet(如果需要)
    // struct tasklet_struct my_tasklet;
    // tasklet_init(&my_tasklet, my_tasklet_handler, 42);
    
    // 调度 Tasklet 执行
    tasklet_schedule(&my_tasklet);
    
    return 0;
}

// 模块卸载函数
static void __exit my_module_exit(void) {
    // 销毁 Tasklet
    tasklet_kill(&my_tasklet);
    
    pr_info("Unloading tasklet example module\n");
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple tasklet example");

工作队列(Workqueues)

工作队列(Workqueues)用于延迟执行需要较长时间处理的任务。与软中断和任务队列(Tasklets)不同,工作队列在内核线程的上下文中运行,因此可以睡眠和阻塞。这使得工作队列适用于更复杂和耗时的任务。

优点
  1. 可以睡眠和阻塞:工作队列在进程上下文中执行,这意味着它们可以调用可能导致睡眠或阻塞的函数。这是它们相对于软中断和任务队列的最大优势。
  2. 易于编写和调试:由于工作队列可以在进程上下文中运行,开发者可以利用更多的内核功能,例如文件系统访问、内存分配和同步机制。
  3. 灵活的调度:工作队列提供了多种调度方法,可以创建单线程或多线程工作队列,以适应不同的并发需求。
  4. 良好的隔离性:每个工作队列可以独立运行,不会相互干扰,适合于需要隔离处理的任务。
缺点
  1. 开销较大:因为工作队列在内核线程中运行,需要线程管理和上下文切换,因此相对于软中断和任务队列,其开销较大。
  2. 延迟:由于工作队列是由调度器调度的,因此可能会有调度延迟,不能保证实时性。
  3. 资源消耗:使用工作队列可能会增加内存和处理器资源的消耗,尤其是在创建多个工作队列或处理高频率任务时。
Api
  • DECLARE_WORK()
  • INIT_WORK()
  • DECLARE_DELAYED_WORK()
  • INIT_DELAYED_WORK()
  • schedule_work()
  • queue_work()
  • schedule_delayed_work()
  • queue_delayed_work()
  • create_workqueue()
  • destroy_workqueue()

三者对比

下面的表格总结了它们之间的区别:

ItemSoftirqTaskletWorkqueue
运行Context软中断上下文软中断上下文进程上下文(内核态)
可以sleep?
关中断?
可重新调度?
可带参数?
谁触发,谁执行硬件/内核,内核硬件/内核,内核内核/用户进程,内核线程
可同时被多CPU执行同一个sorftirp_action可同时被多CPU执行同一个tasklet在任意时刻只能被一个CPU执行由进程调度决定
可延时执行?
数据结构softirq_actiontasklet_structwork_struct, delayed_work
初始化open_softirqDECLARE_TASKLETINIT_WORK, INIT_DELAYED_WORK
使能/静止tasklet_enable/disable
触发raise_softirqtasklet_scheduleschedule_work, queue_work
执行do_softirqtasklet_actionworker_thread
创建线程create_workqueue
结束tasklet_killdestroy_workqueue

新技术:线程化中断处理(Threaded IRQ)

Threaded IRQ 是在 Linux 2.6.30 版本中引入的。在传统的工作队列workqueue中,创建多个workqueue,由于它们在进程上下文执行,一起共享同一个内核线程,因此可能出现排队的情况,为了解决这个问题,Threaded IRQ引入了,每当我们创建一个threaded,就会创建一个新的内核线程,它们运行在独立的内核线程上下文中,所以可以用来执行长时间运行或与用户空间交互的任务。

Threaded IRQ 既不完全属于传统的上半部,也不完全属于下半部。它提供了一种新的中断处理机制,结合了两者的优点,同时避免了它们的一些局限性。

特点

  • 运行在内核线程上下文中,而不是传统的中断上下文或进程上下文。
  • 允许睡眠和阻塞,这使得处理复杂任务更加灵活。
  • 通过内核线程调度,提供了更好的可控性和优先级管理。

接下来用一个类比加深理解:医院的急诊处理过程。

初诊护士(上半部处理程序)

DALL·E 2024-06-28 10.55.37 - An emergency room setting where a triage nurse is the first to receive a patient who has just arrived. The nurse is performing a quick initial assessm.webp

  • 角色:当病人进入急诊室时,初诊护士首先接待病人,进行快速的初步评估(例如测量生命体征、询问症状)。
  • 任务:初诊护士需要尽快完成初步评估,并决定是否需要医生进一步处理。
  • 限制:初诊护士不会进行复杂的治疗,因为他们的主要任务是快速确定病情的严重程度。

值班医生(线程化中断处理程序)

DALL·E 2024-06-28 10.52.03 - An emergency room setting where, based on the triage nurse's initial assessment, an on-duty doctor is called in to perform a detailed diagnosis and tr.webp

  • 角色:根据初诊护士的评估结果,值班医生被叫来进行详细的诊断和治疗。
  • 任务:值班医生可以进行更复杂的检查和治疗,并且可以花更多的时间处理病人的病情。
  • 优点:值班医生有更多的资源和时间,可以进行复杂的处理,例如开药、安排住院等。

Api

  • request_threaded_irq()

request_threaded_irq()用于注册一个中断处理程序,并将其线程化。

int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id);

参数

  • irq:中断号。
  • handler:上半部中断处理程序,快速处理部分,通常只需要唤醒内核线程。如果设置为 NULL,内核将默认调用 thread_fn
  • thread_fn:线程化中断处理程序,在内核线程上下文中运行,可以进行阻塞和睡眠操作。
  • irqflags:中断标志,用于指定中断的触发方式和其他选项(如共享中断)。
  • devname:设备名称,用于在 /proc/interrupts 中显示。
  • dev_id:设备标识符,用于标识共享中断。

返回值

  • 成功时返回 0,失败时返回负数错误代码。

示例代码

static irqreturn_t my_irq_handler(int irq, void *dev_id) {
    pr_info("Top half IRQ handler executed\n");
    return IRQ_WAKE_THREAD;  // 唤醒线程化中断处理程序
}

static irqreturn_t my_thread_fn(int irq, void *dev_id) {
    pr_info("Threaded IRQ handler executed\n");
    msleep(1000);  // 模拟阻塞操作
    pr_info("Threaded IRQ handler completed\n");
    return IRQ_HANDLED;
}

static int __init my_module_init(void) {
    int ret;
    ret = request_threaded_irq(IRQ_NUMBER, my_irq_handler, my_thread_fn, IRQF_SHARED, "my_device", NULL);
    if (ret) {
        pr_err("Failed to request threaded IRQ\n");
        return ret;
    }
    return 0;
}

static void __exit my_module_exit(void) {
    free_irq(IRQ_NUMBER, NULL);
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple threaded IRQ example");

实现一个简单的按键中断驱动程序

接下来我们来实现一个简单的按键中断驱动程序加深理解,这个中断的处理比较简单,只提供上半部ISR,下半部或线程化的方式以练习的方式留给大家。

要实现一个按键驱动程序,需要具备设备树、驱动程序开发基础的前置知识,这里就不再累赘,可以参考我过往的这篇文章进行学习,Linux 驱动程序基础开发详解

配置设备树

阅读原理图、芯片手册,生成pinctrl节点,接着编写我们的节点:

/ {
    leon_gpio {
        compatible = "leon,gpio_key";
        gpios = <&gpio5 1 GPIO_ACTIVE_LOW &gpio4 14 GPIO_ACTIVE_LOW>;
        pinctrl-names = "default";
        pinctrl-0 = <&my_gpio5_1 &my_gpio4_14>;
    };
};

上面的配置表示:定义了一个leon_gpio的节点,它使用gpio第5组的第1个引脚和第4组的第14个引脚,触发时为低电平。然后是复用两个pinctrl节点my_gpio5_1和my_gpio4_14的电气配置。

编译设备树生成dtb文件并放到开发板的/boot目录下,重启开发板。

代码编写

按键驱动程序使用了如下的api:

  • request_irq()
  • free_irq()
  • of_gpio_count()
  • of_get_gpio_flags()
  • kzalloc()
#include "asm/gpio.h"               // 包含GPIO相关的头文件
#include "linux/gfp.h"              // 包含内存分配相关的头文件
#include "linux/interrupt.h"        // 包含中断处理相关的头文件
#include "linux/module.h"           // 包含Linux模块相关的头文件
#include "linux/of_gpio.h"          // 包含设备树GPIO相关的头文件
#include "linux/platform_device.h"  // 包含平台设备相关的头文件
#include "linux/printk.h"           // 包含内核打印相关的头文件
#include "linux/slab.h"             // 包含内存分配相关的头文件

struct gpio_key {
  int gpio;       // GPIO引脚号
  int irq;        // 中断号
  int flag;       // GPIO标志
};

static struct gpio_key *gpio_keys;  // 指向gpio_key结构的指针

static irqreturn_t gpio_key_handler(int irq, void *dev_id) {
  struct gpio_key *gpio_key = dev_id;  // 中断处理程序传递的设备数据结构指针

  printk("key %d val %d\n", irq, gpio_get_value(gpio_key->gpio));  // 打印中断触发的GPIO号和其值

  return IRQ_HANDLED;  // 返回中断处理已完成
}

static int gpio_key_probe(struct platform_device *pdev) {
  struct device_node *dev = pdev->dev.of_node;  // 获取平台设备的设备树节点
  int count;  // GPIO数量
  int i;
  enum of_gpio_flags flags;
  int gpio;
  int irq;
  int err;

  count = of_gpio_count(dev);  // 获取设备树节点的GPIO数量

  gpio_keys = kzalloc(count * sizeof(struct gpio_key), GFP_KERNEL);  // 分配存储GPIO数据结构的内存

  for (i = 0; i < count; i++) {
    gpio = of_get_gpio_flags(dev, i, &flags);  // 获取GPIO引脚号和标志
    irq = gpio_to_irq(gpio);  // 获取GPIO对应的中断号
    gpio_keys[i].gpio = gpio;
    gpio_keys[i].irq = irq;
    gpio_keys[i].flag = flags;

    // 为特定的GPIO注册中断处理程序
    err = request_irq(irq, gpio_key_handler,
                      IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                      "leon_gpio_key", &gpio_keys[i]);
  }

  return 0;
}

int gpio_key_remove(struct platform_device *pdev) {
  struct device_node *dev = pdev->dev.of_node;  // 获取平台设备的设备树节点
  int count;  // GPIO数量
  int i;

  count = of_gpio_count(dev);  // 获取设备树节点的GPIO数量

  gpio_keys = kzalloc(count * sizeof(struct gpio_key), GFP_KERNEL);  // 分配存储GPIO数据结构的内存

  for (i = 0; i < count; i++) {
    free_irq(gpio_keys[i].irq, &gpio_keys[i]);  // 释放GPIO对应的中断
  }

  return 0;
}

// 匹配设备树中的compatible属性值为"leon,gpio_key"的设备
static const struct of_device_id gpio_key_of_device_ids[] = {
    {.compatible = "leon,gpio_key"},
    {}
};

// 平台设备驱动程序结构体
static struct platform_driver gpio_key_platform_driver = {
    .probe = gpio_key_probe,  // 初始化函数
    .remove = gpio_key_remove,  // 卸载函数
    .driver =
        {
            .name = "gpio_key",  // 设备驱动程序名称
            .of_match_table = gpio_key_of_device_ids,  // 匹配设备树节点
        },
};

// 模块初始化函数
static int __init gpio_key_drv_init(void) {
  platform_driver_register(&gpio_key_platform_driver);  // 注册平台设备驱动程序
  return 0;
}

// 模块退出函数
static void __exit gpio_key_drv_exit(void) {
  platform_driver_unregister(&gpio_key_platform_driver);  // 注销平台设备驱动程序
}

module_init(gpio_key_drv_init);  // 指定模块初始化函数
module_exit(gpio_key_drv_exit);  // 指定模块退出函数
MODULE_LICENSE("GPL");  // 模块的许可证信息

参考文献

结语

可以看到,中断在计算机体系中有着一个举足轻重的地位,我们的每一次键盘的敲击,每一个鼠标的移动或点击,都与它息息相关。本文讲解的所有内容,用到了多个类比加深理解,希望能帮助到各位读者~

转载请注明出处,感谢。

  • 25
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值