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 PoolNon-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…