第 5 章 进程与线程 --- 5.9 Windows 线程的调度和切换
本节深入剖析 Windows/ReactOS 内核中线程调度和上下文切换的完整实现机制。
概述
线程调度(Thread Scheduling)是操作系统内核的核心功能之一,它决定了在任意时刻哪个线程获得 CPU 的执行权。上下文切换(Context Switching)则是实现调度的底层机制,负责保存当前线程的执行状态并恢复目标线程的执行状态。
「调度」和「切换」的区别是什么?
- 调度:决定「下一个该谁执行」,涉及选择算法、优先级管理、就绪队列维护;
- 切换:执行「换人」操作,涉及保存寄存器、切换栈、切换地址空间。
想象一个足球教练(调度器)在场边指挥比赛:他决定什么时候换人(调度决策),而球员替换动作(上下文切换)则是执行换人的具体动作。
本节内容概览
- 5.9.0 框架图:调度和切换的完整流程总览;
- 5.9.1 x86 系统结构与线程切换:CPU 架构特点、特权级、栈切换;
- 5.9.2 几个重要的数据结构:KTHREAD、KPROCESS、CONTEXT、TRAP_FRAME;
- 5.9.3 线程的切换:KiSwapThread、KiSwapContext 汇编实现;
- 5.9.4 线程的调度:调度算法、就绪队列、优先级管理;
- 5.9.5 调度器初始化与启动:KiIdleLoop、启动序列;
- 5.9.6 线程优先级与量子:优先级分类、动态调整、量子机制;
- 5.9.7 设计哲学与常见问题:设计原理、调试技巧;
- 5.9.8 为什么会这样------10 个设计哲学问答:深入理解调度设计决策。
学习目标
读完本节后,读者应当能够:
- 说清线程上下文切换的完整过程;
- 理解 x86 架构下的特权级切换和栈切换机制;
- 掌握 KTHREAD、KPROCESS、CONTEXT 等核心数据结构;
- 分析 KiSwapThread 和 KiSwapContext 的实现;
- 理解 Windows 的多级优先级调度算法;
- 解释动态优先级调整和量子机制。
涉及的内核子系统
| 子系统 | 职责 |
|---|---|
| ntoskrnl!Ki | 调度器核心(KiSwapThread、KiSelectReadyThread) |
| ntoskrnl!Ke | 线程管理、调度器初始化 |
| ntoskrnl!I386 | x86 架构上下文切换(ctxswitch.S) |
| ntoskrnl!Ps | 进程/线程创建、调度属性设置 |
5.9.0 框架图
┌──────────────────────────────────────────────────────────────────────────────┐
│ Windows 线程调度与切换完整流程 │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 阶段 A:调度时机触发 │ │
│ │ ├─► 时钟中断(时间片用完) │ │
│ │ ├─► 线程主动放弃(KeYieldProcessor) │ │
│ │ ├─► 等待结束(事件、信号量、I/O 等) │ │
│ │ ├─► 线程终止或挂起 │ │
│ │ └─► 高优先级线程就绪(抢占) │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 阶段 B:调度决策(DISPATCH_LEVEL) │ │
│ │ ├─► KiSwapThread 获取 PRCB 锁 │ │
│ │ ├─► KiSelectReadyThread 扫描就绪队列 │ │
│ │ ├─► 按优先级选择下一个线程 │ │
│ │ └─► 设置 NextThread │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 阶段 C:上下文切换 │ │
│ │ ├─► KiSwapContext 保存当前线程寄存器 │ │
│ │ │ · 保存 EAX, EBX, ESI, EDI, EBP 到 KernelStack │ │
│ │ │ · 更新当前线程的 KernelStack 指针 │ │
│ │ ├─► 切换 CR3(进程地址空间,如有必要) │ │
│ │ └─► KiSwitchThreads 恢复目标线程寄存器 │ │
│ │ · 从目标线程 KernelStack 恢复寄存器 │ │
│ │ · 切换到新线程的栈 │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 阶段 D:新线程执行 │ │
│ │ ├─► 恢复目标线程的 TrapFrame/Context │ │
│ │ ├─► 检查并投递 APC(KiDeliverApc) │ │
│ │ └─► 返回到新线程的中断点继续执行 │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
5.9.1 x86 系统结构与线程切换
5.9.1.1 x86 特权级与 Ring 保护
x86 架构提供了四级特权级(Ring 0-3),Windows 只使用 Ring 0(内核态)和 Ring 3(用户态):
x86 特权级层次
┌─────────────────────────────────────────┐
│ Ring 0 (内核态) ──────► OS 内核、驱动程序 │
│ │
│ Ring 1-2 (未使用) ────► 保留给特殊用途 │
│ │
│ Ring 3 (用户态) ──────► 应用程序代码 │
└─────────────────────────────────────────┘
特权级切换规则:
- 用户态代码不能直接访问内核态资源,必须通过系统调用;
- 系统调用通过
sysenter/syscall(现代 CPU)或int 0x2e(传统方式)触发; - 返回时通过
sysexit/sysret或iret恢复用户态执行。
5.9.1.2 内核栈与用户栈
每个线程拥有两个栈:
| 栈类型 | 位置 | 用途 |
|---|---|---|
| 用户栈 | 用户态地址空间(3GB 以下) | 用户态代码执行时使用 |
| 内核栈 | 内核态地址空间 | 系统调用、异常处理、APC 执行时使用 |
栈切换过程(以系统调用为例):
用户态 → 内核态栈切换
┌──────────────────────────────────────────┐
│ 用户态代码执行 │
│ ESP 指向用户栈 │
│ CS = 0x1B (Ring 3 代码段) │
└──────────────────────────────────────────┘
│
▼ int 0x2e / sysenter
┌──────────────────────────────────────────┐
│ 切换到内核栈 │
│ ESP ← 从 TSS/CPU 寄存器获取内核栈地址 │
│ CS = 0x08 (Ring 0 代码段) │
└──────────────────────────────────────────┘
│
▼ iret / sysexit
┌──────────────────────────────────────────┐
│ 恢复用户栈 │
│ ESP ← 从内核栈恢复(系统调用返回地址) │
│ CS = 0x1B (Ring 3 代码段) │
└──────────────────────────────────────────┘
5.9.1.3 TSS(任务状态段)的作用
TSS(Task State Segment)是 x86 架构提供的硬件支持,用于在任务切换时保存 CPU 状态。在早期的 Windows 版本中,TSS 用于存储内核栈指针:
c
// TSS 结构(简化)
typedef struct _TSS {
USHORT Backlink; // 前一个 TSS 链接
USHORT Spare0;
ULONG Esp0; // Ring 0 栈指针(关键!)
USHORT Ss0; // Ring 0 栈段
USHORT Spare1;
// ... 其他字段
} TSS, *PTSS;
关键作用:
Esp0字段存储从用户态切入内核时使用的栈指针;- CPU 在执行
sysenter/int指令时自动从 TSS 加载Esp0和Ss0。
5.9.1.4 线程切换的本质
线程切换的本质是保存当前线程的 CPU 状态并恢复目标线程的 CPU 状态,使 CPU 能够「无缝」地从断点处继续执行。
需要保存的核心状态包括:
| 状态类型 | 具体内容 | 说明 |
|---|---|---|
| 通用寄存器 | EAX, EBX, ECX, EDX, ESI, EDI, EBP | 程序执行的临时数据 |
| 指令指针 | EIP | 下一条要执行的指令地址 |
| 栈指针 | ESP | 当前栈顶位置 |
| 标志寄存器 | EFLAGS | CPU 状态标志(中断开关等) |
| 段寄存器 | CS, DS, ES, FS, GS, SS | 内存寻址上下文 |
| 浮点寄存器 | ST0-ST7, XMM0-XMM7 | 浮点和 SIMD 运算状态 |
5.9.2 几个重要的数据结构
5.9.2.1 KTHREAD 结构详解
KTHREAD(内核线程对象)是调度器操作的核心对象,定义于 sdk/include/ndk/ketypes.h:
c
typedef struct _KTHREAD {
DISPATCHER_HEADER Header; // 对象头(同步、信号等)
ULONGLONG CycleTime; // 周期时间(用于统计)
PVOID InitialStack; // 栈底(初始栈指针)
ULONG_PTR StackLimit; // 栈限制(检测栈溢出)
PVOID KernelStack; // 当前内核栈指针(切换关键!)
KSPIN_LOCK ThreadLock; // 线程自旋锁
UCHAR State; // 线程状态
KWAIT_STATUS_REGISTER WaitRegister; // 等待寄存器
UCHAR Alerted[2]; // 告警状态 [Kernel, User]
LIST_ENTRY WaitListEntry; // 等待链表项
SCHAR BasePriority; // 基本优先级
SCHAR PriorityDecrement; // 优先级递减
UCHAR AutoBoost; // 自动提升标志
UCHAR WaitReason; // 等待原因
PKPROCESS Process; // 所属进程
LIST_ENTRY ApcListHead[2]; // APC 队列头
KAPC_STATE ApcState; // APC 状态
// ... 更多字段
ULONG_PTR KernelResumeIP; // 内核恢复地址
PKTRAP_FRAME TrapFrame; // 陷阱帧(用户态断点)
} KTHREAD, *PKTHREAD;
关键字段说明:
| 字段 | 说明 | 在切换中的作用 |
|---|---|---|
KernelStack |
当前内核栈指针 | 切换的核心:保存/恢复 ESP |
InitialStack |
栈底地址 | 栈边界检测 |
State |
线程状态 | Running/Ready/Waiting 等 |
Process |
所属进程 | 进程切换时检查 |
TrapFrame |
陷阱帧指针 | 用户态上下文保存 |
5.9.2.2 KPROCESS 结构
KPROCESS(内核进程对象)包含进程级别的调度信息:
c
typedef struct _KPROCESS {
DISPATCHER_HEADER Header; // 对象头
LIST_ENTRY ProfileListHead; // 性能剖析链表
ULONG_PTR DirectoryTableBase; // **CR3 值**:页目录物理地址
ULONG_PTR ThreadListHead; // 进程内线程链表头
ULONG ActiveProcessors; // 活跃处理器掩码
KAFFINITY Affinity; // 处理器亲和性掩码
UCHAR BasePriority; // 进程基本优先级
UCHAR ThreadQuantum; // 线程量子值
UCHAR Type; // 对象类型
UCHAR ThreadUpdate; // 线程更新标志
} KPROCESS, *PKPROCESS;
关键字段 :DirectoryTableBase 是 cr3 寄存器的值,决定进程的虚拟地址空间。
5.9.2.3 CONTEXT 结构
CONTEXT 保存 CPU 寄存器的完整快照,用于线程恢复执行:
c
typedef struct _CONTEXT {
// 控制寄存器
ULONG ContextFlags; // 保存哪些寄存器
ULONG Dr0, Dr1, Dr2, Dr3; // 调试寄存器
ULONG FloatSave[128]; // 浮点寄存器
ULONG SegGs, SegFs; // 段寄存器
ULONG SegEs, SegDs; // 段寄存器
ULONG Edi, Esi, Ebx, Edx; // 通用寄存器
ULONG Ecx, Eax; // 通用寄存器
ULONG Ebp, Eip, SegCs; // 栈帧、指令指针、代码段
ULONG EFlags; // 标志寄存器
ULONG Esp, SegSs; // 栈指针、栈段
} CONTEXT;
使用场景:
GetThreadContext/SetThreadContext:调试器使用;- 线程创建时初始化用户态寄存器;
- 异常处理时保存/恢复上下文。
5.9.2.4 TRAP_FRAME 结构
TRAP_FRAME(陷阱帧)是 CPU 在异常/中断时自动压栈的基础上,内核额外保存的上下文:
c
typedef struct _KTRAP_FRAME {
ULONG Edx; // 保存的寄存器
ULONG Ecx;
ULONG Eax;
ULONG PreviousPreviousMode; // 前一个处理器模式
PVOID ExceptionList; // 异常处理链表
ULONG gs;
ULONG fs;
ULONG es;
ULONG ds; // 段寄存器
ULONG Edi, Esi, Ebp; // 通用寄存器
ULONG Ebx;
ULONG ErrCode; // 错误代码(部分异常有)
PVOID Eip; // 指令指针
UCHAR CodeAccess; // 代码访问级别
UCHAR SavedInitialStack; // 原始栈指针
ULONG Esp; // 栈指针
USHORT SegSs; // 栈段
USHORT Fill2; // 填充
} KTRAP_FRAME, *PKTRAP_FRAME;
5.9.2.5 KPRCB(处理器控制块)
每个 CPU 拥有一个 KPRCB(Processor Control Block),管理该处理器的调度状态:
c
typedef struct _KPRCB {
USHORT MinorVersion; // 版本号
USHORT MajorVersion;
KTHREAD *CurrentThread; // 当前执行的线程
KTHREAD *NextThread; // 下一个要执行的线程
KTHREAD *IdleThread; // 空闲线程
// ...
LIST_ENTRY DispatcherReadyListHead[32]; // 32 个优先级的就绪队列
ULONG ReadySummary; // 就绪位图
// ...
} KPRCB, *PKPRCB;
源码位置:sdk/include/ndk/ketypes.h(file:///d:/reactos/sdk/include/ndk/ketypes.h)
5.9.3 线程的切换
5.9.3.1 KiSwapThread ------ 调度器入口
KiSwapThread 是调度器的 C 语言入口,由 ntoskrnl/ke/thrdschd.c(file:///d:/reactos/ntoskrnl/ke/thrdschd.c) 实现:
c
LONG_PTR
FASTCALL
KiSwapThread(IN PKTHREAD CurrentThread,
IN PKPRCB Prcb)
{
BOOLEAN ApcState = FALSE;
KIRQL WaitIrql;
LONG_PTR WaitStatus;
PKTHREAD NextThread;
// 1. 获取 PRCB 锁
KiAcquirePrcbLock(Prcb);
// 2. 选择下一个线程
NextThread = Prcb->NextThread;
if (NextThread)
{
// 已有预设线程(如被抢占的线程)
Prcb->NextThread = NULL;
Prcb->CurrentThread = NextThread;
NextThread->State = Running;
}
else
{
// 需要从就绪队列中选择
NextThread = KiSelectReadyThread(0, Prcb);
if (NextThread)
{
Prcb->CurrentThread = NextThread;
NextThread->State = Running;
}
else
{
// 没有就绪线程,运行空闲线程
NextThread = Prcb->IdleThread;
Prcb->CurrentThread = NextThread;
NextThread->State = Running;
}
}
// 3. 释放 PRCB 锁
KiReleasePrcbLock(Prcb);
// 4. 保存等待 IRQL
WaitIrql = CurrentThread->WaitIrql;
// 5. 执行上下文切换
ApcState = KiSwapContext(WaitIrql, CurrentThread);
// 6. 获取等待状态
WaitStatus = CurrentThread->WaitStatus;
// 7. 检查是否需要投递 APC
if (ApcState)
{
KeLowerIrql(APC_LEVEL);
KiDeliverApc(KernelMode, NULL, NULL);
}
// 8. 恢复原始 IRQL 并返回
KeLowerIrql(WaitIrql);
return WaitStatus;
}
源码位置:ntoskrnl/ke/thrdschd.c#L426-L498(file:///d:/reactos/ntoskrnl/ke/thrdschd.c#L426-L498)
5.9.3.2 KiSwapContext ------ 汇编实现分析
KiSwapContext 用汇编实现,负责实际的寄存器保存和恢复,由 ntoskrnl/ke/i386/ctxswitch.S(file:///d:/reactos/ntoskrnl/ke/i386/ctxswitch.S) 提供:
asm
; KiSwapContext - 线程上下文切换
;
; 参数:
; cl = WaitIrql (IRQL 级别)
; edx = CurrentThread (当前线程 KTHREAD*)
;
; 关键操作:
; 1. 保存当前线程的寄存器到当前栈
; 2. 更新当前线程的 KernelStack
; 3. 切换到新线程的栈
; 4. 恢复新线程的寄存器
KiSwapContext@8:
; 保存非易失寄存器
sub esp, 4 * 4 ; 分配 16 字节栈空间
mov [esp+12], ebx
mov [esp+8], esi
mov [esp+4], edi
mov [esp+0], ebp
; 合并线程指针和 IRQL
or dl, cl ; dl = CurrentThread | WaitIrql
; 调用内部切换函数
call KiSwapContextInternal@0
; 恢复寄存器(此时已在目标线程上下文)
mov ebp, [esp+0]
mov edi, [esp+4]
mov esi, [esp+8]
mov ebx, [esp+12]
add esp, 4 * 4
ret
; KiSwapContextInternal - 实际切换逻辑
KiSwapContextInternal@0:
sub esp, 2 * 4 ; 构建切换帧
mov ecx, esp
jmp KiSwapContextEntry@8 ; 跳转到链接器提供的入口
5.9.3.3 上下文保存详解
当线程 A 需要让出 CPU 时,KiSwapContext 执行以下保存操作:
线程 A 的上下文保存过程
┌──────────────────────────────────────────────────────────┐
│ 1. KiSwapContext 被调用 │
│ · 当前 ESP 指向线程 A 的内核栈 │
│ · 寄存器包含线程 A 的执行状态 │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 2. 保存非易失寄存器到栈 │
│ push ebp, edi, esi, ebx │
│ (编译器约定:caller-saved 寄存器) │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 3. 保存当前内核栈指针到 KTHREAD │
│ [eax + KTHREAD.KernelStack] = esp │
│ (eax 在调用前被设置为指向 KTHREAD) │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 4. 切换到目标线程的栈 │
│ esp = [edx + KTHREAD.KernelStack] │
│ (edx 指向目标线程的 KTHREAD) │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 5. 从目标线程栈恢复寄存器 │
│ pop ebx, esi, edi, ebp │
│ 现在 esp 指向返回地址 │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 6. 返回,CPU 跳转到新线程的执行点 │
│ ret → 执行目标线程的中断点代码 │
└──────────────────────────────────────────────────────────┘
5.9.3.4 KiSwitchThreads ------ 栈切换
KiSwitchThreads 执行最终的栈切换:
asm
KiSwitchThreads@8:
; edx = NewThread 的 KernelStack
mov esp, edx ; 直接切换栈指针
; 调用退出钩子
call KiSwapContextExit@8
; 清理并返回
add esp, 2 * 4
ret
5.9.3.5 进程切换(地址空间切换)
当目标线程属于不同进程时,还需要切换地址空间:
c
VOID
NTAPI
KiSwapProcess(IN PKPROCESS NewProcess,
IN PKPROCESS OldProcess)
{
PKIPCR Pcr = KeGetPcr();
// 更新 CR3(页目录基址寄存器)
__writecr3(NewProcess->DirectoryTableBase[0]);
// 处理 LDT 切换
if (NewProcess->LdtDescriptor.LimitLow !=
OldProcess->LdtDescriptor.LimitLow)
{
// 加载新的 LDT
Ke386SetLocalDescriptorTable(KGDT_LDT, ...);
}
// 更新 IOPM 偏移
Pcr->TSS->IoMapBase = NewProcess->IopmOffset;
}
源码位置:ntoskrnl/ke/i386/context.c#L19-L58(file:///d:/reactos/ntoskrnl/ke/i386/context.c#L19-L58)
5.9.4 线程的调度
5.9.4.1 调度器概述
Windows 使用基于优先级的抢占式多级反馈队列调度算法:
- 多级:32 个优先级(0-31)
- 反馈:根据线程行为动态调整优先级
- 抢占:高优先级线程可以抢占低优先级线程
5.9.4.2 线程状态机
线程状态转换图
┌──────────────────────────────────┐
│ │
▼ │
┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ Running │──│ Ready │──│ Waiting │ │
│ (执行中) │ │ (就绪) │ │ (等待) │ │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ ▲ ▲ │
│ │ │ │
│ ┌─────────┴──────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────┐ ┌───────────┐ │
│ │ Deferred │──│ Standby │ │
│ │ Ready │ │ (备用) │ │
│ │ (延迟就绪)│ └───────────┘ │
│ └───────────┘ │
│ │
│ ┌──────────────────────┐ │
│ │ │ │
▼ ▼ │ │
┌──────────────────────────────────┐ │
│ Terminated (终止) │ │
└──────────────────────────────────┘ │
| 状态 | 说明 | 转换条件 |
|---|---|---|
| Running | 正在 CPU 上执行 | 时间片耗尽→Ready,被抢占→Ready |
| Ready | 就绪,等待 CPU | 被调度器选中→Running |
| Waiting | 等待事件/资源 | 事件发生→Ready |
| Standby | 已被选中为下一个线程 | CPU 可用→Running |
| DeferredReady | 在其他 CPU 上准备就绪 | 被移入就绪队列 |
| Terminated | 已终止,等待清理 | 资源释放后消失 |
5.9.4.3 KiSelectReadyThread ------ 选择下一个线程
c
PKTHREAD
FASTCALL
KiSelectReadyThread(IN UCHAR Priority,
IN PKPRCB Prcb)
{
PLIST_ENTRY ListEntry;
// 检查指定优先级的就绪队列
if (Prcb->ReadySummary & PRIORITY_MASK(Priority))
{
// 找到非空队列
ListEntry = Prcb->DispatcherReadyListHead[Priority].Flink;
return CONTAINING_RECORD(ListEntry, KTHREAD, WaitListEntry);
}
return NULL;
}
5.9.4.4 KiSelectNextProcessor ------ 选择目标处理器
c
static
ULONG
KiSelectNextProcessor(IN PKTHREAD Thread)
{
KAFFINITY PreferredSet, IdleSet;
ULONG Processor;
// 1. 从线程亲和性开始
PreferredSet = Thread->Affinity;
// 2. 优先选择空闲处理器
IdleSet = PreferredSet & KiIdleSummary;
if (IdleSet != 0)
{
PreferredSet = IdleSet;
}
// 3. 优先选择理想处理器
if (PreferredSet & AFFINITY_MASK(Thread->IdealProcessor))
{
return Thread->IdealProcessor;
}
// 4. 返回第一个可用处理器
BitScanForwardAffinity(&Processor, PreferredSet);
return Processor;
}
5.9.4.5 就绪队列管理
每个处理器 PRCB 维护 32 个按优先级索引的就绪队列:
就绪队列结构(每个 CPU 一个)
┌─────────────────────────────────────────────────────────┐
│ PRCB │
│ ├─► DispatcherReadyListHead[0] ← 空闲线程优先级 │
│ ├─► DispatcherReadyListHead[1] │
│ │ ... │
│ ├─► DispatcherReadyListHead[15] ← 后台进程优先级 │
│ │ ... │
│ └─► DispatcherReadyListHead[31] ← 实时优先级 │
│ │
│ ReadySummary = 0x00010003 ← 位图表示哪些队列非空 │
│ (bit 0 和 bit 16 置位,表示优先级 0 和 16 有线程) │
└─────────────────────────────────────────────────────────┘
入队操作:
c
// 将线程插入就绪队列
VOID
KiReadyThread(IN PKTHREAD Thread)
{
KPRIORITY Priority = Thread->Priority;
// 插入到对应优先级队列尾部(FIFO)
InsertTailList(&Prcb->DispatcherReadyListHead[Priority],
&Thread->WaitListEntry);
// 更新就绪位图
Prcb->ReadySummary |= PRIORITY_MASK(Priority);
}
5.9.4.6 调度时机
线程调度发生在以下时机:
| 时机 | 触发条件 | 处理 |
|---|---|---|
| 时钟中断 | 时间片耗尽 | 递减 Quantum,为当前线程选择新优先级 |
| 等待结束 | 事件/信号量等被满足 | 将等待线程移入就绪队列,可能抢占 |
| 高优先级就绪 | 新线程进入就绪队列 | 抢占当前低优先级线程 |
| 线程终止 | 线程调用 ExitThread | 立即选择下一个线程 |
| 线程挂起 | SuspendThread | 立即选择下一个线程 |
5.9.5 调度器初始化与启动
5.9.5.1 KiIdleLoop ------ 空闲循环
当没有用户线程需要运行时,CPU 执行空闲线程,其主循环:
c
VOID
NTAPI
KiIdleLoop(IN PVOID Context)
{
for (;;)
{
// 1. 检查是否有工作要做
if (KiIdleSummary == 0)
{
// 无就绪线程,执行 HLT 指令降低功耗
__asm { hlt }
}
// 2. 执行空闲调度
KiIdleSchedule(KeGetCurrentPrcb());
}
}
5.9.5.2 SystemIdleThread
SystemIdleThread 是每个 CPU 的空闲线程,优先级最低(0),永不阻塞:
c
// 空闲线程执行路径
KiSystemStartup()
│
└─► KiInitializeKernel()
│
├─► KiInitializePRCB() // 初始化处理器控制块
│
├─► KiInitializeIdleThread() // 创建空闲线程
│
└─► KiStartRuntimeThread() // 启动空闲循环
│
└─► KiIdleLoop() // 进入空闲循环
5.9.5.3 启动序列
从系统启动到第一个用户线程运行的完整序列:
系统启动 → 调度器初始化流程
┌──────────────────────────────────────────────────────────┐
│ 1. BIOS/UEFI 启动加载程序 │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 2. ntldr/OS 加载内核 (ntoskrnl.exe) │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 3. KiSystemStartup() │
│ · 初始化 CPU 寄存器 │
│ · 设置临时栈 │
│ · 调用 KiInitializeKernel │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 4. KiInitializeKernel() │
│ · 初始化 KPRCB │
│ · 初始化 KiIdleSummary │
│ · 调用 Phase1Initialization │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 5. Phase1Initialization() │
│ · 创建系统进程和第一个用户进程 │
│ · 创建 System 线程和 Idle 线程 │
│ · 初始化调度器队列 │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 6. KiStartSystemThread() │
│ · 切换到第一个用户线程 │
│ · 从 DISPATCH_LEVEL 降至 PASSIVE_LEVEL │
│ · 开始用户态执行 │
└──────────────────────────────────────────────────────────┘
5.9.6 线程优先级与量子
5.9.6.1 优先级分类
Windows 使用 32 个优先级,分为三类:
优先级范围
┌──────────────────────────────────────────────────────────┐
│ 优先级 31-16:实时优先级 (Real-Time) │
│ 特点:固定优先级,不随时间衰减 │
│ 用于:音频播放、视频渲染、硬件驱动 │
├──────────────────────────────────────────────────────────┤
│ 优先级 15-1:可变优先级 (Variable) │
│ 特点:可以动态提升和衰减 │
│ 用于:普通应用程序 │
├──────────────────────────────────────────────────────────┤
│ 优先级 0:空闲优先级 (Idle) │
│ 特点:仅在没有其他线程时运行 │
│ 用于:系统空闲任务、后台处理 │
└──────────────────────────────────────────────────────────┘
5.9.6.2 优先级动态调整
Windows 使用优先级提升机制来防止优先级反转和饥饿:
c
// 优先级提升场景
// 场景 1:I/O 完成提升
if (Thread->AdjustReason == AdjustUnwait)
{
if (Thread->BasePriority < LOW_REALTIME_PRIORITY)
{
// I/O 完成后短暂提升优先级
OldPriority = Thread->BasePriority + PsPrioritySeparation;
if (OldPriority > Thread->Priority)
{
Thread->Priority = (SCHAR)OldPriority;
}
}
}
// 场景 2:前台进程提升
if (Process->Vm.Flags.MemoryPriority == MEMORY_PRIORITY_FOREGROUND)
{
Thread->Priority += PsPrioritySeparation;
}
5.9.6.3 量子机制
**量子(Quantum)**是线程一次连续运行的时间片长度:
| 线程类型 | 默认量子 | 配置 |
|---|---|---|
| 前台窗口线程 | 约 60ms | 可以通过系统设置调整 |
| 后台线程 | 约 6ms | 固定 |
| 空闲线程 | 无限 | 永不耗尽 |
c
// 量子耗尽处理
if (--Thread->Quantum <= 0)
{
// 重置量子
Thread->Quantum = Thread->QuantumReset;
// 计算新优先级(可能降低)
Thread->Priority = KiComputeNewPriority(Thread, 1);
// 触发调度
KiSwapThread();
}
5.9.6.4 优先级反转问题
优先级反转是指高优先级线程因等待低优先级线程持有的资源而被阻塞的现象:
优先级反转示例
┌──────────────────────────────────────────────────────────┐
│ 时间线 │
│ │
│ T1: 高优先级线程 T1 需要资源 R │
│ T2: 中优先级线程 M 抢占 T1(因 M 就绪) │
│ T3: 低优先级线程 L 持有资源 R,尚未完成 │
│ T4: M 继续运行,T1 和 L 都被阻塞 │
│ T5: 结果:高优先级线程反而无法运行 │
└──────────────────────────────────────────────────────────┘
解决方案 :Windows 使用优先级继承机制,当高优先级线程等待低优先级线程持有的资源时,临时提升低优先级线程的优先级。
5.9.7 设计哲学与常见问题
5.9.7.1 设计原理
- 公平性:所有同优先级线程应获得相等的 CPU 时间;
- 响应性:高优先级线程应尽快获得 CPU;
- 吞吐量:最大化系统总体处理能力;
- 实时性:对实时任务提供确定性的调度延迟。
5.9.7.2 性能考虑
- 就绪队列查找:使用位图加速,从高优先级向低优先级扫描;
- 锁竞争:PRCB 锁是每 CPU 的,减少了跨 CPU 竞争;
- 上下文切换开销:每次切换约 2-5 微秒(保存/恢复寄存器)。
5.9.7.3 常见问题与调试
| 问题 | 现象 | 调试方法 |
|---|---|---|
| 线程饥饿 | 某些线程长期得不到 CPU | 检查优先级设置、是否有更高优先级线程占用了 CPU |
| 过度调度 | 上下文切换过于频繁 | 使用性能计数器检查 Context Switches/sec |
| 优先级倒置 | 高优先级线程反而卡顿 | 使用 !runaway 查看线程占用时间 |
| CPU 100% | 空闲时 CPU 仍 100% | 检查是否有线程在 DISPATCH_LEVEL 忙等待 |
5.9.8 为什么会这样------10 个设计哲学问答
Q1:为什么线程切换需要保存完整的寄存器上下文?
A :寄存器是 CPU 执行的基本单元,保存了线程的当前计算状态。如果不保存,下次恢复时线程会「失忆」,无法继续正确的执行。关键是保存非易失寄存器(caller-saved),因为函数调用约定规定调用者要保存这些寄存器。
Q2:为什么要有内核栈和用户栈的区分?
A:安全隔离是核心原因。用户栈在用户态地址空间,内核栈在内核态地址空间。这确保了:
- 用户态代码无法直接访问或破坏内核栈数据;
- 内核代码有独立且充足的栈空间;
- 系统调用可以安全地在两者之间切换。
Q3:为什么调度发生在 DISPATCH_LEVEL?
A:DISPATCH_LEVEL 是调度器运行的正确 IRQL 级别:
- 高于 PASSIVE_LEVEL:可以防止被普通中断抢占导致递归调度;
- 低于 HIGH_LEVEL:允许中断处理、DPC 等继续执行;
- 低于 SYNCH_LEVEL:可以安全地获取各种内核锁。
Q4:为什么需要就绪队列而不是简单链表?
A :优先级队列提供了O(1) 优先级选择 :通过 ReadySummary 位图,调度器可以直接知道哪个优先级有就绪线程,无需遍历整个链表。32 个独立队列也保证了同优先级内的公平性(FIFO 顺序)。
Q5:为什么优先级有 32 个级别?
A:32 个优先级提供了足够的粒度来区分任务类型:
- 实时任务(16+):音频、视频、驱动
- 交互任务(9-15):UI 响应
- 后台任务(1-8):批处理
- 空闲任务(0):垃圾回收等
同时,32 位整数使得位图操作非常高效。
Q6:为什么要有量子机制?
A:量子(时间片)解决了多个核心问题:
- 公平性:防止低优先级线程「霸占」CPU;
- 响应性:允许高优先级线程抢占;
- 吞吐量:让更多线程有机会运行;
- 功耗:空闲时可以通过 HLT 降低功耗。
Q7:为什么动态优先级会衰减?
A:优先级衰减防止了线程「霸占」CPU:
- 如果线程一直占用 CPU 而不等待,优先级会逐渐降低;
- 这让其他线程有机会获得平等的执行时间;
- 同时保证了交互线程在等待 I/O 后能快速恢复高优先级。
Q8:为什么 SMP 需要处理器亲和性?
A:亲和性(Affinity)提供了多个优势:
- 缓存友好:线程保持在同一 CPU,缓存命中率更高;
- 实时性保证:关键线程可以在指定 CPU 上独占运行;
- NUMA 优化:线程可以绑定到访问本地内存更快的 CPU。
Q9:为什么 KiSwapContext 要用汇编实现?
A :上下文切换需要精确控制寄存器保存顺序和栈操作:
- C 编译器会优化寄存器使用,难以保证所有必要状态都被保存;
- 栈切换操作(mov esp, xxx)无法在纯 C 中安全实现;
- 汇编保证了切换的原子性和正确性。
Q10:为什么空闲线程不能被换出?
A:空闲线程(Idle Thread)有特殊地位:
- 当没有其他线程可运行时,必须有一个线程执行
hlt降低 CPU 功耗; - 空闲线程优先级为 0,任何其他线程就绪时都会抢占它;
- 如果空闲线程被换出,就没有线程来执行
hlt,CPU 会空转浪费电力。
总结
线程调度和上下文切换是 Windows 内核最核心的机制之一。通过分析 ReactOS 的实现,我们可以看到:
- 上下文切换 :通过汇编实现的
KiSwapContext保存/恢复寄存器,切换内核栈; - 调度决策 :基于优先级的多级反馈队列,
KiSelectReadyThread选择下一个线程; - 状态管理:线程在 Running/Ready/Waiting 等状态间转换;
- 性能优化:每 CPU 就绪队列、位图加速、亲和性支持。
理解这些机制对于深入掌握操作系统原理、调试性能问题、设计高效的多线程应用都至关重要。
核心要点回顾:
- 线程切换通过
KiSwapThread→KiSwapContext完成; - 上下文保存在内核栈,通过
KTHREAD.KernelStack指针追踪; - 32 个优先级的就绪队列使用位图加速查找;
- 调度发生在 DISPATCH_LEVEL,保证原子性;
- 动态优先级调整防止优先级反转和饥饿。
本章代码索引
| 文件 | 内容 |
|---|---|
| ntoskrnl/ke/thrdschd.c(file:///d:/reactos/ntoskrnl/ke/thrdschd.c) | 调度器核心实现 |
| ntoskrnl/ke/i386/ctxswitch.S(file:///d:/reactos/ntoskrnl/ke/i386/ctxswitch.S) | x86 上下文切换汇编 |
| ntoskrnl/ke/i386/context.c(file:///d:/reactos/ntoskrnl/ke/i386/context.c) | 进程上下文切换(CR3) |
| sdk/include/ndk/ketypes.h(file:///d:/reactos/sdk/include/ndk/ketypes.h) | KTHREAD、KPROCESS、KPRCB 定义 |