Skip to content

系统调用与中断(用户态/内核态 / syscall / 软中断)

操作系统 ⭐⭐⭐ 中等 🔥🔥 高频

💡 核心要点

"为什么读个文件还要切到内核?" —— CPU 用特权级(Ring 0/3) 把代码分成用户态和内核态,应用想碰硬件(磁盘、网卡、内存映射)只能通过系统调用这道唯一的门。这道门的开销(模式切换 + 上下文保存)正是 io_uringvDSO、零拷贝想方设法绕过的对象。中断则是硬件反向打断 CPU 的机制,软中断 + 网卡 NAPI 是高并发网络栈的性能命门。

概念

CPU 通过 特权级(Privilege Level / Ring) 把指令执行划分为两种状态:

状态x86 Ring能做什么典型代码
内核态(Kernel Mode)Ring 0执行特权指令、直接访问硬件 / 全部内存内核、驱动
用户态(User Mode)Ring 3只能跑普通指令、访问自己的虚拟内存应用程序
        ┌──────────────── 用户态 Ring 3 ────────────────┐
        │   你的程序:算术、函数调用、访问自己内存          │
        └───────────────────┬───────────────────────────┘
                            │ 只能通过这道门
                  ┌─────────▼─────────┐
                  │   系统调用 syscall  │  ← 受控入口(陷入 trap)
                  └─────────┬─────────┘
        ┌───────────────────▼───────────────────────────┐
        │   内核态 Ring 0:调度、文件、网络、内存、驱动      │
        └────────────────────────────────────────────────┘

为什么要分:① 安全——应用崩溃 / 恶意代码不能直接搞挂系统或读别人内存;② 稳定——硬件访问串行化由内核统一管理;③ 抽象——给应用统一的"一切皆文件"接口,屏蔽硬件差异。


核心原理

1. 系统调用机制(用户态 → 内核态)

应用调用 read() 这种 libc 函数,最终会触发一条陷入指令切到内核:

read() [glibc 封装]
   │  ① 把系统调用号放入 rax,参数放入 rdi/rsi/rdx...
   │  ② 执行 syscall 指令(x86-64)── 触发陷入

内核态 entry_SYSCALL_64
   │  ③ 保存用户态寄存器到内核栈
   │  ④ 按 syscall 号查 sys_call_table → sys_read()
   │  ⑤ 执行内核逻辑(读 Page Cache / 发起磁盘 I/O)
   │  ⑥ 恢复寄存器,sysret 返回用户态

read() 返回字节数
触发方式时代说明
int 0x80老 x86(32 位)软中断,慢(走中断描述符表)
sysenter / syscall现代 x86-64专用快速指令,省去查 IDT
vDSO现代部分调用根本不进内核(见下)

2. 系统调用的开销与优化

一次模式切换要保存 / 恢复寄存器 + 刷新流水线,约 几十~几百 ns。高频调用累积起来很可观。主流优化:

手段原理
vDSO(virtual Dynamic Shared Object)gettimeofday / clock_gettime 等只读调用的数据映射到用户空间,用户态直接读,零陷入
批量提交(io_uring)N 次 I/O 共享一次 syscall,详见 I/O 模型
减少 syscall 次数缓冲(stdio 的 fwrite 攒满 buffer 再 write)、writev 聚合、sendfile 零拷贝
Page Cacheread 命中缓存就不发起真实磁盘 I/O

面试黄金句:系统调用慢不在"函数调用"本身,而在特权级切换 + CPU 流水线刷新 + cache/TLB 局部失效。所以高性能编程的核心是"减少 syscall 次数"——这正是 io_uring 和零拷贝的出发点。

3. 中断(硬件 → CPU 的反向通知)

系统调用是应用主动进内核;中断(Interrupt) 是硬件被动打断 CPU。

类型来源例子
硬中断(Hardware IRQ)外部设备异步触发网卡收包、磁盘完成、键盘、时钟
软中断 / 异常(Trap / Exception)CPU 内部同步触发缺页 Page Fault、除零、syscall
软中断(SoftIRQ)内核延后处理机制网络收发包下半部

中断处理流程:CPU 收到中断 → 暂停当前任务、保存现场 → 查 中断向量表(IDT) → 执行对应 中断处理程序(ISR) → 恢复现场继续。

4. 中断上半部 / 下半部(高频)

中断处理程序运行时通常关中断,必须极快,否则丢中断、伤实时性。Linux 因此把它拆成两半:

硬中断(上半部 top half):
   关中断,只做最紧急的事(应答硬件、把数据搬到队列),几微秒内结束

软中断 / tasklet / workqueue(下半部 bottom half):
   开中断后再慢慢处理耗时逻辑(协议栈解析、唤醒进程)
下半部机制特点能否睡眠
SoftIRQ性能最高,同类型可多核并发❌ 不能睡眠
Tasklet基于 SoftIRQ,同 tasklet 串行
Workqueue交给内核线程,可阻塞✅ 能睡眠

5. 中断 vs 轮询:网卡 NAPI(必背)

高并发收包时,每个包一次硬中断 会导致 中断风暴(中断本身吃光 CPU)。Linux 网络栈用 NAPI 混合方案:

低流量:中断模式 —— 来包就触发硬中断,低延迟
高流量:第一个包触发中断后【关中断】,改为【轮询 poll】批量收包
        ── 收完一批再开中断,避免每包一次中断

这就是"中断触发、轮询收割"。同理 epoll 是事件驱动(等内核通知),io_uring 的 SQPOLL 是纯轮询(内核线程一直转),三者是同一权衡谱系上的不同取舍:延迟 vs CPU 占用

6. 一次"网卡收包"全链路(串起所有概念)

网卡 DMA 把包写入内存环形缓冲区 Ring Buffer
   → 触发硬中断(上半部):只标记"有包了",调度 NET_RX 软中断
   → 软中断(下半部):NAPI poll 批量取包 → 协议栈 IP/TCP 处理 → 放入 socket 接收队列
   → 唤醒阻塞在 epoll_wait / recv 的用户进程(用户态被唤醒,可能伴随上下文切换)
   → 用户进程 read() 系统调用:内核态把数据从内核缓冲拷到用户缓冲(或零拷贝)

面试常问 & 怎么答

Q1:为什么要区分用户态和内核态?切换开销在哪?

为了安全、稳定、抽象:应用不能直接碰硬件和别人的内存,必须通过系统调用这道受控入口。开销不在"调用"本身,而在特权级切换要保存 / 恢复寄存器、刷新 CPU 流水线、TLB / cache 局部失效,约几十到几百 ns。高频场景靠 vDSO(零陷入)、io_uring(批量)、缓冲与零拷贝来摊薄。

Q2:系统调用是怎么实现的?

应用调 libc 封装 → 把系统调用号放 rax、参数放寄存器 → 执行 syscall 指令陷入内核 → 内核保存现场、按号查 sys_call_table 跳到对应内核函数 → 执行后 sysret 返回。老内核用 int 0x80 软中断,现代用专用 syscall/sysenter 指令更快,部分只读调用用 vDSO 直接在用户态完成。

Q3:中断的上半部和下半部为什么要拆开?

硬中断处理时通常关中断,必须极短,否则会丢中断、损害实时性。所以上半部(硬中断)只做最紧急的事(应答硬件、搬数据),把耗时逻辑(协议栈解析等)丢给下半部(SoftIRQ / Tasklet / Workqueue)在开中断后慢慢处理。其中 Workqueue 跑在内核线程里,是唯一能睡眠的下半部。

Q4:网卡高并发收包为什么不能纯中断?

每个包一次硬中断会造成中断风暴,中断处理本身吃光 CPU,吞吐反而下降。Linux 用 NAPI:低流量时中断(低延迟),一旦来包就关中断转为轮询批量收割,收完再开中断。本质是"中断触发、轮询收割",在延迟和 CPU 占用之间取平衡——和 epoll、io_uring SQPOLL 是同一权衡谱系。

看到什么就先想到这类

关键词 / 场景联想到
为什么要切到内核特权级 Ring 0/3 + 系统调用受控入口
系统调用慢 / 减少 syscall模式切换+流水线刷新;vDSO / io_uring / 缓冲
gettimeofday 不进内核vDSO 只读数据映射到用户态
网卡中断打满 CPU中断风暴 → NAPI 中断+轮询
中断处理要快上半部 / 下半部拆分(SoftIRQ)
下半部里要睡眠 / 阻塞只能用 Workqueue(内核线程)
缺页、除零同步异常 Trap,不是外部中断
批量 I/O 省系统调用io_uring → 见 I/O 模型