Windows Kernel Pool
NUMA
SMP 架构
在传统的服务器硬件架构中,协调多个 CPU 进行工作,使用的是 SMP (Symmetrical Multi-Processing)技术。在这种架构下,多个 CPU 使用同一个内存控制器(Memory Controller),共享相同的内存区域。
在这种情况下,当多个 CPU 同时向内存发起访问时,将导致总线带宽被填满。同时,由于固化了全局共享组件(包括内存控制器、内存空间),导致拓展性受到了极大的限制。
NUMA 架构
而在 NUMA (Non-Uniform Memory Access)中,身经百战的 CPU 厂商将内存控制器集成到了每个 CPU 内部中,并使用独立的总线,从指定的内存区域进行存储(即本地内存访问)。在这种情况下,每个 CPU 工作区域都划分为一个节点(Node),服务器可以自由地对节点进行拓展。
同时,一个节点可以横跨到其他节点,对其内存区域进行访问(即远程内存访问),鉴于这种特性,称为 非一致性访问(Non-Uniform)。不同厂商使用不同的连接技术,例如农企的 AMD Hyper-Transport(HT),还有牙膏厂的 Intel Quick-Path Interconnect(QPI),但原则上都是将不同的节点通过特定方式连接起来。
显而易见,同本地内存访问相比,跨节点访问将消耗一定的时间。所以 OS 厂商在针对 NUMA 架构进行支持时,都会对底层访问进行优化,使对应的 CPU 优先对本地内存区域进行操作。
Windows NUMA
在 Windows 中,每个 NUMA 节点对应一个数据结构 KNODE:
kd> dt nt!_KNODE
+0x000 PagedPoolSListHead : _SLIST_HEADER
+0x008 NonPagedPoolSListHead : [3] _SLIST_HEADER
+0x020 Affinity : _GROUP_AFFINITY
+0x02c ProximityId : Uint4B
+0x030 NodeNumber : Uint2B
+0x032 PrimaryNodeNumber : Uint2B
+0x034 MaximumProcessors : UChar
+0x035 Color : UChar
+0x036 Flags : _flags
+0x037 NodePad0 : UChar
+0x038 Seed : Uint4B
+0x03c MmShiftedColor : Uint4B
+0x040 FreeCount : [2] Uint4B
+0x048 CachedKernelStacks : _CACHED_KSTACK_LIST
+0x060 ParkLock : Int4B
+0x064 NodePad1 : Uint4B
操作系统:Windows 7 Ultimate SP1 x86,内核执行体版本:6.1.7601.18247
当系统存在多个 CPU 同时工作时,则 nt!KeNumberNodes
值为对应的节点数量。每个 KNODE
结构的实例存储于 nt!KeNodeBlock
数组中:
kd> dw nt!KeNumberNodes
83f7aab0 0001 // 系统只有一个节点
kd> dps nt!KeNodeBlock
83f452c0 83f45300 nt!KiNode0 // 节点实例
83f452c4 00000000
83f452c8 00000000
但其实从头到尾讲了这么多,KNODE
中最值得注意的是 Color
成员,该数值其实为一个内存池数组索引。
Windows Kernel Pool
内核中的 “堆”
Kernel Pool 的概念实际上同用户态的堆(Heap)类似,用于内核中的动态内存分配。为了简化表述,暂且将其称为 内存池。
系统在初始化时,会分配多个内存池,每个内存池使用 POOL_DESCRIPTOR
结构来描述其基本信息,包括内存类型、已分配和可分配内存等:
kd> dt nt!_POOL_DESCRIPTOR
+0x000 PoolType : _POOL_TYPE
+0x004 PagedLock : _KGUARDED_MUTEX
+0x004 NonPagedLock : Uint4B
+0x040 RunningAllocs : Int4B
+0x044 RunningDeAllocs : Int4B
+0x048 TotalBigPages : Int4B
+0x04c ThreadsProcessingDeferrals : Int4B
+0x050 TotalBytes : Uint4B
+0x080 PoolIndex : Uint4B
+0x0c0 TotalPages : Int4B
+0x100 PendingFrees : Ptr32 Ptr32 Void
+0x104 PendingFreeDepth : Int4B
+0x140 ListHeads : [512] _LIST_ENTRY
在该数据结构中,ListHeads
为可分配的内存空间列表。
内存池可以分为不同的类型:Paged Pool 和 Non-Paged Pool。
内存类型
Paged Pool
Paged Pool 指向的内存区域,在被分配后,对于 非频繁访问 或 系统内存处于紧张状态,则会被存放于磁盘的交换文件中。当 CPU 需要对该区域数据进行访问时,再通过触发 #PF
(Page Fault)异常,将交换页面映射到物理内存。
Page Pool 对应的内核成员信息:
nt!ExpNumberOfPagedPool
,标明对应的 Paged Pool 数量nt!ExpPagedPoolDescriptor
,Paged Pool 实例数组
如果 Windows 运行在单 CPU (单节点)环境下,默认会分配 4 个 Paged Pool 内存池。
而如果为多节点状态,为了优化内存访问,每个节点会对应一个 Paged Pool 内存池。还记得之前的 KNODE->Color
成员吗?每个节点使用 Color
作为索引,从 nt!ExpPagedPoolDescriptor
数组中取出对应的内存池描述符。
但无论是单节点还是多节点,Windows 都会默认分配一个特殊的 Paged Pool,并将内存池描述符存放于 nt!ExpPagedPoolDescriptor
数组的首个索引中。(这个特殊的内存池暂时不讨论
题外话
对于 Paged Pool 区域内存,只有 IRQL < DISPATCH_LEVEL 时才能被分配和使用。
这部分主要是笔者比较好奇而记载,实际上跳过不影响 Kernel Pool 结构理解
先说 IRQL,全称为 Interrupt Request Level,即中断请求级,而这样就不得不说起中断。中断即是程序正在运行时,被外部的请求所中止,并保存当前运行状态,去处理新的请求内容。
但不能你中断请求说暂停,我就停(这样子显得多没面子💩。所以每个中断请求会对应一个级别,数值越高,优先级越高。当系统遇到高优先级的中断请求,则必须暂停当前动作,并处理新的请求。
在 Windows 内核中,预定义了多个优先级,部分级别定义如下:
PASSIVE_LEVEL 0
APC_LEVEL 1
DISPATCH_LEVEL 2
当 Paged Pool 内存被访问时,如果内存处于交换文件中,则需要抛出 #PF
异常中断请求,将其映射到物理内存。而 #PF
中断请求级别为 DISPATCH_LEVEL
。因此,如果运行于相同优先级,或更高优先级的代码,由于 #PF
请求级相对较低,无法抢先中断进行处理,可能导致内核产生死锁,并进入冻结状态。
参考:
Understanding IRQL Why we cannot access memory from paged pool IRQL >= DISPATCH_LEVEL
Non-Paged Pool
相对于 Paged Pool,Non-Paged Pool 指向的内存区域,在被用户分配后,直到被释放之前,这部分内存都将存留于物理内存中。同 Paged Pool 类似,其对应的内核成员如下:
nt!ExpNumberOfNonPagedPools
,Non-Paged Pool 数量nt!ExpNonPagedPoolDescriptor
,多节点 Non-Paged Pool 实例数组nt!PoolVector
,单节点 Non-Paged Pool 实例数组
不同于 Paged Pool,在单节点环境下,内存池描述符存放于 nt!PoolVector
数组中。反之,同样使用 KNODE->Color
索引,对 nt!ExpNonPagedPoolDescriptor
成员进行定位。
Session Pool
怎么会半路杀出一个 Session Pool 呢?Session Pool 其实是一个特殊的内存池,每个用户会话维护一个独立的 Paged Session Pool(Non-Paged Session Pool 仍旧使用全局的 Non-Paged Pool 进行分配)。
- 在 Vista 下,Paged Session Pool 通过
nt!ExpSessionPoolDescriptor
进行引用
不同会话 Paged Session Pool 如何进行分离,是否使用数组形式存储,这部分笔者暂时未测试
- 在 Windows 7 下,该内存池通过
EPROCESS->Session->PagedPool
进行引用
在 Tarjei Mandt 前辈的文章中,指出 Paged Session Pool 通过 KTHREAD->Process->Session->PagedPool
进行索引。
但笔者在调试时发现,KTHREAD
下对应的 Process
成员为 KPROCESS
结构,实际上 Session
成员应该位于 EPROCESS
结构中:
kd> dt nt!_EPROCESS
/* ... */
+0x168 Session : Ptr32 Void
该指针实际上为 MM_SESSION_SPACE
结构指针,其中 PagedPool
对应会话内存池实例:
kd> dt nt!_MM_SESSION_SPACE
+0xe00 PagedPool : _POOL_DESCRIPTOR
内存链表
to be continue…