Linux Device Drivers-第十章 中断处理

第十章 中断处理

当外部硬件希望获得处理器对他的关注时就发一个信号叫中断,当然处理器需要为自己管理的设备的中断注册一个处理程序。有一点需要关注,本质上,中断处理例程与其他代码并发运行,不可避免引入并发问题,竞争结构和硬件。(可以参考本书第五章内容)

10.1 准备并口

原文中为了实际体验中断,需要准备一个并口,但这对我来说已经过时了。

10.2 安装中断处理例程

设备中断通过硬件中断信号线通知处理器,所以中断信号线是珍贵且有限的资源。内核中维护了一个中断信号线的注册列表,模块在使用中断前需要先请求一个中断通道(或中断请求IRQ),使用完毕后释放该通道。在很大场合,模块也会希望和其他驱动程序共享 中断信号线。在 6.12 版本内核<linux/interrupt.h>中声明的函数实现了该接口:

c 复制代码
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
            const char *name, void *dev)
{
        return request_threaded_irq(irq, handler, NULL, flags | IRQF_COND_ONESHOT, name, dev);
}

void *free_irq(unsigned int, void *);

request_irq返回 0 成功,负表示错误,返回-EBUSY表示另一驱动程序已经占用了你要请求的中断信号线。

  • 参数解释

    unsigned int irq:要申请的中断号(内核分配的虚拟中断号,非硬件中断号)

    typedef irqreturn_t (*irq_handler_t)(int, void *):要安装的中断处理函数指针

    unsigned long flags:与中断管理有关的位掩码选项,在include/linux/interrupt.h中以 IRQF_ 开头的宏定义(第一类:中断触发方式是上升沿还是下降沿。第二类:中断处理行为如共享中断,单次中断,禁止线程化等,dts中的 IRQ_TYPE_LEVEL_HIGH 配置也会转化成IRQF_TRIGGER_HIGH标志传递给驱动)。

    const char *name:传递给request_irq的字符串,用于在/proc/interrrupts中显示中断的拥有者。

    void *dev:(在很多内核文档和源码中也常被称为 dev_id)在编写 Linux 驱动时,强烈建议将代表该设备的结构体指针作为 dev 参数传入。作为释放中断的唯一凭证,且能在中断处理函数中传递设备私有数据。

10.2.1 /proc 接口

/proc 并不占用磁盘空间,它是内核内存状态的直接映射,是内核状态的窗口,/proc已经从过去的单纯查看中断计数工具转变为**"性能调优与故障诊断的核心入口"**了。用于排查硬件与驱动问题的第一现场。

10.2.1.1 /proc/interrupts

用于观察中断在各个 CPU 核心上的分布情况。示例:

bash 复制代码
root@imx8mp:~# cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3 [中断控制器][硬件中断号][触发类型][设备名]
 11:    5201752    3973215    4242690    4578831     GICv3  30 Level     arch_timer
 14:    3361486     907943     743331     843655     GICv3  79 Level     timer@306a0000
 15:          0          0          0          0     GICv3  34 Level     30bd0000.dma-controller
 16:          0          0          0          0     GICv3  58 Level     30860000.serial
 19:          7          0          0          0     GICv3 139 Level     30bb0000.spi
 20:          0          0          0          0     GICv3 180 Level     32f10100.usb
 85:          0          0          0          0  gpio-mxc  12 Edge      30b50000.mmc cd
121:          0          0          0          0  gpio-mxc  14 Edge      User Button1
227:          0          0          0          0   PCI-MSI   0 Edge      PCIe PME
244:          0          0          0          0   PCI-MSI 524288 Edge      uiodma_efd_irq
IPI0:      5646      27125      23345      20408       Rescheduling interrupts
IPI1:    850910    1761636    1601721    1124581       Function call interrupts
IPI2:         0          0          0          0       CPU stop interrupts
IPI3:         0          0          0          0       CPU stop NMIs
IPI4:    642461     358178     533719     616936       Timer broadcast interrupts
IPI5:   1695504     964309     752769     713252       IRQ work interrupts
IPI6:         0          0          0          0       CPU backtrace interrupts
IPI7:         0          0          0          0       KGDB roundup interrupts
Err:          0
  • Linux 虚拟中断号(虚拟IRQ number):Linux 内核为了兼容各种不同的硬件架构(比如 x86 的 APIC、ARM 的 GIC),设计了一套统一的中断管理框架。内核会把各种五花八门的硬件中断号,映射成自己内部统一管理的、从 0 开始连续分配的虚拟编号。

  • CPUx Columns:每个 CPU 核心处理该中断的累计次数。这是现代系统调优的重点。

  • 中断控制器名称:GIC (Generic Interrupt Controller),PCI-MSI (Message Signaled Interrupts)

  • 硬件中断号 (Hardware IRQ Number): GIC 硬件内部用来识别该中断的编号。.dts文件的interrupts属性根据文档配置也是Hardware IRQ。根据 ARM 官方规范(IHI0069B_GICv3v4_architecture_specification INTIDs 小节)有如下表格:

    中断 ID (INTID) 范围 中断类型 官方定义与说明
    0 - 15 SGI (Software Generated Interrupt) 软件生成中断。通常用于多核 CPU 之间的通信(即你看到的 IPI 中断)。(仅限于CPU接口内用)
    16 - 31 PPI (Private Peripheral Interrupt) 私有外设中断。某个 CPU 核心"私有的"。比如每个 CPU Core 都有自己的本地定时器,这个定时器的中断只能由该 Core 自己处理,其他 Core 插不了手。
    32 - 1019 SPI (Shared Peripheral Interrupt) 共享外设中断。所有 CPU 核心共享的外部设备(如 UART、USB、网卡等)产生的中断。芯片厂家出场时会将外设与特定SPI硬件绑定(参见10.5 中断共享)。(Distributor 可将 SPI 路由至特定处理单元(PE),或路由至系统中任何作为参与节点的PE)
    1020 - 1023 Special Interrupt number 特殊用途(如表示中断结束等)。
    1024 - 8191 Reserved
    >=8192 LPI (Locality-specific Peripheral Interrupt) GICv3 新增特性,主要用于 PCIe MSI/MSI-x 等基于消息的中断。
  • 中断触发类型:Level 代表关注电平状态,只要信号线维持在高电平(或低电平),中断就会一直存在,CPU就需要持续处理该中断(通过.dtsinterrupts属性可以看出是)(与之相对的是 Edge 边沿触发代表关注瞬间变化,它只在意信号从低变高(上升沿)或从高变低(下降沿)的那一瞬间)

  • 最后一列(如arch_timer):注册该中断的设备/驱动名称。

  • 相较于原书2.6版本内核,早期 Linux 倾向于在CPU0上处理中断以最大化缓存局部性。但这样做在如今高并发场景会导致单核CPU占用率飙升,而其他核心却闲置。现代 Linux 允许我们通过 /proc/irq/ 接口来手动干预这种分配(比如看到arch_timer中断如果在某个核心上中断数量过高就可以手动干预一下):

    • 查看绑定:cat /proc/irq/[IRQ号]/smp_affinity
    • 修改绑定:通过向/proc/irq/[IRQ号]/smp_affinity写入位掩码,可以将特定硬件中断绑定到指定的CPU核心。例如将网卡中断绑定到专用核心。极大减小缓存污染,提升吞吐量。
  • 最后几行:核间通信 IPI (Inter-Processor Interrupts):IPI 的计数能反映出你系统的多核协作频率。如果 IPI 异常高,可能意味着系统存在严重的锁竞争或调度压力。这是现代多核系统独有的、非常硬核的诊断指标。

10.2.1.2 /proc/stat

相比于 interrupts 的细致,/proc/stat 提供的是系统启动以来的宏观统计。示例:

bash 复制代码
root@imx8mp:~# cat /proc/stat
cpu  84007 77 45848 60415788 49890 39202 18393 0 0 0
cpu0 23098 47 11474 15085533 9900 17962 13411 0 0 0
cpu1 24708 3 13531 15103802 12567 7177 1855 0 0 0
cpu2 20177 4 11155 15110778 12838 7012 1784 0 0 0
cpu3 16022 22 9687 15115674 14583 7049 1341 0 0 0
intr 50618939 0 78873 6909066 0 0 3301653 6235878 0 0 0 0 21226415 0 0 9033515 0 0 0 44780 7 0 0 0 0 0 244 0 0 0 0 49 1 127964 2407 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 37 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 580610 65 212 0 0 772 875429 0 0 0 0 46144 0 1641094 0 74 510159 0 846 0 0 170 0 0 0 0 0 0 0 0 0 0 0 0 0 2475 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ctxt 54402845
btime 1779440703
processes 5642
procs_running 1
procs_blocked 0
softirq 22821007 856 5954420 19 1647347 0 0 2630 10011102 4528 5200105
  • CPU行(单位:jiffies):从左到右依次为:user(用户态执行时间), nice(调整优先级用户进程时间), system(内核态执行时间(驱动代码算这里)), idle(空闲时间), iowait, irq( 硬中断处理时间), softirq( 软中断处理时间), steal(被虚拟机偷走的时间), guest, guest_nice。
  • intr 关键字:第1个值 → 所有中断总数,后面 → 每个 IRQ 的计数
  • ctxt:上下文切换总次数
  • btime:系统启动时间(单位秒seconds)
  • processes:创建过多少个 task_struct
  • procs_running:当前运行进程数(用户 + 内核线程)
  • procs_blocked:阻塞进程数(用户 + 内核线程)
  • softirq:软中断统计,是/proc/softirqs所有值的总和(跨类型 + 跨CPU),结构:TOTAL HI(高优先级 softirq) TIMER(内核 timer_list 定时器) NET_TX(网络收包最重要) NET_RX(网络发包) BLOCK(块设备) IRQ_POLL TASKLET(基于 softirq 的 lightweight 机制) SCHED(调度器) HRTIMER(高精度定时器) RCU(RCU回收)。

!IMPORTANT

/proc/interrupts 更适合实时排查:比如使用

shell 复制代码
watch -n 1 'cat /proc/interrupts'

可以动态观察某个中断更新频率是否异常。

/proc/stat 更适合历史总量分析 :它记录了所有处理器上的汇总数据。如果你需要计算系统运行期间的总中断负载,或者编写监控脚本统计长期趋势,解析 intr 行会更加方便。

10.2.2 自动检测 IRQ 号(Virtual IRQ

书中提到的 IRQ 号指的是Linux内核层面的虚拟中断号,在早期古老的 x86(ISA 总线)时代,硬件中断线非常有限且固定(如并口固定用 IRQ 7)内核的虚拟 IRQ 号 和底层的硬件物理中断号 往往是一一对应的,所以当时写驱动常常把两者混为一谈。但现在复杂的 SoC(如 ARM64 + GIC)中,驱动程序申请、注册和探测的,始终是经过内核抽象后的虚拟 IRQ 号

10.2.2.1 自动检测 IRQ 号:从"暴力探测"到"设备树宣告"

书中介绍了三种获取 IRQ 的方法,在现代内核中,它们的地位发生了翻天覆地的变化:

  1. 根据 I/O 地址猜中断

    书中根据并口地址硬编码猜测中断号,现代观点:严禁在驱动代码中硬编码任何硬件资源,驱动必须与硬件描述解耦。

  2. 设备自己"宣告"中断(书中提到的 PCI 标准)

    书中做法:读取 PCI 配置空间来获取中断号。现代观点 :驱动不需要去"猜"或"探测"中断号。驱动只需要在 probe() 函数中,通过标准的内核 API platform_get_irq()向内核申请虚拟IRQ,内核代码会将设备树中描述的硬件中断号 解析并映射为虚拟中断号 最终返回,随后这个虚拟中断号会给到request_irq使用。(内核./drivers/tty/serial/amba-pl011.c中就有这两个函数)

  3. 主动探测(Probe):内核协助与 DIY

    这种方法在现代嵌入式驱动中已极少使用。这种"暴力探测"不仅效率低(需要触发硬件、延时等待),而且非常危险(容易干扰其他设备),且不支持现代普遍的中断共享机制。

10.2.3 快速和慢速处理例程

书中花费大量篇幅讲解的"快速中断"与"慢速中断"的区别,在现代内核中已经几乎不复存在。废弃概念 :早期的"快/慢中断"区分及 SA_INTERRUPT 标志已退出历史舞台。现代最佳实践 :为了降低中断延迟,现代驱动极力避免在硬中断(上半部)中做耗时操作。对于稍微复杂的处理,应使用 中断线程化(Threaded IRQs) 技术,将耗时逻辑移交到内核线程中执行,从而实现极高的系统实时性和吞吐量。

10.3 实现中断处理例程

10.3.1 处理例程的参数及返回值

中断处理例程中,中断发生时有几个有用的参数传递给了中断处理例程很有用,教你怎么看。

书中大部分内容已经过时了,我结合Linux最新发展总结一下有价值的地方,结合串口中的中断处理函数作为例子:

c 复制代码
./drivers/tty/serial/amba-pl011.c 
static irqreturn_t pl011_int(int irq, void *dev_id)
{
    uart_port_lock(&uap->port);
    ......
    uart_unlock_and_check_sysrq(&uap->port);
    return IRQ_RETVAL(handled);
}
  1. 遵循"快进快出"原则:中断上下文严禁睡眠。不能使用非 GFP_ATOMIC 标志的内存分配(如 kmalloc)、不能获取可能引起睡眠的信号量(Semaphore)。不能与用户空间交互。只做"清除中断标志、读取少量核心状态、唤醒中断下半部"这三件事。
  2. 善用 dev_id 传递私有数据:务必在注册时将设备结构体指针通过 dev_id 传入,以便在中断处理函数中无锁、高效地访问设备资源。
  3. 返回处理状态:必须返回 IRQ_HANDLED(已处理)或 IRQ_NONE(非本设备中断),以便内核正确处理共享中断和虚假中断。现代内核中,通常直接返回这两个枚举值,或者使用 return IRQ_RETVAL(条件) 宏。
  4. 拥抱中断线程化:对于有耗时处理需求的现代驱动,优先使用 request_threaded_irq(),将复杂的业务逻辑交给内核线程(下半部)去安全执行。下面会讲到。

10.3.2 启用和禁用中断(尽量不用)

  1. 严禁作为互斥锁:在 SMP 多核系统中,关中断只能锁住当前 CPU,无法防止其他核心的并发访问。保护共享资源请始终使用自旋锁(Spinlock)或互斥锁(Mutex)。
  2. 单中断禁止(disable_irq:属于高风险操作,极易引发共享中断冲突或死锁。在现代驱动中,应交由内核电源管理子系统(PM)自动处理,驱动层尽量避免手动调用。
  3. 全局(本地)中断禁止(local_irq_\* :仅用于保护极短时间的、跨中断上下文和进程上下文的本地 CPU 临界区 。务必使用 local_irq_save(flags)local_irq_restore(flags) 配对,以确保中断状态的完美嵌套恢复。
  4. 接口更新 :相关 API 统一位于 <linux/interrupt.h><linux/irqflags.h>,不再使用古老的 <asm/irq.h><asm/system.h>

10.4 顶半部和底半部

中断处理函数要完成一定量的工作但速度必须要快,为了满足这两个看似矛盾的需求,内核开发者把中断处理过程分成了两部分:

  • 顶半部:用于快速响应中断,就是request_irq注册的中断例程。
  • 底半部:被顶半部调度,在稍后更安全的时间内执行的例程。Linux内核提供了两种机制:①tasklet ②工作队列 ③软中断。已经在本书第七章介绍过了。

10.4.1 tasklet( = softirq 的封装(简化版))

详情请看本书【第7章 7.5 tasklet机制】。在 Linux 6.9 内核中,随着 Workqueue(工作队列)子系统的改进(特别是引入了 BH 下半部工作队列支持),内核维护者们终于找到了 Tasklet 的完美替代品。Linus Torvalds 和内核开发者们多次在邮件列表中明确表示,希望看到 Tasklet 彻底从内核中消失,并正在积极推动这一"灭绝"进程。他有如下缺陷:

  • 无法利用多核(SMP 扩展性差):同一个 Tasklet 在任何时刻只能在一个 CPU 上运行。这意味着即使你的系统有 8 个核心,同一个设备的 Tasklet 也只能占满其中 1 个核,无法并行处理,严重限制了高吞吐量设备的性能。
  • 运行在软中断上下文(不能睡眠) :Tasklet 运行在软中断(Soft IRQ)上下文中,这属于原子上下文,没有进程描述符。绝对不允许睡眠、不能调用可能引起阻塞的函数(如分配内存不能用 GFP_KERNEL,不能使用互斥锁 Mutex)。这给驱动开发带来了极大的限制和风险,稍有不慎就会导致系统崩溃或死锁。
  • 容易引起系统卡顿:由于软中断的优先级极高,如果 Tasklet 处理稍微耗时,就会严重延迟其他软中断甚至用户空间程序的执行,导致系统响应变慢。

10.4.2 工作队列(= kthread + 队列(线程池模型))

详情请看本书【第7章 7.6 工作队列】。工作队列运行在进程上下文,工作队列百分之百由内核线程kworker来执行(kworker 就是内核的打工线程,工作队列就是它的待办清单),用于处理复杂耗时的任务,工作队列可以休眠,可用阻塞。但不能从工作队列向用户空间复制数据。

10.5 中断共享

在传统的老式 PC 架构(如 ISA、早期的 PCI 总线)中,物理中断线(IRQ)的数量极其有限(通常只有 16 条)。为了能让越来越多的外设(网卡、声卡、显卡等)都能通知 CPU,大家被迫挤在同一条中断线上,导致共享中断非常普遍。

而在ARM64平台上,使用的是先进的 GICv3(通用中断控制器)。GICv3 支持成百上千个 SPI(共享外设中断)号,中断资源非常丰富。再加上现代总线(如 PCIe)普遍支持 **MSI / MSI-X(消息信号中断)**技术。MSI 不再依赖物理引脚,而是设备直接向内存写入一个特定的数据包来触发中断,这使得每个设备甚至设备的每个功能都能轻松分配到独立的中断号,从根本上消灭了"抢线"的必要性。

SPI(共享外设中断)的"共享"体现在哪er啊?

这个"共享"指的并不是 "多个设备挤在同一条中断线上"(那是我们上一轮聊的共享中断),而是指"这条中断线可以被路由(分发)给多个 CPU 核心中的任意一个来处理"。它打破了外设与特定 CPU 核心的绑定关系,可以被灵活地路由给系统中的任意 CPU 核心去处理。在 SMP(对称多处理)系统中,这种机制非常有利于操作系统的负载均衡。

GICv3 提供了海量的 SPI 中断号(通常从 32 号一直排到 1019 号甚至更多)。芯片设计厂商(比如 NXP、Rockchip)在设计 SoC 时,会把 UART0、UART1、SPI、I2C、USB 等成百上千个外设,一对一地连接到 GIC 的不同 SPI 编号上

因为资源极其丰富,每个外设都能分到一条专属的"VIP 通道"(比如 UART0 独占 32 号,UART1 独占 33 号),大家井水不犯河水。所以,虽然它们都叫"共享外设中断(SPI)",但在物理连接上,它们绝大多数时候都是独占一条中断线,并不需要和其他设备"挤在一起"。

在以前的通用计算机上,操作系统经常需要靠"猜"或者 BIOS 的模糊分配来给外设分中断。但在嵌入式 SoC 中,所有的硬件拓扑结构都是固定且已知的。通过设备树(DTS),Linux kernel 在启动时就极其精确的知道每一个外设(UART, SPI, I2C 等)物理上连接到了GIC哪一个中断号上,既然硬件连接固定,内核就可以为每个外设静态分配专属虚拟中断(VIRQ),自然不需要共享了。

我在 IMX8mp 板子上发现的唯一的一个共享中断(最后一项有逗号分隔):

bash 复制代码
# cat /proc/interrupts
212:     772      0        0        0  irqsteer   0 Level     32fd8000.hdmi, dw-hdmi-cec
  • 物理本质 :这个特例 32fd8000.hdmi, dw-hdmi-cec,属于**"同一物理设备内部的功能共享"**。HDMI 控制器和 CEC(消费者电子控制,用于电视遥控联动等功能)模块,在芯片内部往往是封装在同一个物理 IP 核里的,或者它们共用同一根物理中断信号线连接到 GIC。

  • 驱动注册 :在 Linux 内核中,HDMI 主驱动和 CEC 子系统驱动是两个相对独立的软件模块。当它们初始化时,都会去请求同一个硬件中断号。两个驱动在调 request_irq() 时都需要声明 IRQF_SHARED 标志,当硬件触发中断时,内核会依次调用 HDMI 驱动和 CEC 驱动的中断处理函数(ISR),每个 ISR 内部会通过读取硬件寄存器来判断:"这次中断是主 HDMI 产生的,还是 CEC 产生的?",如果不是自己的,就返回 IRQ_NONE

10.6 中断驱动的 I/O (例子)

10.6.1 打印机写缓冲区示例 (经AI整理)

我们将整个过程分为三个角色:写数据的人(用户进程)干活的工人(工作队列)报信的(中断与定时器)

角色一:用户进程(负责把数据搬进缓存)

当你在应用程序里调用 write() 时,驱动里的 shortp_write 函数会被触发。它的任务不是直接操作硬件,而是把数据先存到内存(环形缓存)里。

c 复制代码
// 用户进程调用 write() 时执行
while (written < count) {
    // 1. 检查缓存有没有空位
    space = shortp_out_space(); 
    
    // 2. 【关键点】如果缓存满了,进程就"挂起"睡觉(阻塞),等待有空位再醒来
    if (space <= 0) {
        wait_event_interruptible(shortp_out_queue, ...); 
    }

    // 3. 把用户的数据拷贝到驱动的环形缓存里
    copy_from_user(...); 
    written += space;

    // 4. 如果之前没有在往外发数据,就启动"干活工人"(工作队列)
    if (!shortp_output_active) {
        shortp_start_output(); 
    }
}
  • 解读: 这一步实现了"缓存"。写操作非常快,因为只是把数据从用户内存拷贝到驱动内存,并没有去管慢吞吞的打印机。

角色二:干活的工人(负责把缓存的数据发给硬件)

shortp_start_output 会启动一个工作队列(shortp_work)(承担中断下半部 角色),这个工人负责每次从缓存里拿一个字节发给打印机。

c 复制代码
// 工作队列函数:负责实际把数据搬运给硬件,承担了下半部的角色
void shortp_work_handler(...) {
    spin_lock_irqsave(...); // 加锁保护数据

    // 1. 如果缓存空了,说明活干完了
    if (shortp_out_head == shortp_out_tail) {
        shortp_output_active = 0; // 标记停止工作
        wake_up_interruptible(&shortp_empty_queue); // 唤醒可能在等待"发完"的人
        del_timer(&shortp_timer); // 删掉防丢包定时器
    } 
    // 2. 如果还有数据,就发一个字节
    else {
        shortp_do_write(); // 调用下面的硬件操作函数
    }
    // 3. 既然发了一个字节,缓存肯定有空位了,唤醒之前因为缓存满而睡觉的"用户进程"
    if (有足够空间) {
        wake_up_interruptible(&shortp_out_queue); 
    }
    spin_unlock_irqrestore(...);
}

// 真正操作硬件端口发数据的函数
static void shortp_do_write(void) {
    // 1. 复位定时器(防止误判中断丢失)
    mod_timer(&shortp_timer, jiffies + TIMEOUT); 
    // 2. 往打印机数据端口写一个字节
    outb_p(*shortp_out_tail, shortp_base+SP_DATA); 
    // 3. 更新缓存指针,并给打印机发一个"选通"信号(告诉打印机:数据来了,请接收)
    outb_p(cr | SP_CR_STROBE, ...); 
    outb_p(cr & ~SP_CR_STROBE, ...); 
}
  • 解读: 工人每次只发一个字节,然后停下来等。为什么要停?因为打印机很慢,发太快它会处理不过来。

角色三:报信的(中断与定时器)

打印机处理完那一个字节后,会通过硬件线路给 CPU 发一个中断信号 ,下面是中断信号对应的中断处理函数(上半部),你可以看到上半部会启动下半部(再次把"干活的工人"(工作队列)拉起来干活)。

c 复制代码
// 中断处理函数:打印机喊"我处理完了,给我下一个!"
// 该函数是通过 request_irq 注册给硬件中断号的,绝对不能睡眠或阻塞,必须"快进快出",属于上半部
static irqreturn_t shortp_interrupt(...) {
    if (!shortp_output_active) return IRQ_NONE;
    
    // 核心动作:再次把"干活的工人"(工作队列)拉起来干活
    // 工人起来后会执行上面的 shortp_work_handler,发下一个字节
    queue_work(shortp_workqueue, &shortp_work); 
    return IRQ_HANDLED;
}

如果打印机"嗓门太小",中断信号丢了怎么办?

为了防止死机,驱动在发数据时会设置一个定时器

c 复制代码
// 定时器超时函数:专门处理"中断丢失"的情况
static void shortp_timeout(...) {
    // 1. 去查打印机的状态寄存器
    status = inb(shortp_base + SP_STATUS);
    // 2. 如果打印机还在忙,说明没丢中断,只是它太慢了,重置定时器继续等
    if ((status & SP_SR_BUSY) == 0) {
        mod_timer(&shortp_timer, jiffies + TIMEOUT);
        return;
    }
    // 3. 如果打印机显示"空闲/就绪",说明它早就处理完了,但是中断信号丢了!
    // 手动调用中断处理函数,强行让系统继续发下一个字节
    shortp_interrupt(shortp_irq, NULL, NULL); 
}
  • 解读: 这是一个"兜底"机制。正常情况靠中断驱动,异常情况靠定时器轮询状态来补救,保证系统永远不会因为漏掉一个信号而卡死。

总结:整个流程是怎么跑起来的?

  1. 用户 扔给驱动一大包数据,驱动把它存进缓存,然后喊一声"工人开工"。
  2. 工人(工作队列,属于下半部) 从缓存拿 1 个字节给打印机,然后设个闹钟(定时器),就去休息了。
  3. 打印机 慢吞吞地打完这 1 个字节,发个中断喊"打完了"。
  4. 中断处理程序 (顶半部)开始执行,把工人(工作队列,属于下半部)叫醒。
  5. 工人 醒来,再拿 1 个字节给打印机,重置闹钟,继续休息。
  6. 如果闹钟响了工人还没被叫醒(中断丢失 ),定时器就会直接踹工人一脚让他起来干活。
相关推荐
开开心心就好12 小时前
180套模板的图片艺术拼接实用工具
linux·服务器·网络·spring·智能手机·maven·excel
程序leo源12 小时前
Qt界面优化详解
linux·c语言·开发语言·c++·qt·c#
tang74516396212 小时前
Ubuntu 24.04 安装 Nginx 1.29.6 完整版教程20260320
linux·nginx·ubuntu
Tingjct12 小时前
【linux】part1-进程详解
linux·运维·服务器
L16247612 小时前
OpenSSL + OpenSSH 两套安装方案(覆盖系统目录 / 独立目录)
linux·ssh
hweiyu0012 小时前
Linux命令:iotop
linux·运维
齐潇宇13 小时前
Jenkins 自动化部署 Tomcat + PHP
linux·运维·容器·tomcat·jenkins
枳实-叶13 小时前
【Linux驱动开发】第17天:I2C子系统整体架构
linux·驱动开发·架构
Cat_Rocky13 小时前
Linux-基于Jenkins自动打包并部署Tomcat环境
linux·tomcat·jenkins