1. 概述
windows 提供了一种基于 lookaside list 的快速内存分配方案,区别于一般的使用 ExAllocatePoolWithTag() 系列函数的内存分配方式。每次从 lookaside list 里分配 fixed size 的内存。 系统构建两个条 lookaside 链表:ExNPagedLookasideListHead 和 ExPagedLookasideListHead,分别用于 Non-paged 内存和 paged 内存。
lookaside list 链表头是 LIST_ENTRY 结构,因此:整个 lookaside list 链是双向链表。ExNPagedLookasideListHead 和 ExPagedLookasideListHead 是 kernel 的全局变量,由系统进行初始化。
下图是由 ExNPagedLookasideListHead 链头构造的 non-paged lookaside list 示意图:
在使用 ExInitializeNPagedLookasideList() 函数进行初始化 NPAGED_LOOKASIDE_LIST 结构(或 PAGED_LOOKASIDE_LIST 结构)时,将节点的 LIST_ENTRY 域挂接在链表头里,形成一条双向链表。
2. lookaside list 节点结构
lookaside list 的节点是 NPAGED_LOOKASIDE_LIST 结构(用于 non-paged 链)或 PAGED_LOOKASIDE_LIST(用于 paged 链),它们的定义如下
typdef struct _NPAGED_LOOKASIDE_LIST
{ GENERAL_LOOKASIDE L;KSPIN_LOCK Lock; } NPAGED_LOOKASIDE_LIST, *PNPAGED_LOOKASIDE_LIST; typdef struct _PAGED_LOOKASIDE_LIST { GENERAL_LOOKASIDE L;FAST_MUTEX Lock; } PAGED_LOOKASIDE_LIST, *PPAGED_LOOKASIDE_LIST;内部的 GENERAL_LOOKASIDE 子结构维护着 lookaside 的整个工作,下面是 GENERAL_LOOKASIDE 结构的定义:
typedef struct _GENERAL_LOOKASIDE {
SLIST_HEADER ListHead;// 单向链表,用来挂接 pool blockUSHORT Depth; // lookaside 的 depth,缺省为 4USHORT MaximumDepth;// maximum depth,缺省为 256ULONG TotalAllocates; // 记录进行多少次分配ULONG AllocateMisses; // 记录分配从 lookaside list 中分配失败次数ULONG TotalFrees;// 记录进行多少次释放ULONG FreeMisses; // 记录从 lookaside list 中释放 miss 次数POOL_TYPE Type; // pool type 类型,与 ExAllocatePoolWithTag() 参数一致ULONG Tag; // 用于 ExAllocatePoolWithTag()ULONG Size; // 用于 ExAllocatePoolWithTag()PALLOCATE_FUNCTION Allocate; // 自定义的分配函数,缺省使用 ExAllocatePoolWithTag()PFREE_FUNCTION Free; // 自定义的释放函数,缺省使用 ExFreePool()LIST_ENTRY ListEntry; // 双向 link listULONG LastTotalAllocates;// 记录上一次 Allocate 次数ULONG LastAllocateMisses;// 记录上一次 Allocate miss 次数ULONG Future[2];// 保留 } GENERAL_LOOKASIDE, *PGENERAL_LOOKASIDE;每个节点有两条链表:
- 单向链表:用于挂接 pool 块,这些内存块就是从系统 pool 里分配而来的(non-paged 内存或 paged 内存)。
- 双向链表:用于挂接在整个 lookaside list 里,也是就前面所说的 ExNPagedLookasideListHead 链表头和 ExPagedLookasideListHead 链表头。
在前面描述图的基础上,形成像下面的结构:
因此:每个节点都有一条 SLIST_ENTRY 链,挂接着内存块!,这些内存链是在释放 lookaside list 时挂接上去的。在初始化时,这些内存块链是不存在的!
假如:Lookaside 指向其中一个 NPAGED_LOOKASIDE_LIST 节点 有三个 Depth 值是需要分清楚的:
- 节点当前的深度:Lookaside->L.ListHead.Depth 它指示节点当前有多少个内存块挂接着。
- 节点的最小深度:Lookaside->L.Depth 它指示着每个节点最少以挂接多少个内存块。在缺省的情况下,这个 Depth 值为 4,最少可以挂接 3 个内存块。
- 节点的最大深度:Lookaside->L.MaximumDepth 它指示节点最大可以挂接多少个内存块。这个值为 256,也就是最多可以挂接 255 个。
在缺省的时候,每个节点的 Depth(深度)的为 4,那么 kernel 根据使用情况来动态修改这个 Depth 值,最大值为 MaximumDepth。因此:Depth 相当于一个“额度”的概念!
3. 节点内存块 size
在一条 lookaside list 链里,每个节点可以有不同 size 的内存块,如下图所示:
在图中的 lookaside 链里,其中 3 个节点分别挂接着 1K,2K 以及 4K 大小的内存块,当需要分配 1K 内存时,从第1个节点里分配。如下面代码所示:
PVOID p = ExAllocateFromNPagedLookasideList(lookaside1);// 从第 1 个节点分配 1K 内存块
在初始化每个节点时,可以为每个节点的内存块 size 设置不同的值,因此:整个 lookaside list 可以挂接着不同 size 的内存块,软件根据需要从不同 size 的节点里分配内存。
4. 初始化节点
在使用 lookaside list 之前,必须构建和初始化一个 NPAGED_LOOKASIDE_LIST 结构(或 PAGED_LOOKASIDE_LIST 结构用于 paged 内存),如下面代码所示:
NPAGED_LOOKASIDE_LIST Lookaisde; // 节点结构
// // 下面初始化节点 // ExInitializeNPagedLookasideList(&Lookaside, // 节点地址NULL, // 使用缺省的 Allocate() 例程NULL, // 使用缺省的 Free() 例程0, // Flags 保留1024, // 1K size'tag1', // Tag0 // Depth 值,未使用);在对节点初始化完成后,将节点挂接在以 ExNPagedLookasideListHead 为表头的双向链表里,组织成一个 lookaside list 体系。
下面的 ExInitializeNPagedLookasideList() 例程 C 代码是从 windows 2003 里逆出来的:
VOID ExInitializeNPagedLookasideList(
IN PNPAGED_LOOKASIDE_LIST Lookaside,IN PALLOCATE_FUNCTION Allocate,IN PFREE_FUNCTION Free,IN ULONG Flags,IN SIZE_T Size,IN ULONG Tag,IN USHORT Depth) { PGENERAL_LOOKASDIDE L = &Lookaside->L; //// 初始化 SLIST_HEADER//L->ListHead.Next = NULL;L->ListHead.Depth = 0;L->ListHead.Spquence = 0; L->Depth = ExMinimulLooasideDepth;// depth 最小值,为 4L->Type = NonPagedPool | Flags;L->Tag = Tag;L->Size = Size;L->MaximumDepth = 256;// depth 最大值, 256L->TotalAllocates = 0;L->AllocateMisses = 0;L->TotalFrees = 0;L->FreeMisses = 0; if (Allocate == NULL){ L->Allocate = ExAllocatePoolWithTag;}else{ L->Allocate = Allocate;} if (Free == NULL){ L->Free = ExFreePool;}else{ L->Free = Free;} L->LastTotalAllocates = 0;L->LastAllocateMisses = 0; //// 将 &L->ListEntry 挂接在 ExNPagedLookasideListHead 链表头里//ExfInterlockedInsertTailList(&ExNPagedLookasideListHead, &L->ListEntry, &ExNPagedLookasideLock); }从这个代码里,我们可以清楚地看到,节点的单向链表是空的,Depth 和 MaximumDepth 分别是 4 和 256,在对 non-paged 节点初始化时,Flags 值为 NoPagedPool 类型。
对 paged 节点的初始化例程与 non-paged 节点结构是一致的,只是使用了不同的 Flags 值和链表头,下面是 ExInitializePagedLookasideList() 的代码:
VOID ExInitializePagedLookasideList(
IN PPAGED_LOOKASIDE_LIST Lookaside,IN PALLOCATE_FUNCTION Allocate,IN PFREE_FUNCTION Free,IN ULONG Flags,IN SIZE_T Size,IN ULONG Tag,IN USHORT Depth) { .... L->Type = PagedPool | Flags;// PagedPool 类型 ... //// 将 &L->ListEntry 挂接在 ExPagedLookasideListHead 链表头里//ExfInterlockedInsertTailList(&ExPagedLookasideListHead, &L->ListEntry, &ExPagedLookasideLock); }代码的最后,使用 ExfInterlockedInsertTailList() 例程将节点挂在双向链表里(non-paged 或 paged 类型)
下面代码是从 windows 2003 里逆出来的:
PLIST_ENTRY
ExfInterlockedInsertTailList(PLIST_ENTRY ListHead,// 双向链表头PLIST_ENTRY ListEntry,// 双向节点PKSPIN_LOOK Look// 保留接口) { DISABLEINTERRUPT();// 关中断 PLIST_ENTRY Blink = ListHead->Blink; // // 下面将节点挂接在尾部//ListEntry->Flink = ListHead;ListEntry->Blink = Blink;ListHead->Blink = ListEntry;Blink->Fink = ListEntry;ENABLEINTERRUPT();// 开中断 //// 空 list//if (Blink == ListHead){ return NULL;}return ListHead; }5. 分配内存
软件使用 ExAllocateFromNPagedLookasideList() 例程从 non-paged 链的节点里分配 non-paged 内存块,使用 ExAllocateFromPagedLookasideList() 例程从 paged 链的节点里分配 paged 内存块。
如下面代码所示:
PVOID p = ExAllocateFromNPagedLookasideList(lookaside);// 从 non-paged 链节点里分配内存,返回内存块到 p
在从节点里分配内存块时遇到两种情形:
- 当节点的内存块链为空时,直接调用节点的 Allocate 例程来在 pool 里分配内存,此时将记录一次 AllocateMisses 值
- 当节点的内存块链不为空时,将重复使用节点内存块,无需直接分配内存。
当节点挂接着内存块时(不为空),将返回节点地址(SLIST_ENTRY 结构地址),节点为空时将返回内存块地址(如果分配成功的话)。
如上图所示:有两个节点 A 和 B,从节点 A 分配内存将得到节点地址(作为内存块使用),从节点 B 分配将记录一次 miss(从 lookaside 中分配失败),直接调用 Allocate 例程分配内存。
节点挂着的内存块从哪里来?答案是:释放之前分配的内存块时挂接
当使用 ExFreeToNPagedLookasideList() 例程释放内存块时,在 Depth 允许的情况下,kernel 将内存块挂接在节点内存块链里,以备以续的分配重复使用,而无需进行物理的分配内存操作。 因此:当节点内存块链为空时,将产生 miss 情形。表示从节点内存块链里分配不到内存。当返回节点地址时,它属于 SLIST_ENTRY 指针值。而实际上,这个指针值虽然指向 SLIST_ENTRY 结构,但是无需理会类型,它被当作一个空的内存块使用。这是因为:SLIST_ENTRY 结构里只有一个成员,就是 Next 指针(指向下一条 SLIST_ENTRY 结构)。
当返回这个 SLIST_ENTRY 结构指针值的,它里面的 Next 成员被作为“脏”数据而忽略这个数据(作为空的内存块)。
下面我们来看看 ExAllocateFromNPagedLookasideList() 例程的实现,下面代码是从 windows 2003 里逆出来的:
PVOID ExAllocateFromNPagedLookasideList(
IN PNPAGED_LOOKASIDE_LIST Lookaside) { PSLIST_ENTRY ListEntry;//// TotalAllocates 记录分配的次数,包括成功和失败//Lookaside->L.TotalAllocates++;//// 从节点里读取内存块链//ListEntry = ExInterlockedPopEntrySList(&Lookaside->L.ListHead); //// 分配内存有两个情形:// 1. 返回 lookaside list 节点(SLIST_ENTRY) // 2. 返回内存,假如 lookaside list 为空//if (ListEntry == NULL){ //// AllocateMisses 记录从 lookaside list 中分配失败的次数// 也就是从 pool 中分配的次数//Lookaside->L.AllocateMisses++; //// 调用 lookaside list 节点中的 Allocate() 例程//return Lookaside->L.Allocate(Lookaside->L.Type, Lookaside->L.Size, Lookaside->L.Tag);}elsereturn ListEntry; }代码使用 ExInterlockedPopEntrySList() 例程来读取节点内存块地址,如果为空时就分配物理内存块,记录一次 miss 事件!这个 miss 事件将被用作 kernel 定期维护和调整 lookaside 链的 Depth 值之用! 根据 miss 次数在分配操作数的频率而提升链的 Depth,达到更好的性能。
对于 paged 类型的内存分配,它看起来像这样:
#define ExAllocateFromPagedLookasideList(Lookaside) \
ExAllocateFromNPagedLookasideList((PNPAGED_LOOKASIDE_LIST)Lookaside)ExAllocateFromPagedLookasideList() 实际上也是调用 ExAllocateFromNPagedLookasideList() 例程,只不过传递的是 PAGED_LOOKASIDE_LIST 节点!
6. 释放内存
使用 ExFreeToNPagedLookasideList() 例程来释放 non-paged 链的内存块,而 ExFreeToPagedLookasideList() 释放 paged 链的内存块。当 lookaside 节点的 Depth 允许的情况下,释放的内存块被挂接在节点的 SLIST_ENTRY 链上。
下面的代码将分配 4 个内存块,然后分别释放它们:
NPAGED_LOOKAISDE_LIST Lookaside;
ExInitializeNPagedLookasideList(Lookaside, ...); // 初始化 Lookaside 节点 PVOID p1 = ExAllocateFromNPagedLookasideList(Lookaside);// 从节点里第 1 次分配内存 PVOID p2 = ExAllocateFromNPagedLookasideList(Lookaside);// 第 2 次分配内存 PVOID p3 = ExAllocateFromNPagedLookasideList(Lookaside);// 第 3 次分配内存 PVOID p4 = ExAllocateFromNPagedLookasideList(Lookaside);// 第 4 次分配内存 ... ... ExFreeToNPpagedLookasideList(Lookaside, p1); // 释放 p1 ExFreeToNPpagedLookasideList(Lookaside, p2); // 释放 p2 ExFreeToNPpagedLookasideList(Lookaside, p3); // 释放 p3 ExFreeToNPpagedLookasideList(Lookaside, p4); // 释放 p4 ... ... // // 下面代码再次进行分配内存 // p1 = ExAllocateFromNPagedLookasideList(Lookaside); // 此时从节点 ListHead 链里重复使用上次释放过的内存块!(也就是第1个释放的内存块)在前面的分配 4 次内存里,都调用节点的 Allocate() 例程来进行物理的分配内存操作(因为节点的 ListHead 链是空的),将产生 4 个 AllocateMisses 事件!
在后续代码对分配得到的内存块进行释放,这里也遇到两种情形:
- 在 Depth 值允许的情况下:将内存块挂接在节点的 ListHead 链里,以备后续代码重复分配使用,它不会进行物理的释放工作。
- 在节点的 Depth 超限的情况下:将调用节点的 Free() 例程进行释放内存块工作,此时会产生 FreeMisses 事件,指示释放到 lookaside 链中失败!
前面的三次释放(p1,p2 以及 p3)会成功地挂接在节点的 ListHead 链表头里,而最后一次释放 p4 指向的内存块会导致进行物理地释放操作,回收到系统内存 pool 里(会记录在节点的 FreeMiesses 域里)。
Depth 超限:
当节点的 ListHead 域的 Depth 大于或等于节点的 Depth 值时,就属于 Depth 超限! 也就是 Lookaside->L.ListHead.Depth >= Lookaside->L.Depth 时就超限了。Lookaside->L.ListHead.Depth 代表着节点 SLIST_HEADER(也就是内存块链)的 Depth,而 Lookaside->L.Depth 代表示节点的 Depth,从前面的初始化例程里,我们知道节点的 Depth 最初的值为 4。因此:在初始化状态下节点的内存块链最多只能挂接 3 个内存块!超这个数量都将被释放掉,回收到系统 pool 池里。
在运行一段时间后(经过 N 次的分配与释放操作后),系统会定期检查分配的频率以及 miss 的频率,通过这些检查来动态调整节点的 Depth 值,提升节点的 Depth 额度来容纳更多的内存块。由此来达到更好的分配性能。
下面我们来看看 ExFreeToNPagedLookasideList() 例程的实现,在 windows 2003 里逆向出来下面的代码:
VOID ExFreeToNPagedLookasideList(
PNPAGED_LOOKASIDE_LIST Lookaside,PVOID Entry) { //// TotalFrees 记录释放的次数,包括:物理释放和回收到 lookaside list 中//Lookaside->L.TotalFrees++; //// 释放的工作,主要有两个情形:// 1. 当已经被分配的内存块超过 Lookaside list 所支持的深度时,就执行物理的释放工作(调用 Free() ) // 2. 当小于 lookaside list 深度时,回收到 lookaside list 里,等待下次分配,不执行释放!(挂收在 list 里)// if (Lookaside->L.ListHead.Depth < Lookaside->L.Depth){ //// 回收到 lookaside list 中//RtlpInterlockedPushEntrySList(Lookaside, (PSLIST_ENTRY)Entry);}else{ //// 记录回收到 lookaside list 失败次数,也就是:物理释放内存次数//Lookaside->L.FreeMisses++; //// 调用 lookaside list 节点中的 Free() 例程释放内存//Lookaside->L.Free(Entry);} }我们清楚地看到两种情形:在回收到 lookaside list 时,代码调用 RtlpInterlockedPushEntrySList() 例程挂接在节点的 ListHead 链头里。在回到收系统内存 pool 时,就调用 Free() 来释放内存。
RtlpInterlockedPushEntrySList() 例程和 ExInterlocakPushEntrySList() 例程执行的操作是一样的,目的是:挂接在单向链表里。 在回收到 lookaside list 节点的 ListHead 链后,下次使用这个节点进行分配内存时,直接从 ListHead 链里取出内存块就行了(使用 ExInterlockedPopEntrySList() 例程)。
内存块被释放后,内存块里的数据变为“脏”数据(无用的),ExInterlocakPushEntrySList() 例程将内存块的第 1 个 4 字节数据作为 SINGLE_LIST_ENTRY 结构来挂接在 ListHead 链表头上。
在接着的分配内存中,ExInterlockedPopEntrySList() 例程取出 ListHead 链头的下一个节点,因此:下次分配时是取出第1个释放的内存块(也就是 p1 内存块)。那么:内存块首 4 个字节的 SINGLE_LIST_ENTRY 结构数据会被作为无用数据,在正常使用内存块会被覆盖!这一点巧妙地解决了 PVOID 指针与 PSLIST_ENTRY 指针的兼容性!
7. 动态调整 lookaside list 节点深度
kernel 会定期地监视 lookaside list 的使用情况,根据分配的次数频率来动态调整节点的深度。在初始化节点时 Depth 被置为 4(只能挂接 3 个内存块)。
kernel 会考虑下面情况:
- 当节点被频繁地分配内存,这 3 个内存块是不够的!在不足够的情况下会调用节点的 Allocate() 例程进行物理地内存分配操作。kernel 发现这种情况后会提升节点的 Depth 值,使节点能容纳更多的内存块。 如果这种情况持续发生,结果会提升到 256 值(最大的 Depth 值)。
- 在提升节点的 Depth 后,发现后续的分配次数和频率被降下来时。kernel 也会降低节点的 Depth 值。如果这种情况持续发生,最后会降至 4 值(最小的 Depth 值)。
通过这些调整手段来达到最好地利用 lookaside list 目的。kernel 使用 ExAdjustLookasideDepth() 例程来监视来调整 lookaside list,它的实现是:(从 windows 2003 逆出来)
VOID ExAdjustLookasideDepth()
{ //// 分别扫描 3 个 lookaside list。// 每次扫描的 lookaside list 都是不同的//switch (ExpScanCount){ case 0:ExpScanGeneralLookasideList(ExNPagedLookasideListHead, ExNPagedLookasideLock);break;case 1:ExpScanGeneralLookasideList(ExPagedLookasideListHead, ExPagedLookasideLock);break;case 2:ExpScanSystemLookasideList();break;} //// 只有 3 个 lookaside list 需要扫描//ExpScanCount++;if (ExpScanCount == 3){ ExpScanCount = 0;} }这个例程会扫描 non-paged 链,paged 链以及 system 链,每次扫描的对象是不同的。对于 non-paged 链和 paged 链使用 ExpScanGeneralLookasideList() 例程来监视和调整
下面我们看看它的实现(从 windows 2003 里逆出来):
//
// 这个 routine 用来根据分配的频率动态调整 lookaside list 的深度。 // VOID ExpScanGeneralLookasideList(PLIST_ENTRY ListHead,PKSPIN_LOOK Look) { KIRQL OldIrql = KeRaiseIrqlToDpcLevel(); PLIST_ENTRY ListEntry;PNPAGED_LOOKASIDE_LIST Lookaside; //// 从尾部开始遍历整个挂在 ExNPagedLookasideListHead 链表头的 所有 PNPAGED_LOOKASIDE_LIST 节点!//// 这个链由 ExNPagedLookasideListHead 作为链表头(LIST_ENTRY 结构)// 它的节点是 PNPAGED_LOOKASIDE_LIST 结构的内的 ListEntry 成员!// for (ListEntry = ListHead->Flink; // 由尾部点节开始ListEntry != ListHead; // 直到 head 部ListEntry = ListEntry->Flink// 迭代){ //// 得到宿主的 NPAGED_LOOKASIDE_LIST 结构的地址// 结果等于: Lookaside = (NPAGED_LOOKASIDE_LIST *)((ULONG)ListEntry - 0x30)//Lookaside = CONTAINING_RECORD(ListEntry, NPAGED_LOOKASIDE_LIST, ListEntry); //// 计算扫描时的分配情况//ULONG AllocateMisses = Lookaside->L.AllocateMisses - Lookaside->L.LastAllocateMisses;ULONG TotalAllocates = Lookaside->L.TotalAllocates - Lookaside->L.LastTotalAllocates;Lookaside->L.LastAllocateHits = Lookaside->L.AllocateHits;Lookaside->L.LastTotalAllocates = Lookaside->L.TotalAllocates;USHORT Depth = Lookaside->L.Depth;USHORT MaximumDepth = Lookaside->L.MaximumDepth; if (ExMinimumLookasideDepth != 0){ //// 测试是否频繁使用 lookaside 进行分配// 当分配次数(包括成功和失败) 达到 75 次就视为频繁//if (TotalAllocates >= 75){ ULONG MissPercent = AllocateMisses * 1000 / TotalAllocates; //// 假如 miss 次数低于 50% 则降低 depth ,最小的 depth 为 4//if (MissPercent < 5){ Depth--; // 逐步降低节点 Depth 值if (Depth < 4)Depth = 4;}else{ //// 计算提高 depth 的幅度,// 根据剩余的额度来调整:最多每次提升 30 ,最大的 depth 为 256//LONG RaiseDepth = ((MaximumDepth - Depth) * MissPercent) / 2000;if (RaiseDepth > 30){ RaiseDepth = 30;}Depth = Depth + RaiseDepth;// 提升节点 Depth 值 if (Depth > MaximumDepth){ Depth = MaximumDepth;}}}else{ // // 降低 lookaside list 节点的 depth// 当使用不频繁时,下降的幅度为 10//Depth = Depth - 10;if (Depth < 4)Depth = 4;}} Lookaside->L.Depth = Depth;// 调整节点 Depth }kfLowerIrql(OldIrql); }Depth 调整的范围始终在 4 到 256 之间,这是 kernel 定义的最小值与最大值。
至此,lookaside list 的实现基本分析完毕!