Skip to content

中断、异常与系统调用:用户态/内核态切换全过程

基于通用操作系统概念 · 核于 2026-08

速查

  • 中断(Interrupt)外部异步事件,硬件信号通知 CPU(时钟/键盘/磁盘 IO 完成/网卡),与当前指令无关。可屏蔽(INTR,可用 cli 屏蔽)vs 不可屏蔽(NMI,硬件故障)。
  • 异常(Exception/Trap)内部同步事件,由当前执行的指令引发(缺页/除零/非法指令/栈溢出)。分为 fault(可恢复,如缺页,处理完重新执行该指令)、trap(主动,如 syscall/debug 断点)、abort(不可恢复,如硬件错/双重 fault)。
  • 中断向量表(IDT):CPU 维护的中断号→处理程序地址映射表。CPU 收到中断/异常号后,查 IDT 跳到对应处理程序。
  • 中断处理流程:硬件/CPU 发中断号 → CPU 在当前指令结束后(中断)或立即(异常)响应 → 保存现场(PC/EFLAGS/寄存器/栈切换到内核栈)→ 查 IDT 跳处理程序 → 执行处理(内核态)→ iret 返回恢复现场。
  • 系统调用(syscall)异常的一种(trap 类):用户程序执行 syscall/int 0x80/sysenter 指令 → 触发 trap → CPU 切到内核态 → 查 IDT 找 syscall handler → 按系统调用号分发到具体实现 → 执行完 sysret/iret 返回。
  • 用户态→内核态切换代价:保存/恢复寄存器、切换栈、特权级切换、流水线冲刷、(KPTI 时)切换页表——约 1-10 微秒/次。频繁 syscall 是性能杀手,应用层应用缓冲减少次数。
  • **缺页中断(Page Fault)**是最高频异常:访问的虚拟页不在物理内存(被换出/未分配/COW),触发 fault → 内核把页读回/分配 → 重新执行原指令。详见分页与虚拟内存 叶。
  • 中断上下文 vs 进程上下文:中断处理程序运行在中断上下文(不属于任何进程,不可睡眠/不可调度);普通内核代码运行在进程上下文(代表某进程,可睡眠)。
  • 上半部/下半部(Linux 中断处理):上半部(top half)立即响应硬件(关中断,快);耗时工作推迟到下半部(bottom half,softirq/tasklet/workqueue,开中断)。

一、中断:外部异步事件

中断是硬件告诉 CPU"有事发生了"的机制。CPU 在每条指令执行完后会检查是否有中断信号,有则响应:

        CPU 正在执行进程 A 的指令

   ┌──────────┴──────────┐
   │  指令执行完,检查中断 │
   └──────────┬──────────┘
              │ 有中断信号(如磁盘 IO 完成)

   保存当前 PC/寄存器,切换到内核栈


   查 IDT(中断描述符表)→ 磁盘中断处理程序地址


   执行中断处理程序(内核态):标记 IO 完成,唤醒等待进程 B


   iret 返回 → 恢复进程 A 继续执行(或调度器决定改跑 B)
  • 时钟中断是分时 OS 的心跳:定时器硬件每 1-10ms 发一次时钟中断,内核借此回收 CPU(从当前进程切到下一个),否则一个死循环进程会霸占 CPU。
  • 可屏蔽 vs 不可屏蔽:普通外设中断(INTR)可被 cli 指令屏蔽(内核关键区会临时关中断);NMI(如内存校验错、看门狗)不可屏蔽,意味着硬件告急。

二、异常:内部同步事件

异常由当前正在执行的指令引发,是"指令执行出了状况":

类别含义举例处理后行为
Fault(故障)可恢复,处理完重新执行该指令缺页(#PF)、除零(#DE,但通常 abort 进程)、栈段错误缺页:把页读回后重做那条访存指令
Trap(陷阱)主动触发,处理完执行下一条指令syscall、断点(#BP)、溢出syscall 后继续下一条
Abort(终止)不可恢复,终止进程/重启机器检查错(#MC)、双重 fault(#DF)杀进程或 kernel panic
  • 缺页 fault 是最高频异常:进程访问虚拟地址 0x7fff...,页表发现该页不在内存(PTE 的 present 位=0)→ CPU 触发 #PF → 内核缺页处理程序从磁盘读回该页 → 更新页表 → 返回用户态重新执行那条访问指令(这次页已在内存,成功)。
  • syscall 是 trapread() 不是普通函数,而是 syscall 指令触发 trap 进入内核——这就是为什么"系统调用是昂贵的"(有 trap + 上下文切换)。

三、系统调用:受控的内核入口

系统调用是用户态程序合法访问内核服务的唯一通道(中断/异常除外)。完整流程:

c
// 用户程序调用 read(fd, buf, 1024)
// glibc 包装:
mov $0, %eax        // 系统调用号 SYS_read=0(x86_64)
syscall             // 触发 trap,陷入内核
// —— 此处 CPU 切换到内核态 ——
// 内核的 entry_SYSCALL_64:
//   保存用户态寄存器(SWAPGS、保存到内核栈)
//   切换到内核栈
//   查 sys_call_table[%eax] → 找到 sys_read
//   执行 sys_read(读文件/缓存到 buf)
// —— 执行完 ——
sysret              // 返回用户态,恢复寄存器
// read() 返回读到的字节数
  • 系统调用号:每个 syscall 有唯一编号(Linux x86_64:read=0, write=1, open=2, fork=57, execve=59...),通过寄存器(%eax/%rax)传递。
  • 参数传递:参数放在寄存器(%rdi/%rsi/%rdx/%r10/%r8/%r9,最多 6 个),多了则用栈。所以 syscall 不像普通函数调用那样压栈——减少拷贝。
  • POSIX 系统调用分类:进程控制(fork/exec/exit/wait)、文件(open/read/write/close/lseek/stat)、设备(ioctl)、信息(getpid/time/uname)、通信(pipe/shmget/msgsnd/socket)。

四、上下文切换的代价与优化

每次 syscall/中断都涉及用户态↔内核态切换,代价明细:

步骤开销
保存用户态寄存器(PC/SP/通用/EFLAGS)到内核栈~几十 ns
特权级切换(Ring 3→0)、CPU 流水线冲刷~几十 ns
(KPTI 开启时)切换 CR3 页表基址 + flush TLB~几百 ns 到 μs(最大头)
执行内核代码本身视操作而定
返回时反向操作同上
  • KPTI 之前:syscall 约 100-300ns;KPTI 之后(Meltdown 缓解,默认开):约 500ns-数 μs。
  • 性能优化原则
    1. 批量:一次 read 一大块(带缓冲 IO),别逐字节。
    2. 减少:用 mmap 映射文件(一次 syscall 后续直接访内存,无 syscall);用 io_uring/epoll 批量 IO;用 vDSO(部分 syscall 如 gettimeofday 不真陷入内核,在用户态执行)。
    3. 异步:异步 IO(aio/io_uring)避免阻塞等待,CPU 不空转。

五、中断处理的上下文:上半部与下半部

中断处理程序运行在中断上下文(不属于任何进程、不可睡眠、不可调度),所以必须。但有些中断要做耗时工作(如处理网络包),不能在硬中断里做。Linux 的解法是拆两半

  • 上半部(top half,硬中断):立即响应硬件,关中断,只做最紧急的(确认中断、把数据拷到队列、调度下半部),微秒级返回
  • 下半部(bottom half,softirq/tasklet/workqueue):在稍后开中断时执行耗时工作(解析网络包、唤醒进程)。softirq/tasklet 仍在中断上下文(不可睡),workqueue 在进程上下文(可睡)。

这种"立即应答 + 延迟处理"的设计,保证了高中断频率下系统不卡死。

下一步

掌握了 OS 的总览、内核架构、中断与系统调用后,下一站进入 OS 的核心资源管理——从进程与线程基础 开始,理解 CPU 如何被多任务共享。