ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux 内核中断处理实战:以 21285 串口驱动为例,走通 request_irq 到 do_IRQ 的完整链路

Linux 内核中断处理实战:以 21285 串口驱动为例,走通 request_irq 到 do_IRQ 的完整链路 文档教程操作系统【免费下载链接】linux-insidesA book-in-progress about the Linux kernel and its insides.项目地址https://gitcode.com/gh_mirrors/li/linux-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.mdThe 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 波特率计算所需的 uartclkunsigned 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/ttyFB0major/minor指定设备号nr 1表示只管理一个端口cons指向控制台相关配置。打开端口与请求 IRQserial21285_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.hstatic 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_CONTXhandler中断处理函数指针serial21285_rx_chars/serial21285_tx_charsflags中断标志位掩码0name中断拥有者名称显示在/proc/interruptsFootbridge UARTdev共享中断线时用于区分的设备指针portuart_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的内核线程把处理函数放到线程上下文执行即中断线程化。参数合法性检查函数开头做了一系列校验任何一项不满足都返回-EINVALif (((irqflags IRQF_SHARED) !dev_id) || (!(irqflags IRQF_SHARED) (irqflags IRQF_COND_SUSPEND)) || ((irqflags IRQF_NO_SUSPEND) (irqflags IRQF_COND_SUSPEND))) return -EINVAL;共享中断必须提供真实的dev_idIRQF_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 devidif (!irq_settings_can_request(desc) || WARN_ON(irq_settings_is_per_cpu_devid(desc))) return -EINVAL;handler 缺省与 irqaction 分配如果调用者没有传handler只传了thread_fn则退化为使用默认的主处理函数irq_default_primary_handlerif (!handler) { if (!thread_fn) return -EINVAL; handler irq_default_primary_handler; }随后用kzalloc为零初始化方式分配struct irqaction失败则返回-ENOMEMkzalloc的细节属于内存管理章节范畴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) vectorFIRST_EXTERNAL_VECTOR .rept (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR) pushq $(~vector0x80) vectorvector1 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_irqirq __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_descdesc 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 与 iretqdo_IRQ返回后控制权回到arch/x86/entry/entry_64.S的ret_from_intr标签。首先用DISABLE_INTERRUPTS宏展开为cli指令关闭中断记录TRACE_IRQS_OFF并递减 per-CPU 的irq_count变量——它之前在进入中断上下文时被置为 1DISABLE_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_RETURNiretq恢复现场。从理论到驱动、从汇编到 C、从注册到分发这条链路的每个环节都在本仓库对应章节中有更细的展开向量与vector_irq初始化见 linux-interrupts-8.md延迟中断的软中断机制见 linux-interrupts-9.mdinitcall 机制见 Concepts/linux-cpu-3.mdper-CPU 变量见 Concepts/linux-cpu-1.md。需要提醒的是本章与本文远未穷尽中断处理的所有侧面——中断线程化、irq 域irqdomain、NUMA 亲和与均衡、中断控制器驱动等主题都还有大量可深入的空间但掌握本文这条主线后读者已经具备直接钻进内核中断相关代码进行探索的坐标与工具。赞分享文档教程操作系统【免费下载链接】linux-insidesA book-in-progress about the Linux kernel and its insides.项目地址https://gitcode.com/gh_mirrors/li/linux-insides点击查看免费下载相关推荐硬件中断处理全链路实战从串行驱动请求 IRQ 到中断返回的内核之旅硬件中断处理全链路实战从串行驱动请求 IRQ 到中断返回的内核之旅 导读 本文是 linux insides zh 仓库《中断和中断处理》章节的终篇解析以TVM 代码库实战导览以向量加法为例走通编译管线全链路TVM 代码库实战导览以向量加法为例走通编译管线全链路 本篇技术指南以 TVM 仓库Apache TVM开源深度学习编译器中的官方文档 codebase编译器深度学习模型优化BabelDOC PDF保格式翻译完整指南如何用3条命令把英文论文翻成能读的版本BabelDOC PDF保格式翻译完整指南如何用3条命令把英文论文翻成能读的版本 用在线翻译工具翻一篇英文论文结果常常是分栏全乱、公式变成一串乱码、表格拉人工智能AI 应用NLP计算机视觉上一篇YOLO-Face人脸检测技术从入门到实战的完整指南下一篇career-ops 高频问题完全指南从 Windows 安装报错到跨平台求职自动化实战创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表