- 文档
- 教程
- 操作系统
【免费下载链接】linux-insides
A book-in-progress about the Linux kernel and its insides.
本文是 linux-insides 仓库 Interrupts 章节 收尾篇(原文档)的技术详解。前面九部分依次覆盖了中断理论、早期中断门、异常处理、外部硬件中断初始化以及 softirq/tasklet/workqueue 等延迟中断机制,本篇则将视线落到一个真实硬件驱动——StrongARM SA-110/21285 评估板串口驱动(drivers/tty/serial/21285.c)上,完整追踪"驱动如何注册中断处理函数、中断号如何映射、中断到来后内核如何一路分发到驱动回调、最后如何退出中断上下文"的整条调用链。读完本篇,你将掌握request_irq的五个参数语义与底层request_threaded_irq实现、IRQ 号到irq_desc的转换、x86 上从irq_entries_start汇编入口到do_IRQ再到generic_handle_irq_desc的分发路径,以及ret_from_intr返回用户态/内核态时的收尾细节。
章节定位:为什么用串口驱动收官
本部分之前的 linux-interrupts-9.md 已经介绍过延迟中断的概念:中断处理要求"快",但有时又要做大量工作,因此被拆成 top half(主处理程序)与 bottom half(延迟处理),内核提供softirq、tasklet、workqueue三类延迟机制。本章要回答的最后一个问题是:这些机制最终服务的对象——真实的设备驱动,是如何与中断子系统对接的?
串口(UART)驱动是一个理想的示例:它只有两根数据线(RX接收、TX发送),中断模型简单清晰,且驱动源码短小完整。原文档选取的是 StrongARM SA-110/21285 评估板的串口驱动,源码位于内核树drivers/tty/serial/21285.c。本文将沿该驱动的生命周期展开:初始化 → 打开端口时请求 IRQ → 中断触发后的分发 → 中断退出,四段式走完一次硬件中断的完整旅程。
驱动初始化:module_init 与 module_exit 的两种形态
与本书此前介绍新概念的方式一致,分析驱动同样从初始化入口开始。Linux 内核为驱动/内核模块的初始化和卸载提供了两个宏:
module_init—— 模块加载时执行的初始化函数;module_exit—— 模块卸载时执行的清理函数。
在 21285 驱动中可以看到它们的典型用法:
module_init(serial21285_init); module_exit(serial21285_exit);绝大多数设备驱动既可以编译为可加载内核模块(loadable kernel module),也可以静态链接进内核。这两种形态下宏的定义完全不同。
当驱动作为可加载模块时,module_init/module_exit宏定义在include/linux/init.h中:
#define module_init(initfn) \ static inline initcall_t __inittest(void) \ { return initfn; } \ int init_module(void) __attribute__((alias(#initfn))); #define module_exit(exitfn) \ static inline exitcall_t __exittest(void) \ { return exitfn; } \ void cleanup_module(void) __attribute__((alias(#exitfn)));这里用 GCC 的alias属性把init_module/cleanup_module这两个内核约定的导出符号别名到驱动自定义函数上,随后由内核的 initcall 机制在合适的阶段调用。initcall 机制本身在本书 Concepts/linux-cpu-3.md(The initcall mechanism)中有专门论述,其调用层级按初始化时机从早到晚排列:
early_initcallpure_initcallcore_initcallpostcore_initcallarch_initcallsubsys_initcallfs_initcallrootfs_initcalldevice_initcalllate_initcall
这些 initcall 统一由init/main.c中的do_initcalls依次执行。模块方式的实际加载代码位于kernel/module.c,初始化发生在do_init_module函数中。
当驱动静态链接进内核时,宏退化为最简单的形式:
#define module_init(x) __initcall(x); #define module_exit(x) __exitcall(x);serial21285_init:注册 uart 驱动并挂载端口
回到我们的serial21285_init函数,从函数名即可看出它与驱动初始化有关:
static int __init serial21285_init(void) { int ret; printk(KERN_INFO "Serial: 21285 driver\n"); serial21285_setup_ports(); ret = uart_register_driver(&serial21285_reg); if (ret == 0) uart_add_one_port(&serial21285_reg, &serial21285_port); return ret; }初始化流程分三步:
- 打印驱动信息:
printk(KERN_INFO "Serial: 21285 driver\n")向内核缓冲区输出一条Serial: 21285 driver日志。 - 设置端口基础时钟:调用
serial21285_setup_ports,把板级外设时钟换算成 UART 波特率计算所需的 uartclk:
unsigned int mem_fclk_21285 = 50000000; static void serial21285_setup_ports(void) { serial21285_port.uartclk = mem_fclk_21285 / 4; }mem_fclk_21285是板上 50MHz 内存总线时钟,UART 时钟取其四分之一(12.5MHz)。 3.注册 uart 驱动并附加端口:uart_register_driver(&serial21285_reg)把描述驱动的uart_driver结构注册进串口核心;注册成功(返回 0)后再用uart_add_one_port(来自drivers/tty/serial/serial_core.c)把具体的serial21285_port端口附加到该驱动名下。
serial21285_reg是描述该 uart 驱动的核心结构:
static struct uart_driver serial21285_reg = { .owner = THIS_MODULE, .driver_name = "ttyFB", .dev_name = "ttyFB", .major = SERIAL_21285_MAJOR, .minor = SERIAL_21285_MINOR, .nr = 1, .cons = SERIAL_21285_CONSOLE, };其中driver_name/dev_name为ttyFB,表示该驱动对应的设备节点形如/dev/ttyFB0;major/minor指定设备号;nr = 1表示只管理一个端口;cons指向控制台相关配置。
打开端口与请求 IRQ:serial21285_startup
当一个 UART 端口被打开时,串口核心层的uart_open(同样在drivers/tty/serial/serial_core.c)会调用uart_startup启动串口,进而调用每个 uart 驱动都必须实现的uart_ops结构中的startup回调。21285 驱动的uart_ops定义如下:
static struct uart_ops serial21285_ops = { ... .startup = serial21285_startup, ... }serial21285_startup是本篇与中断主题最相关的一段代码:
static int serial21285_startup(struct uart_port *port) { int ret; tx_enabled(port) = 1; rx_enabled(port) = 1; ret = request_irq(IRQ_CONRX, serial21285_rx_chars, 0, serial21285_name, port); if (ret == 0) { ret = request_irq(IRQ_CONTX, serial21285_tx_chars, 0, serial21285_name, port); if (ret) free_irq(IRQ_CONRX, port); } return ret; }先说TX/RX:串行总线只有两根线,一根发送(TX)、一根接收(RX),所以串口设备有两个串行引脚。开头两个宏tx_enabled/rx_enabled就是使能这两根线的收发能力。
接下来是重点——两次request_irq调用。该函数"注册一个中断处理函数并启用一条中断线",声明在include/linux/interrupt.h:
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, name, dev); }request_irq 的五个参数
| 参数 | 含义 | 本例取值 |
|---|---|---|
irq | 请求的中断号 | IRQ_CONRX/IRQ_CONTX |
handler | 中断处理函数指针 | serial21285_rx_chars/serial21285_tx_chars |
flags | 中断标志位掩码 | 0 |
name | 中断拥有者名称,显示在/proc/interrupts | "Footbridge UART" |
dev | 共享中断线时用于区分的设备指针 | port(uart_port 结构指针) |
中断号从哪来:IRQ_CONRX / IRQ_CONTX
两次request_irq分别注册接收中断IRQ_CONRX和发送中断IRQ_CONTX。这两个宏定义在 21285 板的平台头文件arch/arm/mach-footbridge/include/mach/irqs.h中:
#define IRQ_CONRX _DC21285_IRQ(0) #define IRQ_CONTX _DC21285_IRQ(1) ... ... ... #define _DC21285_IRQ(x) (16 + (x))该板上的 ISA IRQ 编号从 0 到 15,因此 21285 板自定义中断从 16 起算:IRQ_CONRX = 16、IRQ_CONTX = 17。也就是说,串口驱动实际注册的是 16、17 号两条中断线。
标志位与名称
两个请求的flags均为 0,等价于IRQF_TRIGGER_NONE,不指定任何边沿/电平触发行为(触发方式通常由硬件或中断控制器配置决定)。include/linux/interrupt.h中定义的全部合法标志以IRQF_*开头,常用者包括:
IRQF_SHARED—— 允许多个设备共享该中断线;IRQF_PERCPU—— 中断是 per-CPU 的;IRQF_NO_THREAD—— 该中断不能被线程化;IRQF_NOBALANCING—— 将该中断排除在 irq 均衡之外;IRQF_IRQPOLL—— 中断用于轮询;- 等等。
第四参数serial21285_name定义为:
static const char serial21285_name[] = "Footbridge UART";它会作为中断所有者名称显示在/proc/interrupts的输出中。最后一参数传入port指针,在共享中断线时用于区分究竟是哪个设备触发了中断。
深入 request_threaded_irq:注册一条中断线的底层逻辑
request_irq只是一个内联包装,真正干活的是kernel/irq/manage.c中的request_threaded_irq。它的签名比request_irq多了一个thread_fn参数:
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) { struct irqaction *action; struct irq_desc *desc; int retval; ... ... ... }本章此前已经介绍过两个核心结构:irqaction表示"每个中断上的动作描述符",包含中断处理函数指针、设备名、中断号等字段;irq_desc表示"中断描述符",是中断管理代码的基石,内含指向irqaction的指针、中断标志等。更多关于irq_desc初始化(early_irq_init、init_IRQ与vector_irq的填充)的内容可回看 linux-interrupts-8.md。
注意:request_irq调用request_threaded_irq时传入的thread_fn为NULL。如果thread_fn非空,内核会创建名为irq/<n>-<name>的内核线程,把处理函数放到线程上下文执行(即中断线程化)。
参数合法性检查
函数开头做了一系列校验,任何一项不满足都返回-EINVAL:
if (((irqflags & IRQF_SHARED) && !dev_id) || (!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) || ((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND))) return -EINVAL;共享中断必须提供真实的dev_id;IRQF_COND_SUSPEND只在共享中断时才有意义;IRQF_NO_SUSPEND与IRQF_COND_SUSPEND不能同时出现。
中断号 → irq_desc 的转换
随后用irq_to_desc(定义于kernel/irq/irqdesc.c)把中断号转换成中断描述符:
desc = irq_to_desc(irq); if (!desc) return -EINVAL;struct irq_desc *irq_to_desc(unsigned int irq) { return (irq < NR_IRQS) ? irq_desc + irq : NULL; }即校验中断号小于NR_IRQS,然后以中断号为偏移量从全局irq_desc数组取出对应描述符。接下来继续校验该描述符是否允许请求、以及是否为 per-CPU devid:
if (!irq_settings_can_request(desc) || WARN_ON(irq_settings_is_per_cpu_devid(desc))) return -EINVAL;handler 缺省与 irqaction 分配
如果调用者没有传handler(只传了thread_fn),则退化为使用默认的主处理函数irq_default_primary_handler:
if (!handler) { if (!thread_fn) return -EINVAL; handler = irq_default_primary_handler; }随后用kzalloc为零初始化方式分配struct irqaction,失败则返回-ENOMEM(kzalloc的细节属于内存管理章节范畴):
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL); if (!action) return -ENOMEM;填充 irqaction 并调用 __setup_irq
分配成功后依次填入各字段:
action->handler = handler; action->thread_fn = thread_fn; action->flags = irqflags; action->name = devname; action->dev_id = dev_id;最后,在chip_bus_lock与chip_bus_sync_unlock的包裹下调用__setup_irq真正注册该 irqaction(这两个锁用于保护 i2c 等慢速总线上芯片的寄存器访问):
chip_bus_lock(desc); retval = __setup_irq(irq, desc, action); chip_bus_sync_unlock(desc); if (retval) kfree(action); return retval;__setup_irq 内部:线程创建与描述符填充
__setup_irq开头做若干检查:中断描述符非空、irqchip 非空、描述符模块 owner 非空;如果中断嵌套在另一个中断线程中,则把主处理函数替换为irq_nested_primary_handler。对于非嵌套且提供了thread_fn的中断,用kthread_create创建中断线程:
if (new->thread_fn && !nested) { struct task_struct *t; t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name); ... }线程名形如irq/16-Footbridge UART。至此,16、17 号两条中断线注册完毕,之后一旦中断控制器产生相关事件,serial21285_rx_chars与serial21285_tx_chars就会被调用。
中断发生时:从向量到 do_IRQ 的分发路径
中断门的建立:native_init_IRQ 与 irq_entries_start
上一节完成了"驱动侧注册",本节看"内核侧分发"。在 linux-interrupts-8.md 中我们见过native_init_IRQ函数——它初始化本地 APIC,并完成外部中断向量的中断门设置。其中最关键的一段是:
for_each_clear_bit_from(i, used_vectors, first_system_vector) { set_intr_gate(i, irq_entries_start + 8 * (i - FIRST_EXTERNAL_VECTOR)); }这里遍历used_vectors位图中从first_system_vector起的未用位,为每个向量号i设置中断门,入口地址为irq_entries_start + 8 * (i - FIRST_EXTERNAL_VECTOR)。其中:
int first_system_vector = FIRST_SYSTEM_VECTOR; // 0xefirq_entries_start定义在汇编文件arch/x86/entry/entry_64.S中,是一段中断入口桩代码:
.align 8 ENTRY(irq_entries_start) vector=FIRST_EXTERNAL_VECTOR .rept (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR) pushq $(~vector+0x80) vector=vector+1 jmp common_interrupt .align 8 .endr END(irq_entries_start)GNU 汇编器的.rept指令会把.endr之前的序列重复FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR次。已知FIRST_SYSTEM_VECTOR = 0xef、FIRST_EXTERNAL_VECTOR = 0x20:
>>> 0xef - 0x20 207即重复 207 次。每次循环把取负的中断向量号压栈(使用负数是为了避开已保留给系统调用的正数向量号),递增vector变量,然后跳转到common_interrupt标签。在common_interrupt中先修正栈上的向量号,再以do_IRQ为参数执行中断宏:
common_interrupt: addq $-0x80, (%rsp) interrupt do_IRQinterrupt宏同样定义在entry_64.S:保存通用寄存器到栈上,必要时用SWAPGS指令把用户态gs换成内核态gs,递增 per-CPU 的irq_count变量(标记已进入中断上下文),最后调用do_IRQ。关于 per-CPU 变量的机制可参阅 Concepts/linux-cpu-1.md。
do_IRQ:取出中断号并进入中断上下文
do_IRQ定义在arch/x86/kernel/irq.c,是设备中断的实际处理入口,它接收保存用户态寄存器值的pt_regs:
__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs) { struct pt_regs *old_regs = set_irq_regs(regs); unsigned vector = ~regs->orig_ax; unsigned irq; irq_enter(); exit_idle(); ... ... ... }函数开头:
set_irq_regs(regs)保存并返回原来的 per-CPU irq 寄存器指针;irq_enter()通过更新__preempt_count变量进入中断上下文;exit_idle()检查当前进程是否 idle 进程(PID 为 0),若是则通过idle_notifier通知IDLE_END。
随后从 per-CPU 的vector_irq[vector]读出该向量对应的 irq 号(这个数组在init_IRQ阶段填充,详见 linux-interrupts-8.md),并调用handle_irq:
irq = __this_cpu_read(vector_irq[vector]); if (!handle_irq(irq, regs)) { ... ... ... } ... ... ...handle_irq 与 generic_handle_irq_desc:最终命中驱动回调
handle_irq定义在arch/x86/kernel/irq_64.c,它校验中断描述符后调用generic_handle_irq_desc:
desc = irq_to_desc(irq); if (unlikely(!desc)) return false; generic_handle_irq_desc(irq, desc);static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc) { desc->handle_irq(irq, desc); }这里会产生一个疑问:我们明明知道irqaction才指向真正的中断处理函数,为什么从中断描述符调用handle_irq?答案是:irq_desc->handle_irq是"调用中断处理例程的高层 API",它在设备树与 APIC 初始化阶段被选定,内核在此处根据硬件类型选择正确的函数并调用链式的irq->action(s)。也就是说,中断发生后,serial21285_tx_chars或serial21285_rx_chars正是经由这条路径被最终执行。
do_IRQ的收尾是退出中断上下文并恢复旧寄存器:
irq_exit(); set_irq_regs(old_regs); return 1;我们已知:当 IRQ 主处理完成、中断上下文退出时,若存在待处理的延迟中断(softirq 等),它们会在irq_exit阶段被调度执行——这正是上一部分 linux-interrupts-9.md 介绍的机制。
退出中断:ret_from_intr 与 iretq
do_IRQ返回后,控制权回到arch/x86/entry/entry_64.S的ret_from_intr标签。首先用DISABLE_INTERRUPTS宏(展开为cli指令)关闭中断,记录TRACE_IRQS_OFF,并递减 per-CPU 的irq_count变量——它之前在进入中断上下文时被置为 1:
DISABLE_INTERRUPTS(CLBR_NONE) TRACE_IRQS_OFF decl PER_CPU_VAR(irq_count)最后检查此前是用户态还是内核态,按正确方式恢复上下文,并以INTERRUPT_RETURN退出中断:
INTERRUPT_RETURNINTERRUPT_RETURN宏定义如下:
#define INTERRUPT_RETURN jmp native_iret而native_iret最终执行iretq指令从栈上恢复中断现场:
ENTRY(native_iret) .global native_irq_return_iret native_irq_return_iret: iretq至此,一次硬件中断从设备触发、经中断控制器通知 CPU、向量映射、do_IRQ分发、驱动回调执行、延迟中断处理,到iretq恢复现场的全过程画上句号。
总结:一条贯穿驱动与内核的中断全景链路
本篇作为 Interrupts 章节 的收尾,把整章知识串成了一条可追踪的链路:
- 驱动初始化:
module_init(serial21285_init)通过 initcall 机制注册串口驱动(uart_register_driver)并挂载端口(uart_add_one_port); - 请求 IRQ:端口打开时
serial21285_startup以IRQF_TRIGGER_NONE标志为 16/17 号中断线调用request_irq,底层经由request_threaded_irq→__setup_irq分配并注册irqaction; - 向量建立:
native_init_IRQ借助arch/x86/entry/entry_64.S中的irq_entries_start入口桩,把 207 个外部向量逐一映射到common_interrupt; - 中断分发:中断发生后
do_IRQ依据vector_irq[vector]还原 irq 号,经handle_irq→generic_handle_irq_desc→desc->handle_irq调用链命中驱动注册的serial21285_rx_chars/serial21285_tx_chars; - 退出中断:
ret_from_intr递减irq_count后用INTERRUPT_RETURN(iretq)恢复现场。
从理论到驱动、从汇编到 C、从注册到分发,这条链路的每个环节都在本仓库对应章节中有更细的展开:向量与vector_irq初始化见 linux-interrupts-8.md,延迟中断的软中断机制见 linux-interrupts-9.md,initcall 机制见 Concepts/linux-cpu-3.md,per-CPU 变量见 Concepts/linux-cpu-1.md。需要提醒的是,本章与本文远未穷尽中断处理的所有侧面——中断线程化、irq 域(irqdomain)、NUMA 亲和与均衡、中断控制器驱动等主题都还有大量可深入的空间,但掌握本文这条主线后,读者已经具备直接钻进内核中断相关代码进行探索的坐标与工具。
- 文档
- 教程
- 操作系统
【免费下载链接】linux-insides
A book-in-progress about the Linux kernel and its insides.
相关推荐
硬件中断处理全链路实战:从串行驱动请求 IRQ 到中断返回的内核之旅
硬件中断处理全链路实战:从串行驱动请求 IRQ 到中断返回的内核之旅 导读 :本文是 linux insides zh 仓库《中断和中断处理》章节的终篇解析,以
TVM 代码库实战导览:以向量加法为例走通编译管线全链路
TVM 代码库实战导览:以向量加法为例走通编译管线全链路 本篇技术指南以 TVM 仓库(Apache TVM,开源深度学习编译器)中的官方文档 codebase
编译器深度学习模型优化BabelDOC PDF保格式翻译完整指南:如何用3条命令把英文论文翻成能读的版本
BabelDOC PDF保格式翻译完整指南:如何用3条命令把英文论文翻成能读的版本 用在线翻译工具翻一篇英文论文,结果常常是:分栏全乱、公式变成一串乱码、表格拉
人工智能AI 应用NLP计算机视觉
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考