第 5 章 进程与线程 --- 5.1 概述
5.1.0 框架图
┌──────────────────────────────────────────────────────────────────────────────────┐
│ Windows 进程/线程体系结构 │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ 用户态 (User Mode) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 进程 A │ │ 进程 B │ │ 进程 C │ │ │
│ │ │ (PEB) │ │ (PEB) │ │ (PEB) │ │ │
│ │ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │ │
│ │ │ │线程1 │ │ │ │线程1 │ │ │ │线程1 │ │ │ │
│ │ │ │(TEB) │ │ │ │(TEB) │ │ │ │(TEB) │ │ │ │
│ │ │ ├───────┤ │ │ ├───────┤ │ │ ├───────┤ │ │ │
│ │ │ │线程2 │ │ │ │线程2 │ │ │ └───────┘ │ │ │
│ │ │ │(TEB) │ │ │ │(TEB) │ │ │ │ │
│ │ │ └───────┘ │ │ └───────┘ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ 系统调用边界 │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ 内核态 (Kernel Mode) │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────────────────┐ │ │
│ │ │ Process Manager (PS) │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ PsActiveProcessHead │ │ │ │
│ │ │ │ ┌───────┐ ┌───────┐ ┌───────┐ │ │ │ │
│ │ │ │ │EPROCESS│───▶│EPROCESS│───▶│EPROCESS│───▶ ... │ │ │ │
│ │ │ │ │(进程A) │ │(进程B) │ │(进程C) │ │ │ │ │
│ │ │ │ └───────┘ └───────┘ └───────┘ │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ PspCidTable (全局 PID/TID 表) │ │ │ │
│ │ │ │ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ │ │ │
│ │ │ │ │PID=4 │ │PID=8 │ │TID=12 │ │TID=16 │ │ │ │ │
│ │ │ │ │EPROCESS│ │EPROCESS│ │ETHREAD│ │ETHREAD│ │ │ │ │
│ │ │ │ └───────┘ └───────┘ └───────┘ └───────┘ │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ EPROCESS 与 ETHREAD 的嵌套关系 │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ EPROCESS ETHREAD │ │ │ │
│ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ │
│ │ │ │ │ KPROCESS │ │ KTHREAD │ │ │ │ │
│ │ │ │ │ (调度器用) │ │ (调度器用) │ │ │ │ │
│ │ │ │ ├─────────────┤ ├─────────────┤ │ │ │ │
│ │ │ │ │ UniquePid │◀───────────│ ThreadsProcess│ │ │ │ │
│ │ │ │ │ ObjectTable │ │ Cid.Pid/Tid │ │ │ │ │
│ │ │ │ │ Token │ │ Teb │ │ │ │ │
│ │ │ │ │ Peb │ │ WaitBlock │ │ │ │ │
│ │ │ │ │ ThreadListHead────▶ThreadListEntry │ │ │ │ │
│ │ │ │ └─────────────┘ └─────────────┘ │ │ │ │
│ │ │ └─────────────────────────────────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Memory │ │ Object │ │ Security │ │ I/O │ │ │
│ │ │ Manager │ │ Manager │ │ Manager │ │ Manager │ │ │
│ │ │ (MM) │ │ (OB) │ │ (SE) │ │ (IO) │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
5.1.0.1 设计意图
核心问题
Windows 如何组织进程和线程?为什么需要两个独立的概念?它们之间是什么关系?
当我们审视现代操作系统时,会发现进程和线程是两个无处不在的概念,但它们的起源和演化却很少被深入探讨。在早期的单任务操作系统(如 MS-DOS)中,整个系统只有一个执行上下文------当前正在运行的程序。这种设计简单直接,但无法有效利用现代多任务处理器的计算能力。随着多任务操作系统的出现,系统需要一种方式来同时运行多个"程序",这就产生了进程的概念。
然而,进程作为资源容器的定位带来了一个问题:如果每次做一点小工作都要创建一个完整的进程,开销会非常大。例如,一个 Web 服务器需要同时处理多个客户端请求,如果每个请求都创建一个新进程,系统的资源消耗将极其可观。线程的出现解决了这个问题------线程是轻量级的执行单元,同一进程内的线程共享进程的资源,但各自拥有独立的执行流。
设计哲学 :「进程是资源容器,线程是执行实体」
理解这句话是掌握 Windows 进程与线程体系的关键。让我详细解释这个设计哲学的内涵:
-
进程作为资源容器:当你创建一个进程时,系统会分配一块独立的虚拟地址空间、创建空的句柄表、分配安全令牌、初始化 PEB。这些资源一旦分配,就归该进程所有,其他进程不能直接访问。进程还负责管理程序代码本身的加载和执行------可执行文件的镜像通过 SectionObject 映射到进程的地址空间中。
-
线程作为执行实体:线程是 CPU 调度的基本单位。每个线程有自己的栈(用户态栈和内核态栈)、寄存器上下文、TEB。当我们说"一个程序在运行",实际上是"某个线程在执行程序的代码"。线程生命周期中最重要的状态转换就围绕着"谁获得 CPU 时间"这个问题。
-
共享与独立的关系:一个进程可以包含多个线程,它们共享进程的资源但有独立的执行上下文。这种设计有什么好处?想象一下,一个图形处理程序需要在后台进行复杂计算,同时响应用户界面操作。如果只有一个执行线程,程序必须小心翼翼地在计算和界面更新之间切换,代码会变得极其复杂。有了多个线程,一个线程专门负责 UI,另一个线程专门负责计算,两者互不干扰。
本节定位
本节是第 5 章的基础章节,为后续深入分析 5.2(用户空间 PEB/TEB)和 5.3(NtCreateProcess)提供概念框架。读完本节后,读者应当能够:
- 理解进程与线程的本质区别:进程是资源的集合,线程是执行的集合
- 识别 EPROCESS/ETHREAD/KPROCESS/KTHREAD 的关系:EPROCESS 是对象管理器包装的完整进程对象,KPROCESS 是调度器专用的视图;类似地,ETHREAD 是完整线程对象,KTHREAD 是调度器视图
- 理解 PspCidTable 和 PsActiveProcessHead 的作用:PspCidTable 提供 PID/TID 到对象的映射,PsActiveProcessHead 链接所有活动进程
- 熟悉进程/线程相关的系统调用全景:从用户态 CreateProcess/CreateThread 到内核态 NtCreateProcess/NtCreateThread
5.1.1 进程与线程的本质区别
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 进程 vs 线程:资源划分 │
│ │
│ 进程 (Process) 线程 (Thread) │
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
│ │ • 虚拟地址空间 (VAD) │ │ • 线程栈 (User/Kernel) │ │
│ │ • 页表 (DirectoryTableBase)│ │ • 寄存器上下文 (CONTEXT) │ │
│ │ • 句柄表 (ObjectTable) │ │ • 线程局部存储 (TLS) │ │
│ │ • 安全令牌 (Token) │ │ • 线程环境块 (TEB) │ │
│ │ • 进程环境块 (PEB) │ │ • 调度优先级 (Priority) │ │
│ │ • 工作集 (Working Set) │ │ • 时间片 (Quantum) │ │
│ │ • 配额 (Quota) │ │ • 等待状态 (WaitReason) │ │
│ │ • 设备映射 (DeviceMap) │ │ • APC 队列 │ │
│ └─────────────────────────────┘ └─────────────────────────────┘ │
│ │
│ 一对多关系: │
│ │
│ ┌─────────────────────────────────┐ │
│ │ 进程 (EPROCESS) │ │
│ │ ┌───────────────────────────┐ │ │
│ │ │ 地址空间 + 句柄表 + 令牌 │ │ │
│ │ └───────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────┼──────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌───────┐ ┌───────┐ ┌───────┐│ │
│ │ │线程1 │ │线程2 │ │线程3 ││ │
│ │ │(ETHREAD)││(ETHREAD)││(ETHREAD)││ │
│ │ └───────┘ └───────┘ └───────┘│ │
│ └─────────────────────────────────┘ │
│ │
│ 共享 vs 独占: │
│ • 进程资源:所有线程共享 │
│ • 线程资源:每个线程独占 │
└──────────────────────────────────────────────────────────────────────────────────┘
进程的核心职责
进程是一个"容器",它为线程提供运行所需的所有资源:
- 地址空间隔离:每个进程有独立的虚拟地址空间,通过页表(DirectoryTableBase)实现
- 资源所有权:进程拥有打开的文件句柄、事件、互斥体等(存储在 ObjectTable)
- 安全上下文:进程有自己的安全令牌(Token),决定了进程能访问哪些系统资源
- 执行环境:PEB 存储了进程的用户态环境信息(模块列表、堆、命令行参数等)
线程的核心职责
线程是真正执行代码的实体:
- 指令执行:线程拥有自己的指令指针(IP)、栈指针(SP)和寄存器状态
- 调度单元:操作系统调度器以线程为单位进行调度
- 并发执行:同一进程内的多个线程可以同时执行不同的代码路径
- 独立状态:每个线程有自己的栈、TEB、优先级和时间片
5.1.2 内核结构总览
┌──────────────────────────────────────────────────────────────────────────────────┐
│ EPROCESS 内存布局 │
│ │
│ 低地址 ────────────────────────────────────────────────────────────────► 高地址 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ OBJECT_HEADER (对象管理器头部) │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ PointerCount / HandleCount / Type / NameInfoOffset │ │ │
│ │ │ SecurityDescriptor / QuotaInfoOffset / CreatorInfoOffset │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ ├─────────────────────────────────────────────────────────────────────┤ │
│ │ EPROCESS (进程对象体) │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 0x00: KPROCESS Pcb; // 内核进程结构 │ │ │
│ │ │ 0x18: EX_PUSH_LOCK ProcessLock; │ │ │
│ │ │ 0x20: LARGE_INTEGER CreateTime; │ │ │
│ │ │ 0x28: LARGE_INTEGER ExitTime; │ │ │
│ │ │ 0x30: EX_RUNDOWN_REF RundownProtect; │ │ │
│ │ │ 0x38: HANDLE UniqueProcessId; // PID │ │ │
│ │ │ 0x40: LIST_ENTRY ActiveProcessLinks; │ │ │
│ │ │ 0x48: SIZE_T QuotaUsage[PsQuotaTypes]; │ │ │
│ │ │ 0x60: SIZE_T QuotaPeak[PsQuotaTypes]; │ │ │
│ │ │ 0x78: SIZE_T CommitCharge; │ │ │
│ │ │ 0x80: PVOID DebugPort; │ │ │
│ │ │ 0x88: PVOID ExceptionPort; │ │ │
│ │ │ 0x90: PHANDLE_TABLE ObjectTable; // 句柄表 │ │ │
│ │ │ 0x98: EX_FAST_REF Token; // 安全令牌 │ │ │
│ │ │ 0xA0: PVOID SectionObject; // 镜像 Section │ │ │
│ │ │ 0xA8: PVOID SectionBaseAddress; // 镜像基址 │ │ │
│ │ │ 0xB0: PEPROCESS_QUOTA_BLOCK QuotaBlock; │ │ │
│ │ │ 0xB8: PVOID Win32Process; // Win32k 进程指针 │ │ │
│ │ │ 0xC0: PVOID Job; // 所属 Job │ │ │
│ │ │ 0xC8: PVOID DeviceMap; // 设备映射 │ │ │
│ │ │ 0xD0: PVOID *Win32WindowStation; // 窗口站 │ │ │
│ │ │ 0xD8: HANDLE InheritedFromUniqueProcessId; // 父进程 PID │ │ │
│ │ │ 0xE0: CHAR ImageFileName[16]; // 进程名 │ │ │
│ │ │ 0xF0: LIST_ENTRY ThreadListHead; // 线程链表 │ │ │
│ │ │ 0xF8: struct _PEB *Peb; // 用户态 PEB │ │ │
│ │ │ ... (更多字段) │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ KEY: KPROCESS = 调度器直接使用的最小进程结构 │
│ EPROCESS = Object Manager 包装的完整进程对象 │
│ PEB = 用户态可见的进程环境块 │
└──────────────────────────────────────────────────────────────────────────────────┘
KPROCESS vs EPROCESS
| 维度 | KPROCESS | EPROCESS |
|---|---|---|
| 定义位置 | 调度器层(ke) | 对象管理器层(ps) |
| 使用者 | HAL/调度器 | 对象管理器/子系统 |
| 大小 | 较小(约 0x18 字节) | 较大(约 0x180+ 字节) |
| 包含 | 调度相关字段(优先级、时间片、亲和性) | 完整的进程状态(句柄表、令牌、PEB) |
| 类型 | 纯数据结构 | 对象(Object) |
EPROCESS 关键字段
- Pcb (KPROCESS): 调度器直接访问的内核进程结构
- UniqueProcessId: 进程标识符(PID)
- ActiveProcessLinks: 链接到 PsActiveProcessHead 全局链表
- ObjectTable: 进程的句柄表
- Token: 安全令牌(EX_FAST_REF 结构)
- Peb: 用户态进程环境块指针
- SectionObject: 可执行镜像的 Section 对象
- ThreadListHead: 进程所有线程的链表头
- ImageFileName: 进程名(16 字符)
5.1.3 关键字段速查表
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 关键字段速查表 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ EPROCESS 关键字段 │ │
│ │ │ │
│ │ 字段名 类型 说明 │ │
│ │ ────────────────── ────────────────────── ──────────────────────── │ │
│ │ Pcb KPROCESS 调度器使用的进程结构 │ │
│ │ UniqueProcessId HANDLE 进程 ID (PID) │ │
│ │ ObjectTable PHANDLE_TABLE 句柄表指针 │ │
│ │ Token EX_FAST_REF 安全令牌 │ │
│ │ Peb PPEB 用户态进程环境块 │ │
│ │ SectionObject PSECTION 镜像 Section │ │
│ │ ThreadListHead LIST_ENTRY 线程链表头 │ │
│ │ ActiveProcessLinks LIST_ENTRY 活动进程链表 │ │
│ │ ImageFileName CHAR[16] 进程名 │ │
│ │ DebugPort PDEBUG_OBJECT 调试端口 │ │
│ │ ExceptionPort PLPC_PORT 异常端口 │ │
│ │ Job PEJOB 所属 Job 对象 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ ETHREAD 关键字段 │ │
│ │ │ │
│ │ 字段名 类型 说明 │ │
│ │ ────────────────── ────────────────────── ──────────────────────── │ │
│ │ Tcb KTHREAD 调度器使用的线程结构 │ │
│ │ Cid CLIENT_ID 线程 ID (TID) + 进程 ID │ │
│ │ ThreadsProcess PEPROCESS 所属进程 │ │
│ │ Teb PTEB 用户态线程环境块 │ │
│ │ ThreadListEntry LIST_ENTRY 链接到进程线程链表 │ │
│ │ CreateTime LARGE_INTEGER 创建时间 │ │
│ │ ExitTime LARGE_INTEGER 退出时间 │ │
│ │ ExitStatus NTSTATUS 退出状态码 │ │
│ │ StartAddress PVOID 线程入口地址 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ KPROCESS 关键字段 │ │
│ │ │ │
│ │ 字段名 类型 说明 │ │
│ │ ────────────────── ────────────────────── ──────────────────────── │ │
│ │ Header DISPATCHER_HEADER 调度器对象头部 │ │
│ │ DirectoryTableBase ULONG_PTR[2] 页目录基址 (CR3) │ │
│ │ Affinity KAFFINITY CPU 亲和性掩码 │ │
│ │ BasePriority KPRIORITY 基础优先级 │ │
│ │ QuantumReset UCHAR 时间片重置值 │ │
│ │ State KPROCESS_STATE 进程状态 │ │
│ │ ReadyListHead LIST_ENTRY 就绪链表 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ KTHREAD 关键字段 │ │
│ │ │ │
│ │ 字段名 类型 说明 │ │
│ │ ────────────────── ────────────────────── ──────────────────────── │ │
│ │ Header DISPATCHER_HEADER 调度器对象头部 │ │
│ │ State KTHREAD_STATE 线程状态 │ │
│ │ Priority KPRIORITY 当前优先级 │ │
│ │ BasePriority KPRIORITY 基础优先级 │ │
│ │ WaitMode KWAIT_MODE 等待模式(User/Kernel) │ │
│ │ WaitReason KWAIT_REASON 等待原因 │ │
│ │ WaitBlock KWATCH_BLOCK[4] 等待块数组 │ │
│ │ ApcState KAPC_STATE APC 状态 │ │
│ │ StackBase PVOID 内核栈基址 │ │
│ │ StackLimit PVOID 内核栈界限 │ │
│ │ Teb PVOID 用户态 TEB 指针 │ │
│ │ IdealProcessor UCHAR 理想处理器 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
5.1.4 系统调用全景
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 进程/线程系统调用全景 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 进程相关系统调用 │ │
│ │ │ │
│ │ NtCreateProcess 创建进程 │ │
│ │ NtCreateProcessEx 创建进程(扩展参数) │ │
│ │ NtOpenProcess 打开已存在进程 │ │
│ │ NtTerminateProcess 终止进程 │ │
│ │ NtQueryInformationProcess 查询进程信息 │ │
│ │ NtSetInformationProcess 设置进程信息 │ │
│ │ NtResumeProcess 恢复进程 │ │
│ │ NtSuspendProcess 挂起进程 │ │
│ │ PsCreateSystemProcess 内核创建系统进程 │ │
│ │ PsLookupProcessByProcessId 通过 PID 获取进程对象 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 线程相关系统调用 │ │
│ │ │ │
│ │ NtCreateThread 创建线程 │ │
│ │ NtCreateThreadEx 创建线程(扩展参数) │ │
│ │ NtOpenThread 打开已存在线程 │ │
│ │ NtTerminateThread 终止线程 │ │
│ │ NtQueryInformationThread 查询线程信息 │ │
│ │ NtSetInformationThread 设置线程信息 │ │
│ │ NtResumeThread 恢复线程 │ │
│ │ NtSuspendThread 挂起线程 │ │
│ │ NtGetContextThread 获取线程上下文 │ │
│ │ NtSetContextThread 设置线程上下文 │ │
│ │ PsCreateSystemThread 内核创建系统线程 │ │
│ │ PsLookupThreadByThreadId 通过 TID 获取线程对象 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ 调用层次 │ │
│ │ │ │
│ │ 用户态 API 内核系统调用 │ │
│ │ ────────────────────────── ────────────────────────────────── │ │
│ │ CreateProcess → NtCreateProcess → NtCreateProcessEx → PspCreateProcess │ │
│ │ CreateThread → NtCreateThread → NtCreateThreadEx → PspCreateThread │ │
│ │ OpenProcess → NtOpenProcess → PsLookupProcessByProcessId │ │
│ │ OpenThread → NtOpenThread → PsLookupThreadByThreadId │ │
│ │ │ │
│ │ 注: 用户态 API(kernel32) 包装 NTDLL 系统调用, │ │
│ │ NTDLL 通过 syscall 进入内核, │ │
│ │ 内核系统调用最终调用 Psp* 内部函数 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
5.1.5 关键设计决策的深度分析
5.1.5.1 进程与线程分离的设计哲学
Windows 采用"进程是资源容器,线程是执行实体"的设计,这种分离有以下优势:
优势分析
-
资源复用:多个线程可以共享同一进程的地址空间、句柄表和令牌,避免重复创建。当一个进程启动时,系统已经为它分配了地址空间、加载了必要的动态库、建立了安全上下文。如果每次执行新任务都要创建新进程,这些资源都需要重新分配,造成极大的浪费。而同一进程内的线程可以直接使用这些资源,无需额外开销。
-
轻量级并发:创建线程比创建进程开销小得多。创建一个进程需要:分配新的虚拟地址空间、创建新的页表结构、初始化新的句柄表、分配和复制各种数据结构。而创建一个线程只需要:分配线程栈(通常几MB)、创建线程内核对象(ETHREAD/KTHREAD)、将线程加入进程的线程列表。这些操作的复杂度差异直接体现在性能上------创建线程通常比创建进程快一个数量级。
-
灵活调度:调度器可以独立调度每个线程,实现细粒度的并行。在多处理器系统上,同一进程的不同线程可以同时运行在不同的 CPU 核心上,真正实现并行计算。而在单处理器系统上,调度器可以在同一进程的线程之间快速切换,利用线程等待I/O的时间片执行其他线程的计算任务。
-
资源隔离:不同进程之间完全隔离,同一进程内的线程共享资源。进程隔离提供了安全性------一个进程崩溃不会直接影响其他进程,恶意进程无法直接访问其他进程的内存。而线程共享资源提供了协作能力------同一进程的线程可以轻松地共享数据、互相通信(通过共享内存)、协调工作。
-
编程模型简化:对于需要并发执行但共享大量数据的应用,线程模型比进程模型更易于编程。想象一个Web服务器:所有请求处理线程需要访问相同的缓存数据、相同的配置信息、相同的日志系统。如果使用进程模型,每次访问这些共享资源都需要通过进程间通信(IPC),代码会变得复杂且低效。使用线程模型,线程可以直接访问这些共享数据,代码简洁且高效。
对比 Unix 的 fork() 模型
理解 Windows 和 Unix 在进程/线程模型上的差异,有助于更深刻地理解两者设计哲学的不同:
| 维度 | Windows CreateProcess | Unix fork() |
|---|---|---|
| 资源复制 | 全新创建地址空间 | 复制父进程地址空间(写时复制) |
| 线程创建 | 需显式调用 CreateThread | 继承一个线程(继承后跟父进程执行相同代码) |
| 开销 | 较大(完整地址空间) | 较大(写时复制) |
| 内存共享 | 通过 Section 对象显式共享 | 通过共享内存显式共享 |
| 执行起点 | PE 文件的 EntryPoint | 继承父进程入口(fork 返回点) |
| 父子关系 | 父进程句柄可继承给子进程 | 子进程继承父进程所有状态 |
5.1.5.2 KPROCESS/EPROCESS 分离的双层结构
c
// EPROCESS 定义(简化版)
typedef struct _EPROCESS {
KPROCESS Pcb; // 第一个字段:调度器用
// ... EPROCESS 特有的字段 ...
} EPROCESS, *PEPROCESS;
为什么需要两层结构?
理解这个设计需要从操作系统的分层架构说起。在 Windows 内核中,不同子系统需要访问进程和线程的不同视图:
-
职责分离的必要性:调度器(Dispatcher)是内核中最频繁执行的组件之一,它需要在每次时钟中断和每次等待条件满足时做出调度决策。调度器只需要知道:线程是否就绪、线程的优先级、线程的 CPU 亲和性、线程的时间片等------这些正是 KPROCESS 和 KTHREAD 包含的信息。而对象管理器(Object Manager)需要的是另一种视图:进程/线程对象如何命名、如何被引用计数、如何被安全检查、如何被遍历------这些是 EPROCESS 和 ETHREAD 包含的信息。如果只有一层结构,要么调度器需要跳过大量无关字段,要么对象管理器需要理解调度器的内部表示,两者都会造成紧耦合。
-
性能优化的考量:调度器在每次上下文切换时都需要访问当前线程和目标线程的调度相关字段。如果这些字段分散在庞大的结构体中,CPU 缓存的效率会大大降低------现代 CPU 的缓存行大小通常为 64 字节,如果频繁访问的字段跨越多个缓存行,缓存未命中带来的延迟会显著影响性能。将调度器需要的字段集中在一个小而紧凑的结构(KPROCESS/KTHREAD)中,可以确保这些字段能始终保持在 L1 缓存中。
-
类型系统的设计:EPROCESS 是对象管理器定义的对象类型,它有 OBJECT_HEADER、对象名称空间、引用计数等对象特性。而 KPROCESS 只是一个数据结构,不是对象------它没有 OBJECT_HEADER,不参与对象管理器的管理。当我们需要将进程对象传递给对象管理器时,传递的是 EPROCESS 指针;当我们需要将进程信息传递给调度器时,传递的是 KPROCESS 指针。这种类型分离让代码的意图更加清晰。
-
扩展性的保证:Windows 操作系统经历了几十年的演进,从 32 位到 64 位,从单处理器到多处理器,从桌面系统到服务器系统。每个演进阶段都对进程/线程结构提出了新的需求。如果 KPROCESS 和 EPROCESS 混在一起,对 KPROCESS 的任何修改都可能影响到对象管理器的行为,反之亦然。分离之后,调度器团队可以在不触动对象管理器代码的情况下优化 KPROCESS,数据结构团队也可以在不触及调度器的情况下扩展 EPROCESS 的字段。
5.1.5.3 KPROCESS 作为 EPROCESS 第一个字段的内存布局智慧
c
// 这允许类型转换
PEPROCESS Process = ...;
PKPROCESS Kproc = &Process->Pcb; // 直接获取
// 或者更简洁的方式(因为 Pcb 是第一个字段):
PKPROCESS Kproc = (PKPROCESS)Process; // 地址相同!
为什么这样设计?
这个看似简单的内存布局决定,实际上蕴含着深刻的设计考量,它在系统内核中被广泛使用:
-
零开销类型转换的魔法:由于 KPROCESS 位于 EPROCESS 结构体的第一个字节偏移(偏移量为 0),EPROCESS 指针和 KPROCESS 指针实际上指向同一个内存地址。这意味着当我们有一个 PEPROCESS 指针时,可以直接将它的地址赋值给 PKPROCESS 指针,不需要任何算术运算或函数调用。这种零成本转换在内核中无处不在------当你查看调度器代码时,你会看到大量直接将 PEPROCESS 当作 PKPROCESS 使用的例子。反过来,调度器代码中的 PKPROCESS 指针也可以直接被当作 PEPROCESS 使用(只要不访问超出 KPROCESS 范围的字段)。
-
缓存友好的数据访问:现代 CPU 的缓存系统以缓存行(通常 64 字节)为单位工作。当 CPU 访问一个内存地址时,不仅该地址的数据被加载到缓存,相邻的字节也会被预取到缓存中。KPROCESS 位于 EPROCESS 的开头,这意味着对 KPROCESS 字段的访问会预取到附近可能访问的 KPROCESS 其他字段。如果 KPROCESS 位于结构体中间或末尾,缓存行为会效率较低。在一个每秒执行数百万次上下文切换的系统中,这种优化带来的性能提升是相当可观的。
-
内存分配的便利性:当我们需要分配一个 EPROCESS 结构时,只需要分配一块足够大的内存即可------这块内存同时作为 EPROCESS 和 KPROCESS 使用。不需要分别分配两个独立的内存块,也不需要担心它们的内存位置是否相邻。这种设计简化了内存管理的复杂度,也保证了 KPROCESS 和 EPROCESS 的其他字段在同一个缓存行或相邻的缓存行中。
-
历史兼容性的考量:Windows 的内核结构设计可以追溯到 1993 年的 Windows NT 3.1。在那个时代,硬件资源非常宝贵,软件设计必须极度高效。KPROCESS 作为 EPROCESS 第一个字段的设计被证明是成功的,因此在后续版本中一直被保留。即使在 64 位 Windows 中,这个设计依然适用,只是地址空间更大了而已。这种长期保持一致性的设计决策证明了其最初的远见卓识。
5.1.5.4 PspActiveProcessMutex 全局锁的合理性
┌──────────────────────────────────────────────────────────────────────────────────┐
│ PspCidTable 全局表结构 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ PspCidTable (全局 PID/TID 句柄表) │ │
│ │ │ │
│ │ 索引 (PID/TID) 对象指针 │ │
│ │ ────────────────── ────────────────────── │ │
│ │ 4 → EPROCESS (进程 A) │ │
│ │ 8 → EPROCESS (进程 B) │ │
│ │ 12 → ETHREAD (线程 1) │ │
│ │ 16 → ETHREAD (线程 2) │ │
│ │ 20 → EPROCESS (进程 C) │ │
│ │ ... │ │
│ │ │ │
│ │ 访问方式: │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ PsLookupProcessByProcessId(Pid) │ │ │
│ │ │ │ │ │ │
│ │ │ └─► ExMapHandleToPointer(PspCidTable, Pid) │ │ │
│ │ │ │ │ │ │
│ │ │ └─► 返回 EPROCESS* │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 安全性: │ │
│ │ - 使用 PspActiveProcessMutex 保护并发访问 │ │
│ │ - 引用计数机制防止对象被提前释放 │ │
│ │ - 类型检查确保返回正确的对象类型 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
为什么需要全局锁?
理解 PspActiveProcessMutex 的必要性,需要从操作系统内核面临的并发挑战说起。在一个多任务操作系统中,多个 CPU 核心可以同时执行不同的代码路径,这意味着内核数据结构可能同时被多个执行上下文访问:
-
全局唯一性的代价:PID 和 TID 必须在系统范围内唯一。这意味着 PID 的分配过程必须是串行的------两个进程不能同时获得相同的 PID。PspCidTable 作为存储所有 PID/TID 引用的全局表,任何修改它的操作都需要同步保护。PspActiveProcessMutex 确保了在分配新 PID 或释放旧 PID 时,不会出现竞态条件导致重复的 PID。
-
链表操作的风险:PsActiveProcessHead 是一个双向链表,每个 EPROCESS 通过 ActiveProcessLinks 字段链接到其中。当新进程创建时,需要将新进程插入链表;当进程退出时,需要将进程从链表移除。这些链表操作涉及多个指针的修改,如果两个操作同时进行且涉及同一组节点,链表可能会被破坏。互斥锁确保了同一时刻只有一个执行上下文可以修改链表。
-
防止对象过早释放的引用计数机制:虽然 PspActiveProcessMutex 主要保护链表操作,但配合引用计数机制才能完全确保对象安全。当通过 PID 获取 EPROCESS 指针时,调用方会获得一个引用(ObReferenceObject),这会增加对象的引用计数。只有当引用计数归零时,对象才会被释放。这意味着即使一个进程已经终止并从链表中移除,只要还有代码持有它的引用,对象就不会被释放。
-
调试器和系统工具的依赖:任务管理器、Process Explorer、调试器等工具需要遍历活动进程列表来显示系统状态。如果在遍历过程中链表被另一个线程修改,工具可能会看到不一致的数据,甚至可能因为访问已被释放的内存而崩溃。互斥锁确保了遍历操作看到的是稳定的快照。
5.1.6 概念解释
5.1.6.1 EPROCESS / KPROCESS / PEPROCESS
EPROCESS 是 Windows 内核中完整进程对象的定义,它不仅仅是一个数据结构,更是对象管理器(Object Manager)体系中的一等公民。每一个 EPROCESS 结构体都对应系统中一个正在运行或曾经运行的进程。当我们谈论"进程对象"时,指的就是 EPROCESS 加上它前面的 OBJECT_HEADER。EPROCESS 结构体包含了进程在内核态的所有状态信息:进程的虚拟地址空间布局通过 SectionObject 字段引用;进程打开的所有内核对象(文件、事件、互斥体等)通过 ObjectTable 句柄表管理;进程的安全上下文由 Token 字段定义,这个令牌决定了进程可以访问哪些受保护的资源。
KPROCESS 是 EPROCESS 结构体开头的第一个字段,但它有着独立的语义------它专供调度器和 HAL 使用。KPROCESS 包含了调度器做出调度决策所需的所有信息:进程的 CPU 亲和性(Affinity)定义了进程可以在哪些处理器上运行;基础优先级(BasePriority)影响进程内线程的默认优先级;时间片(Quantum)决定了进程每次获得 CPU 时间的长度。KPROCESS 设计的核心思想是"最小化调度器需要访问的字段",因为调度器在每次上下文切换时都需要频繁访问这些字段,将它们集中在一个紧凑的结构中可以提高缓存命中率。
PEPROCESS 只是一个指针类型,它指向 EPROCESS 结构体。在代码中,我们通常通过 PEPROCESS 来传递和存储进程对象引用。
5.1.6.2 ETHREAD / KTHREAD / PETHREAD
ETHREAD 是完整的线程对象结构,与 EPROCESS 的设计哲学一脉相承。ETHREAD 包含线程在对象管理器层面的所有信息:线程所属的进程通过 ThreadsProcess 指针引用;线程的客户端ID(CLIENT_ID)由 CID 结构表示,包含唯一的进程ID和线程ID;线程的创建时间和退出时间记录在线程生命周期中的关键时间点;StartAddress 指向线程开始执行的代码地址,这通常是用户态函数或系统线程的入口点。
KTHREAD 是 ETHREAD 的第一个字段,也是调度器视角下的线程表示。KTHREAD 包含调度所需的核心信息:线程的当前状态(State)决定了线程是否就绪、运行中、等待中或已终止;当前优先级(Priority)和基础优先级(BasePriority)共同决定线程的调度顺序;线程的内核栈基址和界限(StackBase/StackLimit)定义了线程内核态调用栈的范围;TEB 指针让内核代码可以快速访问线程的用户态环境信息;APC状态(ApcState)记录了线程的异步过程调用队列。
PETHREAD 是指向 ETHREAD 的指针类型。
5.1.6.3 PspCidTable / PspActiveProcessMutex
PspCidTable 是进程管理器维护的一个全局句柄表,它不同于普通进程的句柄表------它专门用于存储系统范围内所有进程和线程的引用。可以把它理解为一个巨大的数组,索引就是 PID 或 TID,而数组元素是指向对应 EPROCESS 或 ETHREAD 对象的指针。当调用 PsLookupProcessByProcessId(Pid) 时,内核实际上是在查询 PspCidTable:ExMapHandleToPointer(PspCidTable, Pid) 将 PID 作为句柄值进行查找,返回对应的 EPROCESS 指针。这个表的存在使得 PID 和 TID 具有全局唯一性------任何一个内核组件都可以通过 PID 找到对应的进程对象。
PspActiveProcessMutex 是一个内核全局互斥锁,它保护两个关键数据结构的并发访问:PsActiveProcessHead 链表和 PspCidTable。任何遍历或修改活动进程列表的操作都需要先获取这个锁。例如,当创建一个新进程时,PspCreateProcess 会在持有此锁的情况下将新进程插入到 PsActiveProcessHead 链表并分配新的 PID。这个锁的存在确保了在高并发场景下(如系统同时启动大量进程)进程列表的完整性和一致性。
5.1.6.4 PsInitialSystemProcess / PsIdleThread
PsInitialSystemProcess 是系统引导时创建的第一个进程,通常对应的是 smss.exe(Session Manager Subsystem)或其祖先。在系统启动的早期阶段,内核需要创建一些基础进程来管理更高级的子系统,而 PsInitialSystemProcess 就是这个创建过程的起点。这个进程的特别之处在于:它拥有 SYSTEM 安全令牌,拥有极高的特权级别;它的父进程字段为空,因为没有更早的进程可以创建它;它负责启动 CSRSS(Client Server Runtime Subsystem) 和 Win32k.sys 等关键系统组件。
PsIdleThread 是系统空闲线程,每个处理器核心都有一个对应的空闲线程。当系统中没有任何其他线程需要运行时,调度器会切换到空闲线程。空闲线程的存在确保了 CPU 不会处于完全空闲状态,它可以执行诸如电源管理、休眠检查等后台任务。PsIdleThread 的优先级最低(T-IDLE 级别),任何就绪的用户态线程都可以抢占它。
5.1.6.5 CLIENT_ID
c
typedef struct _CLIENT_ID {
HANDLE UniqueProcess; // 进程 ID (PID)
HANDLE UniqueThread; // 线程 ID (TID)
} CLIENT_ID, *PCLIENT_ID;
CLIENT_ID 是一个简单但至关重要的结构,它用于唯一标识系统中的一个线程。在用户态,我们通常只看到线程的 TID,但内核需要同时知道线程属于哪个进程,这就是为什么 CLIENT_ID 同时包含进程ID和线程ID。在 ETHREAD 结构中,Cid 字段就是 CLIENT_ID 类型。UniqueProcess 指向线程所属进程的 PID,而 UniqueThread 则是线程自己的 TID。需要注意的是,UniqueProcess 实际上是进程对象句柄表中的句柄值,而不是直接的 EPROCESS 指针。
5.1.7 为什么要这样设计
5.1.7.1 为什么 Windows 用 EPROCESS+ETHREAD 分离设计而不是单一结构?
答案:职责分离和性能优化。
- 职责分离:进程管理资源,线程执行代码。这种分离让系统可以独立管理资源生命周期和执行调度。
- 性能优化:创建线程比创建进程快得多,因为不需要分配新的地址空间。
- 灵活性:一个进程可以有多个线程,实现并发执行。
5.1.7.2 为什么有 KPROCESS/KTHREAD 而不只是 EPROCESS/ETHREAD?
答案:分层设计和性能优化。
- 分层设计:KPROCESS/KTHREAD 是调度器的"视图",EPROCESS/ETHREAD 是对象管理器的"视图"。
- 性能优化:调度器只需要 KPROCESS/KTHREAD 的字段,这些字段紧凑且缓存友好。
- 兼容性:KPROCESS/KTHREAD 可以独立于 EPROCESS/ETHREAD 演进。
5.1.7.3 为什么 EPROCESS 把 KPROCESS 作为第一个字段?
答案:零开销类型转换。
- 直接指针转换:EPROCESS* 可以直接转换为 KPROCESS*,不需要计算偏移。
- 缓存友好:KPROCESS 字段在结构体开头,更容易被 CPU 缓存。
- 历史原因:早期 Windows 设计延续下来的惯例。
5.1.7.4 为什么需要 PspActiveProcessMutex 全局锁?
答案:保护全局进程链表的并发访问。
- 线程安全:多个线程可能同时遍历或修改 PsActiveProcessHead 链表。
- 防止竞态条件:在插入/删除进程时需要原子性保证。
- 调试支持:确保调试器读取的进程列表是一致的。
5.1.7.5 为什么进程/线程句柄表(PspCidTable)是系统全局的?
答案:全局唯一性和跨进程访问。
- 全局唯一:PID/TID 在系统范围内唯一。
- 跨进程访问:允许通过 PID/TID 打开其他进程/线程的句柄。
- 调试支持:调试器需要全局访问所有进程/线程。
5.1.7.6 PID/TID 是怎么分配的(ExCreateHandle 机制)?
答案:通过 ExCreateHandle 在 PspCidTable 中分配。
c
// 简化的分配流程
Process->UniqueProcessId = ExCreateHandle(PspCidTable, &CidEntry);
- 句柄表机制:PspCidTable 是一个句柄表,PID/TID 实际上是句柄值。
- 唯一性保证:ExCreateHandle 保证分配唯一的句柄值。
- 可复用性:关闭进程后,PID 可以被重新分配。
5.1.7.7 为什么需要 PEB/TEB 在用户态?
答案:让用户态代码可以访问进程/线程信息。
- 性能优化:用户态代码可以直接访问 PEB/TEB,不需要系统调用。
- 调试支持:调试器可以直接读取 PEB/TEB 了解进程/线程状态。
- 兼容性:很多用户态代码依赖 PEB/TEB 中的信息(如模块列表、堆信息)。
5.1.7.8 父进程/子进程关系是如何建立的?
答案:通过 InheritedFromUniqueProcessId 字段和 PsActiveProcessLinks 链表。
c
// 创建子进程时设置
if (Parent) {
Process->InheritedFromUniqueProcessId = Parent->UniqueProcessId;
}
- 单向引用:子进程知道父进程,但父进程不知道子进程。
- 链表遍历:通过 PsActiveProcessHead 可以遍历所有进程。
- 继承关系:子进程继承父进程的配额、设备映射等。
5.1.7.9 为什么系统进程(PsInitialSystemProcess)特殊?
答案:它是系统启动时创建的第一个进程。
- 初始化角色:负责初始化系统子系统(如 CSRSS、Win32k)。
- 特权级别:拥有 SYSTEM 权限。
- 无父进程:InheritedFromUniqueProcessId 为 NULL。
- 特殊处理:创建时不需要 SectionHandle(镜像文件)。
5.1.7.10 为什么 PspActiveProcessHead 是内核全局链表?
答案:需要全局遍历所有活动进程。
- 枚举支持:允许枚举系统中所有进程(如任务管理器)。
- 调试支持:调试器需要遍历所有进程。
- 管理需求:系统需要知道所有活动进程的状态。
5.1.8 增强子节 1:进程与线程的运行模型
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 进程与线程的运行模型 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 分时与抢占 │ │
│ │ │ │
│ │ 时间轴 ───────────────────────────────────────────────────────► │ │
│ │ │ │
│ │ 线程A ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │
│ │ 线程B ░░░░░░░░░░░░░░░░░██████████████████████████████ │ │
│ │ 线程C ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██████████ │ │
│ │ │ │
│ │ 图例: ████ = 正在执行 ░░░░ = 等待/就绪 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 上下文切换开销 │ │
│ │ │ │
│ │ 线程切换 (轻量): │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 1. 保存当前线程寄存器到 KTHREAD.Context │ │ │
│ │ │ 2. 加载目标线程寄存器从 KTHREAD.Context │ │ │
│ │ │ 3. 更新内核栈指针 (如果需要) │ │ │
│ │ │ 开销: ~1-10 微秒 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 进程切换 (重量级): │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 1. 保存当前线程寄存器 │ │ │
│ │ │ 2. 刷新 TLB (Translation Lookaside Buffer) │ │ │
│ │ │ 3. 切换 CR3 寄存器 (页目录基址) │ │ │
│ │ │ 4. 加载目标线程寄存器 │ │ │
│ │ │ 5. 刷新分支预测器/指令缓存 │ │ │
│ │ │ 开销: ~10-100+ 微秒 (取决于内存访问模式) │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 为什么进程切换更重? │ │
│ │ - TLB 刷新: 地址空间变化导致翻译缓存失效 │ │
│ │ - CR3 切换: 硬件级别的页表切换 │ │
│ │ - 缓存失效: 指令/数据缓存需要重新填充 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
5.1.9 增强子节 2:Windows vs Unix 进程模型
┌──────────────────────────────────────────────────────────────────────────────────┐
│ Windows vs Unix 进程模型对比 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ fork() vs CreateProcess() │ │
│ │ │ │
│ │ Unix fork(): │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 1. 复制父进程地址空间 (写时复制) │ │ │
│ │ │ 2. 继承父进程的文件描述符 │ │ │
│ │ │ 3. 创建一个线程 (与父进程相同的入口点) │ │ │
│ │ │ 4. 返回子进程(子进程返回 0,父进程返回子进程 PID) │ │ │
│ │ │ 5. 子进程通常调用 exec() 加载新程序 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Windows CreateProcess(): │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 1. 创建全新的地址空间 │ │ │
│ │ │ 2. 解析 PE 文件头 │ │ │
│ │ │ 3. 映射可执行文件到地址空间 │ │ │
│ │ │ 4. 创建初始线程,入口点为 PE 的 EntryPoint │ │ │
│ │ │ 5. 继承父进程句柄(如果指定 INHERIT_HANDLES) │ │ │
│ │ │ 6. 返回进程句柄 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 对比表 │ │
│ │ │ │
│ │ 维度 Unix fork() Windows CreateProcess │ │
│ │ ────────────────── ────────────────────── ─────────────────── │ │
│ │ 地址空间 复制(写时复制) 全新创建 │ │
│ │ 文件描述符 自动继承 选择性继承 │ │
│ │ 线程数量 1 1 (可指定) │ │
│ │ 执行入口 继承父进程 PE EntryPoint │ │
│ │ 内存开销 低(写时复制) 高(完整地址空间) │ │
│ │ 创建速度 快(早期) 较慢 │ │
│ │ 安全性 较高(隔离) 较高(隔离) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
5.1.10 小结
5.1.10.1 关键知识点
| 主题 | 关键点 |
|---|---|
| 进程定义 | 资源容器(地址空间、句柄表、令牌、PEB) |
| 线程定义 | 执行实体(栈、寄存器、TEB、优先级) |
| 结构层次 | EPROCESS > KPROCESS, ETHREAD > KTHREAD |
| 全局管理 | PsActiveProcessHead 链表 + PspCidTable 句柄表 |
| 系统调用 | NtCreateProcess/NtCreateThread 系列 |
| 性能差异 | 线程切换(~1-10μs) vs 进程切换(~10-100μs) |
5.1.10.2 设计原则
- 职责分离:进程管资源,线程管执行
- 分层设计:KPROCESS/KTHREAD 用于调度器,EPROCESS/ETHREAD 用于对象管理器
- 性能优化:KPROCESS 作为 EPROCESS 第一个字段实现零开销转换
- 全局可见性:PspCidTable 提供全局 PID/TID 查找
- 安全性:进程间完全隔离,线程间共享资源
5.1.10.3 常见陷阱
- 混淆进程和线程:修改进程优先级不会影响已有线程的优先级
- PID 复用:PID 会被复用,不能作为持久标识符
- 伪句柄:NtCurrentProcess() 返回的是伪句柄,不能存储使用
- 句柄继承:默认不继承,需要显式指定 INHERIT_HANDLES
5.1.10.4 后续学习路径
- 5.2 节:Windows 进程的用户空间(PEB/TEB)
- 5.3 节:系统调用 NtCreateProcess()
- 第 6 章:线程调度
- WRK:Windows Research Kernel 源码
5.1.10.5 调试技巧
- !process:WinDbg 命令,查看进程信息
- !thread:WinDbg 命令,查看线程信息
- !peb:WinDbg 命令,查看 PEB
- !teb:WinDbg 命令,查看 TEB
- Process Explorer:Sysinternals 工具,查看进程树
5.1.11 交叉引用
| 引用方 | 被引用方 | 关系 |
|---|---|---|
NtCreateProcess |
PspCreateProcess |
核心创建函数 |
PsLookupProcessByProcessId |
PspCidTable |
通过 PID 查找进程 |
PsGetNextProcess |
PsActiveProcessHead |
遍历进程列表 |
ObInsertObject |
PsProcessType |
将进程对象插入对象目录 |
MmCreateProcessAddressSpace |
EPROCESS.Pcb.DirectoryTableBase |
创建地址空间 |
源码位置:ntoskrnl/ps/process.c(file:///d:/reactos/ntoskrnl/ps/process.c)、ntoskrnl/ps/thread.c(file:///d:/reactos/ntoskrnl/ps/thread.c)、sdk/include/ndk/pstypes.h(file:///d:/reactos/sdk/include/ndk/pstypes.h)