站在cpu的角度看调度线程
- 初始化阶段
操作系统启动时,初始化调度器的数据结构(如就绪队列、等待队列等),并设置定时器中断或其他机制来定期检查是否需要进行任务切换。 - 线程/进程执行
CPU按照调度器的选择开始执行某个线程或进程。
在执行过程中,线程或进程可能会因为各种原因(如等待I/O操作完成、达到时间片限制等)进入阻塞状态或者结束当前时间片。 - 中断或系统调用触发调度
当一个硬件中断(如时钟中断)发生,或者某个线程发出系统调用请求时,控制权会转移到内核态。
如果是时钟中断,中断服务例程(ISR)处理完后通常会调用调度器,判断是否需要进行上下文切换。
对于系统调用,某些情况下也可能导致调度器被调用(例如,当前线程主动放弃CPU或者改变其优先级)。 - 调度器逻辑
选择下一个要执行的任务:调度器根据一定的算法(如CFS - 完全公平调度器,在Linux中使用)从就绪队列中挑选出下一个最合适的任务。
保存当前任务的状态:如果确定需要切换,则将当前正在执行的任务的所有寄存器值(包括PC/IP、栈指针SP/BP、通用寄存器等)保存到该任务的进程控制块(PCB)中。
加载新任务的状态:从选定的新任务的PCB中恢复其寄存器值,使CPU准备好执行这个新任务。 - 返回用户态继续执行
调度完成后,通过返回指令(如iret在x86架构下),CPU从内核态切换回用户态,开始执行新选中的线程或进程。
三种上下文
硬件上下文(Hardware Context)
定义:CPU 执行某段代码时,所有寄存器的当前状态集合。
包含内容:
程序计数器(PC / RIP):下一条要执行的指令地址
栈指针(SP / RSP):当前栈顶位置
通用寄存器(RAX, RBX, RCX…)
段寄存器(CS, DS…,x86特有)
标志寄存器(EFLAGS / RFLAGS):进位、零标志等
控制寄存器(如 CR3,但通常不保存在“常规”上下文中)
特点:
纯硬件视角,与操作系统无关。
是 CPU “此刻正在做什么”的完整快照。
线程/进程切换的本质就是保存和恢复硬件上下文。
✅ 简单说:硬件上下文 = CPU 寄存器的全部状态。线程上下文(Thread Context)
定义:操作系统为一个线程维持的全部执行状态信息,用于在被抢占后能准确恢复运行。
包含内容:
完整的硬件上下文(寄存器状态)
内核栈指针(指向该线程的内核栈)
用户栈指针(指向该线程的用户栈)
页表基址(CR3 值,即 mm_struct 指针)
调度信息:优先级、时间片、状态(就绪/阻塞等)
打开的文件描述符、信号处理函数、CPU亲和性等
存储位置:主要保存在 task_struct(Linux)或 EPROCESS/ETHREAD(Windows)等内核数据结构中。
切换时机:由调度器在时间片用完、主动让出(如 sleep)、或被高优先级任务抢占时触发。
✅ 简单说:线程上下文 = 硬件上下文 + 内存空间 + 调度属性 + 资源句柄。
它是操作系统层面的概念,比硬件上下文更丰富。中断上下文(Interrupt Context)
定义:CPU 在响应硬件中断或异常时所处的执行环境。
关键特征:
不是任何用户线程的一部分!它属于“内核的紧急事务处理模式”。
使用专用的中断栈(per-CPU IRQ stack),而不是当前线程的内核栈。
不能睡眠/阻塞(因为没有关联的 task_struct,调度器无法将其挂起)。
执行的是中断服务例程(ISR),必须快速完成。
包含什么?
中断发生时的硬件上下文(会被临时保存)
当前 CPU 的中断栈
中断号、设备信息等
切换过程:
CPU 收到中断 → 自动压入部分寄存器(如 RIP, CS, RFLAGS)
跳转到中断向量表对应入口
内核汇编代码切换到中断栈
调用 C 语言 ISR(如 do_IRQ())
✅ 简单说:中断上下文 = 一个“无主”的、临时的、高优先级的内核执行环境,不属于任何线程。
CR3
✅ 1. CR3 是什么?
CR3 是 x86/x86_64 架构中的控制寄存器,存储当前页目录表(PML4 in 64-bit)的物理地址。
它决定了 CPU 使用哪一套页表来将虚拟地址翻译成物理地址。
每个进程都有自己的页表树,因此有自己的 CR3 值。
✅ 2. “常规上下文”指的是什么?
在上下文切换时,内核会保存一组通用 CPU 寄存器,比如:
RAX, RBX, RCX, RDX…(通用寄存器)
RIP(指令指针)
RSP(栈指针)
RFLAGS(标志寄存器)
这些寄存器的值会被批量压入/弹出到一个连续的内存区域(通常叫 pt_regs 或类似结构)。
这个过程高度优化,常由汇编代码用 pusha / popa 或循环指令完成。
👉 这就是所谓的 “常规硬件上下文” —— 指那些频繁变化、每次切换都必须保存的通用寄存器。
✅ 3. 为什么 CR3 “通常不保存在常规上下文中”?
因为 CR3 的管理方式不同:
(1)CR3 值已经“隐含”在线程的内核数据结构中
每个进程的 task_struct(Linux)中有一个字段 mm,指向 mm_struct。
而 mm_struct 中就直接存储了该进程的页全局目录(PGD)的物理地址,也就是 CR3 的值。
所以,不需要像 RAX 那样临时压栈保存 CR3,它的“备份”一直就在内存里。
(2)切换时直接从数据结构加载,而非从“寄存器保存区”恢复
当调度器决定切换到进程 B 时:
next_cr3 = next_task->mm->pgd; // 从 next_task 的 mm_struct 中取出 CR3 值
write_cr3(next_cr3); // 直接写入 CR3 寄存器
没有“先保存当前 CR3 到 pt_regs,再从 pt_regs 恢复新 CR3”的步骤。
因为 CR3 不属于“通用计算状态”,而是内存管理的元信息,和 RAX/RBX 性质完全不同。
(3)性能与安全考虑
如果每次上下文切换都把 CR3 和其他寄存器一起 push/pop,反而低效。
更重要的是:CR3 必须和页表生命周期严格一致。把它和通用寄存器混在一起管理容易出错。
✅ 4. 那 CR3 到底“保存”了吗?
保存了,但不是以“寄存器快照”的方式,而是以“数据结构字段”的方式。
当前进程的 CR3 值 → 存在于 current->mm->pgd
下一个进程的 CR3 值 → 存在于 next->mm->pgd
切换时:mov %new_pgd, %cr3
所以,CR3 是上下文的一部分,但它被“结构化管理”,而不是“扁平化保存”。
✅ 类比理解
想象你要搬家:
通用寄存器(RAX, RSP 等) → 像你的衣服、手机、钥匙,打包进一个箱子(pt_regs),搬完再 unpack。
CR3 → 像你的“新家地址”。你不会把地址写在衣服箱子里,而是记在通讯录(mm_struct)里,搬家时直接查通讯录去新地址。
✅ 总结
“CR3 通常不保存在‘常规’上下文中” 的意思是:
它不属于通用寄存器保存区(如 pt_regs);
它的值始终维护在进程的内存管理结构(mm_struct);
上下文切换时直接从该结构读取并写入 CR3,而非通过寄存器快照机制;
这是更安全、更高效的设计。
因此,虽然 CR3 是线程上下文的关键组成部分,但它的处理方式与 RAX、RSP 等“常规”寄存器不同——这就是那句话的真正含义
PML4
PML4(Page Map Level 4)是 x86-64 架构中四级页表结构的最顶层,用于将虚拟地址转换为物理地址。它的“过程”指的是 CPU 如何利用 PML4 完成一次完整的地址翻译。
下面从 CPU 执行视角,详细说明整个过程:
🔢 1. 虚拟地址的结构(x86-64 标准 48 位寻址)
一个 64 位虚拟地址实际只使用低 48 位(高 16 位是符号扩展),被划分为 5 段:
| 63 … 48 | 47 … 39 | 38 … 30 | 29 … 21 | 20 … 12 | 11 … 0 |
|---|---|---|---|---|---|
| 符号扩展 | PML4 索引 | PDPT 索引 | PD 索引 | PT 索引 | 页内偏移 |
| (16b) | (9b) | (9b) | (9b) | (9b) | (12b) |
🧭2. PML4 地址翻译的完整过程(CPU 视角)
✅步骤 1:从 CR3 获取 PML4 表基址
CPU 读取 CR3 寄存器,得到 PML4 表的物理基地址(4KB 对齐,低 12 位为 0)。
此时已进入当前进程的地址空间。
✅步骤 2:用 PML4 索引查找 PML4E(PML4 Entry)
取虚拟地址的 bit 47–39(共 9 位),作为索引 i1。
计算 PML4E 的物理地址:
PML4E_addr = CR3 + i1 × 8 // 每个 PML4E 占 8 字节
从内存读取该 PML4E(64 位)。
⚠️ 若 PML4E 的 P 位(存在位)= 0 → 触发 Page Fault(缺页异常)。
✅步骤 3:从 PML4E 获取 PDPT 表基址
PML4E 的高 40 位(bit 51–12)是 PDPT 表的物理基地址。
注意:PML4E 的 PS 位必须为 0(PML4 不支持大页)。
✅步骤 4:用 PDPT 索引查找 PDPTE
取虚拟地址的 bit 38–30 作为索引 i2。
计算 PDPTE 地址:
PDPTE_addr = (PML4E[51:12] << 12) + i2 × 8
读取 PDPTE。
✅步骤 5:从 PDPTE 获取 PD 表基址(或 1GB 大页)
若 PDPTE 的 PS = 1 → 这是一个 1GB 大页,直接组合物理地址:
Text
编辑
物理地址 = (PDPTE[51:30] << 30) + 虚拟地址[29:0]
若 PS = 0 → 继续下一级,获取 页目录(PD)表基址。
✅步骤 6:用 PD 索引查找 PDE
取虚拟地址 bit 29–21 作为索引 i3。
计算 PDE 地址,读取 PDE。
若 PDE.PS = 1 → 2MB 大页,物理地址 = (PDE[51:21] << 21) + 虚拟地址[20:0]
否则 → 获取 页表(PT)基址。
✅步骤 7:用 PT 索引查找 PTE
取虚拟地址 bit 20–12 作为索引 i4。
计算 PTE 地址,读取 PTE。
若 PTE.P = 0 → Page Fault。
✅步骤 8:计算最终物理地址
PTE 的高 40 位(bit 51–12)是 4KB 页的物理基地址。
加上虚拟地址的 低 12 位(页内偏移):
物理地址 = (PTE[51:12] << 12) + 虚拟地址[11:0]
📊 图示:PML4 地址翻译流程
虚拟地址: [47:39] [38:30] [29:21] [20:12] [11:0]
↓ ↓ ↓ ↓ ↓
┌───────────────┐
│ CR3 │ ← 控制寄存器(PML4 基址)
└───────┬───────┘
↓ (×8 + 索引)
┌───────────────┐
│ PML4 Table │ ← 512 项,每项 8B
└───────┬───────┘
↓ (PML4E → PDPT 基址)
┌───────────────┐
│ PDPT Table │ ← 可指向 1GB 大页 或 PD 表
└───────┬───────┘
↓
┌───────────────┐
│ Page Dir (PD)│ ← 可指向 2MB 大页 或 PT 表
└───────┬───────┘
↓
┌───────────────┐
│ Page Table │ ← 指向 4KB 物理页
└───────┬───────┘
↓
┌───────────────┐
│ 4KB 物理页 │ + 页内偏移 → 最终物理地址
└───────────────┘
💡 关键点总结
项目 说明
PML4 表大小 固定 4KB(512 项 × 8 字节)
CR3 存储内容 PML4 表的物理地址(不是虚拟地址!)
是否支持大页 ❌ PML4E 的 PS 位必须为 0,不能直接映射大页
TLB 作用 缓存虚拟页号 → 物理页框的映射,避免重复查表
进程切换影响 切换 CR3 即切换整套页表,实现地址空间隔离
✅ 一句话总结
PML4 是 x86-64 虚拟内存翻译的起点:CPU 通过 CR3 找到 PML4 表,再逐级索引(PML4 → PDPT → PD → PT),最终将虚拟地址转换为物理地址。整个过程由 MMU 硬件自动完成,对软件透明。
这个机制使得每个进程都能拥有独立的 128TB 虚拟地址空间,而互不干扰。
CR3和TLB刷新
CR3 与 TLB(Translation Lookaside Buffer)刷新之间的关系是 x86/x86-64 架构中虚拟内存管理的关键机制之一。以下是几个核心关键点,清晰解释它们的关联与影响:
✅ 1. 写入 CR3 会触发 TLB 刷新(传统行为)
TLB 是页表项的高速缓存,用于加速虚拟地址 → 物理地址的转换。
当 CR3 被写入新值(即切换到另一个进程的页表)时,CPU 自动使 TLB 中所有非全局(non-global)。
原因:旧 TLB 条目属于前一个进程的地址空间,对新进程无效,若不清除会导致地址翻译错误(如访问错物理页)。
📌 例外:标记为 Global(G 位 = 1)的页表项(通常用于内核代码/数据)不会被刷新,因为所有进程共享。
✅ 2. TLB 刷新开销很大,是上下文切换的主要性能瓶颈
TLB 容量有限(几十到几千项),命中率对性能至关重要。
每次进程切换都清空 TLB → 后续大量 TLB miss → 需要遍历多级页表 → 显著增加内存访问延迟。
在高并发、频繁切换的场景下(如 Web 服务器),这会成为严重性能瓶颈。
✅ 3.PCID(Process Context ID)技术:避免不必要的 TLB 刷新
目的:允许多个进程的 TLB 条目共存于 TLB 中,通过 ID 区分,避免全刷。
机制:
CR3 的低 12 位(原保留位)用作 PCID(0~4095,共 4096 个上下文 ID)。
每个 TLB 条目除了虚拟页号和物理页框,还存储一个 PCID 标签。
地址翻译时,CPU 只匹配 当前 CR3.PCID 相同的 TLB 条目。
效果:
写入 CR3 不再自动刷新 TLB(需配合 INVPCID 指令精细控制)。
进程切换后,若之前访问过的页面仍在 TLB 中且 PCID 匹配,可直接命中。
启用条件:
CPU 支持(Intel Nehalem+,AMD Bulldozer+)
内核开启(Linux 4.14+ 默认启用,通过 CR4.PCIDE = 1)
💡 Linux 中可通过 /proc/cpuinfo 查看是否支持:pcid 和 invpcid 标志。
✅ 4. Lazy TLB 模式:进一步优化只读内核路径
场景:当切换到内核线程(无用户地址空间)时,其实不需要刷新用户 TLB。
机制:
内核线程“借用”上一个用户进程的 CR3(不修改 CR3)。
设置 CPU 处于 “lazy TLB mode”。
只有当下一个真正的用户进程被调度时,才真正刷新 TLB。
效果:减少内核线程切换带来的 TLB 开销。
✅ 5. TLB 刷新粒度控制
除了 CR3 切换,还有更精细的刷新方式:
指令 / 接口 作用
mov %cr3, %rax + mov %rax, %cr3 刷新所有 non-global TLB(传统方式)
invlpg (addr) 仅刷新单个虚拟地址对应的 TLB 项
invpcid(带 PCID) 刷新指定 PCID 的 TLB,或全局刷新等(现代方式)
操作系统根据场景选择最合适的刷新策略。
🎯 总结:关键点速览
关键点 说明
CR3 写入 ⇒ TLB 刷新 传统行为,确保地址翻译正确性
TLB 刷新代价高 是进程切换的主要性能开销来源
PCID 技术 允许 TLB 条目按进程 ID 缓存,避免全刷,大幅提升多任务性能
Global 页不刷新 内核常用页面设 G=1,跨进程共享 TLB 条目
Lazy TLB 优化内核线程切换,推迟 TLB 刷新
精细控制 现代系统使用 invlpg / invpcid 实现按需刷新
💡 一句话总结
CR3 切换是进程地址空间隔离的硬件基础,而 TLB 刷新是其伴随的性能代价;PCID 等现代技术通过“打标签”的方式,让 TLB 条目可跨进程复用,极大缓解了这一开销。
理解 CR3 与 TLB 的关系,是掌握高性能操作系统内存管理的核心。
TLB具体存了什么?
TLB(Translation Lookaside Buffer)是 CPU 中的一个硬件高速缓存,专门用于加速虚拟地址到物理地址的转换。它不存储 CR3,而是缓存页表项(Page Table Entry, PTE) 的关键信息。
✅ TLB 中缓存的具体内容(以 x86-64 为例)
每个 TLB 表项(entry)通常包含以下字段:
📌 注意:具体字段和位宽因 CPU 架构而异,但核心是 (虚拟页号 → 物理页框 + 元数据)。
🔍 举个实际例子(x86-64 4KB 页)
假设虚拟地址:0x00007f1234567000
页内偏移 = 0x000(低 12 位)
虚拟页号(VPN)= 0x00007f1234567
当 CPU 首次访问该地址:
MMU 查多级页表,最终从 PTE 得到:
物理页框 = 0x1a2b3c4d5
权限:用户可读可写(R/W=1, U/S=1)
G=0(非全局)
假设当前 PCID = 5
将以下信息填入 TLB:
[TLB Entry]
- Tag (Key): VPN = 0x7f1234567, PCID = 5
- Data (Value): PFN = 0x1a2b3c4d5
R/W=1, U/S=1, G=0, …
下次再访问 0x7f1234567xxx 且 CR3.PCID=5 时:
TLB 直接命中 → 拼出物理地址 0x1a2b3c4d5xxx → 无需查页表!
❗ 重要澄清
TLB 不存储整个页表,只缓存“最近用过的”虚拟页 → 物理页映射。
TLB 不存储 CR3,但会利用 CR3 中的 PCID(如果启用)作为匹配标签的一部分。
TLB 是透明的:软件无法直接读写 TLB,只能通过 invlpg、写 CR3 或 invpcid 间接控制。
🧠 TLB 的作用本质
TLB 是“页表的缓存”,把慢速的多级内存查表(4~5 次内存访问)变成一次高速缓存查找(1 个周期)。
没有 TLB:每次内存访问都要查 4 级页表 → 性能灾难。
有 TLB:99%+ 的地址翻译在 CPU 内部完成 → 接近物理内存速度。
📊 总结:TLB 缓存什么?
✅ 一句话答案:
TLB 缓存的是“虚拟页号 → 物理页框”的映射关系及其访问权限、PCID、全局标志等元数据,用于加速地址翻译,但绝不存储 CR3 本身。