当前位置:首页>Linux>搞懂ARM64异常处理,才算真正入门Linux中断管理

搞懂ARM64异常处理,才算真正入门Linux中断管理

  • 2026-06-23 08:53:08
搞懂ARM64异常处理,才算真正入门Linux中断管理

大家好,我是蟹老板~

我组来了个实习僧,一个挺聪明的小伙子,背中断 API 背得飞起。request_irq()、free_irq()、上半部下半部,张嘴就来。

但是他今天竟然对着中断处理那一块代码一脸懵逼,问我:“哥,这个el1_irq是什么?为什么跳转过去之后还要保存上下文?这个eret又是干嘛的?”

那一刻,我看着他笑了,不是笑他菜,是真的觉得这个场景太熟悉了。

Linux中断管理这东西,玄之又玄,仿佛有一层窗户纸,没捅破觉得是天书,捅破了也就那样。而这层窗户纸的核心,其实就是ARM64的异常处理机制。

说句掏心窝子的话,如果你搞不懂ARM64的异常处理,你永远只能停留在“会用”Linux中断API的层面,一旦遇到内核崩了(Oops)、中断不响应、或者是上下文保存恢复出错这种底层问题,你就只能干瞪眼,然后把锅甩给“硬件不稳定”或者“内核版本有bug”。但其实,锅往往在你自己没搞懂底层机制。

所以,今天咱们就来把这层窗户纸彻底捅破,把ARM64异常处理和Linux中断管理的关系理清楚。

下面我尽量用代码和流程说话,但也会夹带一些我踩过的坑。有些观点可能不够严谨——我毕竟不是搞芯片出身的,若有错误,欢迎指正ღ( ´・ᴗ・` )。

流程图:

硬件外设触发中断      │      ▼GIC 置位 IAR,向 CPU 发出 IRQ      │      ▼CPU 查询 VBAR_EL1,跳转到 el1_irq / el0_irq      │      ▼kernel_entry(保存上下文,切换栈)      │      ▼handle_arch_irq → gic_handle_irq()      │            ├── 读 GICC_IAR → hwirq      │            ├── 写 GICC_EOIR(deactivate)      │            └── handle_domain_irq(domain, hwirq)      │                      │      │                      ├── hwirq → virq(irq_find_mapping)      │                      ├── virq → irq_desc(irq_to_desc)      │                      └── generic_handle_irq_desc      │                                │      │                                └── irq_desc->handle_irq()      │                                          │      │                                          ├── 【上半部】驱动中断回调      │                                          │      ├── 清中断标志      │                                          │      └── 挂起下半部      │                                          │      │                                          └── irq_exit()      │                                                │      │                                                └── 检查 softirq 标志位      │                                                      │      ▼                                                      ▼kernel_exit(恢复上下文,eret)                        【下半部】softirq/tasklet/work      │      ▼返回被中断的代码继续执行

一、ARM64异常处理的核心机制

1.1 ARM64异常不是“中断”

程序正常运行的时候,CPU 很像一个流水线工人:取指、译码、执行、继续下一条,整个过程非常机械。但现实世界不讲武德,网卡来了数据、键盘按下去了、UART 收到字节、定时器时间到了,这时候 CPU 不可能还傻乎乎继续执行用户代码,它必须停下来去处理更紧急的事情。于是,“异常”就出现了。

很多人以为“异常”就是程序崩了。在 ARM 世界里,异常其实是 CPU 的一种“强制切换状态”。像 IRQ、FIQ、SWI、Data Abort、Undefined Instruction,本质上都是:“别干你现在的活了,先处理这个。”这个动作就是异常。你会发现,ARM 的整个异常机制,本质上像一个极其暴躁的领导。你代码跑得正开心,它啪一下拍桌子:“别跑了!先去处理这个!”然后 CPU 就被强行切过去了。

刚开始学 ARM64 的时候,千万别被老文档带沟里。你可能听过什么“banked register”(分组寄存器),说什么 IRQ 模式有自己专属的 R13,FIQ 有自己专属的 R8-R12。打住!那是 ARMv6 (32位) 的老黄历了。

在 ARM64 (AArch64) 里,CPU 的设计哲学变了。它取消了大部分的分组通用寄存器。这意味着什么?意味着当 IRQ(普通中断)来的时候,CPU 不会像以前那样自动给你换一套寄存器用。

那现场怎么保护?全靠“硬存”!

也就是你在汇编代码里看到的那些 stp x0, x1, [sp]。Linux 内核必须像苦力一样,把当前用到的寄存器一个一个手动压入栈中。这就是为什么 ARM64 的中断入口汇编代码特别长,因为它是在给通用寄存器“擦屁股”。

FIQ(快速中断),在早期的 ARMv6(32 位)架构中,FIQ 拥有专属的硬件特权——独占 R8~R14 寄存器。当 FIQ 触发时,硬件直接切换至这套专用寄存器,省去了软件保存现场的耗时操作,专为工业控制、DSP 数据处理等极致低延迟的实时场景而生。这种“送你一套寄存器”的激进设计,在当时堪称颠覆性创新。

如今 ARM64(AArch64)架构中,这一“硬件魔法”已让位于软件灵活性:X0-X30 是物理上唯一的通用寄存器,但不同异常级别无自动隔离机制:当异常发生时,CPU 不会切换寄存器组,X0-X30 的值保持不变,需软件主动保存现场以防被覆盖,FIQ 触发时,内核必须像处理 IRQ 一样,通过软件手动保存现场(如压栈 X0-X30)。ARM64 中,FIQ 的“快速”本质仅体现为优先级——在 GIC(通用中断控制器)中,高优先级中断可通过 FIQn 信号线直接送达 CPU(无视中断屏蔽状态),而低优先级中断走 IRQn 信号线(受 PSTATE.I/F 位控制)。但进入 CPU 后,两者均由同一套异常处理流程接管,**仅优先级配置不同,无寄存器级硬件特权。

今非往昔啊~ 现 FIQ 已非主流,仅用于固件交互(如 TrustZone 的安全监控调用 SMC)或极少数需要微秒级延迟的极端场景,**现在的主角是 IRQ。**这种转变背后,是 ARM 在实时性与架构统一性间的深刻权衡:ARM64 用软件可控的灵活性取代了硬件特化,通过统一的异常模型实现分层能力——应用程序运行在 EL0,内核在 EL1,Hypervisor 在 EL2,安全监控在 EL3。异常发生时,CPU 自动保存程序状态至 SPSR_ELx、返回地址至 ELR_ELx,并根据 VBAR_ELx 跳转到异常向量表入口。处理完毕时,一条 eret 指令即可从 ELR/SPSR 恢复现场并切换回原执行级别。

第一次理解这个设计的时候,我真的拍大腿,太牛了。这帮做 CPU 的人脑回路真的有点东西。

ARM64 最大支持四个异常级别,EL0 权限最低,EL3 权限最高。当异常发生时,CPU 的异常级别会迁移到更高或维持当前级别,但绝不会降低,也不会有任何异常去到 EL0。常见等级如下所示:

异常级别描述典型用途
EL0
非特权模式
普通用户应用程序
EL1
特权模式
操作系统内核(Linux)
EL2
虚拟化管理级
Hypervisor(KVM等)
EL3
安全监控级
Secure Monitor、TrustZone 安全世界

异常发生时,CPU 只做三件事(这三件事是硬件自动做的,非常重要):

  1. 1. 保存状态 (SPSR_ELx):把发生异常那一刻的“环境”(比如你是处于 EL0 还是 EL1,中断有没有被屏蔽)保存到 SPSR_ELx (Saved Program Status Register)。
  2. 2. 保存位置 (ELR_ELx):把"案发现场"的地址保存到 ELR_ELx对于中断(IRQ/FIQ),它指向被中断指令的下一条指令;对于系统调用或异常错误,它指向触发问题的具体指令
  3. 3. 跳转入口 (VBAR_ELx):根据异常向量表基址寄存器 VBAR_ELx 指向的地方,直接跳过去执行。

前面实习僧提到的 eret 指令?那是异常处理完后的“回家路”。执行 eret 时,CPU 会自动把 SPSR 里的状态恢复回去,并跳转回 ELR 保存的地址继续执行。简单说,eret 就是把上面的过程倒放。

1.2 异常向量表

不管你是做驱动开发还是内核调试,ARM64的异常向量表是你必须要啃下的。

ARMv8规范要求每个异常等级维护自己的向量表,基地址分别存于VBAR_EL1、VBAR_EL2、VBAR_EL3寄存器。每个向量表占2KB空间,包含16个条目(entry),每个条目128字节,刚好放下32条指令。

这16个条目怎么组织的呢?ARMv8把异常场景按两个维度切分:

维度1:异常来源和栈指针类型

  • • 当前异常等级,使用SP_EL0栈
  • • 当前异常等级,使用SP_ELx栈(x=当前等级)
  • • 低等级异常,使用AArch64模式
  • • 低等级异常,使用AArch32模式

维度2:异常类型

  • • 同步异常
  • • IRQ
  • • FIQ
  • • SError

4×4 = 16,正好填满整个向量表。

Linux内核在entry.S里定义了这张表,看看它是怎么写的:

/* * Exception vectors. */    .align  11ENTRY(vectors)    ventry  el1_sync_invalid       // Synchronous EL1t    ventry  el1_irq_invalid        // IRQ EL1t    ventry  el1_fiq_invalid        // FIQ EL1t    ventry  el1_error_invalid      // Error EL1t    ventry  el1_sync               // Synchronous EL1h    ventry  el1_irq                // IRQ EL1h    ventry  el1_fiq_invalid        // FIQ EL1h    ventry  el1_error_invalid      // Error EL1h    ventry  el0_sync               // Synchronous 64-bit EL0    ventry  el0_irq                // IRQ 64-bit EL0    ventry  el0_fiq_invalid        // FIQ 64-bit EL0    ventry  el0_error_invalid      // Error 64-bit EL0#ifdef CONFIG_COMPAT    ventry  el0_sync_compat        // Synchronous 32-bit EL0    ventry  el0_irq_compat         // IRQ 32-bit EL0    ventry  el0_fiq_invalid_compat // FIQ 32-bit EL0    ventry  el0_error_invalid_compat// Error 32-bit EL0#endifEND(vectors)

来看看几个你刷代码一定刷到过的symbol:

  • • el0_sync:用户空间64位应用搞出的同步错误——比如空指针、缺页
  • • el0_irq:用户空间被硬件中断打断——网卡来包了、定时器到点了
  • • el1_sync:内核自己同步错误——内核oops大多走这条路
  • • el1_irq:内核态被中断打断——中断嵌套的核心场景

注意那个带“_invalid”结尾的:el1_irq_invalid、el0_fiq_invalid这些。意思是“理论上不应该进这个入口,如果进了说明你代码有bug”。Linux的处理方式是直接调用bad_mode()——内核panic给你看,告诉你出大事了。你如果调试的时候看到bad_mode调用栈,那十有八九是DAIF掩码配置出了问题。

有些资料说异常向量表是4组每组4条,也有说是4条4组——其实说法不同是一个事。ARMv8的设计是:根据两个维度组合确定目标向量位置,这个过程完全由硬件完成。你只需要把VBAR_EL1设对,剩下的事CPU替你搞定。

但话说回来,背下这张表的结构并不难,难的是理解每个入口后面关联的巨量代码——以及在什么场景下会走到哪个入口,这才是真正的难点。

二、从EL0到EL1:硬件自动保存与汇编入口

2.1 EL0到EL1的跳转:CPU在忙什么

现在咱们用一个具体例子把这流程走通——比如说你现在正在刷视频号,突然网卡收到一个数据包。这时候ARM64处理器到底做了什么?

CPU还在EL0跑着用户态代码,网卡产生了中断信号送给GIC,GIC路由后向CPU发出IRQ请求。

处理器在每条指令执行完后都会检查这个信号——发现IRQ来了,硬件开始自动“搬东西”:

第一步:把当前处理器状态PSTATE保存到SPSR_EL1寄存器

这个PSTATE里存着啥?当前是AArch64还是AArch32,DAIF掩码的状态,条件标志位……整个程序的执行快照。

第二步:把返回地址保存到ELR_EL1寄存器

注意,IRQ是异步异常,返回地址指向的是“第一条没来得及执行的指令”而不是产生异常的指令。如果是同步异常,返回地址通常等于产生异常那条指令本身——因为这条指令可能要重试(比如缺页异常处理完页表后重新执行访问指令)。

第三步:把异常原因写入ESR_EL1寄存器

ESR_EL1会告诉你这是什么类型的异常。它的EC(Exception Class,bits 31:26)字段是关键——你要是在调试中断跟着代码走,很多分支判断就是读EC字段来确定下一步怎么跳的。

第四步:如果是同步异常且跟地址有关,错误地址写入FAR_EL1寄存器

比如你访问了一个空指针,CPU会把那个坏地址记录在FAR_EL1里。内核oops打印的信息里,faulting address就是从这里读的。

第五步:CPU把DAIF掩码全部置1——关掉所有可屏蔽异常

这步非常重要。为什么?因为这时候你还在中断处理的中间态,内核栈还没完全准备好,如果再来一个中断,上下文就全乱了。等处理程序的入口代码跑完之后,内核会视情况重新打开中断。

第六步:硬件根据VBAR_EL1+异常类型+来源等级计算出向量地址,跳转到目标指令

这一步不需要软件参与——全是硬件搞定的。如果异常来自EL0、AArch64模式、类型为IRQ,那么硬件就会跳转到向量表中的el0_irq入口。

说实话,干开发10年,说实话真正需要理解这个详细流程的情况也遇到过不少,有一次一个血的教训:如果你在中断上半部调用了一个会schedule的函数,CPU一旦切出去,LR和SPSR就可能被覆盖——然后eret回去要么跑飞,要么随机Crash。后来我养成了一个条件反射——看到ISR里调用的任何函数,都下意识地用grep查一遍会不会引起调度。

2.2 entry.S的汇编宏:保存现场的隐秘角落

当你跟着代码跑进el0_irq入口之后,首先执行的是一个叫做kernel_entry的宏。这个宏在entry.S里占据了几十行,干的事却很简单——把所有通用寄存器全部压到当前进程的内核栈上。

为什么必须保存所有寄存器?

因为中断处理程序是用C写的,C编译器假定所有通用寄存器都可以自由使用。如果你不先把中断打断时的那套寄存器保存下来,中断处理跑完之后,原来的程序发现自己x0-x30全变了,那结果可想而知。

简化的伪代码逻辑大概是这样的:

.macro  kernel_entry, el    sub     sp, sp, #S_FRAME_SIZE       /* 栈往下走,留出空间 */    stp     x0, x1, [sp, #16 * 0]       /* 保存 x0-x29 */    stp     x2, x3, [sp, #16 * 1]    ...    stp     x28, x29, [sp, #16 * 14]    mrs     x21, sp_el0                  /* 读用户态栈指针 */    mrs     x22, elr_el1                 /* 读异常返回地址 */    stp     lr, x21, [sp, #S_LR]        /* 保存这两个关键的 */    mrs     x23, spsr_el1                /* 读处理器状态 */    str     x23, [sp, #S_PSTATE]         /* 保存状态 */.endm

然后就是从ESR_EL1寄存器里读出异常原因,根据异常类别分发到不同的handler。以同步异常为例,内核会进入el0_sync的处理逻辑:

el0_sync:    kernel_entry 0    mrs     x25, esr_el1                /* 读 ESR (Exception Syndrome Register) */    lsr     x24, x25, #ESR_ELx_EC_SHIFT /* 提取 EC (Exception Class) 字段 */    cmp     x24, #ESR_ELx_EC_SVC64      /* 跟 SVC 系统调用的异常码比较 */    b.eq    el0_svc                     /* 系统调用就走 el0_svc */    cmp     x24, #ESR_ELx_EC_DABT_LOW   /* 跟数据中止异常码比较 */    b.eq    el0_da                      /* 数据中止就走 el0_da */    cmp     x24, #ESR_ELx_EC_IABT_LOW   /* 指令取指中止异常码 */    b.eq    el0_ia                      /* 指令中止走 el0_ia */    ...    b       el0_inv                     /* 其他——无效异常,报错 */

看到没,汇编里就是这么干的:读ESR寄存器,提取EC字段,跟各种已知异常码比较,匹配上就跳对应的处理函数。el0_svc就是系统调用处理入口,el0_da就是缺页处理入口。

而不带“el0_”前缀的那组——像el1_sync、el1_irq——是内核本身跑着跑着出了异常。内核态缺页走的是el1_sync,中断嵌套走的是el1_irq。

估计很多人学到这的时候开始范迷糊——这么多入口,不同的场景,到底哪个场景走哪条路?

其实你只需要记住一个原则:看异常发生的那一刻CPU在哪个EL等级,而不是看异常本身是谁产生的

换句话说,同样是一个中断IRQ,如果CPU当时在跑应用(EL0),那就走el0_irq;如果CPU当时在内核态(EL1)执行,那就走el1_irq。区别在哪里?栈不同。EL0到EL1需要把用户态栈指针保存到SP_EL0,然后切到使用SP_EL1栈;EL1到EL1只需要在SP_EL1上继续压栈就行。细节不一样,但异常向量表的设计依据是一样的。

这里穿插一个我个人面试别人的经验。我经常会问一个问题:请简述el0_irq和el1_irq的主要区别。大概有一半的人回答“一个在用户态产生中断,一个在内核态产生中断”——这个回答对,但不够深。我更想听到的是stack切换机制和DAIF掩码管理的差异。还有不少面试者分不清异常向量表里的“SP_EL0 vs SP_ELx”分支分别适用什么场景,这在我眼里说明根本没看过entry.S源码——光看博客是不够的。

如果你能说出kernel_entry宏在处理EL0异常时多了一步“读sp_el0保存到栈上”,那就说明你真看了代码。这样的候选者,我直接给加分。

三、进入Linux中断管理框架

3.1 linux的arch代码怎么调中断处理的

kernel_entry跑完之后,汇编代码通过处理程序把控制权交给C函数,这里才是我们熟悉的Linux内核逻辑登场。

以IRQ为例,el0_irq最终会调用handle_arch_irq这个函数指针。这个指针是由中断控制器驱动——也就是GIC驱动——初始化的。绕了半天,终于从硬件走进了Linux内核的中断管理框架。

GIC(通用中断控制器)接管了中断路由与分发的硬件任务。GICv3的逻辑组件包括Distributor、Redistributor、CPU Interface和可选的ITS模块。一个设备产生的中断先被Distributor接管,给它标上SPI/PPI/SGI/LPI类型,设置优先级和目标CPU核心了;Redistributor负责PPI和SGI管理,并且把Pending的中断交给CPU Interface;CPU Interface把中断信号提供给连接的CPU核心。

Linux内核的GIC驱动会调用set_handle_irq来注册架构相关的中断处理入口,驱动把一个叫gic_handle_irq的函数地址赋给handle_arch_irq。从el0_irq的汇编处理中跳转到gic_handle_irq的调用链大致是这样的:

/* kernel/irq/handle.c */int __init set_handle_irq(void (*handle_irq)(struct pt_regs *)){    handle_arch_irq = handle_irq;    ...}/* arch/arm64/kernel/irq.c */void do_IRQ(struct pt_regs *regs){    if (handle_arch_irq)        handle_arch_irq(regs);}

然后gic_handle_irq的核心逻辑是这样:

/* drivers/irqchip/irq-gic-v3.c */static void gic_handle_irq(struct pt_regs *regs){    u32 irqnr;    do {        irqnr = gic_read_iar();          /* 读IAR寄存器,获取硬件中断号 */        if (irqnr == 0x3FF)              /* 特殊值,表示该CPU没有待处理中断 */            break;        gic_write_eoir(irqnr);           /* 优先写EOI(GICv3规范) */        if (likely(irqnr > 15 && irqnr < 1020)) {            handle_domain_irq(gic_data.domain, irqnr, regs);  /* 核心 */        }    } while (1);}

IAR(Interrupt Acknowledge Register)读出的是硬件中断号(hwirq)。GIC domain负责把hwirq映射到Linux系统的虚拟中断号(virq),这个映射关系记录在irq_domain结构里。

这里必须要讲一下为什么GIC驱动要在handle_domain_irq之前写EOIR寄存器。我调过一个中断风暴的bug——系统在中等负载下频繁触发“nobody cared”错误,设备直接失活。我们当时用的是GICv2,驱动是在处理完中断链之后再写EOI。问题在于:在处理中断链的期间,CPU的DAIF已经打开(softirq需要响应新中断),如果同一中断源因为电平还没清除再次触发,GIC会把这个新中断缓存在硬件里面等到下一个窗口期再输入给CPU——但你处理链已经结束了,这个新中断没赶上趟。

后来升级到GICv3,规范允许提前写EOI(也叫EOI优先模式),驱动程序写EOI但不做deactivate,新中断可以立即被CPU再次接收,中断丢失的概率大大降低。所以你现在看GICv3的驱动,都是读IAR→写EOIR→handle_domain_irq这个顺序。搞错顺序,生产环境的中断丢失问题极难复现。

handle_domain_irq会调用到通用中断子系统,也就是kernel/irq下面的那堆代码。

3.2 中断描述符irq_desc与中断处理函数注册

说到这,咱们终于来到了Linux中断管理的核心数据结构:irq_desc。

每个Linux中断号(virq)都对应一个irq_desc结构。你在驱动里调用的request_irq,本质上就是往对应irq_desc的action链表上挂一个irqaction节点。这个设计的核心是中断共享——同一个硬件中断线可以被多个设备驱动挂载。

具体流程大概是:

struct irq_desc {    struct irq_data        irq_data;      /* 硬件相关数据,包含irq_chip、irq_domain等 */    irq_flow_handler_t     handle_irq;    /* 中断流控处理函数 */    struct irqaction       *action;       /* 从中断响应链表,每个挂一个irqaction */    unsigned int           depth;         /* disable深度计数(0=使能,正数=嵌套disable次数) */    raw_spinlock_t         lock;          /* 自旋锁,保护该描述符 */    ...};struct irqaction {    irq_handler_t   handler;              /* 你写在驱动里的中断处理函数 */    unsigned long   flags;                /* IRQF_SHARED、IRQF_ONESHOT等 */    const char      *name;                /* 设备名字 */    void            *dev_id;              /* 私有数据指针 */    struct irqaction *next;              /* 下一个irqaction节点 */};

当你调用request_irq(irq, my_handler, IRQF_SHARED, "my_device", dev),内核首先检查irq是否已经被占用,然后分配一个irqaction结构体,把handler填进去,挂到对应irq_desc的action链表的末尾。

IRQF_SHARED标志意味着这条中断线允许多个设备共享。如果这条中断线被共享,每个设备的handler都必须在处理前先检查是不是自己的设备产生的中断。检查的方法是读取设备状态寄存器——如果设备没标记中断位,handler应立即返回IRQ_NONE;中断系统会沿着action链表继续调用下一个handler。如果所有handler都返回IRQ_NONE,内核会记录一次“spurious interrupt(虚假中断)”,超过一定阈值后直接禁用这条中断线。

这部分的代码逻辑在kernel/irq/manage.c里:

irqreturn_t __irq_wake_thread(struct irq_desc *desc, struct irqaction *action) {    ...    do {        ret = action->handler(irq, action->dev_id);    } while (ret == IRQ_WAKE_THREAD);    ...}irqreturn_t handle_irq_event(struct irq_desc *desc) {    struct irqaction *action = desc->action;    irqreturn_t ret = IRQ_NONE;    ...    do {        ret = action->handler(desc->irq_data.irq, action->dev_id);        switch (ret) {        case IRQ_HANDLED:            /* 设备处理成功 */            break;        case IRQ_WAKE_THREAD:            /* 需要唤醒中断线程 */            break;        default:            /* IRQ_NONE: 可能是共享中断的其他设备 */            break;        }        action = action->next;    } while (action);    ...}

说到request_irq,我忍不住想吐槽一下我带过的那些实习生。十个里面有八个注册中断的时候,直接把return IRQ_HANDLED写在handler第一行——设备状态都不查。共享中断的情况下这么做简直是灾难:你的设备没事,别人的设备中断被吃掉了,然后整个系统通信中断。我被这种错误坑过不下三次。这种细节书上是不会教的,但这才是真正的工程经验。

3.3 中断流控处理:handle_level_irq vs handle_edge_irq

有了入口和注册机制,下一个问题是:中断流控。

这部分对应kernel/irq/chip.c里面的几个标准流控处理函数,最常用的是这几种:

  • • handle_simple_irq:最简单的流控,几乎什么都没做就是直接调处理链。用于多路复用的子中断
  • • handle_level_irq:电平触发中断的流控
  • • handle_edge_irq:边沿触发中断的流控
  • • handle_fasteoi_irq:现代中断控制器的主流选择,处理链路里的EOI在handler前还是后决定
  • • handle_percpu_irq:每个CPU核独立处理的中断(如PL390的私有定时器)

电平触发和边沿触发的区别,写过MCU的同学都应该很清楚。但在Linux内核层面,这两者带来的处理差异很大。

电平触发:中断线只要保持在有效电平,中断就会被一直请求。所以必须先mask中断,然后ack,处理完之后再unmask。如果不先mask,同样一个中断会反复触发,CPU直接被打爆。

边沿触发:中断在处理链路中需要防止同一个interrupt正被别的CPU处理(通过IRQ_INPROGRESS标志判断),处理期间新到的边沿要mark成pending,等当前处理完再触发一次。

GICv3常用的handle_fasteoi_irq则把处理成不同的流程:

void handle_fasteoi_irq(struct irq_desc *desc){    raw_spin_lock(&desc->lock);    desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);    if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {        desc->istate |= IRQS_PENDING;        goto out;    }    handle_irq_event(desc);         /* 遍历action链表 */out:    if (desc->istate & IRQS_PENDING)        /* 有新的中断边沿到达,再处理一次 */        cond_unmask_eoi_irq(desc, NULL);    raw_spin_unlock(&desc->lock);}

注意desc->lock这个自旋锁。中断处理的上半部必须快速进出,这锁不能持有太久——如果你在handler里调用耗时的操作(比如轮询读设备状态寄存器上千次),这把锁会堵死其他CPU核试图处理这个中断的请求,结果就是系统延迟飙升。正确处理是在下半部做耗时操作,上半部只干最少的事。

四、下半部机制:软中断、tasklet与工作队列

光有上半部还不够。Linux要求上半部(hardirq)尽可能短,耗时操作放到下半部。这个设计看似简单,但执行时的各种时序竞态足以让你烧掉几周调试时间。

软中断(softirq) 是最传统的下半部机制。中断上半部通过raise_softirq()标记一个软中断位,待当前hardirq处理完成,内核在irq_exit()中检查是否有待处理的softirq,如果有就调用do_softirq()处理。

这里有段代码是我当年花了两个晚上排查出来的一个问题,特有意思:

void irq_exit(void) {    ...    if (!in_interrupt() && local_softirq_pending())        invoke_softirq();    ...}

注意in_interrupt()检查。如果当前正在处理另一个中断(hardirq嵌套),就不会启动softirq处理——softirq必须在最顶层中断退出时统一处理。这个设计的目的是避免softirq在嵌套中断上下文中执行,因为softirq内部可能触发调度,而嵌套中断上下文不允许调度。

tasklet是软中断的一种封装。tasklet的代码在kernel/softirq.c里,对应的数据结构如下:

struct tasklet_struct {    struct tasklet_struct *next;     /* 链表下一个 */    unsigned long state;             /* TASKLET_STATE_SCHED / TASKLET_STATE_RUN */    atomic_t count;                  /* 0=使能,非0=禁用 */    void (*func)(unsigned long);     /* 用户回调函数 */    unsigned long data;              /* 回调函数的参数 */};

tasklet的特点是同一个tasklet不会在多个CPU上同时执行。这比softirq安全,比workqueue性能好。但tasklet也有问题——新内核正在逐步取消tasklet机制,因为它容易导致延迟波动。

再说工作队列(workqueue)。它运行在进程上下文,可以睡眠。所以如果下半部需要访问磁盘、申请锁可能导致睡眠,就必须用workqueue。

结构:

static irqreturn_t nvic_uart_handler(int irq, void *dev_id) {    struct uart_dev *dev = dev_id;    /* 上半部:快速读取,快速清除 */    dev->data = readl(dev->base + UART_DATA_REG);    writel(0, dev->base + UART_INT_CLR);     /* 清除中断标志 */    /* 下半部:把数据提交给tty层 */    schedule_work(&dev->rx_work);    return IRQ_HANDLED;}static void nvic_uart_work_handler(struct work_struct *work) {    struct uart_dev *dev = container_of(work, struct uart_dev, rx_work);    /* 这里可以做耗时操作 */    tty_insert_flip_char(&dev->port, dev->data, TTY_NORMAL);    tty_flip_buffer_push(&dev->port);}

去年帮朋友调一个ARM服务器上的NVMe中断抢占问题。NVMe设备的中断频率很高,跑FIO测试的时候CPU hardirq负载直接拉到了100%。原因是上半部做了太多事——每个中断都去扫描完成队列,队列不空不出来。

改法很简单,上半部只做ack中断和唤醒处理线程,实际队列扫瞄工作交给中断线程(request_threaded_irq)。改完CPU负载从100%降到20%左右,I/O延迟也稳定了不少。

我当时的反应就是:“真的绝了,就这么改一下,效果天差地别。”这感觉很像在STM32上调DAM中断——你以为是配置不对,结果发现是上半部干太多活把下一个中断堵死了。

说到这,顺便说说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);

如果你把handler设为NULL,而thread_fn非NULL,内核会采用默认的handler搭配你的thread_fn实现全线程化中断处理。这种方式在嵌入式实时系统里用得很多——中断上半部几乎什么都不做,所有处理都交给线程函数。

但全线程化中断也有代价的——响应延迟增加。对实时性要求高的场景(比如电机控制中断),还是需要在handler里直接完成关键操作。

五、中断性能调优三板斧

玩了十年Linux,中断性能调优跑不脱这三件事。

第一板斧:中断亲和性(SMP affinity)

多核系统里,通过/proc/irq//smp_affinity把中断绑定到指定的CPU核上。这能避免中断在所有核之间跳来跳去导致的cache失效。比如网卡中断一般绑到CPU0,NVMe中断绑到CPU1-3,定时器中断在每个核上独立触发(PPI类型,不需要绑)。

操作很简单:

echo 2 > /proc/irq/37/smp_affinity      # 绑定到 CPU1echo f > /proc/irq/37/smp_affinity      # 绑定到 CPU0-3

但有个坑,就是绑核后如果那个CPU跑满100%,中断得不到及时响应,延迟反而比不绑更差。所以绑核的同时要配合进程亲和性,把对应的用户态处理进程也绑到同一个核上,数据在同一个cache hierarchy里流转最快。

第二板斧:中断合并

网卡中断可以用NAPI(New API)机制来合并。传统网卡每收一个包触发一次中断,中断风暴很厉害;NAPI的做法是收到第一个包触发中断后关闭中断,用轮询方式收包,收完再打开中断。包速率越高,NAPI省下的中断开销越明显。

第三板斧:处理线程化

正如前面NVMe的案例,request_threaded_irq是性能调优的利器。但要注意线程优先级设置——默认的中断线程优先级是MAX_USER_RT_PRIO/2(即50),如果你在高优先级进程还没运行完的时候中断线程被调度,照样影响延迟。在实时系统里,合理做法是给中断线程设RT优先级。

六、实战

6.1 按键中断驱动开发

基于野火 LubanCat 板

步骤一:设备树配置

在设备树中声明按键节点,指定中断父节点和触发方式。注意,我们编写的按键节点是“中断使用者”而非“中断控制器”,所以无需设置 interrupt-controller 标签。

#include <dt-bindings/interrupt-controller/arm-gic.h>button_interrupt {    compatible = "button_interrupt";    interrupt-parent = <&pio>;                               /* 父中断控制器是pio */    interrupts = <PH 9 IRQ_TYPE_LEVEL_LOW>;                  /* 引脚 + 低电平触发 */    button-gpios = <&pio PH 9 GPIO_ACTIVE_HIGH>;    status = "okay";};

步骤二:驱动程序中使用 request_irq 注册

#include <linux/interrupt.h>#include <linux/gpio.h>static irqreturn_t button_irq_handler(int irq, void *dev_id){    /* 上半部:只做最紧急的事 */    /* 注意:此处不能调用 msleep / mutex_lock 等可能引起调度的函数 */    printk(KERN_INFO "Button pressed! IRQ: %d\n", irq);    return IRQ_HANDLED;}static int button_probe(struct platform_device *pdev){    int irq, ret;    irq = platform_get_irq(pdev, 0);       /* 从DTS中获取中断号 */    if (irq < 0)        return irq;    ret = request_irq(irq, button_irq_handler,                      IRQF_TRIGGER_LOW,     /* 低电平触发 */                      "button_irq",         /* 名称,会出现在 /proc/interrupts 中 */                      NULL);                /* dev_id,共享中断时用于区分 */    if (ret)        dev_err(&pdev->dev, "Failed to request IRQ %d\n", irq);    return ret;}static int button_remove(struct platform_device *pdev){    int irq = platform_get_irq(pdev, 0);    free_irq(irq, NULL);                   /* 释放中断资源 */    return 0;}

步骤三:验证与调试

加载驱动后,通过以下命令观察中断是否正常触发:

# 查看中断计数(设备名 "button_irq" 对应 request_irq 注册的名称)cat /proc/interrupts | grep button# 实时追踪中断watch -n1 cat /proc/interrupts | grep button

步骤四:引入下半部优化

按键消抖或 I2C/SPI 读取等耗时操作不能在上半部执行。推荐使用 Threaded IRQ,只需将 request_irq 替换为 request_threaded_irq——上半部仍负责清中断标志,耗时逻辑安心放进线程回调中,配合 mutex/m sleep 安全可靠。

/* 上半部:只做最紧急的清中断操作 */static irqreturn_t button_hardirq(int irq, void *dev_id){    return IRQ_WAKE_THREAD;         /* 唤醒下半部线程 */}/* 下半部线程:可以包含消抖、SPI读取等耗时操作 */static irqreturn_t button_threadfn(int irq, void *dev_id){    /* 这里可以安全使用 msleep / mutex_lock 等 */    msleep(20);                     /* 消抖延时 */    return IRQ_HANDLED;}ret = request_threaded_irq(irq, button_hardirq, button_threadfn,                           IRQF_TRIGGER_LOW, "button_irq", NULL);

6.2 kprobe 探测系统调用路径

以下展示如何在 ARM64 上用 kprobe 探测 do_el0_svc(系统调用入口)的调用行为,帮助理解异常分发路径:

# 注册 kprobe 探测点,在 do_el0_svc 入口处打印寄存器echo 'p do_el0_svc %x0 %x1' > /sys/kernel/debug/tracing/kprobe_events# 启用 kprobe 事件echo 1 > /sys/kernel/debug/tracing/events/kprobes/enable# 在另一个终端触发系统调用ls /# 查看捕获的追踪数据cat /sys/kernel/debug/tracing/trace# 清理echo 0 > /sys/kernel/debug/tracing/events/kprobes/enableecho '-:p do_el0_svc' >> /sys/kernel/debug/tracing/kprobe_events

执行 ls 命令时,用户态通过 svc 触发 EL0 → EL1 的同步异常,经 VBAR_EL1 查表进入 el0_sync → el0_svc → do_el0_svc,kprobe 在 do_el0_svc 入口处拦截,打印寄存器快照。

注意事项:不要在任何带有 noinstr 标记的函数或通过 NOKPROBE_SYMBOL() 明确排除的函数上设置 kprobe 探测点,否则可能引发 kprobe 递归异常,导致系统崩溃。

七、实战应用

7.1 设备驱动中断注册

这是中断管理最常见的应用场景。驱动开发者通过设备树(Device Tree)描述中断资源,在驱动代码中调用 request_irq() 完成注册。ARM64 的设备树使用 interrupt-parent 指定父中断控制器节点,用 interrupts 属性声明中断引脚和触发方式。

7.2 网络数据包处理

网络子系统的 NAPI 机制是 softirq 下半部的典型应用。网卡收到数据包后,上半部关中断、将数据写入环形缓冲区、触发 NET_RX_SOFTIRQ 软中断。下半部的 net_rx_action() 以轮询(polling)方式批量收取数据包,显著减少了高频中断下的上下文切换开销——这正是 softirq 相比 tasklet 的优势所在:同一类型的 softirq 可在多核上同时执行,充分利用 SMP 并发能力以应对万兆网卡级别的吞吐压力。

7.3 内核调试与动态追踪

ARM64 上 kprobe 利用 BRK 指令的异常机制实现函数探测。当 BRK 指令执行时,硬件产生同步异常,CPU 经由异常向量表分发到 el1_dbg 处理函数,再由 kprobe 子系统接管。基于此,开发者可以注册 pre/post handler 完成函数级追踪、性能分析等工作。kretprobe 则在函数入口处通过 BRK 异常寄存器获取上下文信息来抓取返回值。

7.4 虚拟化中断注入

KVM 利用异常级别的层次设计,将物理中断经由 EL2 路由并注入到 Guest OS(EL1)。具体表现为 KVM 使用 HCR_EL2 寄存器控制哪些中断需要路由到 EL2 由 Hypervisor 截获处理,再通过虚拟中断注入机制通知 Guest。KVM 的 irqfd 接口允许用户空间通过 ioctl 向 vCPU 注入中断。

7.5 实时系统的高精度中断响应

嵌入式场景(工业控制、自动驾驶)中,中断响应延时直接影响系统安全。ARM64 的 FIQ 模式被部分实时操作系统(RTOS)用来处理超高优先级中断。I-pipe 中断流水线技术则对 ARM64 的物理中断 IRQ 相关代码进行改造,确保实时内核触发的中断能够快速处理。

八、写在最后

为什么说“懂异常”才算真正理解 Linux 中断?因为 Linux 中断根本不是凭空出现的,它下面全是 ARM 异常模型。你不懂 CPSR、SPSR、异常模式、vector、LR 修正、banked register,你看 Linux IRQ 永远像在看天书。

很多人学驱动喜欢背 API,背 request_irq、free_irq、spin_lock_irqsave,但这些只是表面。真正底层是 CPU 异常。我工作十年,越往后越发现,技术这东西,你逃不掉底层。以前总觉得“我会写业务就行”,后来线上事故一个接一个,你会发现真正救命的全是底层知识。

尤其 ARM,这玩意特别喜欢给你惊喜。有时候你 debug 一晚上,最后发现是 IRQ stack overflow,或者是 SPSR 被覆盖,再或者是 vector table 映射错了。你说气不气?

如果你现在还看不懂 Linux IRQ,应该怎么办?

别急着看驱动,先干三件事:

  1. 1. 把 ARM 异常模式搞懂,至少理解 CPSR、SPSR、LR、异常向量。
  2. 2. 跟一遍 vector_irq -> asm_do_IRQ -> generic_handle_irq 的代码流。
  3. 3. 自己写一个裸机 IRQ,哪怕只点亮 LED。只要你亲手配中断控制器、开 IRQ、写 vector、返回异常,你会瞬间理解很多东西。

写这篇文章的时候,我翻了很多以前的代码,看到一堆 fix irq bug、temporary workaround、TODO,突然有点感慨。我们天天在解决问题,可问题永远解决不完。ARM 异常也是,你以为搞懂 IRQ 就结束了,后面还有 SMP IPI、cache coherency、TLB shootdown、preempt、RCU,像一层层地狱。+

但偏偏,也正因为这样,技术才有意思。你会在某个凌晨,突然搞懂一个以前完全看不懂的机制,那种快乐,挺上头的。

我也不知道这么说对不对,但对很多写底层的人来说,这可能就是继续爱干这行的原因吧。至少,对我来说是。

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-07-03 12:35:31 HTTP/2.0 GET : https://f.mffb.com.cn/a/493773.html
  2. 运行时间 : 0.595219s [ 吞吐率:1.68req/s ] 内存消耗:4,519.45kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=f11c0b3716e63d48d95a02086dc1f5dd
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.001177s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001808s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.030093s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.080574s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001986s ]
  6. SELECT * FROM `set` [ RunTime:0.016625s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001848s ]
  8. SELECT * FROM `article` WHERE `id` = 493773 LIMIT 1 [ RunTime:0.131358s ]
  9. UPDATE `article` SET `lasttime` = 1783053332 WHERE `id` = 493773 [ RunTime:0.025148s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 67 LIMIT 1 [ RunTime:0.004412s ]
  11. SELECT * FROM `article` WHERE `id` < 493773 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.011274s ]
  12. SELECT * FROM `article` WHERE `id` > 493773 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.006341s ]
  13. SELECT * FROM `article` WHERE `id` < 493773 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.024848s ]
  14. SELECT * FROM `article` WHERE `id` < 493773 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.052864s ]
  15. SELECT * FROM `article` WHERE `id` < 493773 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.025241s ]
0.599105s