专栏文章
专栏文章
Linux 专栏
1. Linux 专栏 #01:进程与线程 2. Linux 专栏 #02:内存管理 3. Linux 专栏 #03:文件系统 4. Linux 专栏 #04:网络与 IO 模型 5. Linux 专栏 #05:中断机制 6. Linux 专栏 #05:性能分析工具 7. Linux 专栏 #06:eBPF 技术实战

Linux 专栏 #05:中断机制

发布于 2026-06-08 07:30 👁 8 次阅读
#并发#操作系统#java#computer-architecture#linux

中断机制

中断是操作系统与硬件协作的核心机制,也是线程调度、I/O 处理、进程通信的底层基础。本文从硬件中断出发,逐层剖析 OS 如何利用中断驱动调度,再到 Java Thread.interrupt() 的完整实现原理,理清三个层面"中断"之间的关系。


目录

章节 说明
三个层面的"中断" 区分硬件中断、OS 信号、Java 线程中断
硬件中断原理 CPU 中断机制、IDT、APIC
中断处理流程 上半部与下半部、时钟中断
Linux 线程睡眠与唤醒 futex、TASK_INTERRUPTIBLE vs TASK_UNINTERRUPTIBLE
Java Thread.interrupt() 原理 标志位 + OS 唤醒的完整链路
各场景响应中断对比 wait/sleep/synchronized/lockInterruptibly
常见误区 中断 ≠ 停止,轮询 vs 唤醒

三个层面的"中断"

"中断"这个词在不同语境下含义截然不同,必须先区分清楚:

硬件中断(Hardware Interrupt)
    ↓ 驱动
OS 调度系统(时间片、进程切换)
    ↓ 提供
Java Thread.interrupt()(协作式线程中断)
层面 触发者 本质 可被屏蔽?
硬件中断 外设(网卡/定时器/键盘) CPU 响应 IRQ 信号,跳转执行 ISR 可通过 CLI 指令屏蔽(临时)
OS 信号(Signal) 进程/内核 软件模拟的异步通知机制 可 signal(SIGXXX, SIG_IGN)
Java 线程中断 Java 代码 thread.interrupt() 标志位 + OS 唤醒的协作机制 线程可选择不响应

硬件中断原理

CPU 如何感知中断

CPU 有一根专用引脚 INTR(Interrupt Request)。当外设或中断控制器拉高这根引脚时,CPU 在执行完当前指令后会检测到中断请求,并响应它。

外设 ──IRQ──▶ APIC ──INTR──▶ CPU
                 ↑
         (决定优先级、仲裁)

APIC(Advanced Programmable Interrupt Controller):现代 x86 系统每个 CPU 核都有一个 Local APIC,外设通过 I/O APIC 发送中断,I/O APIC 再路由给对应的 Local APIC。APIC 负责:

IDT:中断向量表

CPU 收到中断信号后,需要知道去哪里执行处理代码。IDT(Interrupt Descriptor Table) 就是这张索引表,存储在内存中,由 CPU 的 IDTR 寄存器指向。

中断向量号(0~255)
    ↓
IDT[向量号] → 中断门描述符
    ↓
包含:处理程序的段选择子 + 偏移地址 + 特权级
    ↓
CPU 跳转执行对应的中断服务程序(ISR)

x86 中断向量分配:

向量号 用途
0~31 CPU 异常(除零、缺页、非法指令等)
32~255 外部中断(IRQ 映射,可由 OS 自由分配)
0x80 Linux 系统调用入口(历史遗留,现用 syscall 指令)

整体架构图

interrupt hardware flow


中断处理流程

上半部(Top Half):硬中断,必须快

CPU 跳入 ISR 时,中断是被禁止的(CLI),不能被其他中断打断。这段代码称为上半部,要求:

// 示例:网卡中断 ISR 上半部(伪代码)
irqreturn_t nic_interrupt(int irq, void *dev_id) {
    // 读取网卡状态寄存器,确认有数据到达
    status = read_nic_status();
    // 把数据包放入 ring buffer
    fill_rx_ring();
    // 调度下半部处理
    napi_schedule(&nic->napi);
    return IRQ_HANDLED;
}

下半部(Bottom Half):软中断,可延迟

耗时的处理工作推迟到下半部,此时中断已重新开启,可以被其他中断打断:

机制 运行上下文 可睡眠? 典型用途
softirq 软中断上下文 网络收包(NET_RX_SOFTIRQ)
tasklet 软中断上下文 简单延迟处理
workqueue 内核线程上下文 需要睡眠的耗时处理

时钟中断:调度器的心跳

时钟中断(Timer IRQ) 是所有调度的根基:

关键:没有时钟中断,调度器就无法运转。即使一个线程被 wake_up_process() 唤醒,也要等下一次时钟中断触发调度器,才能真正被分配到 CPU。

CPU 保存/恢复上下文

响应中断时,CPU 硬件自动完成以下操作:

① 将 SS、RSP、RFLAGS、CS、RIP 压入内核栈
② 从 IDT 加载新的 CS:RIP(ISR 地址)
③ 切换到内核特权级(Ring 0)
④ 执行 ISR

ISR 返回时执行 iret 指令:
① 从内核栈恢复 RIP、CS、RFLAGS、RSP、SS
② 切换回用户特权级(Ring 3)
③ 继续执行被打断的用户代码

Linux 线程睡眠与唤醒

futex:用户态快速锁

Linux 线程的阻塞等待(wait()/sleep()/LockSupport.park())最终都落到 futex(Fast Userspace Mutex) 系统调用:

// 睡眠:如果 *addr == val,则将当前线程挂起
syscall(SYS_futex, addr, FUTEX_WAIT, val, timeout, ...);

// 唤醒:唤醒等待在 addr 上的最多 n 个线程
syscall(SYS_futex, addr, FUTEX_WAKE, n, ...);

内核维护一张 futex 等待哈希表

futex 地址(哈希后)→ 等待队列(task_struct 链表)

线程调用 FUTEX_WAIT 后,内核将其加入等待队列,设置状态为 TASK_INTERRUPTIBLE,从运行队列移出,不再占用 CPU。

两种睡眠状态(关键区别)

状态 说明 能被信号/中断唤醒? 典型场景
TASK_INTERRUPTIBLE(S) 可中断睡眠 ✅ 能,futex 返回 -EINTR wait()sleep()park()lockInterruptibly()
TASK_UNINTERRUPTIBLE(D) 不可中断睡眠 ❌ 不能,必须等事件完成 等待磁盘 I/O、pthread_mutex_lock()

D 状态(不可中断睡眠)kill -9 也杀不死的原因——进程在等待内核 I/O 完成,强制打断会导致数据不一致。

synchronized 抢锁底层调用 pthread_mutex_lock(),该函数内部使用 FUTEX_WAIT 时传入的是不可中断等待(实际上 pthread_mutex_lock 在等待时会重试,不响应信号),这就是为什么 synchronized 抢锁阶段无法响应 Thread.interrupt()

唤醒机制

// 内核唤醒一个等待线程
void wake_up_process(struct task_struct *p) {
    // 将线程状态改为 TASK_RUNNING
    p->state = TASK_RUNNING;
    // 将 task_struct 加入对应 CPU 的运行队列
    enqueue_task(rq, p, flags);
    // 如果目标 CPU 是其他核,发送 IPI(核间中断)通知
}

Java Thread.interrupt() 原理

完整调用链图

thread interrupt flow

两件事同时发生

thread.interrupt() 实际做了两件独立的事:

thread.interrupt()
    ├─ ① 设置 interrupted 标志位 = true(volatile,保证可见性)
    └─ ② 若线程正在阻塞 → 调用 OS 接口唤醒它

① 标志位:供运行中的线程主动检查:

// 线程自己在代码中检查(轮询)
while (true) {
    doWork();
    if (Thread.interrupted()) {  // 检查并清除标志位
        throw new InterruptedException();
    }
}

② OS 唤醒:针对阻塞中的线程,JVM 调用底层接口:

// JVM 源码(hotspot/src/os/linux/vm/os_linux.cpp)
// 对应 Java 的 LockSupport.unpark() 或 interrupt 唤醒
pthread_kill(thread->osthread()->pthread_id(), SIGINT);
// 或直接操作 futex:
syscall(SYS_futex, addr, FUTEX_WAKE, 1);

内核收到后:

  1. 将目标线程从 futex 等待队列移出
  2. 设置状态为 TASK_RUNNING,加入运行队列
  3. 等待下一次时钟中断触发调度器,线程得到 CPU
  4. futex 系统调用返回 -EINTR(被信号打断)
  5. JVM 检查到 -EINTR + 中断标志位 → 抛出 InterruptedException清除标志位

为什么阻塞时不需要轮询

线程阻塞时被 OS 挂起,完全不占 CPU,无法做任何事情(包括轮询标志位)。interrupt() 调用方通过 OS 接口主动"戳醒"目标线程——这是**推(push)模型,不是拉(pull/轮询)**模型。

运行中的线程  ← 只能靠轮询 interrupted 标志位(拉模型)
阻塞中的线程  ← 由调用方通过 OS 主动唤醒(推模型)

各场景响应中断对比

场景 线程状态 中断响应 底层原因
synchronized 抢锁 TASK_UNINTERRUPTIBLE(实际是重试等待) ❌ 不响应,仅设置标志位 pthread_mutex_lock 不可被信号打断
Object.wait() TASK_INTERRUPTIBLE ✅ 立即唤醒,抛 InterruptedException,清除标志位 futex FUTEX_WAIT 可中断
Thread.sleep() TASK_INTERRUPTIBLE ✅ 立即唤醒,抛 InterruptedException,清除标志位 同上
LockSupport.park() TASK_INTERRUPTIBLE ✅ 唤醒,抛异常,清除标志位 futex 可中断,但语义不同
lock.lock() TASK_INTERRUPTIBLE(AQS) ❌ 不响应,仅设置标志位 AQS 内部忽略中断,等获锁后补抛
lock.lockInterruptibly() TASK_INTERRUPTIBLE ✅ 立即唤醒,抛 InterruptedException AQS 检查中断标志,主动抛出
线程正常运行 TASK_RUNNING 仅设置标志位,需代码主动检查 无阻塞,无 OS 唤醒

LockSupport.park() 的特殊性:被中断唤醒后不抛异常,标志位保留,由上层代码(AQS)自行决定如何处理。这是 Java 并发框架的设计选择,让 AQS 有机会在持锁后再统一抛出中断。


常见误区

误区 正确理解
interrupt() 会立即停止线程 ❌ 仅设置标志位 + 可能唤醒阻塞,线程是否停止由自身代码决定
线程阻塞时靠轮询检测中断 ❌ 阻塞时不占 CPU,由调用方通过 OS 接口主动唤醒(推模型)
硬件中断和 Java 线程中断是同一回事 ❌ 完全不同层面,前者是 CPU 响应 IRQ,后者是 Java 协作式标志位机制
synchronized 能响应中断 ❌ 抢锁阶段无法响应,需改用 lock.lockInterruptibly()
时钟中断只是计时 ❌ 时钟中断是调度器的驱动力,没有它线程永远不会被切换
SIGKILL 能杀死 D 状态进程 ❌ D 状态(TASK_UNINTERRUPTIBLE)不响应任何信号,必须等 I/O 完成

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论