前言
前面已经把 Java 程序、JVM、用户态、内核态以及 System Call 串到了一起。
但只知道:
Java 调用系统接口
↓
进入 Kernel
↓
Kernel 帮我们操作硬件
还不够。
因为继续往下追,很快会遇到一组特别容易混在一起的概念:
- 键盘按下一次,CPU 为什么立刻知道?
- 网卡收到一个数据包,操作系统怎么被通知?
- CPU 明明正在运行 Java 程序,硬件事件来了以后谁让它暂停?
- Interrupt、Exception、System Call 到底是不是一回事?
- Linux 里的 Softirq 和“软件中断”是不是同一个概念?
int 0x80、sysenter、syscall为什么网上都有?- Java 的
InputStream.read()最后真的会执行int 0x80吗? - System Call 会从用户态进入内核态,那是不是一定发生 Context Switch?
- 中断处理的“上半部、下半部”又是谁在执行?
如果把这些名词分开背,很容易形成一个过时的模型:
应用程序
↓
int 0x80
↓
软中断
↓
Kernel
↓
硬件
这个模型在讲早期 32 位 x86 历史时有一定价值,但拿它解释今天的 x86-64 Linux 就会出问题。
更完整的关系应该是:
这篇文章就从两个方向同时往中间走:
硬件事件 → Interrupt → Kernel
用户程序 → System Call → Kernel
最后再把它们和 Java I/O 接起来。
一、中断为什么存在?
假设 CPU 正在连续执行一个计算任务:
指令 1
指令 2
指令 3
指令 4
...
与此同时,网卡突然收到数据。
最笨的办法是让 CPU 不停询问:
网卡有数据吗?
没有。
网卡有数据吗?
没有。
网卡有数据吗?
还是没有。
这种方式叫 Polling(轮询)。
轮询并不是错误技术,在一些对延迟极端敏感、可以接受占用 CPU 的场景里反而非常有价值。但如果所有硬件都要求 CPU 一刻不停地主动询问,显然会浪费大量计算资源。
于是硬件需要另一种能力:
平时 CPU 做自己的事,有事件时设备主动通知 CPU。
这就是 Interrupt 最重要的价值。
1. 一次硬件中断怎么到达 CPU?
以网卡收到数据为例,可以先建立一个抽象模型:
真实系统比这个复杂得多:
- 中断控制器可能是 APIC;
- 中断可能被路由到不同 Core;
- MSI / MSI-X 可以让 PCIe 设备通过消息触发中断;
- Kernel 还可能使用 NAPI、IRQ Affinity 等机制优化高负载设备。
但最核心的认知不变:
Interrupt 是外部事件让 CPU 暂时偏离当前控制流、进入内核处理路径的一种硬件机制。
2. 中断不是“CPU 把当前程序杀掉”
假设 CPU 此刻正在执行 Java 进程 A:
Java Process A
↓
CPU 正在执行它的机器指令
此时发生硬件中断,不代表:
Process A 被结束
更接近:
所以:
发生 Interrupt
和:
发生进程 / 线程切换
不是同一个概念。
二、Interrupt、Exception、System Call 有什么区别?
这三个概念之所以容易混淆,是因为它们最终都可能让 CPU 从普通用户代码进入内核或特权处理路径。
但它们的来源完全不同。
| 类型 | 谁触发 | 是否与当前指令同步 | 典型场景 |
|---|---|---|---|
| Interrupt | 外部硬件 | 通常异步 | 网卡、时钟、设备完成事件 |
| Exception | 当前指令执行 | 同步 | Page Fault、除零、非法指令 |
| System Call | 用户程序主动请求 | 同步 | read、write、mmap、futex |
1. Interrupt:外部突然有事
例如:
CPU 正在执行 A
↓
网卡收到数据
↓
IRQ 到达
网卡事件和 CPU 当前执行的那条 Java 指令通常没有直接因果关系,所以它属于异步事件。
2. Exception:当前指令自己出了特殊情况
例如程序访问一个尚未建立物理页映射的虚拟地址:
Load [virtual address]
↓
页表无法完成当前访问
↓
Page Fault
Page Fault 是由当前指令触发的,因此是同步异常。
而且 Exception 并不等于“程序崩溃”。
Page Fault 就是一个非常经典的反例:操作系统可能只是建立页表、分配物理页或把数据调入内存,然后让原指令重新执行。
3. System Call:程序主动敲 Kernel 的门
应用程序明知道自己没有权限直接完成某些操作,于是主动调用 Kernel 提供的入口:
read()
write()
mmap()
futex()
clone()
所以它本质上是:
User Space 主动请求 Kernel Service 的受控入口。
可以把三者画成:
它们可能都会“进入 Kernel”,但原因并不一样。
三、中断的上半部和下半部是谁执行?
中断处理有一个天然矛盾。
一方面,硬件事件希望尽快被响应;另一方面,如果中断处理函数干太久:
中断到来
↓
CPU 一直困在中断处理里
↓
普通任务迟迟得不到运行
系统响应会变差。
于是 Linux 很早就形成了一种核心思想:
紧急工作立即做,非紧急工作延后做。
1. Hard IRQ:先完成最急的部分
硬件中断入口中的处理通常要求尽可能短。
例如网卡中断到来后,Kernel 可能先完成:
- 确认中断来源;
- 读取必要状态;
- 禁止或重新配置某些设备中断;
- 安排后续处理;
- 尽快退出 Hard IRQ Context。
可以理解成:
2. 延后工作仍然在 Kernel 中完成
传统 Linux 里常见的 Deferred Work 机制包括:
Softirq
Tasklet(历史机制)
Workqueue
Threaded IRQ
NAPI
不同子系统会选不同机制。
最重要的是纠正一个常见误解:
中断“下半部”不是交给 Java、Office 或普通用户应用去执行。
它仍然属于 Linux Kernel 的中断延迟处理体系。
应用程序最终能感知到键盘输入、Socket 数据等,是因为 Kernel 在完成底层事件处理后:
更新 Kernel 数据结构
↓
数据进入缓冲区
↓
唤醒等待该事件的 Task
↓
User Process 再被调度运行
这是两个阶段,不要混为一谈。
3. Linux Softirq 不等于“软件触发的 int 0x80”
这是中文资料里最容易被术语坑到的地方之一。
Linux 中的:
softirq
是 Kernel 内部的一种 Deferred Work 机制。
而早期 x86 Linux 中用户程序通过:
int 0x80
进入系统调用,是使用 software interrupt instruction 触发 CPU 的特权入口。
两者虽然中文都可能被翻译成“软中断”,但不是同一个东西:
所以后面讨论系统调用时,最好直接使用:
System Call Entry
而不是一句“系统调用就是软中断”带过。
四、System Call 为什么不能再简单理解成 int 0x80?
网上很多 Linux 底层文章都会出现:
int 0x80
这不是假的,只是年代和 ABI 范围需要说清楚。
1. int 0x80 是经典的 32 位 x86 路径
传统 32-bit x86 Linux 可以通过:
int 0x80
进入 Kernel System Call Handler。
后来 x86 又出现更专门、更快的系统调用入口,例如:
SYSENTER / SYSEXIT
SYSCALL / SYSRET
2. 现代 x86-64 Linux 正常使用 syscall
在 64 位用户程序中,主流路径是:
syscall
Linux Kernel 的 x86 Entry 文档明确区分了:
64-bit code
→ syscall
compat / 32-bit path
→ int 0x80 / sysenter 等
所以今天写一个 x86-64 Java 程序,不能再默认说:
InputStream.read()最后一定执行int 0x80。
更准确的表达是:
Java/JDK 最终需要通过目标 OS 和 ABI 对应的 System Call 入口请求 Kernel 服务;在现代 x86-64 Linux 上,普通 64 位系统调用通常通过
syscall指令进入内核。
3. 为什么 CPU 要专门设计 System Call 指令?
普通函数调用:
call foo
仍然处在相同权限级。
而 System Call 需要完成的是:
User Mode
↓
受控进入
↓
Kernel Mode
CPU 必须:
- 切换到预定义的 Kernel Entry;
- 保存必要的返回状态;
- 改变执行特权环境;
- 让 Kernel 接管参数并验证请求;
- 最终安全返回 User Space。
因此它不是普通 call 指令能够直接代替的。
五、x86-64 System Call 参数怎么传?
这一部分很值得实际看一眼,因为它能把:
Java / C 函数参数
和:
CPU Register
真正接起来。
1. x86-64 Linux 的调用约定
在 x86-64 Linux System Call ABI 中,可以先记住这张表:
| 内容 | 寄存器 |
|---|---|
| System Call Number | RAX |
| 参数 1 | RDI |
| 参数 2 | RSI |
| 参数 3 | RDX |
| 参数 4 | R10 |
| 参数 5 | R8 |
| 参数 6 | R9 |
| 返回值 | RAX |
所以:
“AX 放调用号,BX/CX/DX/SI/DI 最多传 5 个参数”
描述的是另一套 32 位历史 ABI 思路,不能直接搬到 x86-64。
2. 实验:看看当前系统里的 System Call Number
在本文测试环境:
uname -m
架构为:
x86_64
可以查看系统安装的 x86-64 System Call Header:
grep -E '__NR_(read|write|fork|clone|clone3|openat) ' \
/usr/include/x86_64-linux-gnu/asm/unistd_64.h
本文环境实际得到:
#define __NR_read 0
#define __NR_write 1
#define __NR_clone 56
#define __NR_fork 57
#define __NR_openat 257
#define __NR_clone3 435
这说明两个很重要的问题。
第一:
read = 1
write = 2
fork = 3
并不是 x86-64 Linux 的 System Call Number。
第二,更重要:
System Call Number 属于具体 Architecture + ABI,不应该脱离平台死背。
文章或面试里真正值得理解的是:
用户态准备 syscall number + 参数
↓
执行 System Call Entry
↓
Kernel 根据 syscall table 找到对应实现
而不是背一套很可能来自 32 位旧系统的数字。
六、一次 Java read 是怎么进入 Linux Kernel 的?
现在回到 Java。
假设代码是:
byte[] data = inputStream.readNBytes(1024);
我们真正关心的不是某一个 JDK 类内部今天具体调用了哪个私有方法,而是跨层路径。
一个合理的抽象是:
这个图里其实同时出现了:
System Call
Interrupt
Scheduler
Context Switch
但它们各自负责不同事情。
1. System Call 是“请求”
Java 线程主动说:
我要读取数据。
2. Interrupt 可以是“完成通知”
设备稍后说:
数据准备好了。
3. Scheduler 决定“什么时候重新运行你”
Kernel 知道数据好了以后,并不等于当前 Java Thread 在这一纳秒就立刻得到 CPU。
它需要先变成 Runnable,之后由 Scheduler 安排真正执行。
于是完整关系变成:
七、System Call 一定会发生 Context Switch 吗?
不一定。
这是理解操作系统性能时非常关键的一点。
1. Mode Switch 和 Context Switch 不是同一个东西
当前 Thread A 调用一个很快的系统调用:
Thread A / User Mode
↓
syscall
↓
Thread A / Kernel Mode
↓
Kernel 立即处理完
↓
Thread A / User Mode
整个过程可能始终还是同一个 Thread A。
变化的是:
Privilege / Execution Mode
而不是:
Running Task Identity
所以这是:
Mode Switch
而不是必然的:
Context Switch
2. 阻塞以后才可能发生任务切换
如果:
read()
发现没有数据,Kernel 可能让 Thread A 等待:
此时才真正出现:
Thread A → Thread B
也就是 Context Switch。
所以不要写成:
每次系统调用都需要线程上下文切换,因此特别重。
更准确的是:
System Call 本身需要跨越用户态 / 内核态边界;如果请求导致当前 Task 阻塞、抢占或调度器选择其他任务,才进一步发生 Task Context Switch。
八、硬件交互是不是全部通过中断?
也不是。
硬件 I/O 通常包含几种不同机制,它们会组合使用。
1. CPU 先要给设备“下命令”
Kernel / Driver 可能通过:
MMIO
PIO
读写设备寄存器。
例如:
把 DMA Buffer 地址告诉网卡
告诉网卡开始发送
这一步不是“中断帮 CPU 写设备”。
2. 大块数据常常由 DMA 搬运
以网卡为例,可以抽象成:
CPU 并不需要自己一字节一字节搬运整个数据包。
3. 某些高性能场景甚至故意减少中断
当数据包特别多时,如果:
每一个 Packet
↓
都打一次 Interrupt
中断本身会成为开销。
所以高性能网络会使用:
- Interrupt Moderation;
- NAPI;
- Polling;
- Busy Poll;
等方式在“中断响应”和“批量轮询”之间取平衡。
因此:
中断是硬件通知 CPU 的重要机制,但不是 CPU 与设备交互的唯一机制。
九、Java 的 Thread.interrupt() 和 CPU Interrupt 有关系吗?
名字一样,很容易产生联想。
但它们处于完全不同层级。
1. CPU / Hardware Interrupt
解决的是:
Hardware / CPU / Kernel
之间的异步事件通知。
例如:
网卡 IRQ
时钟 IRQ
设备完成事件
2. Java Thread.interrupt()
解决的是:
Java Thread 协作式取消 / 中断状态
例如:
thread.interrupt();
它并不是:
Java 直接向 CPU 发一个 IRQ
它的语义是 Java 线程中断协议的一部分:设置中断状态,并让某些可中断阻塞操作通过 InterruptedException 等方式响应。
可以直接区分:
看到 interrupt 这个单词相同,不代表底层就是同一套机制。
十、常见误区
1. 关于中断
误区 1:中断就是线程切换。
错误。
中断只是 CPU 进入某个 Interrupt Handler 的事件入口。处理完以后完全可能继续执行原任务;是否切换 Task 由后续调度决定。
误区 2:中断处理的下半部由用户应用执行。
错误。
Linux 的 Bottom Half / Deferred Work 仍然属于 Kernel。用户程序只是在 Kernel 完成底层处理、事件就绪并被调度后继续运行。
误区 3:Linux Softirq 就是 int 0x80。
错误。
softirq 是 Linux Kernel 的 Deferred Work 机制;int 0x80 是 x86 software interrupt instruction,可用于传统/兼容系统调用入口。
2. 关于 System Call
误区 4:所有 Linux System Call 都通过 int 0x80。
错误。
现代 x86-64 普通系统调用使用 syscall;int 0x80、sysenter 更多属于历史 32 位和兼容入口。
误区 5:System Call Number 是固定的,例如 read=1、write=2、fork=3。
错误。
System Call Number 与 Architecture / ABI 相关。在本文 x86-64 Linux 环境里,read=0、write=1、fork=57。
误区 6:x86-64 System Call 仍用 BX/CX/DX/SI/DI 传五个参数。
错误。
x86-64 Linux 使用 RDI、RSI、RDX、R10、R8、R9 传递最多六个主要 System Call 参数,RAX 保存 System Call Number 和返回值。
误区 7:每次 System Call 都一定发生 Context Switch。
错误。
一定会涉及受控的 User / Kernel Mode Entry,但不一定换到另一个 Task。
3. 关于硬件 I/O
误区 8:所有硬件访问都靠中断完成。
错误。
Driver 还会使用 MMIO / PIO 配置设备,DMA 可以直接在设备与内存之间搬数据,高负载场景甚至会主动采用 Polling 减少中断。
误区 9:硬件 Interrupt 和 Java Thread.interrupt() 是一套机制。
错误。
一个属于 CPU / Kernel 硬件事件机制,一个属于 Java Thread 协作语义。
十一、把整条链串起来
现在把 Java、System Call、Kernel、Driver、DMA、Interrupt 和 Scheduler 全部放进一张图里:
如果是一次不会阻塞的 System Call,链路可能短很多:
Java
↓
System Call
↓
Kernel
↓
直接返回
如果是一次真正等待设备完成的 I/O,链路才可能扩展成:
Java 发起请求
↓
System Call
↓
Kernel
↓
Task 阻塞
↓
设备 / DMA 工作
↓
IRQ
↓
Kernel 完成处理
↓
唤醒 Task
↓
Scheduler
↓
Java Thread 再次运行
到这里,我们已经不再只是记:
“系统调用会进入内核态”
而是能真正看见一次 I/O 如何穿过整个软件和硬件栈。
十二、总结
Interrupt 和 System Call 都可能让 CPU 进入 Kernel,但它们的方向完全不同:
Interrupt
硬件 → CPU → Kernel
而:
System Call
User Process → CPU → Kernel
Exception 则来自当前指令执行过程中出现的同步事件。
Linux 处理中断时,又会尽量把最紧急的 Hard IRQ 工作快速完成,把更多任务延后到 Kernel 的 Deferred Work 机制中,而不是让用户应用承担所谓“下半部”。
在 System Call 这一边,int 0x80 是理解 x86 Linux 历史的重要入口,但现代 x86-64 不能再用它解释一切。64 位普通 System Call 通常通过:
syscall
进入 Kernel,并使用 x86-64 ABI 对应的寄存器传递调用号和参数。
对于 Java 程序员,更值得掌握的不是背某个固定 System Call Number,而是这条完整链:
Java API
↓
JDK / JVM Runtime
↓
System Call
↓
Linux Kernel
↓
Filesystem / Network / Driver
↓
Device / DMA
↓
Interrupt / Completion
↓
Wakeup
↓
Scheduler
↓
Java Thread
同时要牢牢记住两个“不等于”:
System Call ≠ Context Switch
Interrupt ≠ Thread Switch
它们可能最终导致调度和 Context Switch,但并不是同一个动作。
用户程序通过 System Call 主动进入 Kernel,硬件通过 Interrupt 主动通知 Kernel;Kernel 位于两者中间,负责把软件请求、安全边界、设备事件和任务调度真正连接成一个完整系统。
参考资料
- Linux Kernel Documentation — Kernel Entries (x86-64)
https://docs.kernel.org/arch/x86/entry_64.html - Linux Kernel Documentation — Generic IRQ Handling
https://docs.kernel.org/core-api/genericirq.html - Linux Kernel Documentation — Unreliable Guide To Hacking The Linux Kernel: Hard IRQs / Softirqs
https://docs.kernel.org/kernel-hacking/hacking.html - Linux
syscall(2)Manual Page
https://man7.org/linux/man-pages/man2/syscall.2.html - Intel 64 and IA-32 Architectures Software Developer Manuals
https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
评论区