什么是用户态、内核态?如何切换的?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 基础概念掌握:面试官不只是想知道你知不知道这两个名词,更想看你是否理解为什么要区分这两种状态——考察的其实是操作系统的保护与隔离思想。

  2. 机制原理深度:能否说清楚用户态和内核态之间是怎么来回切的(系统调用、中断、异常),以及切换过程中 CPU、寄存器、栈发生了什么。

  3. 性能与工程意识:知不知道这种切换是有代价的,以及实际开发中如何减少不必要的切换(比如 epoll 为什么比 select 高效、BufferedReader 为什么能减少系统调用)。

核心答案

直接给结论:

  • 用户态(User Mode):进程运行在自己私有的地址空间里,只能访问 "用户空间" 的内存,不能直接碰硬件,也不能访问内核的代码和数据。CPU 处于低特权级(x86 上一般是 Ring 3)。

  • 内核态(Kernel Mode):操作系统内核运行的特权模式,可以访问所有内存、所有硬件资源,执行任何 CPU 指令。CPU 处于高特权级(x86 上是 Ring 0)。

  • 切换方式:从用户态 → 内核态有 三种触发方式——系统调用(主动)、异常(被动)、外部硬件中断(被动)。从内核态 → 用户态则是执行完内核逻辑后通过特殊的返回指令切回去。

简单一张表:

对比项 用户态 内核态
特权级(x86) Ring 3 Ring 0
可访问内存 只能访问本进程用户空间 全部内存空间
可执行指令 普通指令,不能执行特权指令 所有指令(含特权指令,如 I/O、关中断)
是否可直接访问硬件
运行的代码 应用程序 操作系统内核

为什么要这么设计?一句话:保护系统不被搞坏。如果用户程序能随便访问硬件、改内核数据,一个写得烂的程序(或者恶意程序)就能让整个系统崩溃。这种 "特权隔离" 的思想,是现代操作系统的根本。

用户态内核态隔离
用户态内核态隔离

深度解析

一、用一张图理解切换的全过程

用户态内核态切换
用户态内核态切换

用户态内核态切换流程
用户态内核态切换流程

上图把一次典型的 "用户态 → 内核态 → 用户态" 往返过程拆成了几个关键步骤:

  • 触发阶段:应用程序在用户态跑得好好的,遇到需要内核帮忙的事情(比如读文件、网络 I/O),就主动发起 系统调用;或者出了意外(除零、缺页),触发 异常;再或者外部硬件(时钟、网卡)打断 CPU,触发 中断
  • 切换阶段:CPU 从 Ring 3 切到 Ring 0,把当前的寄存器、程序计数器(PC)、栈指针都保存起来(一般是压到内核栈),然后切到内核栈继续执行。
  • 执行阶段:内核根据系统调用号找到对应的内核函数(比如 sys_read),执行真正的逻辑。
  • 返回阶段:内核执行完后,恢复之前保存的用户态上下文,通过 iretsysret 这类特权指令,把 CPU 特权级切回 Ring 3,应用程序接着跑。

二、三种切换触发方式

很多人答这道题只会说 "系统调用",其实完整答案是三种。

1. 系统调用(主动切换)

最常见。用户程序需要内核帮忙做事时,主动发起。典型场景:

  • 文件操作:read() / write() / open()
  • 进程操作:fork() / exec() / exit()
  • 网络操作:socket() / connect() / accept()
  • 内存操作:mmap() / brk()
  • 时间:gettimeofday() / nanosleep()

底层是怎么切的?以 x86-64 Linux 为例,应用程序调用 C 库的 read() 函数后,最终会执行一条 syscall 指令(32 位时代是 int 0x80 软中断)。这条指令一执行,CPU 立刻:

  1. 把系统调用号放进 rax 寄存器
  2. 从用户态切到内核态
  3. 跳到内核里预先注册好的入口 entry_SYSCALL_64

2. 异常(被动切换)

程序执行过程中出了 "状况",CPU 自动切换到内核态去处理。典型例子:

  • 缺页异常(Page Fault):访问的虚拟地址还没映射到物理内存,内核要帮忙分配页面
  • 除零异常:除数为 0,内核会发 SIGFPE 信号把进程干掉
  • 非法指令异常:执行了不存在的指令,同样会被信号终结

3. 外部硬件中断(被动切换)

外部设备给 CPU 发信号,要求 CPU 暂停当前工作去处理。注意,这个是 硬件 触发的,跟用户程序无关。典型例子:

  • 时钟中断:操作系统靠它来调度进程,每个进程分到的时间片用完了就被切走
  • I/O 完成中断:网卡收到包了、磁盘读完数据了,通知 CPU 来处理
  • 键盘中断:你按了一个键,CPU 暂停当前工作去响应

三、切换的成本,比你想象的大

这一点很多同学会忽略,但面试官特别爱追问。系统调用不是免费的,一次切换的成本主要包括:

开销类型 说明
寄存器上下文保存/恢复 用户态和内核态的寄存器都要存一遍、再恢复一遍
栈切换 从用户栈切到内核栈,开销不大但绝对存在
CPU 流水线 flush 特权级切换会清掉 CPU 流水线里的指令
指令缓存失效 内核代码可能不在 icache 里,要重新加载
TLB 部分失效 如果切换还伴随着地址空间切换(比如进程切换),TLB 可能要刷新
安全检查 内核要校验用户传进来的参数是否合法

具体多少?纯特权级切换的硬件开销大概 100 纳秒量级,加上内核执行逻辑,一次完整系统调用通常在几百纳秒量级。听起来很短,但如果你在高并发场景下每个请求都做几十次系统调用,这点开销累加起来就吃人了。

系统调用切换成本
系统调用切换成本

这也是为什么很多高性能框架都在拼命减少系统调用次数:

  • epoll 替代 select/poll:一次系统调用处理大量连接,不用每次都遍历所有 fd
  • Java NIO 的 DirectByteBuffer:直接在内核和用户态之间共享一块内存,省掉数据拷贝的那次切换
  • BufferedReader / BufferedWriter:缓冲区攒一波再读写,减少 read/write 的次数
  • Java 19 虚拟线程(Loom):本质上是把大量用户级线程映射到少量 OS 线程上,减少线程切换带来的内核态开销

四、一段代码感受一下系统调用

下面这段 Java 代码看起来人畜无害,背后其实经历了多次用户态/内核态切换。

import java.io.FileInputStream;
import java.io.IOException;

public class UserKernelDemo {
    public static void main(String[] args) {
        // open 系统调用:进入内核态,打开文件
        try (FileInputStream fis = new FileInputStream("test.txt")) {
            byte[] buf = new byte[1024];
            // read 系统调用:进入内核态,把数据从内核缓冲区拷贝到用户缓冲区
            int n = fis.read(buf);
            System.out.println("读到字节数:" + n);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // close 系统调用:进入内核态,释放文件描述符
    }
}

这段代码至少触发了三次系统调用:openreadclose,每次都要经历完整的用户态 → 内核态 → 用户态往返。

面试高频追问

  1. 追问一:用户态和内核态为什么要分离开?
  • 为了保护。操作系统必须保证用户程序不能直接操作硬件、不能改内核数据、不能干扰其他进程。特权级机制把 "危险动作" 限制在内核态,用户程序要用就发系统调用,让内核代劳。
  1. 追问二:系统调用的过程具体是什么样的?
  • 应用程序调用库函数(如 glibc 的 read)→ 库函数把系统调用号放到寄存器 → 执行 syscall 指令 → CPU 切换到内核态,跳到内核入口 → 内核根据调用号查表执行对应函数 → 执行完毕通过 sysret/iret 返回用户态。
  1. 追问三:系统调用为什么慢?
  • 主要是上下文切换的开销——保存恢复寄存器、栈切换、CPU 流水线 flush、可能的 TLB 失效。还有内核要对用户参数做安全校验。频繁系统调用的累积成本很高。
  1. 追问四:用户态切换到内核态,栈怎么变?
  • 每个进程都有两个栈:用户栈和内核栈。切换到内核态时,CPU 会自动切到该进程的内核栈(栈指针从 TSS 之类的结构里取出来),用户栈指针会被保存起来,等切回用户态时再恢复。
  1. 追问五:线程切换和系统调用是一回事吗?
  • 不是。系统调用是用户态主动进入内核态,不一定发生线程切换。线程切换是操作系统把当前线程挂起、调度另一个线程执行,往往伴随着进程地址空间切换(开销更大)。

常见面试变体

  • "为什么系统调用开销大?"
  • "什么是特权级?x86 有几个特权级?"
  • "进程切换和模式切换(用户态/内核态切换)有什么区别?"
  • "Java 程序里有哪些场景会触发系统调用?"
  • "epoll 为什么比 select 快?"(背后涉及到减少系统调用次数)

记忆口诀

"两态三种切,主动被动分清楚"

  • 两态:用户态(Ring 3)、内核态(Ring 0)
  • 三种切:系统调用(主动)、异常(被动)、中断(被动)

切换成本记一句:"保存恢复上下文,栈要切、流水线要 flush"

总结

一句话总结:用户态和内核态是 CPU 的两种特权级,前者权限低只能跑用户代码,后者权限高能管硬件和内存。两者之间靠系统调用、异常、中断三种方式切换,每次切换都有寄存器保存、栈切换、流水线 flush 这些开销。把这套机制和高性能编程里 "减少系统调用" 的实践联系起来,面试官基本就知道你是真懂,而不是背了个名词。