侧边栏壁纸
  • 累计撰写 140 篇文章
  • 累计创建 21 个标签
  • 累计收到 3 条评论

目 录CONTENT

文章目录

中断_异常与Linux系统调用

YaFuX
2023-10-03 / 0 评论 / 0 点赞 / 0 阅读 / 0 字
温馨提示:
部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

前言

前面已经把 Java 程序、JVM、用户态、内核态以及 System Call 串到了一起。

但只知道:

Java 调用系统接口
    ↓
进入 Kernel
    ↓
Kernel 帮我们操作硬件

还不够。

因为继续往下追,很快会遇到一组特别容易混在一起的概念:

  • 键盘按下一次,CPU 为什么立刻知道?
  • 网卡收到一个数据包,操作系统怎么被通知?
  • CPU 明明正在运行 Java 程序,硬件事件来了以后谁让它暂停?
  • Interrupt、Exception、System Call 到底是不是一回事?
  • Linux 里的 Softirq 和“软件中断”是不是同一个概念?
  • int 0x80sysentersyscall 为什么网上都有?
  • Java 的 InputStream.read() 最后真的会执行 int 0x80 吗?
  • System Call 会从用户态进入内核态,那是不是一定发生 Context Switch?
  • 中断处理的“上半部、下半部”又是谁在执行?

如果把这些名词分开背,很容易形成一个过时的模型:

应用程序
   ↓
int 0x80
   ↓
软中断
   ↓
Kernel
   ↓
硬件

这个模型在讲早期 32 位 x86 历史时有一定价值,但拿它解释今天的 x86-64 Linux 就会出问题。

更完整的关系应该是:

主动请求
配置设备 / 提交 I/O
事件完成
返回结果 / 唤醒任务
Java / User Process
System Call
Linux Kernel
Driver
Hardware Device
Hardware IRQ

这篇文章就从两个方向同时往中间走:

硬件事件 → Interrupt → Kernel
用户程序 → System Call → Kernel

最后再把它们和 Java I/O 接起来。

一、中断为什么存在?

假设 CPU 正在连续执行一个计算任务:

指令 1
指令 2
指令 3
指令 4
...

与此同时,网卡突然收到数据。

最笨的办法是让 CPU 不停询问:

网卡有数据吗?
没有。

网卡有数据吗?
没有。

网卡有数据吗?
还是没有。

这种方式叫 Polling(轮询)

轮询并不是错误技术,在一些对延迟极端敏感、可以接受占用 CPU 的场景里反而非常有价值。但如果所有硬件都要求 CPU 一刻不停地主动询问,显然会浪费大量计算资源。

于是硬件需要另一种能力:

平时 CPU 做自己的事,有事件时设备主动通知 CPU。

这就是 Interrupt 最重要的价值。

1. 一次硬件中断怎么到达 CPU?

以网卡收到数据为例,可以先建立一个抽象模型:

Network DriverLinux KernelCPU CoreInterrupt Controller网卡Network DriverLinux KernelCPU CoreInterrupt Controller网卡产生 IRQ 请求投递中断保存必要的执行现场根据中断入口进入 Kernel调用对应中断处理逻辑确认 / 读取设备状态完成紧急处理恢复后续执行

真实系统比这个复杂得多:

  • 中断控制器可能是 APIC;
  • 中断可能被路由到不同 Core;
  • MSI / MSI-X 可以让 PCIe 设备通过消息触发中断;
  • Kernel 还可能使用 NAPI、IRQ Affinity 等机制优化高负载设备。

但最核心的认知不变:

Interrupt 是外部事件让 CPU 暂时偏离当前控制流、进入内核处理路径的一种硬件机制。

2. 中断不是“CPU 把当前程序杀掉”

假设 CPU 此刻正在执行 Java 进程 A:

Java Process A
      ↓
CPU 正在执行它的机器指令

此时发生硬件中断,不代表:

Process A 被结束

更接近:

CPU 正在执行 Process A
硬件中断到达
CPU 进入中断入口
Kernel 执行中断处理
是否需要调度?
返回 Process A 继续执行
Scheduler 选择其他任务

所以:

发生 Interrupt

和:

发生进程 / 线程切换

不是同一个概念。

二、Interrupt、Exception、System Call 有什么区别?

这三个概念之所以容易混淆,是因为它们最终都可能让 CPU 从普通用户代码进入内核或特权处理路径。

但它们的来源完全不同

类型谁触发是否与当前指令同步典型场景
Interrupt外部硬件通常异步网卡、时钟、设备完成事件
Exception当前指令执行同步Page Fault、除零、非法指令
System Call用户程序主动请求同步readwritemmapfutex

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 的受控入口。

可以把三者画成:

异步
同步
同步
CPU 当前执行流
外部硬件事件
Interrupt
当前指令异常
Exception
用户程序主动请求
System Call
Kernel Entry

它们可能都会“进入 Kernel”,但原因并不一样。

三、中断的上半部和下半部是谁执行?

中断处理有一个天然矛盾。

一方面,硬件事件希望尽快被响应;另一方面,如果中断处理函数干太久:

中断到来
   ↓
CPU 一直困在中断处理里
   ↓
普通任务迟迟得不到运行

系统响应会变差。

于是 Linux 很早就形成了一种核心思想:

紧急工作立即做,非紧急工作延后做。

1. Hard IRQ:先完成最急的部分

硬件中断入口中的处理通常要求尽可能短。

例如网卡中断到来后,Kernel 可能先完成:

  • 确认中断来源;
  • 读取必要状态;
  • 禁止或重新配置某些设备中断;
  • 安排后续处理;
  • 尽快退出 Hard IRQ Context。

可以理解成:

硬件 IRQ
Hard IRQ Handler
快速确认设备状态
安排 Deferred Work
尽快退出 Hard IRQ

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 的特权入口。

两者虽然中文都可能被翻译成“软中断”,但不是同一个东西:

Linux softirq
Kernel Deferred Work
x86 int 0x80
CPU Software Interrupt Instruction
Legacy / Compat System Call Entry

所以后面讨论系统调用时,最好直接使用:

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 NumberRAX
参数 1RDI
参数 2RSI
参数 3RDX
参数 4R10
参数 5R8
参数 6R9
返回值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 类内部今天具体调用了哪个私有方法,而是跨层路径

一个合理的抽象是:

DeviceFile / Socket SubsystemLinux KernelCPUJDK / JVM RuntimeJava ApplicationDeviceFile / Socket SubsystemLinux KernelCPUJDK / JVM RuntimeJava Applicationalt[数据已经准备好][数据尚未准备好]read(...)准备系统调用参数syscall执行对应 Kernel I/O 路径立即返回数据返回 User Mode返回结果Java 获得数据当前 Task 进入等待等待设备 / 网络事件IRQ / completion唤醒等待 Task未来重新调度执行System Call 返回Java 获得数据

这个图里其实同时出现了:

System Call
Interrupt
Scheduler
Context Switch

但它们各自负责不同事情。

1. System Call 是“请求”

Java 线程主动说:

我要读取数据。

2. Interrupt 可以是“完成通知”

设备稍后说:

数据准备好了。

3. Scheduler 决定“什么时候重新运行你”

Kernel 知道数据好了以后,并不等于当前 Java Thread 在这一纳秒就立刻得到 CPU。

它需要先变成 Runnable,之后由 Scheduler 安排真正执行。

于是完整关系变成:

Java read
System Call
Kernel
数据是否已就绪?
立即返回
Task 等待
设备工作
IRQ / Completion
Kernel 唤醒 Task
Scheduler
Java Thread 恢复执行

七、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 / User Mode
syscall
Thread A / Kernel Mode
数据可用?
直接返回 Thread A
Thread A 进入 Sleep / Wait
Scheduler
切换到 Thread B

此时才真正出现:

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 搬运

以网卡为例,可以抽象成:

配置 Descriptor
DMA
完成后 IRQ
CPU / Driver
网卡
Main Memory

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 等方式响应。

可以直接区分:

不是同一层
Hardware Interrupt
CPU / Kernel Event Mechanism
Thread.interrupt
Java Thread Coordination Mechanism

看到 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 普通系统调用使用 syscallint 0x80sysenter 更多属于历史 32 位和兼容入口。

误区 5:System Call Number 是固定的,例如 read=1、write=2、fork=3。

错误。

System Call Number 与 Architecture / ABI 相关。在本文 x86-64 Linux 环境里,read=0write=1fork=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 全部放进一张图里:

Java Application
JDK / JVM Runtime
System Call Entry
Linux Kernel
Filesystem / Network Stack
Device Driver
Hardware Device
DMA to/from Memory
Hardware IRQ
Hard IRQ Handler
Deferred Kernel Work
Wake Waiting Task
Scheduler
Java Thread Continues

如果是一次不会阻塞的 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 位于两者中间,负责把软件请求、安全边界、设备事件和任务调度真正连接成一个完整系统。

参考资料

0

评论区