中断机制
中断是操作系统与硬件协作的核心机制,也是线程调度、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 负责:
- 收集各路 IRQ 信号
- 决定中断优先级(高优先级中断可打断低优先级中断处理)
- 向 CPU 核发送中断向量号
IDT:中断向量表
CPU 收到中断信号后,需要知道去哪里执行处理代码。IDT(Interrupt Descriptor Table) 就是这张索引表,存储在内存中,由 CPU 的 IDTR 寄存器指向。
中断向量号(0~255)
↓
IDT[向量号] → 中断门描述符
↓
包含:处理程序的段选择子 + 偏移地址 + 特权级
↓
CPU 跳转执行对应的中断服务程序(ISR)
x86 中断向量分配:
| 向量号 | 用途 |
|---|---|
| 0~31 | CPU 异常(除零、缺页、非法指令等) |
| 32~255 | 外部中断(IRQ 映射,可由 OS 自由分配) |
| 0x80 | Linux 系统调用入口(历史遗留,现用 syscall 指令) |
整体架构图
中断处理流程
上半部(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) 是所有调度的根基:
- 触发源:CPU 内部的 APIC Timer,默认每 1ms 触发一次(HZ=1000)
- 作用:
- 更新系统时间(jiffies)
- 检查当前线程时间片是否耗尽
- 触发调度器(
scheduler_tick()),决定是否切换线程 - 处理定时器到期事件
关键:没有时钟中断,调度器就无法运转。即使一个线程被
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() 实际做了两件独立的事:
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);
内核收到后:
- 将目标线程从 futex 等待队列移出
- 设置状态为
TASK_RUNNING,加入运行队列 - 等待下一次时钟中断触发调度器,线程得到 CPU
futex系统调用返回-EINTR(被信号打断)- 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 完成 |
参考资料
- Linux Kernel Memory Barriers — David Howells, Paul McKenney
- Linux Kernel Source: kernel/sched/core.c — wake_up_process
- Linux Kernel Source: kernel/futex/core.c
- Intel 64 and IA-32 Architectures Software Developer's Manual, Vol.3A — Chapter 6: Interrupt and Exception Handling
- 《深入理解 Linux 内核》— Daniel P. Bovet & Marco Cesati,第 4 章(中断和异常)
- 《Linux 内核设计与实现》— Robert Love,第 7 章(中断和中断处理程序)
- 《深入理解计算机系统》(CSAPP)— 第 8 章(异常控制流)
- 《Java 并发编程实战》— Brian Goetz 等,第 7 章(取消与关闭)
评论 (0)
发表评论