
【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/hust-open-atom-club/linux-insides-zh点击查看免费下载导读本文是 linux-insides-zh 仓库《中断和中断处理》章节的终篇解析以 StrongARM SA-100/21285 评估板的串行驱动drivers/tty/serial/21285.c为真实案例完整走通一条硬件中断的生命周期驱动如何通过module_init初始化、如何用request_irq请求中断线、中断触发后处理器如何经由 IDT 进入do_IRQ、最终如何通过iretq返回被中断的上下文。读完本文你将掌握从驱动程序注册中断处理函数到CPU 完成中断响应这条链路中每个环节的源码实现与调用关系为自行阅读驱动代码和内核中断子系统打下基础。本节在整个中断章节中的位置《中断和中断处理》是 linux-insides-zh 中讲解 Linux 内核中断子系统的完整章节共十节见章节导读前几节依次讲解了中断理论、异常处理、不可屏蔽中断以及外部硬件中断的早期/非早期初始化第八节IRQ 的非早期初始化第九节 介绍了延后中断机制softirq、tasklet与工作队列workqueue回答了中断处理必须快但有时要做很多事的矛盾本节第十节是终篇把视角从机制层面拉回到真实世界——观察一个真实的硬件驱动如何请求中断线、一个中断被触发时内核究竟发生了什么。前九节我们已经见到过irqaction、irq_desc这两个核心数据结构也了解到中断被分为上半部直接响应与下半部延后执行。本节将这些概念串成一条完整的调用链。在开始之前建议先回顾中断与中断处理入门中对中断向量、IDT、APIC 的铺垫。内核模块的初始化一切从 module_init 开始两个初始化宏Linux 内核为驱动程序或内核模块的初始化和终止提供了两个宏module_init—— 注册驱动的初始化函数module_exit—— 注册驱动的退出清理函数。在 21285 串行驱动的源码中可以看到它们的用法module_init(serial21285_init); module_exit(serial21285_exit);大多数驱动既可以编译成可装载的内核模块loadable 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)));可以看到宏的核心技巧是用__attribute__((alias(...)))把init_module/cleanup_module这两个固定符号名别名为驱动传入的initfn/exitfn这样内核装载模块时调用init_module实际执行的就是驱动的初始化函数。这些宏会被一系列initcall函数调用按执行阶段从早到晚排列early_initcallpure_initcallcore_initcallpostcore_initcallarch_initcallsubsys_initcallfs_initcallrootfs_initcalldevice_initcalllate_initcall这些 initcall 又会被init/main.c中的do_initcalls函数统一调用——这正是内核启动阶段按优先级依次执行各子系统初始化代码的机制对应初始化章节中描述的启动流程。对于静态链接进内核的驱动这两个宏的实现则退化为#define module_init(x) __initcall(x); #define module_exit(x) __exitcall(x);此时模块装载逻辑位于kernel/module.c实际初始化发生在do_init_module函数内。可装载模块的完整细节属于Linux 内核模块专题本节不再展开。serial21285_init注册 uart 驱动与端口本例中module_init接收的参数是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)把驱动名称写入内核环形缓冲区dmesg 中可见。设置端口时钟调用serial21285_setup_ports()为serial21285_port设备设置基本的 uart通用异步收发器时钟unsigned int mem_fclk_21285 50000000; static void serial21285_setup_ports(void) { serial21285_port.uartclk mem_fclk_21285 / 4; }这里把板上 50MHz 的 FCLK 时钟四等分12.5MHz作为串口的波特率基准时钟uartclk。注册驱动与端口调用uart_register_driver注册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, };它声明了设备名ttyFB、主/次设备号以及只管理1个端口.nr 1。注册成功后调用serial_core.c中的uart_add_one_port把驱动定义的端口serial21285_port添加进来然后返回if (ret 0) uart_add_one_port(serial21285_reg, serial21285_port); return ret;到此驱动初始化完毕。打开端口uart_ops 与 serial21285_startup当一个 uart 端口被serial_core.c中的uart_open打开时该函数会调用uart_startup来启动串行端口后者会调用startup回调。这个回调是uart_ops结构体的一部分——每个 uart 驱动都必须定义这样一个结构体。本例中static struct uart_ops serial21285_ops { ... .startup serial21285_startup, ... }.startup字段指向serial21285_startup函数。这个函数与中断和中断处理密切相关正是本节关注的重点。请求中断线request_irq 的五个参数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串行总线的两条线一个设备的串行总线仅由两条线组成一条用于发送数据另一条用于接收数据。与此对应串行设备有两个串行引脚接收器RX和发送器TX。函数开头通过tx_enabled/rx_enabled两个宏激活这两条线。随后是本例的核心调用两次request_irq注册中断处理程序并激活给定中断线。第二次调用若失败ret非零则用free_irq(IRQ_CONRX, port)回滚第一次的注册保证要么两条中断线都注册成功要么都不注册。参数一中断号 IRQ_CONRX / IRQ_CONTXrequest_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); }它接受五个参数参数含义irq被请求的中断号handler中断处理程序指针flags掩码选项IRQF_*name中断拥有者的名称显示在/proc/interruptsdev用于共享中断线的指针本例第一个参数是IRQ_CONRX。CONRX是什么这个宏定义在arch/arm/mach-footbridge/include/mach/irqs.h头文件中那里列出了 21285 主板能够产生的全部中断#define IRQ_CONRX _DC21285_IRQ(0) #define IRQ_CONTX _DC21285_IRQ(1) ... #define _DC21285_IRQ(x) (16 (x))该主板的 ISA 中断号分布在0到15范围内因此IRQ_CONRX和IRQ_CONTX就是其后的头两个值16和17。串行驱动在这两个中断上分别处理RX接收和TX发送事件。参数二中断处理函数两次调用分别传入serial21285_rx_chars和serial21285_tx_chars。当RX或TX中断发生时这两个函数就会被调用。本节不深入这两个函数体——本章主题是中断与中断处理而非设备与驱动细节。参数三flags两次调用的flags都是0。所有合法标志都在include/linux/interrupt.h中定义为IRQF_*宏常见的有标志含义IRQF_SHARED允许多个设备共享此中断号IRQF_PERCPU此中断号是 per-cpu 的IRQF_NO_THREAD中断不能线程化IRQF_NOBALANCING此中断不参与 irq 平衡IRQF_IRQPOLL此中断用于轮询等等这里传入的0即IRQF_TRIGGER_NONE表示不配置任何水平触发或边缘触发的行为。参数四、五名称与设备指针第四个参数serial21285_name定义为static const char serial21285_name[] Footbridge UART;它会显示在/proc/interrupts的输出中方便运维人员识别中断的拥有者。最后一个参数传入指向uart_port结构体的指针即dev_id——在共享中断时内核用它区分究竟是哪台设备产生了中断。request_threaded_irq注册动作的真正实现request_irq内部只是调用了kernel/irq/manage.c中的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) { struct irqaction *action; struct irq_desc *desc; int retval; ... }注意request_irq调用它时多带了一个参数irq_handler_t thread_fn在request_irq中传NULL。如果thread_fn不为NULL内核会创建一个 irq 线程并在该线程中执行给定的处理程序——这就是中断线程化的入口。函数体内会用到两个在之前章节已介绍过的结构体irqaction—— 中断动作描述符包含中断处理程序指针、设备名称、中断号等信息irq_desc—— 中断描述符包含指向irqaction的指针、中断标志等是整个中断管理代码的基石其初始化详见第八节。第一步参数合法性校验if (((irqflags IRQF_SHARED) !dev_id) || (!(irqflags IRQF_SHARED) (irqflags IRQF_COND_SUSPEND)) || ((irqflags IRQF_NO_SUSPEND) (irqflags IRQF_COND_SUSPEND))) return -EINVAL;三个校验条件分别是共享中断IRQF_SHARED时必须传入真正的dev_id否则中断发生时无法区分是哪台设备IRQF_COND_SUSPEND仅对共享中断生效IRQF_NO_SUSPEND与IRQF_COND_SUSPEND互斥。任一条件不满足直接返回-EINVAL。第二步中断号转中断描述符desc irq_to_desc(irq); if (!desc) return -EINVAL;irq_to_desc定义于kernel/irq/irqdesc.c检查给定的中断号是否小于最大中断号并返回中断描述符——中断号就是irq_desc数组的偏移量struct irq_desc *irq_to_desc(unsigned int irq) { return (irq NR_IRQS) ? irq_desc irq : NULL; }第三步检查描述符可请求性if (!irq_settings_can_request(desc) || WARN_ON(irq_settings_is_per_cpu_devid(desc))) return -EINVAL;第四步处理程序缺省值如果调用者没有传入handler只传了thread_fn则把handler设为默认的irq_default_primary_handler两者都为NULL则返回-EINVALif (!handler) { if (!thread_fn) return -EINVAL; handler irq_default_primary_handler; }第五步分配并初始化 irqaction通过kzalloc为irqaction分配内存不成功返回-ENOMEM。kzalloc是内核内存管理章节中介绍过的清零分配函数。随后初始化结构体字段action kzalloc(sizeof(struct irqaction), GFP_KERNEL); if (!action) return -ENOMEM; action-handler handler; action-thread_fn thread_fn; action-flags irqflags; action-name devname; action-dev_id dev_id;第六步__setup_irq 正式注册在request_threaded_irq末尾调用kernel/irq/manage.c中的__setup_irq注册这个irqactionchip_bus_lock(desc); retval __setup_irq(irq, desc, action); chip_bus_sync_unlock(desc); if (retval) kfree(action); return retval;注意__setup_irq的调用被chip_bus_lock与chip_bus_sync_unlock包裹——这两个函数对慢速总线如 i2c上的中断芯片进行锁定/解锁防止并发访问芯片寄存器。__setup_irq开头是各种检查给定的中断描述符不为NULL、irqchip不为NULL、中断描述符的模块拥有者不为NULL。接着检查中断是否嵌套在其他中断线程中——如果是则用irq_nested_primary_handler替换默认的irq_default_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/中断号-名称。创建成功后__setup_irq最后为中断描述符的剩余字段赋值中断注册完成。于是21285 串行驱动的16和17号中断请求线注册完毕。当中断控制器收到相关事件时serial21285_rx_chars和serial21285_tx_chars函数就会被调用。那么一个中断发生时CPU 内部到底发生了什么中断被触发后从中断门到 do_IRQ上文完成了请求中断号、注册irqaction。现在把视角切换到 CPU 一侧。当一个中断事件发生时中断控制器向处理器发出通知处理器尝试为该中断寻找合适的中断门IDT 表项。native_init_IRQ 设置中断门如果你读过第八节应该记得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)); }这里从第first_system_vector位开始依次迭代used_vectors位图中所有被清除的位int first_system_vector FIRST_SYSTEM_VECTOR; // 0xef每遇到一个未使用的向量i就调用set_intr_gate设置中断门i是向量号irq_entries_start 8 * (i - FIRST_EXTERNAL_VECTOR)是入口地址。irq_entries_start汇编生成的中断入口irq_entries_start这个符号定义在arch/x86/entry/entry_64.S汇编文件中提供了中断入口。它是用 GNU 汇编器的.rept指令批量生成的.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).rept指令会把.endr之前的几行代码重复FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR次。已知FIRST_SYSTEM_VECTOR为0xefFIRST_EXTERNAL_VECTOR为0x20于是重复次数为 0xef - 0x20 207即 207 次。在每次重复中把~vector0x80压入栈中——注意这里使用负数表示中断向量号因为正数0x80以上留作标识系统调用之用vector变量加 1跳转到common_interrupt标签。在common_interrupt中先调整栈中的向量号再执行interrupt指令并传入do_IRQcommon_interrupt: addq $-0x80, (%rsp) interrupt do_IRQinterrupt宏定义在同一个汇编源文件中它把通用寄存器pt_regs的值保存在栈中如果需要通过SWAPGS指令在内核态与用户态之间切换gs寄存器递增 per-cpu 的irq_count变量表明当前处于中断状态per-cpu 机制参见《CPU 相关概念》调用do_IRQ函数。do_IRQ进入 C 世界处理设备中断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_enter()进入中断上下文更新__preempt_count变量exit_idle()检查当前进程是否是 pid 为0的idle进程若是则向idle_notifier发送IDLE_END事件。接着从当前 cpu 的vector_irq数组中读取该向量对应的 irq 号并调用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); }一个自然的问题是irq_desc-handle_irq是什么为什么在知道irqaction指向真正处理函数的情况下偏偏通过中断描述符间接调用实际上irq_desc-handle_irq是一个上层 API用于调用真正的中断处理程序。它在设备树和 APIC 的初始化过程中就被设定好了内核通过它选择正确的分发函数以及irq-actions(s)的调用链。这样设计的好处是中断处理的分发策略是简单的边缘触发分发还是需要处理级联、共享等复杂场景由描述符层统一管理而具体的irqaction链表则在分发时被遍历执行。于是当一个中断发生时serial21285_tx_chars或serial21285_rx_chars函数就会被调用。在do_IRQ函数末尾调用irq_exit退出中断上下文恢复先前的用户空间寄存器最后返回irq_exit(); set_irq_regs(old_regs); return 1;正如上一节所讲当一个 IRQ 工作结束之后如果有延后中断softirq、tasklet、工作队列它们会被执行——irq_exit正是在这个时机检查__softirq_pending并唤醒ksoftirqd实现上半部快速完成、下半部延后处理的经典分工。退出中断ret_from_intr 与 iretq中断处理程序执行完毕后必须从中断中返回。do_IRQ工作结束后控制流回到arch/x86/entry/entry_64.S汇编代码的ret_from_intr标签处。首先通过DISABLE_INTERRUPTS宏禁止中断被展开为cli指令并将 per-cpu 的irq_count变量值减 1——记住处于中断上下文时这个变量的值是1DISABLE_INTERRUPTS(CLBR_NONE) TRACE_IRQS_OFF decl PER_CPU_VAR(irq_count)最后一步检查之前的上下文用户空间还是内核空间正确恢复它然后通过指令退出中断INTERRUPT_RETURN此处的INTERRUPT_RETURN宏是#define INTERRUPT_RETURN jmp native_iret而ENTRY(native_iret) .global native_irq_return_iret native_irq_return_iret: iretqiretq从栈中弹出返回地址与标志寄存器把 CPU 状态恢复为中断发生前的样子被中断的进程得以继续执行——中断的完整生命周期至此结束。总结中断与中断处理章节收官本节是《中断和中断处理》章节的最后一节。以 StrongARM SA-100/21285 串行驱动为线索我们完整观察了硬件中断从注册到响应的全过程驱动侧module_init→serial21285_init→ 注册 uart 驱动与端口 →serial21285_startup用request_irq请求IRQ_CONRX/IRQ_CONTX两条中断线内核侧注册request_irq→request_threaded_irq→ 校验参数、irq_to_desc转换描述符、分配初始化irqaction→__setup_irq挂接动作必要时创建 irq 线程触发侧中断控制器上报 →irq_entries_start汇编入口压栈 →common_interrupt→do_IRQ→handle_irq→generic_handle_irq_desc→desc-handle_irq分发到irqaction链serial21285_rx_chars/serial21285_tx_chars得以执行返回侧irq_exit处理延后中断 →ret_from_intr→cli关中断、递减irq_count→INTERRUPT_RETURNiretq恢复现场。回望整个章节开篇阐述了中断理论什么是中断、中断类型、向量号与 IDT随后深入异常及其处理、不可屏蔽中断、外部硬件中断的早期与非早期初始化再讲解延后中断softirq、tasklet、工作队列最后在本节落实到真实硬件驱动的完整处理链路。当然本节乃至整个章节都不可能覆盖 Linux 内核中断与中断处理的所有方面——中断线程化、irq 域irq domain、MSI 中断、中断负载均衡等大量主题仍值得继续深入研究。但正如本节所展示的顺着中断源码逐层深入是理解这个宏大子系统最有效的方式。后续可以结合仓库中初始化章节、内存管理章节、CPU 相关概念以及延后中断详解继续拓展阅读把中断子系统与内核其他模块的协作关系串成完整的知识网络。赞分享【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/hust-open-atom-club/linux-insides-zh点击查看免费下载相关推荐Linux 内核 IRQ 基础概念解读从设备中断请求到 IRQ 号的完整映射Linux 内核 IRQ 基础概念解读从设备中断请求到 IRQ 号的完整映射 导读 本文基于 Linux 内核文档 Documentation/core ap操作系统内核驱动驱动开发虚拟化嵌入式网络存储Linux 内核 IRQ 中断子系统核心指南从 IRQ 概念、irq_domain 映射到 SMP 亲和性与托管中断Linux 内核 IRQ 中断子系统核心指南从 IRQ 概念、irq_domain 映射到 SMP 亲和性与托管中断 本文基于当前 Linux 内核源码树的操作系统内核驱动驱动开发虚拟化嵌入式网络存储Linux 内核 irq_domain 中断号映射库完全指南从 hwirq 到 Linux IRQ 的架构与实战Linux 内核 irq_domain 中断号映射库完全指南从 hwirq 到 Linux IRQ 的架构与实战 导读 irq_domain 是 Linux操作系统内核驱动驱动开发虚拟化嵌入式网络存储上一篇【亲测免费】 探索RapidOCR一款高效、易用的OCR技术解决方案下一篇探索Tomcat Redis Session Manager高效、可靠的会话管理解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考