跳转到内容

用户态和内核态切换开销详解(了解即可)

草稿难度:中级#操作系统

原文:CSDN(历史文章导入,当前状态为草稿)

如果是Java开发,不需要了解特别深度,当个阅读资料即可,大概明白里面最大的开销是TLB,缓存失效和上下文保存于恢复即可.

CPU周期是CPU执行一条最简单指令所需的基本时间单位。

举例:

  • 2GHz的CPU:1个周期 = 0.5纳秒
  • 一次态切换1000周期 = 500纳秒 = 0.5微秒
用户程序需要操作系统帮助时:
├─ 系统调用(read、write、open等)
├─ 硬件中断(键盘输入、网络数据到达)
├─ 异常处理(页错误、除零错误)
└─ 信号处理(进程间通信)

总开销:约 1000-1500 CPU周期 (0.5-1.5微秒)
┌─────────────────────────────────────────┐
│ 开销分布饼图 │
├─────────────────────────────────────────┤
│ │
│ TLB刷新 ████████████ 30-35% │
│ 缓存失效 ████████████ 30-35% │
│ 上下文保存 ████████ 15-20% │
│ 安全检查 ██████ 10-15% │
│ 其他 ████ 5-10% │
│ │
└─────────────────────────────────────────┘
操作 CPU周期 时间(@2GHz) 相对开销
寄存器访问 1 0.5ns
L1缓存访问 3 1.5ns
函数调用 10-20 5-10ns 10-20×
L2缓存访问 15 7.5ns 15×
主内存访问 200 100ns 200×
态切换 1000-1500 0.5-1.5μs 1000-1500×
进程切换 3000-5000 1.5-2.5μs 3000-5000×

关键结论:一次态切换 = 500次函数调用!


3.1 上下文保存与恢复(200-300周期)

Section titled “3.1 上下文保存与恢复(200-300周期)”

上下文是程序运行时的“快照”,包括:

  • 程序计数器(PC/EIP):当前执行到哪行代码
  • 栈指针(SP/ESP):栈顶位置
  • 通用寄存器(EAX, EBX等):临时数据
  • 状态寄存器(EFLAGS):CPU状态标志
场景:读取文件
┌──────────────────────────────────────┐
│ 用户程序执行: │
│ int n = read(fd, buf, 100); │
│ │
│ 寄存器状态: │
│ EAX = 0x1234 (临时计算结果) │
│ EBX = fd (文件描述符) │
│ ECX = buf (缓冲区地址) │
│ EIP = 0x401000 (当前指令位置) │
└──────────────────────────────────────┘
│ 调用系统调用
┌──────────────────────────────────────┐
│ 内核执行 sys_read(): │
│ │
│ 如果不保存,寄存器会被内核代码覆盖! │
│ EAX = 内核计算结果 (覆盖了0x1234) │
│ EBX = 内核临时数据 │
│ ... │
└──────────────────────────────────────┘
│ 返回用户态
┌──────────────────────────────────────┐
│ 问题:返回到哪里?EAX的值是什么? │
│ │
│ 必须恢复之前保存的上下文! │
└──────────────────────────────────────┘

x86架构(32位):

基本寄存器(8个):
├─ 通用寄存器:EAX, EBX, ECX, EDX
├─ 索引寄存器:ESI, EDI
├─ 栈寄存器:EBP, ESP
└─ 指令指针:EIP
段寄存器(6个):
├─ 代码段:CS
├─ 数据段:DS, ES
├─ 栈段:SS
└─ 附加段:FS, GS
控制寄存器:
└─ 状态标志:EFLAGS

x86-64架构(64位)更多:

通用寄存器:16个 (RAX, RBX, ..., R15)
段寄存器:6个
浮点寄存器:16个 XMM寄存器 (每个128位)
控制寄存器:CR0, CR2, CR3, CR4
调试寄存器:8个
总计:50+ 个寄存器
保存过程:
┌────────────────────────────────┐
│ PUSH EAX → 内核栈 (10周期) │
│ PUSH EBX → 内核栈 (10周期) │
│ PUSH ECX → 内核栈 (10周期) │
│ ... (10个寄存器) │
│ │
│ 总计:10 × 10 = 100周期 │
└────────────────────────────────┘
恢复过程:
┌────────────────────────────────┐
│ POP ECX ← 内核栈 (10周期) │
│ POP EBX ← 内核栈 (10周期) │
│ POP EAX ← 内核栈 (10周期) │
│ ... (10个寄存器) │
│ │
│ 总计:10 × 10 = 100周期 │
└────────────────────────────────┘
保存 + 恢复 = 200周期
x86-64更多 = 300周期

3.2 TLB刷新(300-500周期)⚠️ 最大开销

Section titled “3.2 TLB刷新(300-500周期)⚠️ 最大开销”

TLB(Translation Lookaside Buffer)是虚拟地址到物理地址的转换缓存

为什么需要TLB?

现代操作系统使用虚拟内存:

  • 程序看到的地址(虚拟地址):0x7fff1234
  • 实际硬件地址(物理地址):0x12345678
  • 每次访问内存都需要转换

没有TLB(慢):

访问地址:0x7fff1234
┌─────────────────────────┐
│ 1. 查页目录 (内存访问) │ ← 100周期
│ → 找到页表地址 │
└────────┬────────────────┘
┌─────────────────────────┐
│ 2. 查页表 (内存访问) │ ← 100周期
│ → 找到物理页地址 │
└────────┬────────────────┘
┌─────────────────────────┐
│ 3. 计算物理地址 │ ← 10周期
│ → 0x12345678 │
└─────────────────────────┘
总耗时:200+ 周期

有TLB(快):

访问地址:0x7fff1234
┌─────────────────────────┐
│ 查TLB缓存 │
│ 0x7fff1234 → 0x12345678 │ ← 1周期!
└─────────────────────────┘
总耗时:1周期
速度提升:200倍!
┌───────────────────────────────────┐
│ TLB 缓存 │
├───────────────────────────────────┤
│ 虚拟页号 │ 物理页号 │ 有效位 │
├───────────────────────────────────┤
│ 0x7fff │ 0x1234 │ 1 │
│ 0x7ffe │ 0x5678 │ 1 │
│ 0x401 │ 0xabcd │ 1 │
│ ... │ ... │ ... │
│ (64项) │ │ │
└───────────────────────────────────┘
通常只有64-1024项缓存

切换前(用户态):

TLB缓存的是用户空间映射:
┌─────────────────────────┐
│ 0x401000 → 物理地址A │
│ 0x7fff1234 → 物理地址B │
│ 0x402000 → 物理地址C │
│ ... │
└─────────────────────────┘

切换到内核态:

需要访问内核空间地址:
┌─────────────────────────┐
│ 0xc0100000 (内核代码) │
│ 0xc0200000 (内核数据) │
│ ... │
└─────────────────────────┘
但TLB里没有这些映射!
必须刷新TLB,加载内核空间映射
刷新后的影响:
第1次访问:TLB未命中 → 查页表 (200周期)
第2次访问:TLB未命中 → 查页表 (200周期)
第3次访问:TLB未命中 → 查页表 (200周期)
...
第64次访问:TLB未命中 → 查页表 (200周期)
前64次访问额外开销:64 × 200 = 12,800周期!
平摊到每次切换:约 300-500周期
┌─────────────────────────────────────────────┐
│ TLB命中 vs 未命中 │
├─────────────────────────────────────────────┤
│ │
│ 正常访问(TLB命中): │
│ ████ 1周期 │
│ │
│ 切换后访问(TLB未命中): │
│ ████████████████████████████████████████ │
│ ████████████████████████████████████████ │
│ 200周期 │
│ │
│ 200倍差距! │
└─────────────────────────────────────────────┘

3.3 缓存失效(300-500周期)⚠️ 第二大开销

Section titled “3.3 缓存失效(300-500周期)⚠️ 第二大开销”
┌────────────────────────────────────┐
│ CPU核心 │
│ ┌──────────────────────────────┐ │
│ │ L1 Cache (32-64KB) │ │
│ │ ├─ L1d (数据缓存) │ │
│ │ └─ L1i (指令缓存) │ │
│ │ 访问速度:1-3周期 │ │
│ │ 命中率:>95% │ │
│ └──────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────────┐ │
│ │ L2 Cache (256KB-1MB) │ │
│ │ 统一缓存(指令+数据) │ │
│ │ 访问速度:10-20周期 │ │
│ │ 命中率:>90% │ │
│ └──────────────────────────────┘ │
└────────────┬───────────────────────┘
┌────────────────────────────────────┐
│ L3 Cache (8-32MB) │
│ 多核共享 │
│ 访问速度:40-50周期 │
│ 命中率:>80% │
└────────────┬───────────────────────┘
┌────────────────────────────────────┐
│ 主内存 RAM (8-32GB) │
│ 访问速度:200-300周期 │
│ 总是命中(但慢) │
└────────────────────────────────────┘

场景分析:

用户态程序执行:
┌────────────────────────────────┐
│ L1缓存内容: │
├────────────────────────────────┤
│ • read() 函数代码 │
│ • printf() 函数代码 │
│ • buffer[] 数组数据 │
│ • 其他用户数据 │
└────────────────────────────────┘
缓存命中率:95%
速度:很快
↓ 系统调用
内核态执行:
┌────────────────────────────────┐
│ L1缓存内容(被替换): │
├────────────────────────────────┤
│ • sys_read() 内核代码 │
│ • 文件系统代码 │
│ • 设备驱动代码 │
│ • 内核数据结构 │
└────────────────────────────────┘
用户态数据被挤出缓存!
↓ 返回用户态
用户态继续执行:
┌────────────────────────────────┐
│ 需要访问: │
│ • read() 函数 ← 缓存未命中! │
│ • printf() 函数 ← 缓存未命中! │
│ • buffer[] 数组 ← 缓存未命中! │
└────────────────────────────────┘
需要从主内存重新加载
每次:200周期
时间线:
─────────────────────────────────────────────→
用户态 内核态 用户态
│ │ │
│ L1缓存: │ L1缓存: │ L1缓存:
│ [用户代码A] │ [内核代码X] │ [用户代码A] ← 重新加载
│ [用户代码B] │ [内核代码Y] │ [用户代码B] ← 重新加载
│ [用户数据1] │ [内核数据M] │ [用户数据1] ← 重新加载
│ [用户数据2] │ [内核数据N] │ [用户数据2] ← 重新加载
│ │ │
│ 命中率:95% │ 命中率:30% │ 命中率:50% (恢复中)
│ ────快速──── │ ──部分慢── │ ──较慢──
假设L1缓存大小:32KB
缓存行大小:64字节
总缓存行数:32KB ÷ 64B = 512行
态切换影响:
├─ 80%缓存行失效:512 × 80% = 410行
├─ 每行重新加载:200周期
└─ 总开销:410 × 200 = 82,000周期
但这是累积开销,分摊到多次内存访问
单次切换直接摊销:约 300-500周期
// 用户程序
void process_data() {
char buffer[1024];
// 第一次调用:缓存热(命中率高)
read(fd, buffer, 1024); // 快
process(buffer); // 快(buffer在缓存中)
// 第二次调用:缓存被污染
read(fd, buffer, 1024); // 慢(态切换污染缓存)
process(buffer); // 慢(buffer被挤出缓存)
}
性能对比:
- 缓存命中:1-3周期/访问
- 缓存未命中:200周期/访问
- 速度差:100倍!

内核必须防止恶意或错误的用户程序:

  • 访问非法内存
  • 操作无权限的文件
  • 传递非法参数
  • 破坏系统稳定性
系统调用:read(fd, buf, count)
│ │ │
│ │ └─→ 需要检查
│ └─────→ 需要检查
└──────────→ 需要检查

详细检查流程:

1. 文件描述符检查(20-30周期)
┌────────────────────────────────┐
│ if (fd < 0 || fd >= MAX_FDS) │
│ return -EBADF; │
│ │
│ file = current->files[fd]; │
│ if (!file) │
│ return -EBADF; │
└────────────────────────────────┘
2. 内存地址检查(50-80周期)
┌────────────────────────────────┐
│ // buf在用户空间吗? │
│ if (buf >= KERNEL_START) │
│ return -EFAULT; │
│ │
│ // 缓冲区范围合法吗? │
│ if (!access_ok(buf, count)) │
│ return -EFAULT; │
└────────────────────────────────┘
3. 权限检查(30-40周期)
┌────────────────────────────────┐
│ // 文件是否可读? │
│ if (!(file->f_mode & FMODE_READ))│
│ return -EBADF; │
│ │
│ // 进程有权限吗? │
│ if (!inode_permission(inode)) │
│ return -EACCES; │
└────────────────────────────────┘
4. 参数合理性检查(10-20周期)
┌────────────────────────────────┐
│ // count太大吗? │
│ if (count > MAX_RW_COUNT) │
│ count = MAX_RW_COUNT; │
│ │
│ // count溢出吗? │
│ if (count + pos < 0) │
│ return -EINVAL; │
└────────────────────────────────┘
总计:100-200周期
// Linux内核 fs/read_write.c (简化版)
asmlinkage long sys_read(unsigned int fd,
char __user *buf,
size_t count)
{
struct fd f;
ssize_t ret = -EBADF;
// ① 检查文件描述符
f = fdget_pos(fd); // 20周期
if (!f.file)
goto out;
// ② 检查内存地址
if (!access_ok(VERIFY_WRITE, // 50周期
buf, count))
goto out_fput;
// ③ 检查文件权限
if (!(f.file->f_mode & // 10周期
FMODE_READ))
goto out_fput;
// ④ 检查参数
if (count > MAX_RW_COUNT) // 5周期
count = MAX_RW_COUNT;
// ⑤ 执行实际读取
ret = vfs_read(f.file, buf,
count, &pos);
out_fput:
fdput_pos(f);
out:
return ret;
}
// 恶意用户程序尝试攻击
char *evil_buf = (char*)0xc0000000; // 内核地址!
read(fd, evil_buf, 1024); // 尝试写入内核空间
内核检查:
┌────────────────────────────────┐
│ if (buf >= KERNEL_START) │
│ return -EFAULT; ← 拒绝! │
└────────────────────────────────┘
// 另一个攻击:整数溢出
size_t huge_count = 0xFFFFFFFF;
read(fd, buf, huge_count);
内核检查:
┌────────────────────────────────┐
│ if (count > MAX_RW_COUNT) │
│ count = MAX_RW_COUNT; │
│ ← 限制到安全范围 │
└────────────────────────────────┘
没有安全检查的后果:
1. 内存破坏
用户程序:write(fd, 0xc0000000, 1024)
结果:覆盖内核代码 → 系统崩溃
2. 权限提升
恶意程序:open("/etc/shadow", O_RDWR)
结果:修改密码文件 → 获取root权限
3. 拒绝服务
用户程序:read(fd, buf, 0x7FFFFFFF)
结果:消耗所有内存 → 系统卡死
安全检查的开销是必须的代价!

特权指令是只能在内核态(Ring 0)执行的CPU指令。

特权指令分类:

1. 系统控制指令
├─ MOV CR3, reg (切换页表)
├─ LGDT/LIDT (加载描述符表)
└─ LMSW (加载机器状态字)
2. 中断控制指令
├─ CLI/STI (禁用/启用中断)
├─ LIDT (加载中断描述符表)
└─ INT n (触发中断)
3. I/O指令
├─ IN/OUT (端口I/O)
└─ INS/OUTS (字符串I/O)
4. 其他
├─ HLT (停机)
└─ INVLPG (刷新TLB)

普通指令:

MOV EAX, EBX (寄存器到寄存器)
├─ 权限检查:无
├─ 执行时间:1周期
└─ 副作用:无

特权指令:

MOV CR3, EAX (切换页表)
├─ 权限检查:20周期
│ └─ 检查CPL是否为0
├─ 执行时间:10周期
│ └─ 修改控制寄存器
└─ 副作用:100周期
├─ 刷新TLB
├─ 刷新流水线
└─ 更新内存管理单元
总计:130周期

进入内核态:

; 用户程序执行
MOV EAX, 3 ; fd = 3 (普通指令, 1周期)
MOV ECX, buffer ; buf (普通指令, 1周期)
MOV EDX, 100 ; count (普通指令, 1周期)
INT 0x80 ; 系统调用 (特权指令, 50周期)
; ↑ 触发态切换
; INT 0x80 做了什么:
; 1. 检查权限 (10周期)
; 2. 保存用户态SS:ESP (10周期)
; 3. 从TSS加载内核栈 (20周期)
; 4. 保存用户态CS:EIP (10周期)
; 5. 跳转到中断处理程序 (10周期)
; 总计:60周期

内核态执行:

; 内核代码可以执行特权指令
CLI ; 禁用中断 (20周期)
MOV CR3, EAX ; 切换页表 (150周期,如果需要)
OUT DX, AL ; I/O操作 (100周期)
STI ; 启用中断 (20周期)

返回用户态:

IRET ; 中断返回 (特权指令, 50周期)
; IRET 做了什么:
; 1. 从栈弹出EIP (10周期)
; 2. 从栈弹出CS (10周期)
; 3. 检查特权级切换 (10周期)
; 4. 从栈弹出EFLAGS (10周期)
; 5. 如果切换到Ring 3:
; - 从栈弹出ESP (10周期)
; - 从栈弹出SS (10周期)
; 总计:60周期
指令 类型 周期 原因
MOV EAX, EBX 普通 1 简单寄存器传输
ADD EAX, 10 普通 1 简单运算
JMP label 普通 1-2 简单跳转
INT 0x80 特权 50 切换栈、保存状态
IRET 特权 50 恢复状态、切换栈
CLI/STI 特权 20 修改中断标志
MOV CR3 特权 150 切换页表+刷新TLB
IN/OUT 特权 100 访问I/O端口

Intel和AMD引入了更快的系统调用指令:

传统方式(INT 0x80):
├─ 执行时间:50-60周期
├─ 需要查中断向量表
└─ 通用但较慢
现代方式(SYSCALL/SYSRET):
├─ 执行时间:25-30周期
├─ 直接跳转,无需查表
└─ 专为系统调用优化
性能提升:2倍

SYSCALL指令:

; x86-64 系统调用
MOV RAX, 0 ; 系统调用号:read
MOV RDI, 3 ; 参数1:fd
MOV RSI, buffer ; 参数2:buf
MOV RDX, 100 ; 参数3:count
SYSCALL ; 快速系统调用 (25周期)
; SYSCALL 做了什么:
; 1. RIP → RCX (保存返回地址)
; 2. RFLAGS → R11 (保存标志)
; 3. MSR_LSTAR → RIP (跳转到内核)
; 4. CPL: 3 → 0 (切换特权级)
; 无需访问内存!更快!

每个进程有自己的虚拟地址空间,互不干扰。

地址空间布局(32位Linux):

0xFFFFFFFF ┌─────────────────┐
│ 内核空间 │ 1GB
│ (所有进程共享) │
0xC0000000 ├─────────────────┤
│ 栈 │ ↓ 向下增长
0xBFFFFFFF │ ↓ │
│ ... │
│ ... │
│ ↑ │
│ 堆 │ ↑ 向上增长
0x08048000 ├─────────────────┤
│ .data (数据段) │
│ .text (代码段) │
0x00400000 ├─────────────────┤
│ 保留 │
0x00000000 └─────────────────┘

进程隔离:

进程A的视角 进程B的视角
┌─────────────────┐ ┌─────────────────┐
│ 0x00400000 │ │ 0x00400000 │
│ 进程A的代码 │ │ 进程B的代码 │
│ │ │ │
│ 0x7fff1234 │ │ 0x7fff1234 │
│ 进程A的栈 │ │ 进程B的栈 │
└─────────────────┘ └─────────────────┘
│ │
└──────────┬───────────────┘
┌──────────────┐
│ 物理内存: │
│ A → 0x1000 │
│ B → 0x5000 │
└──────────────┘
CR3寄存器:页目录基址寄存器
├─ 存储:当前页表的物理地址
└─ 作用:指示CPU如何转换虚拟地址
进程A:CR3 = 0x00001000
┌────────────────┐
│ 进程A的页表 │
│ 0x401000 → ... │
│ 0x7fff... → ...│
└────────────────┘
进程B:CR3 = 0x00002000
┌────────────────┐
│ 进程B的页表 │
│ 0x401000 → ... │
│ 0x7fff... → ...│
└────────────────┘

情况1:系统调用(同一进程)

用户态 → 内核态(同一进程)
用户态:
├─ CR3 = 0x1000 (进程A的页表)
└─ 访问用户空间 + 内核空间
内核态:
├─ CR3 = 0x1000 (不变!)
└─ 访问用户空间 + 内核空间
无需切换CR3!
开销:0周期

为什么不用切换?

┌────────────────────────────┐
│ 虚拟地址空间(4GB) │
├────────────────────────────┤
│ 0xFFFFFFFF │
│ 内核空间 (1GB) │ ← 所有进程共享
│ 映射相同 │ ← 相同的页表项
│ 0xC0000000 │
├────────────────────────────┤
│ 用户空间 (3GB) │ ← 每个进程独立
│ 进程A的映射 │
│ 0x00000000 │
└────────────────────────────┘
内核空间在所有进程页表中映射相同
所以不需要切换CR3!

情况2:进程切换

进程A → 进程B
进程A:
├─ CR3 = 0x1000
└─ 页表指向进程A的物理页
切换:
├─ MOV CR3, 0x2000 (150周期)
└─ 副作用:刷新整个TLB!
进程B:
├─ CR3 = 0x2000
└─ 页表指向进程B的物理页
开销:150周期 + TLB刷新

传统问题:

每次切换CR3 → 刷新整个TLB → 性能下降

PCID优化(Process-Context Identifier):

TLB条目增加进程ID标签:
旧TLB:
┌──────────────────┐
│ 虚拟地址 │ 物理地址│
├──────────────────┤
│ 0x401000 │ 0x1234 │
└──────────────────┘
↑ 切换进程必须刷新
新TLB(带PCID):
┌─────────────────────────┐
│ PCID│虚拟地址 │ 物理地址│
├─────────────────────────┤
│ 1 │ 0x401000│ 0x1234 │ ← 进程A
│ 2 │ 0x401000│ 0x5678 │ ← 进程B
└─────────────────────────┘
↑ 可以共存!无需刷新
性能提升:减少30-50%的TLB未命中

态切换过程中,CPU处于不一致状态

  • 正在切换栈
  • 正在保存寄存器
  • 正在修改特权级

如果此时发生中断 → 系统崩溃!

时间线:
────────────────────────────────────────────→
用户态 用户态
│ │
│ INT 0x80 │
▼ ▼
┌──────────────────────────────────────┐
│ 内核态 │
│ │
│ CLI ← 禁用中断 │
│ ├─ 保存用户上下文 │
│ ├─ 切换栈 │
│ ├─ 修改特权级 │
│ ├─ ... │
│ └─ 执行系统调用 │
│ STI ← 启用中断 │
│ │
└──────────────────────────────────────┘
↑ ↑
│ 这段时间:中断被延迟 │
└──────────────────────────────────┘
约 100-200 周期
(0.05-0.1微秒)
中断源:
┌────────────────────────────────┐
│ 1. 时钟中断 (每1ms) │ ← 延迟
│ 2. 网络数据包到达 │ ← 延迟
│ 3. 磁盘I/O完成 │ ← 延迟
│ 4. 键盘/鼠标输入 │ ← 延迟
│ 5. 其他硬件中断 │ ← 延迟
└────────────────────────────────┘
│ 中断被禁用期间
│ 这些事件被暂时忽略
┌────────────────────────────────┐
│ 待处理中断队列 │
│ [网络] [键盘] [磁盘] ... │
└────────────────────────────────┘
│ STI (启用中断)
┌────────────────────────────────┐
│ 立即处理所有待处理中断 │
└────────────────────────────────┘

对延迟的影响:

正常情况:
网络包到达 → 立即中断 → 0.001ms后处理
中断禁用期间:
网络包到达 → 等待中断启用 → 0.1ms后处理
↑ 延迟增加100倍!

对实时性的影响:

实时系统要求:
├─ 硬实时:必须在截止时间前响应
│ └─ 例:工业控制、医疗设备
└─ 软实时:尽量快速响应
└─ 例:音视频播放、游戏
中断禁用影响:
├─ 最大延迟:100-200周期 (0.1微秒)
├─ 对硬实时:可能不可接受
└─ 对软实时:通常可以接受
// 音频播放场景
void audio_interrupt_handler() {
// 每10ms调用一次,填充音频缓冲区
fill_audio_buffer();
}
正常情况:
10.000ms → 中断 → 处理
20.000ms → 中断 → 处理
30.000ms → 中断 → 处理
声音流畅
系统调用频繁时:
10.000ms → 中断 → 处理
20.050ms → 中断 → 处理 (延迟0.05ms)
30.100ms → 中断 → 处理 (延迟0.1ms)
可能出现爆音或卡顿
中断分级:
┌─────────────────────────────────┐
│ 不可屏蔽中断 (NMI) │ ← 总是执行
├─────────────────────────────────┤
│ 高优先级中断 │ ← 部分可屏蔽
├─────────────────────────────────┤
│ 普通中断 │ ← CLI屏蔽
└─────────────────────────────────┘
关键中断(如NMI)即使在CLI期间也能执行

// 测试代码
#include <stdio.h>
#include <unistd.h>
#include <time.h>
// 普通函数
int normal_function(int a, int b, int c) {
return a + b + c;
}
// 测试函数调用
void test_function_call() {
for (int i = 0; i < 1000000; i++) {
normal_function(1, 2, 3);
}
}
// 测试系统调用
void test_syscall() {
int fd = open("/dev/null", O_WRONLY);
char buf[1];
for (int i = 0; i < 1000000; i++) {
write(fd, buf, 0); // 写0字节
}
close(fd);
}
int main() {
clock_t start, end;
// 测试函数调用
start = clock();
test_function_call();
end = clock();
printf("函数调用: %f 秒\n",
(double)(end - start) / CLOCKS_PER_SEC);
// 测试系统调用
start = clock();
test_syscall();
end = clock();
printf("系统调用: %f 秒\n",
(double)(end - start) / CLOCKS_PER_SEC);
return 0;
}

测试结果(Intel i7, 2.6GHz):

函数调用: 0.002 秒 (每次 2 纳秒)
系统调用: 0.500 秒 (每次 500 纳秒)
性能差距:250倍!
// 低效:频繁系统调用
void inefficient_write() {
int fd = open("test.txt", O_WRONLY);
char data[1000][4];
for (int i = 0; i < 1000; i++) {
write(fd, data[i], 4); // 1000次系统调用
}
close(fd);
}
// 高效:批量操作
void efficient_write() {
int fd = open("test.txt", O_WRONLY);
char data[4000];
write(fd, data, 4000); // 1次系统调用
close(fd);
}

性能对比:

低效方式:
├─ 系统调用次数:1000次
├─ 态切换开销:1000 × 1000周期 = 100万周期
├─ 实际I/O:4000字节
└─ 总耗时:~0.5毫秒
高效方式:
├─ 系统调用次数:1次
├─ 态切换开销:1000周期
├─ 实际I/O:4000字节
└─ 总耗时:~0.001毫秒
性能提升:500倍!
// 处理HTTP请求
void handle_request() {
char request[4096];
char response[4096];
// 1. 接收请求
read(socket, request, 4096); // 系统调用1
// 2. 处理请求(用户态,快)
process_request(request);
// 3. 读取文件
int fd = open("index.html", O_RDONLY); // 系统调用2
read(fd, response, 4096); // 系统调用3
close(fd); // 系统调用4
// 4. 发送响应
write(socket, response, 4096); // 系统调用5
}
系统调用次数:5次
态切换开销:5 × 1000周期 = 5000周期 ≈ 2.5微秒
如果每秒处理10000个请求:
开销:10000 × 2.5微秒 = 25毫秒 = CPU 2.5%

优化后(使用sendfile):

void handle_request_optimized() {
char request[4096];
read(socket, request, 4096); // 系统调用1
process_request(request);
int fd = open("index.html", O_RDONLY); // 系统调用2
sendfile(socket, fd, NULL, 4096); // 系统调用3(零拷贝)
close(fd); // 系统调用4
}
系统调用次数:4次(减少20%)
且sendfile在内核态直接传输,无需用户态缓冲
性能提升:30-50%
// 查询1000条记录
// 低效方式:逐条读取
void query_inefficient() {
for (int i = 0; i < 1000; i++) {
lseek(fd, i * 1024, SEEK_SET); // 系统调用
read(fd, buffer, 1024); // 系统调用
process_record(buffer);
}
}
// 系统调用:2000次
// 开销:2000 × 1000周期 = 200万周期 ≈ 1毫秒
// 高效方式:批量读取
void query_efficient() {
char *big_buffer = malloc(1024 * 1000);
read(fd, big_buffer, 1024 * 1000); // 系统调用1次
for (int i = 0; i < 1000; i++) {
process_record(big_buffer + i * 1024);
}
}
// 系统调用:1次
// 开销:1000周期 ≈ 0.0005毫秒
性能提升:2000倍!
# 统计系统调用次数和耗时
strace -c ./program
输出示例:
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
45.23 0.000523 10 51 read
28.67 0.000331 8 41 write
12.45 0.000144 12 12 open
8.21 0.000095 8 12 close
5.44 0.000063 21 3 mmap
------ ----------- ----------- --------- --------- ----------------
100.00 0.001156 119 total
# 测量系统调用开销
perf stat -e 'syscalls:sys_enter_*' ./program
输出示例:
Performance counter stats for './program':
1,234 syscalls:sys_enter_read
567 syscalls:sys_enter_write
123 syscalls:sys_enter_open
0.523456789 seconds time elapsed
0.234567890 seconds user
0.289012345 seconds sys
分析:
- 用户态时间:0.23秒
- 内核态时间:0.29秒
- 内核态占比:55%(系统调用太多!)

// ❌ 错误:频繁小量写入
for (int i = 0; i < 1000; i++) {
write(fd, &data[i], sizeof(int)); // 1000次系统调用
}
// ✅ 正确:批量写入
write(fd, data, 1000 * sizeof(int)); // 1次系统调用
// ❌ 错误:直接I/O
int fd = open("file.txt", O_WRONLY);
for (int i = 0; i < 1000; i++) {
write(fd, buffer, 1); // 每字节一次系统调用
}
// ✅ 正确:使用标准I/O库(自动缓冲)
FILE *fp = fopen("file.txt", "w");
for (int i = 0; i < 1000; i++) {
fputc(buffer[i], fp); // 库内部缓冲,减少系统调用
}
fclose(fp); // 最后flush一次

工作原理:

用户程序
│ fputc() × 1000 (用户态函数调用)
┌─────────────────┐
│ stdio库缓冲区 │ 8KB
│ [数据累积...] │
└────────┬────────┘
│ 缓冲区满
│ write() × 1 (系统调用)
内核/磁盘
// ❌ 多次系统调用
write(fd, header, header_len);
write(fd, body, body_len);
write(fd, footer, footer_len);
// ✅ 一次向量I/O
struct iovec iov[3];
iov[0].iov_base = header;
iov[0].iov_len = header_len;
iov[1].iov_base = body;
iov[1].iov_len = body_len;
iov[2].iov_base = footer;
iov[2].iov_len = footer_len;
writev(fd, iov, 3); // 1次系统调用写3个缓冲区
// ❌ 传统方式
char buf[4096];
int fd = open("large_file.dat", O_RDONLY);
while (read(fd, buf, 4096) > 0) { // 多次系统调用
process(buf);
}
close(fd);
// ✅ 内存映射
int fd = open("large_file.dat", O_RDONLY);
struct stat sb;
fstat(fd, &sb);
char *mapped = mmap(NULL, sb.st_size,
PROT_READ, MAP_PRIVATE, fd, 0); // 1次系统调用
for (int i = 0; i < sb.st_size; i += 4096) {
process(mapped + i); // 直接内存访问,无系统调用!
}
munmap(mapped, sb.st_size);
close(fd);

优势:

传统read():
用户态 → 系统调用 → 内核读取 → 复制到用户缓冲区 → 返回用户态
↑────── 每次读取都要态切换 ──────↑
mmap():
用户态 → 系统调用 → 建立映射 → 返回用户态
↑── 只有一次态切换 ──↑
用户态 → 直接内存访问(页错误时自动加载)
↑── 无系统调用!──↑
// ❌ 同步I/O:每次都等待
for (int i = 0; i < 10; i++) {
read(fd, buf[i], 1024); // 阻塞等待
}
// 10次系统调用,10次等待
// ✅ 异步I/O:提交后继续工作
struct aiocb cb[10];
for (int i = 0; i < 10; i++) {
memset(&cb[i], 0, sizeof(struct aiocb));
cb[i].aio_fildes = fd;
cb[i].aio_buf = buf[i];
cb[i].aio_nbytes = 1024;
cb[i].aio_offset = i * 1024;
aio_read(&cb[i]); // 提交请求,立即返回
}
// 继续其他工作...
do_other_work();
// 等待所有完成
for (int i = 0; i < 10; i++) {
aio_suspend(&cb[i], 1, NULL);
}

5.4 使用vDSO(虚拟动态共享对象)

Section titled “5.4 使用vDSO(虚拟动态共享对象)”

某些系统调用可以在用户态完成:

// 传统gettimeofday:需要系统调用
struct timeval tv;
gettimeofday(&tv, NULL); // 进入内核态
// vDSO优化:直接读取内存映射的内核数据
// 内核将时间数据映射到用户空间
// glibc自动使用vDSO版本,无系统调用!
性能对比:
- 传统方式:1000周期
- vDSO方式:10周期
- 提升:100倍

支持vDSO的系统调用:

- gettimeofday()
- time()
- clock_gettime()
- getcpu()
// 传统网络I/O
send(socket, data, len, 0); // 系统调用
recv(socket, buffer, len, 0); // 系统调用
// DPDK(用户态协议栈)
struct rte_mbuf *mbuf = rte_pktmbuf_alloc(pool);
rte_memcpy(rte_pktmbuf_mtod(mbuf, void*), data, len);
rte_eth_tx_burst(port, queue, &mbuf, 1); // 无系统调用!
性能提升:
- 传统:100万包/秒
- DPDK:1000万包/秒
- 提升:10倍

┌────────────────────────────────────────┐
│ 态切换开销的本质 │
├────────────────────────────────────────┤
│ │
│ 1. 硬件层面 │
│ ├─ TLB刷新:地址转换缓存失效 │
│ ├─ 缓存污染:CPU缓存被替换 │
│ └─ 特权级切换:Ring 3 ↔ Ring 0 │
│ │
│ 2. 软件层面 │
│ ├─ 上下文保存/恢复:50+寄存器 │
│ ├─ 安全检查:防止恶意代码 │
│ └─ 栈切换:用户栈 ↔ 内核栈 │
│ │
│ 3. 系统层面 │
│ ├─ 中断禁用:影响实时性 │
│ └─ 地址空间:进程隔离机制 │
│ │
└────────────────────────────────────────┘
避免系统调用
┌─────────┐
│ vDSO │ 最快(用户态)
├─────────┤
│ mmap │ 快(减少调用)
├─────────┤
│批量I/O │ 较快(合并调用)
├─────────┤
│ 缓冲I/O │ 普通(减少调用)
├─────────┤
│直接I/O │ 慢(每次调用)
└─────────┘
  1. 最小化系统调用次数

    • 批量操作优于多次小操作
    • 缓冲I/O优于直接I/O
  2. 选择合适的系统调用

    • sendfile优于read+write
    • writev优于多次write
  3. 考虑用户态方案

    • mmap优于read/write
    • DPDK优于传统网络I/O
  4. 性能测量

    • 使用strace统计系统调用
    • 使用perf分析性能瓶颈
  • Linux内核文档:https://www.kernel.org/doc/
  • Intel手册:System Programming Guide
  • 《深入理解Linux内核》
  • 《性能之巅》(Systems Performance)