尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
ARM64中断处理全链路解析:从GIC硬件触发到Linux驱动回调
这段时间一直在调一块 ARM64 板卡上的外设中断丢事件问题被迫把 ARM64 中断处理这条路从头到尾捋了一遍。说实话网上讲 ARM64 中断的文章不少但大多停留在GIC 是中断控制器、内核里有中断子系统这种概念层面真正从硬件信号到 Linux 代码入口的完整链路分析不多。这篇文章想把一条外设中断从触发到最终执行处理函数的全过程拆开重点放在中断号映射、入口汇编、软中断时机这些容易绕晕的地方。适合做底层驱动、BSP bringup或者想系统理解 Linux 中断机制的朋友哪怕之前没读过内核源码按这篇文章的顺序也能把链路走通。1. 一条外设中断从硬件到内核的完整地图在进入代码之前先把整个路线图画清楚。一个外设中断被 ARM64 CPU 感知并最终执行到 Linux 驱动注册的回调函数中间要经过四层硬件中断控制器GIC负责收集外设的中断信号判断优先级并把中断路由到某个 CPU 核。CPU 的异常处理机制感知到 IRQ 信号跳转到 Linux 设置的异常向量表保存现场。中断控制器驱动从 GIC 读取中断号再通过 irq domain 把它转换成 Linux 内核通用的中断号。通用中断子系统根据中断号找到对应 irq_desc调用设备驱动注册的 handler最后在中断返回阶段处理软中断或唤醒中断线程。这四层里每一层都有自己的核心知识点也都有对应的坑。比如硬件层要搞懂 GIC 的 SPI 和 PPI 区别异常向量表要搞懂 EL1 和 EL0 的入口差异irq domain 要理解为什么设备树里写的中断号和你 request_irq 拿到的不一定是同一个数。很多人有个误解觉得 ARM64 的中断处理跟 x86 差不多无非是 APIC 换成 GIC。实际上差别挺大的x86 在 Linux 里大部分中断走 IO-APIC 加 MSIARM64 这边除了 GIC 分发还有异常级别EL0/EL1的切换问题中断现场保存方式也更重。这也是为什么 ARM64 的 IRQ 入口路径明显比 x86 长单次中断开销更大。还有一个常见的困惑是为什么 ARM64 上每个核都能看到同样的中断其实对于 SPI 来说GIC 会把同一个中断路由给一个或多个核而 PPI每核私有中断则是每个核一套独立信号。这个共享与私有的区别会直接影响设备树里中断号的写法也影响驱动里要不要用 percpu 标志。接下来的内容我会按一条外设中断从 GIC 出发经过异常向量表到 gic_handle_irq再进入 generic_handle_irq最后落到 softirq 或 irq thread 的顺序展开。每段都会给出代码里对应的位置和我在实际调试时踩过的问题。2. GIC v3 的结构与中断类型不是所有中断号都一样2.1 GIC 的组件划分分发器、重分发器与 CPU 接口GICGeneric Interrupt Controller是 ARM64 平台上最主流的中断控制器。v3 版本基本成了标准配置所以下面以 GICv3 为主。它内部有三个关键组件Distributor分发器管理 SPIShared Peripheral Interrupt的开关、优先级、路由。外设发出的中断线连接到分发器由分发器决定这个中断要发给哪个核心。Redistributor重分发器每个核心都有一个管理 PPIPrivate Peripheral Interrupt和 SGISoftware Generated Interrupt并缓存每个核的优先级过滤配置。CPU InterfaceCPU 接口每个核心内部的逻辑负责和 CPU 核直接打交道。v3 开始CPU 接口不再映射到一段固定内存地址而是用系统寄存器方式访问比如 ICC_IAR1_EL1、ICC_EOIR1_EL1。当我们说某个中断号时其实指的是硬件中断号 hwirq。GICv3 把 hwirq 划分成了几段类型中断号范围说明SGI0-15核间中断软件触发常用于 IPIPPI16-31每核私有比如每个核自己的 timerSPI32-1019全局共享外设中断LPI8192 及以上消息中断MSI需要通过 ITS 配置注意 PPI 和 SPI 在 Linux 驱动里的处理方式完全不同。PPI 的 enable、mask 操作走 Redistributor并且不参与传统意义上的全局中断号分配SPI 的 enable、mask 走 Distributor设备树里最常见的就是它。SGI 一般不由普通驱动直接注册而是内核内部 IPI 机制在用。2.2 中断状态机与 level/edge 触发的实际影响GIC 内部的每个中断都有状态inactive、pending、active、active and pending。外设中断信号到达后GIC 把它标记为 pendingCPU 接口通知 CPU 核产生 IRQ 异常内核在中断入口处读 ICC_IAR1_EL1读到中断号后该中断变成 active处理完成后写 ICC_EOIR1_EL1中断变为 inactive。这个状态机直接决定了 level 触发和 edge 触发在外设驱动里的写法差异。level 触发的中断在输入信号仍然保持有效电平时即使写了 EOIR也会立刻再次变成 pending导致中断风暴。所以 level 型中断的 handler 第一件事通常是操作外设寄存器清掉中断源再写 EOIR。edge 触发则相反只要 GIC 采样到一次跳变即使信号后来一直保持也不会重复触发必须等 pending 被清掉。我在调一块 GPIO 扩展芯片时遇到过一个问题外设只有一条电平中断线驱动里用了 handle_level_irq但 handler 里忘了先清外设的中断状态寄存器结果cat /proc/interrupts看到中断计数每秒钟飙升几十万CPU 直接 soft lockup。这个现象其实就是状态机在起作用不是驱动逻辑写错了而是没按 level 触发的规则处理。2.3 中断路由GIC 把中断送到哪个核v3 的 SPI 路由是通过 GICD_IROUTER 寄存器配置的可以指定某个中断发给哪个 PEProcessing Element也可以配置为任意 PE。v2 时代用的是 GICD_ITARGETSR 按位掩码v3 改成了每个中断独立的 64 位路由寄存器配置更灵活。在 Linux 里SMP 场景下每个中断的亲和性affinity可以动态调整。默认情况下irq 子系统会根据 irqbalance 或内核自身的分配策略把中断分到不同的核上。对实时性要求高的设备可以手动把处理函数钉在某个核上通过/proc/irq/中断号/smp_affinity写入十六进制 CPU 掩码例如echo 2 /proc/irq/29/smp_affinity表示只允许 CPU1 处理这个中断。不过硬件层路由和软件亲和性不是一回事。GICD_IROUTER 决定的是硬件把中断信号送到哪个核软件 affinity 只是请求内核把中断使能/屏蔽的配置往哪个核上做。多数时候 Linux 会在目标核上操作 GIC 的 enable 寄存器从而让硬件只把中断发给允许的核。3. 异常向量表与 el1_irq 入口CPU 是如何跳到 Linux 代码的3.1 ARM64 的异常向量表布局与 VBAR_EL1ARM64 的中断和 x86 的一个重大区别x86 用 IDTInterrupt Descriptor Table每个向量可以对应不同处理函数ARM64 用一张固定布局的 exception vector table基址存在 VBAR_EL1 寄存器里。Linux 启动时会把这张表放在内核镜像的一个专用段并把 VBAR_EL1 指过去。这张表按异常来源分成四组每组四个异常类型每个 entry 占用 0x80 字节偏移异常类型0x000EL1 同步异常0x080EL1 IRQ0x100EL1 FIQ0x180EL1 Error0x200EL0 同步异常0x280EL0 IRQ0x300EL0 FIQ0x380EL0 Error所谓 EL1 IRQ就是 CPU 在特权态内核态运行时收到中断EL0 IRQ 是用户态运行时收到中断。两者的现场保存要求不同所以 Linux 分别写了不同的入口。FIQ 在标准 Linux 内核里基本是 invalid 状态除非开启 FIQ 支持否则不会走正常中断流程。一条外设中断到达 CPU 后硬件自动完成三件事把当前 PSTATE 保存到 SPSR_EL1把返回地址保存到 ELR_EL1然后跳转到 VBAR_EL1 0x080EL1 IRQ或 0x280EL0 IRQ。这里有个细节值得注意虽然是从 EL0 陷入 EL1但 Linux 把内核态的 IRQ 入口和用户态的 IRQ 入口分开是因为用户态被中断时需要重新建立内核栈上的 pt_regs而内核态被中断时可以直接在当前栈上继续压。3.2 kernel_entry保存现场的实际动作拿到一个 IRQ 后CPU 自动进入 EL1接下来就是 Linux 手里的事了。arch/arm64/kernel/entry.S 里的 el1_irq 标号开始处理el1_irq: kernel_entry 1 ... mov x0, sp bl handle_arch_irq ... kernel_exit 1kernel_entry 宏做的是把 x0-x29、x30link register、sp、pc、spsr、orig_x0 等全部压到栈上形成一个 struct pt_regs。这个 pt_regs 就是后面传给中断处理函数的核心数据结构handle_arch_irq 的参数 x0 指向的正是它。ARM64 的中断入口还有一个特殊机制irq stack。问题背景是内核线程的任务栈通常只有 16KB如果中断处理里再压一层现场和调用栈任务栈很容易溢出。所以 Linux 给每个 CPU 准备了一块独立的 irq stack只有内核栈不足时才切换。切换逻辑在汇编里判断当前 sp 是否已经落在 irq stack 范围内如果不在就切过去并把原来的 sp 存到 pt_regs 里。x86 也有类似机制但 ARM64 的实现更像一个简单的栈顶指针替换。3.3 irqentry_enter 与 irqentry_exithardirq 上下文的边界进入 el1_irq 后内核调用 irqentry_enter把当前上下文标记为硬中断上下文。这一步在 preempt_count 里加了一个 hardirq offset之后内核就可以通过 in_interrupt() 判断自己是否处于中断上下文。硬中断处理期间当前任务不能被抢占也不能再触发软中断。irqentry_exit 做的事情相反把 preempt_count 减回去并检查是否设置了 TIF_NEED_RESCHED。如果中断发生在内核态且开启了内核抢占可能在返回路径上直接调用 preempt_schedule_irq 完成抢占调度如果是返回用户态则顺路处理信号和调度。这部分代码在不同内核版本里位置有差异有的在汇编里调有的在 handle_domain_irq 里调但语义不变。我建议读源码时不用纠结 irqentry_enter 具体在哪一行调用只要记住从 el1_irq 到 gic_handle_irq 之间的整段路径都处于 hardirq context任何可能导致睡眠的操作在这里都是非法的。驱动里如果在这个阶段调用 mutex、msleep、kmalloc(GFP_KERNEL)大概率会触发 scheduling while atomic 的报错。4. gic_handle_irq 与中断号的秘密hwirq 不等于 Linux 中断号4.1 handle_arch_irq 函数指针的由来在 el1_irq 汇编代码里核心就是bl handle_arch_irq。handle_arch_irq 是一个函数指针启动早期被设置为一个空实现GIC 驱动初始化时通过 set_handle_irq() 注册自己的处理函数。对 GICv3 来说这个函数就是 drivers/irqchip/irq-gic-v3.c 里的 gic_handle_irq。这个函数做的事情可以浓缩成几个步骤读 ICC_IAR1_EL1得到当前要处理的中断硬件号。如果读到的值是 1023说明是 spurious 中断伪中断退出循环。调用 irq_find_mapping()把 hwirq 转换成 Linux irq number。调用 generic_handle_irq()执行这个中断对应的处理流。流程看起来简单但有个关键点GICv3 里一次 IAR 只返回一个中断号所以 gic_handle_irq 里是个循环不断读取 IAR直到读到 1023 为止。这会带来一个性能隐患如果同一时刻有大量中断 pending这个循环会连续处理多个中断导致其他优先级较低的中断被拖后。所以 GIC 驱动会根据优先级配置决定是否在处理完一个中断后主动退出循环。4.2 irq domainhwirq 到 Linux irq number 的映射关系为什么不能直接用 GIC 的硬件中断号作为 Linux 驱动里 request_irq 的参数最典型的原因是 PPI。PPI 在每个核上都有相同的硬件号比如 16-31但它们在不同核上是不同的物理中断必须分配不同的 Linux irq number。另外Linux 内核为了支持多级中断控制器比如 GIC 下面再挂一个子中断控制器需要一套统一的编号机制让上层驱动只面对一个虚拟的 irq number而不关心下面经过了几级控制器。这套机制就是 irq domain。每个中断控制器在注册时都会创建自己的 irq domain并定义 map/unmap 回调。GICv3 使用线性映射的 domainirq_domain_add_linear把 hwirq 直接作为索引去查找对应的 irq_desc。设备树里的中断属性描述的是硬件中断号不是 Linux 中断号。比如interrupt-parent gic; interrupts 0 29 4;这段描述的是一个 SPI 中断0 表示 SPI 类型29 是 hwirq4 表示高电平触发。内核在解析设备树时会调用 GIC 的 irq domain 的 map 回调把 hwirq29 映射到一个全局分配的 Linux irq number并初始化对应的 irq_desc 和 irq_chip。调试时你可以通过/proc/interrupts验证这一点。它第一列是 Linux irq number中间一列会显示GICv3 29这种格式后者是 domain 名称加 hwirq。如果驱动写死了一个中断号并直接 request_irq很可能请求的是另一个设备的中断。4.3 irq_chip中断控制器的操作抽象irq_chip 是通用中断子系统和具体中断控制器之间的操作接口。对 GICv3 来说chip 里主要实现了 irq_mask、irq_unmask、irq_eoi、irq_set_type、irq_set_affinity 等回调。核心中断子系统并不知道 GIC 的寄存器细节它只需要调用这些回调函数。这里有一个容易误解的地方很多人以为每个中断都必须在 irq_chip 里实现 irq_ack 回调实际上 GIC 在 gic_handle_irq 的 IAR 读取阶段已经完成了 ack把中断从 pending 变成 active所以 GIC 的 chip 里 irq_ack 经常是空函数真正重要的是 irq_eoi它负责写 ICC_EOIR1_EL1。如果把 ack 和 eoi 的执行顺序搞反GIC 状态机就会错乱。设备树里触发类型为 level 还是 edge会决定 desc-handle_irq 被设置成 handle_level_irq 还是 handle_edge_irq。这两个处理函数在 GIC 上的语义差异很大handle_level_irqmask 中断调用驱动的 handler然后 unmask。如果驱动 handler 没有清掉外设的中断源EOI 后会立刻再触发。handle_edge_irqirq 处理时会把 pending 记录清掉如果驱动 handler 执行期间同一个中断再次到来会重新标记 pending不会丢失事件。所以调试中断丢失问题时如果设备是边沿触发优先怀疑驱动是否忽略了第二个 pending如果是电平触发优先怀疑中断源是否清除干净。5. irq_exit 与 softirq真正执行中断下半部的地方5.1 中断返回路径上 softirq 的触发时机当 gic_handle_irq 返回后中断处理进入 irq_exit 阶段。这个阶段的判断逻辑是如果当前已经不在任何中断上下文!in_interrupt()而且有 pending 的 softirq就调用 do_softirq()。这里 in_interrupt() 判断的实际上是 preempt_count。hardirq 处理期间 preempt_count 带有 HARDIRQ_OFFSET所以不会执行 softirq如果中断处理过程中又触发了另外一个中断形成中断嵌套preempt_count 会更深同样不会执行 softirq。只有最外层中断即将返回时才满足执行 softirq 的条件。这个设计的初衷很清楚软中断本来就是为了延迟处理一部分不需要立即完成的工作比如网卡收包、块设备 IO 完成。如果每次硬中断处理一结束就立刻跑 softirq中断延迟会变得不可控但如果不跑又需要等 ksoftirqd 内核线程调度延迟又太大。所以 Linux 选择了一个折中策略中断返回阶段检查一次能处理就处理处理不完再唤醒 ksoftirqd。5.2 do_softirq 的循环限制与 ksoftirqd 唤醒条件do_softirq 并不是把所有 pending 的 softirq 一口气处理完的。它按照 softirq 的优先级位图逐位执行执行完一轮之后如果又有新的 softirq pending会继续循环。但循环有上限MAX_SOFTIRQ_RESTART默认 2 次。超过这个次数还会继续有 pending内核就不再自己跑而是唤醒 ksoftirqd 线程去处理。这个限制在网络收包场景尤其重要。如果高速网卡持续产生大量数据包NET_RX_SOFTIRQ 会一直被触发如果 do_softirq 无限循环处理用户态进程几乎得不到 CPU 时间。唤醒 ksoftirqd 相当于把一部分收包处理挪到线程上下文给用户进程留出运行机会。你可以通过/proc/net/softnet_stat里的 time_squeeze 字段判断是不是经常发生 softirq 处理超时的情况。5.3 中断线程化与 threaded irq 的实际选择除了 softirqLinux 还有一条下半部路径中断线程。request_threaded_irq() 允许驱动同时注册一个快速 handler 和一个线程化 handler。快速 handler 在硬中断上下文执行如果返回 IRQ_WAKE_THREAD内核会唤醒对应的 irq 线程执行 thread_fn。这个线程带实时优先级可以睡眠可以持锁比硬中断上下文宽松得多。内核还可以通过threaded_irq机制把所有未标记 IRQF_NO_THREAD 的中断强制线程化。启动参数threadirqs或内核配置CONFIG_IRQ_FORCED_THREADING都能做到。强制线程化的好处是极大降低硬中断上下文中的不可控开销对桌面和嵌入式设备都友好坏处是牺牲一点中断响应速度不适合对延迟极端敏感的场景。我个人的倾向是驱动里能用 thread_fn 实现的尽量用 thread_fn不要在硬中断 handler 里做大量寄存器轮询或者大块内存拷贝。硬中断里代码越短系统整体的中断延迟越可控。6. 排查 ARM64 中断问题的实用工具与源码阅读路线6.1 从 /proc/interrupts 开始的逐层定位真遇到中断不工作的时候第一步永远是看/proc/interrupts。这个文件的每一行代表一个 Linux irq number列对应 CPU 核数值是中断触发次数。看几秒内数值是否增长就能判断硬件中断是否真正到达了内核。如果计数不涨问题很可能在硬件到 GIC 的链路或者 GIC 本身没有把中断路由给当前 CPU。这时候检查设备树里的 interrupt-cells 配置确认 SPI/PPI 类型、hwirq、触发方式是否匹配外设手册。最常见的问题是触发方式写反外设输出低电平有效设备树写的是 4高电平触发这时候中断在信号到达前一直是 pending内核处理逻辑会反复触发但计数可能异常。如果计数在涨但驱动 handler 没执行问题在 request_irq 阶段。看/sys/kernel/debug/irq/irqs/中断号文件里面记录了这个 irq 的 chip、handler、actions、状态位。一个典型的错误是同一个 hwirq 被两个驱动以不同触发方式重复注册导致 request_irq 返回 -EINVALdmesg 会提示 IRQ handler type mismatch。6.2 irq domain debugfs 与 tracepoint 的用法当怀疑中断号映射不对时/sys/kernel/debug/irq/domains是个很直接的调试入口。里面列出每个 irq domain 的名称、映射方式、范围以及已映射的 hwirq 和对应的 Linux irq number。GIC 的 domain 一般显示为 GICv3线性映射范围从 0 到 1019。你可以从设备树里取出 hwirq再在这个文件里查到对应的 Linux irq number对比/proc/interrupts里的行号。更细的观察可以用 perf 或 trace-cmd。tracepoint 中 irq_handler_entry / irq_handler_exit 记录了每次硬中断处理函数的名字和执行时长softirq_entry / softirq_exit 记录软中断情况。我曾经用perf record -e irq:irq_handler_entry -e softirq:softirq_entry -a sleep 5定位过一个网卡中断处理时间过长的问题通过统计 handler 的执行次数和时长发现某个驱动在硬中断里循环读了一千多次寄存器总耗时接近 2ms直接导致系统 in irq 时间超标。6.3 按什么顺序读源码最容易理解最后给一份源码阅读路线我按这个顺序读了两遍思路非常清晰arch/arm64/kernel/entry.S先看向量表布局和 el1_irq 的现场保存。arch/arm64/kernel/irq.c看 handle_arch_irq 的定义和 gic 驱动的注册入口。drivers/irqchip/irq-gic-v3.c重点看 gic_handle_irq 和 gic_irq_domain_map。kernel/irq/irqdomain.c看 irq_find_mapping 和 irq_domain_add_linear 的实现。kernel/irq/chip.c、handle.c、manage.c看 handle_level_irq、handle_edge_irq、request_irq 的下半部分。kernel/softirq.c看 irq_exit 之后 do_softirq 的逻辑。按这条路线读下来之后再回到某个具体驱动的中断注册代码很多疑问会自然解开。比如为什么 request_irq 返回的中断号不是设备树里那个数为什么有的中断必须用 IRQF_NO_THREAD为什么 edge 触发的中断偶尔丢失计数。这些问题其实都指向同一个核心ARM64 中断不是CPU 处理一条中断那么简单它是一整套硬件分发机制加软件映射机制的组合。调试中断问题最忌讳一上来就改驱动代码。先花十分钟把链路图在脑子里过一遍再按链路逐层确认往往比盲目加 printk 高效得多。希望这篇文章能帮你把这条链路彻底顺一遍。
RELATED

相关推荐

AI日报系统设计:分层流水线架构与本地化实践

AI日报系统设计:分层流水线架构与本地化实践

1. 项目概述:这不是一份新闻简报,而是一套可复用的AI信息流处理系统“AI 日报(2026年10月8日)”这个标题乍看像一份时效性极强的媒体简报,但作为从业十多年、亲手搭建过二十多个垂直领域信息聚合系统的博主&#xff0c…

📅 2026/10/16 4:51:38
16进制文件头识别指南:从魔数表到Python实战脚本

16进制文件头识别指南:从魔数表到Python实战脚本

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/16 4:51:38
VS2019 C++中文乱码原因与解决方案:从源文件编码到控制台代码页根治

VS2019 C++中文乱码原因与解决方案:从源文件编码到控制台代码页根治

先说一下,这个问题我太有发言权了。Windows上用VS2019写过C的同学,基本十个里有八个碰见过中文乱码:明明编辑器里看着好好的中文,一编译跑起来全是“銆愬鏂囨坊璇曘”这种乱七八糟的字符;或者更恶心一点,代…

📅 2026/10/16 4:51:38
MORE NEWS

更多资讯

📰

SAP Fiori Elements实战:从CDS注解到RAP应用开发全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

实训5指纹设置全解析:从传感器原理到实操与安全

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

语音特征提取全流程:从原始波形到MFCC特征向量

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Java线程start()方法原理与线程生命周期解析

1. 线程生命周期与start()方法本质当面试官抛出"为什么线程start()不能重复调用"这个问题时,实际上是在考察你对Java线程生命周期的理解深度。让我们先看一个典型的错误示例:Thread thread new Thread(() -> System.out.println("Run…

📰

Spring Cloud Gateway GatewayFilter 源码解析与责任链机制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

粒子群算法优化LQR的二级倒立摆控制设计与避坑实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬