用户态和内核态 (User Mode & Kernel Mode)
一、核心概念
什么是用户态和内核态
内核态 (Kernel Mode / Supervisor Mode / System Mode / Ring 0):
- CPU 运行的最高特权级,能访问所有硬件资源和全部内存
- 操作系统的内核代码运行在此模式
- 可以执行特权指令 (privileged instructions),如 I/O、修改寄存器、配置 MMU 等
用户态 (User Mode / Ring 3):
- CPU 运行的低特权级,只能访问受限资源
- 应用程序(浏览器、游戏、Office 等)运行在此模式
- 不能直接访问硬件,不能访问内核内存,不能执行特权指令
- 需要通过系统调用 (System Call) 才能进入内核态
一句话总结:内核态是"上帝模式",可以做任何事;用户态是"平民模式",只能按规矩办事。
二、为什么要区分用户态和内核态
1. 保护操作系统内核
- 用户程序千千万万,质量参差不齐
- 如果允许用户程序直接操作硬件,一个 bug 就会让整个系统崩溃
- 例如:用户程序直接写硬盘控制器,可能把磁盘格式化
2. 保护进程之间的隔离
- 每个进程拥有独立的虚拟地址空间
- 进程 A 看不到进程 B 的内存,更无法修改
- 防止恶意程序窃取其他程序的数据
3. 稳定性
- 用户程序崩溃,内核可以清理资源,系统不挂
- 内核崩溃,整个系统挂(著名的 Linux Kernel Panic、Windows 蓝屏)
4. 安全性
- 防止恶意软件直接控制硬件
- 所有特权操作都必须经过 OS 审核
5. 资源可控
- 内核统一管理 CPU、内存、I/O 设备
- 公平调度,避免某个程序独占资源
三、CPU 特权级详解
各架构的特权级
| 架构 | 特权级 | 模式名 |
|---|---|---|
| x86 | 4 级: Ring 0~3 | Ring 0 = 内核,Ring 3 = 用户,Ring 1/2 几乎不用 |
| ARM | 4 级: EL0~EL3 | EL0 = 用户,EL1 = 内核,EL2 = 虚拟化,EL3 = 安全(TrustZone) |
| RISC-V | 3 级: M/S/U | M = 机器,S = 监管者(内核),U = 用户 |
x86 Ring 详解 (从高到低):
- Ring 0:操作系统内核,可执行所有指令
- Ring 1:基本不用,早期为驱动设计
- Ring 2:基本不用
- Ring 3:应用程序,只能执行非特权指令
ARM Exception Level (EL):
- EL0:用户应用程序
- EL1:操作系统内核(非安全)
- EL2:Hypervisor(虚拟化)
- EL3:Secure Monitor(TrustZone 安全世界)
关键寄存器:CPL (Current Privilege Level)
- CPL:当前特权级,记录在 CS 段寄存器的低 2 位
- Ring 0 → CPL=0,Ring 3 → CPL=3
- 某些指令(如
LGDT、MOV CR0)只能在 CPL=0 时执行
关键标志
x86 通过EFLAGS 寄存器中的标志位控制某些行为:
- IOPL (I/O Privilege Level):决定能否直接执行 I/O 指令
- IF (Interrupt Flag):是否响应可屏蔽中断
- TF (Trap Flag):单步调试
- AC (Alignment Check):对齐检查
四、用户态 ↔ 内核态 切换
切换的 3 种方式
┌─────────────────┐
│ 用户态 (Ring 3) │
└─────────────────┘
↕ 切换
┌──────────────────────────┐
│ 1. 系统调用 │
│ 2. 中断 │
│ 3. 异常 │
└──────────────────────────┘
↕
┌─────────────────┐
│ 内核态 (Ring 0) │
└─────────────────┘
1. 系统调用 (System Call) — 主动进入
场景:应用程序主动请求 OS 服务。
例子:
- read() 读文件
- write() 写文件
- open() 打开文件
- malloc() 申请内存(底层走 brk/mmap)
- fork() 创建进程
- socket() 创建网络连接
实现机制:
x86 上的两种系统调用方式:
| 方式 | 指令 | 速度 | 现状 |
|---|---|---|---|
| 旧方式 | INT 0x80 |
慢 | 已废弃 |
| 快速方式 | SYSENTER / SYSCALL |
快 | 主流 |
Linux x86_64 系统调用流程:
1. 用户程序调用 glibc 包装函数,如 read()
2. glibc 把系统调用号(0 = read)放入 rax 寄存器
3. 执行 syscall 指令
4. CPU 切换到 Ring 0,跳到内核的 syscall_entry 入口
5. 内核根据 rax 中的调用号,跳到 sys_read() 实现
6. 内核执行真正的读操作
7. 返回值放回 rax
8. 执行 sysret 指令,CPU 切回 Ring 3
9. glibc 把 rax 返回给用户程序
系统调用开销:约 100-1000 个 CPU 周期,所以频繁系统调用是性能杀手。
2. 中断 (Interrupt) — 被动进入
场景:硬件通知 CPU 有事件发生。
分类:
- 硬中断 (Hardware Interrupt):
- 来自硬件设备:键盘按键、网卡收包、磁盘完成 I/O、定时器
- 由中断控制器(8259A、APIC)传递给 CPU
- 软中断 (Soft Interrupt):软件触发的中断(
INT n) - 系统调用
INT 0x80就是软中断 - BIOS 调用 INT 10h/13h/16h
- 可屏蔽中断 (Maskable Interrupt, IRQ):可以用
CLI指令关闭 - 不可屏蔽中断 (Non-Maskable Interrupt, NMI):无法关闭,系统最紧急事件(电源故障、内存校验错)
中断处理流程:
1. 硬件触发中断,中断控制器把中断号送到 CPU
2. CPU 立即停下当前指令(可能保存现场)
3. CPU 查中断向量表 (IDT),获得中断处理函数地址
4. CPU 切换到 Ring 0
5. 跳到中断处理函数执行
6. 中断处理完,执行 IRET 指令
7. CPU 恢复现场,切回 Ring 3
3. 异常 (Exception) — 被动进入
场景:CPU 执行指令时遇到错误。
分类:
- Fault (故障):可恢复,回到原指令重新执行
- 缺页异常 (Page Fault)
- 除零错误
- 段错误 (Segfault)
- Trap (陷阱):报告后继续
- 断点 (INT 3)
- 单步 (Trap Flag)
- Abort (终止):致命,无法恢复
- 机器检查异常 (Machine Check)
- 三重错误 (Triple Fault) → 通常重启
典型异常处理:
- Page Fault (缺页):
- 访问虚拟地址时,该页不在物理内存
- 内核从磁盘换入页面,然后重新执行该指令
- 用户态进程触发的页错误会进入内核,如果是合法访问,内核处理后返回用户态继续
- 非法访问(如访问空指针)→ SIGSEGV,进程被杀掉
- 除零错误:算术异常,SIGFPE,进程被杀掉
- 断点 (INT 3):调试器用,触发后陷入内核,内核通知调试器
4. 切换过程详解
进入内核态 (以 syscall 为例):
1. CPU 处于 Ring 3,执行 syscall 指令
2. CPU 自动做以下事:
a. 切换 CS/SS 段寄存器(指向内核代码/数据段)
b. 保存用户态的 RIP(下一条指令地址)到 RCX
c. 保存用户态的 RFLAGS 到 R11
d. 加载新的 RIP(MSR 寄存器 IA32_LSTAR 指向的内核入口)
e. 加载新的 RFLAGS(关闭中断 IF=0)
f. 切换到 Ring 0
3. 跳到内核入口(syscall_entry)执行
回到用户态 (以 sysret 为例):
1. 内核执行 sysret 指令
2. CPU 自动:
a. 从 RCX 恢复用户态 RIP
b. 从 R11 恢复用户态 RFLAGS
c. 切换 CS/SS 回用户态
d. 切换到 Ring 3
3. 用户程序从 syscall 后的下一条指令继续
五、内核态的进一步细分
内核态中的"用户"与"内核"
- Ring 0 = 内核态,但内核态内部也可以有"低特权"区
- Linux:
- 用户态:Ring 3
- 内核态:Ring 0
- 内核态内部没有更细的特权级(除了硬件本身 Ring 1/2)
- Windows:
- 用户态:Ring 3
- 内核态:Ring 0
- 内核有PatchGuard、Driver Signature Enforcement 等机制防止恶意内核代码
虚拟化后的更多层
┌──────────────────────┐
│ Guest OS 用户态 │ (VM Ring 3)
├──────────────────────┤
│ Guest OS 内核态 │ (VM Ring 0)
├──────────────────────┤
│ Hypervisor │ (Host Ring 0 / -1)
├──────────────────────┤
│ Host 硬件 │
└──────────────────────┘
- Guest OS 以为自己在 Ring 0
- 实际运行在 VMX Non-Root Mode
- 敏感指令触发 VM Exit,回到 Hypervisor
- Hypervisor 处理后,执行 VM Entry 让 Guest 继续
ARM TrustZone
┌──────────────────────┐ ┌──────────────────────┐
│ Normal World 用户 │ │ Secure World 用户 │
├──────────────────────┤ ├──────────────────────┤
│ Normal World OS │ │ Secure OS (TEE) │
└──────────────────────┘ └──────────────────────┘
↕
SMC 指令切换(EL3 监控)
- TEE (Trusted Execution Environment):安全世界,跑 TrustZone OS,处理密码、生物特征等敏感数据
- REE (Rich Execution Environment):普通世界,跑 Android / iOS / Linux
六、用户态与内核态的内存布局
典型 64 位 Linux 进程地址空间
高地址 0xFFFFFFFFFFFFFFFF
┌─────────────────────┐
│ 内核空间 │ ← 4GB ~ 128TB,所有进程共享
│ (Kernel Space) │
│ - 内核代码 │
│ - 内核数据 │
│ - 内核栈 │
├─────────────────────┤ ← 0x00007FFFFFFFFFFF (用户/内核边界)
│ ... 栈 (Stack) ... │ ← 高地址,向下增长
│ ... mmap 区 ... │
│ ... 堆 (Heap) ... │ ← 向上增长
│ ... BSS ... │
│ ... 数据段 ... │
│ ... 代码段 ... │
└─────────────────────┘
低地址 0x0
- 用户空间:每个进程独立,大小 128TB(只用到一部分)
- 内核空间:所有进程共享同一份映射(但通过页表保护)
进程切换时的细节
- 进程 A 切换到进程 B:
- 保存 A 的寄存器(包括 RIP、CR3 页表基址等)
- 切换 CR3 → B 的页表(硬件自动完成,仅在内核态可配置 CR3)
- 加载 B 的寄存器
- 跳到 B 上次停止的地方
- TIB / TLS:线程本地存储,每个线程独立
七、关键系统调用详解
Linux 常用系统调用
| 系统调用 | 编号 (x86_64) | 用途 | 用户态函数 |
|---|---|---|---|
read |
0 | 读文件 | read() |
write |
1 | 写文件 | write() |
open |
2 | 打开文件 | open() |
close |
3 | 关闭文件 | close() |
stat |
4 | 取文件状态 | stat() |
fstat |
5 | 取文件状态 | fstat() |
mmap |
9 | 内存映射 | mmap() |
brk |
12 | 调整堆 | brk() |
fork |
57 | 创建子进程 | fork() |
execve |
59 | 加载程序 | execve() |
exit |
60 | 进程退出 | exit() |
wait4 |
61 | 等待子进程 | waitpid() |
kill |
62 | 发信号 | kill() |
getpid |
39 | 取进程 ID | getpid() |
socket |
41 | 创建 socket | socket() |
connect |
42 | 连接 socket | connect() |
accept |
43 | 接受连接 | accept() |
sendto |
44 | 发数据 | sendto() |
recvfrom |
45 | 收数据 | recvfrom() |
ioctl |
16 | 设备控制 | ioctl() |
系统调用性能优化
为什么系统调用贵:
- 上下文切换(保存/恢复寄存器):~50 ns
- CPU 模式切换(Ring 3 → Ring 0):~50 ns
- 切换页表(可能):~50 ns
- 内核参数检查、安全检查:~几十 ns
- 真正的服务执行:几十 ns ~ μs
优化策略:
- vDSO (virtual Dynamic Shared Object):Linux 把某些无需真正进入内核的调用(如
gettimeofday)映射到用户态,避免切换 - 批处理 (Batching):用
readv/writev一次系统调用做多次操作 - io_uring (Linux 5.1+):异步 I/O,共享 ring buffer,极少进入内核
- eBPF:在内核态运行用户定义的程序,避免切换
八、用户态/内核态交互方式
1. 系统调用 (System Call)
最常用,前面已详述。
2. 中断 (Interrupt)
硬件设备用中断通知 CPU,内核响应中断,处理完回到原任务。
3. 信号 (Signal)
Linux 特有的用户态 ↔ 内核态交互:
- 内核可以给进程发信号 (kill)
- 进程间可以发信号 (kill)
- 进程自己也可以给自己发信号 (raise)
常见信号:
| 信号 | 编号 | 含义 | 默认动作 |
|---|---|---|---|
| SIGHUP | 1 | 终端挂起 | 终止 |
| SIGINT | 2 | Ctrl+C | 终止 |
| SIGQUIT | 3 | Ctrl+\ | 终止 + core |
| SIGKILL | 9 | 强制终止,不可捕获 | 终止 |
| SIGSEGV | 11 | 段错误 | 终止 + core |
| SIGTERM | 15 | 优雅终止 | 终止 |
| SIGCHLD | 17 | 子进程结束 | 忽略 |
| SIGSTOP | 19 | 暂停 | 暂停 |
| SIGCONT | 18 | 继续 | 继续 |
| SIGALRM | 14 | 定时器到时 | 终止 |
信号处理流程:
1. 内核决定给某进程发信号
2. 设置进程的信号位图
3. 当进程从内核态返回用户态时,内核检查信号位图
4. 如果有未处理信号:
- 默认动作:终止/暂停/忽略/core
- 注册了 handler:跳到用户注册的信号处理函数
- 用户处理完,通过 sigreturn 系统调用返回内核
- 内核恢复现场,继续原任务
4. 共享内存 (Shared Memory)
- 多个进程把同一段物理内存映射到各自的虚拟地址空间
- 是最快的 IPC 方式,无需进入内核
- 典型:mmap、shmget/shmat
5. 内存映射文件 (mmap)
- 把文件映射到进程地址空间
- 读写内存就是读写文件
- 适合大文件处理
6. procfs / sysfs (Linux)
/proc和/sys是内核暴露给用户态的窗口- 通过读写文件就能查询/配置内核参数
- 例如:
cat /proc/cpuinfo、echo 1 > /proc/sys/net/ipv4/ip_forward
7. netlink (Linux)
- 用户态 ↔ 内核态的双向通信机制
- 网络配置、路由、防火墙都靠它
九、内核态的关键任务
内核态处理的主要工作
| 功能 | 核心任务 |
|---|---|
| 进程管理 | 创建/销毁进程、调度、上下文切换、信号处理 |
| 内存管理 | 物理内存分配、虚拟内存映射、缺页处理、回收 |
| 文件系统 | VFS 分发、文件系统挂载、inode/目录管理、读写缓冲 |
| 设备管理 | 设备驱动、I/O 调度、中断处理、DMA |
| 网络管理 | TCP/IP 协议栈、Socket、Netfilter、网卡驱动 |
| 安全管理 | 权限检查、ACL、SELinux/AppArmor、加密 |
| 时钟/定时器 | 定时器中断、jiffies 更新、定时任务 |
| 电源管理 | 待机、休眠、CPU 调频 |
| 同步原语 | 自旋锁、信号量、RCU、内存屏障 |
内核态 vs 用户态的对比
| 维度 | 用户态 | 内核态 |
|---|---|---|
| 特权级 | Ring 3 (CPL=3) | Ring 0 (CPL=0) |
| 可访问内存 | 受限(用户空间) | 全部(用户+内核空间) |
| 可执行指令 | 非特权指令 | 所有指令 |
| 硬件访问 | 必须通过系统调用 | 直接访问 |
| 出错后果 | 进程被 kill (SIGSEGV 等) | 系统崩溃 (Kernel Panic / 蓝屏) |
| 栈空间 | MB 级 | 几 KB(必须小) |
| 代码来源 | 用户编译 | 内核编译 |
| 调试 | gdb 简单 | kgdb、printk 困难 |
| 性能 | 受保护,不能滥用硬件 | 直接操作,快但危险 |
十、内核态的安全与攻击
内核态是攻击的高价值目标
- Rootkit:恶意程序植入内核,完全控制系统
- 隐藏进程、文件、连接
- 截获系统调用,篡改返回结果
- 直接读写磁盘、网络包
- 内核漏洞利用:用户态通过漏洞进入内核态
- Dirty COW (CVE-2016-5195):mmap 竞态条件
- Dirty Pipe (CVE-2022-0847):管道缓冲区漏洞
- 各类驱动漏洞
- 侧信道攻击:Meltdown、Spectre 利用 CPU 推测执行漏洞越权读内存
内核态保护机制
- KASLR (Kernel Address Space Layout Randomization):内核代码地址随机化
- SMEP/SMAP (x86):禁止内核访问用户空间内存
- PXN/PAN (ARM):等同 SMEP/SMAP
- Driver Signature Enforcement (Windows):只允许加载签名的驱动
- Secure Boot:启动链验证
- eBPF Verifier (Linux):eBPF 程序在加载前进行形式化验证
- Hypervisor:Windows VBS、Linux KVM 提供内核态隔离
十一、调试用户态和内核态
用户态调试
- gdb:GNU 调试器,设置断点、看变量、看栈
- strace:跟踪系统调用
- ltrace:跟踪库函数调用
- valgrind:内存错误检测
strace -p <pid> # 跟踪指定进程的系统调用
strace ls -la # 跟踪 ls 的系统调用
内核态调试
- printk:内核打印,信息在 dmesg 中
- kgdb:远程 gdb,需要双机
- /proc/kmsg:内核消息缓冲
- perf / ftrace / bpftrace:Linux 内核追踪工具
- kdump:内核崩溃转储
// 在内核中
printk(KERN_INFO "my module loaded\n");
十二、操作系统实例对比
Linux
- 用户态 ↔ 内核态:
syscall/int 0x80 - 系统调用约 300+ 个,glibc 包装成 POSIX API
- 信号处理完整实现
/proc//sys暴露大量内核信息- io_uring:最新高性能异步 I/O
Windows
- 用户态 ↔ 内核态:
syscall(Windows 10+),int 0x2e(老) - Win32 API 包装成 Nt* 系统调用
- APC (Asynchronous Procedure Call):类似但更复杂的机制
- ETW (Event Tracing for Windows):内核追踪
- WDF (Windows Driver Framework):驱动开发框架
macOS (XNU)
- Mach 微内核 + BSD 层
- BSD 系统调用 + Mach 陷阱 (mach_trap)
- POSIX 兼容,信号、procfs、sysctl
- kqueue:事件通知机制
十三、CPU 模式相关概念
32 位 vs 64 位
- x86 (32 位):Ring 0~3 完整支持
- x86_64 (64 位):只实现了 Ring 0 和 Ring 3,Ring 1/2 保留
- 长模式 (Long Mode):x86_64 的 64 位运行模式
- 兼容模式 (Compatibility Mode):x86_64 运行 32 位代码的方式
实模式 vs 保护模式 vs 长模式
| 模式 | 时代 | 特点 |
|---|---|---|
| 实模式 | 8086 时代 | 16 位,1MB 地址空间,无保护,无分页 |
| 保护模式 | 80286 起 | 32 位,4GB 地址空间,有保护、有分页 |
| 长模式 | x86_64 起 | 64 位,128TB 地址空间,兼容保护模式 |
| 虚拟 8086 模式 | 保护模式内 | 模拟实模式,跑老 DOS 程序 |
OS 启动中的模式切换:
开机 → 实模式 (BIOS)
↓ 进入保护模式
↓ 启动内核
↓ 切换到长模式 (x86_64)
↓ 启动用户程序
十四、关键概念辨析
| 概念 | 含义 |
|---|---|
| 内核态 | CPU 特权级 Ring 0,可执行所有指令 |
| 用户态 | CPU 特权级 Ring 3,只能执行非特权指令 |
| 特权指令 | 只能在 Ring 0 执行的指令(如 CLI、HALT) |
| 系统调用 | 用户态进入内核态的合法入口 |
| 中断 | 硬件或软件触发的事件 |
| 异常 | CPU 执行指令时的错误 |
| 上下文切换 | 进程/线程间切换 |
| 模式切换 | Ring 3 ↔ Ring 0 切换 |
| 进程切换 | A 进程 → B 进程(均在内核态) |
| 线程切换 | A 线程 → B 线程 |
| 协程切换 | 用户态内的"伪线程"切换 |
| POSIX | Unix 系统调用标准 |
| CPL | Current Privilege Level,当前特权级 |
| DPL | Descriptor Privilege Level,描述符特权级 |
| RPL | Requestor's Privilege Level,请求者特权级 |
十五、核心要点速记
- 内核态 = Ring 0 = 上帝模式 = 可做一切
- 用户态 = Ring 3 = 平民模式 = 受限
- 3 种进入内核方式:系统调用(主动)、中断(被动硬件)、异常(被动错误)
- 3 种返回用户方式:
sysret/iret/signal return - 每种架构有不同特权级:x86 Ring 0~3,ARM EL0~EL3,RISC-V M/S/U
- 系统调用开销:约 100-1000 CPU 周期,频繁调用是性能瓶颈
- 优化方式:vDSO、io_uring、批处理、eBPF
- 进程崩溃只影响该进程;内核崩溃 = 系统挂
- Linux 进程地址空间:用户空间(独立)+ 内核空间(共享)
- 信号是用户态 ↔ 内核态的异步通知机制
- 共享内存是最快的 IPC(无需进入内核)
- Rootkit 攻击内核态 = 完全控制系统
- CPU 模式:实模式 → 保护模式 → 长模式
- 虚拟化又增加一层 Ring -1 (Hypervisor)
- TrustZone 增加 EL3 安全世界
- 关键寄存器和标志:CPL、IOPL、IF、AC