锁定可分页代码或数据

某些内核模式驱动程序(如串行驱动程序和并行驱动程序)不必驻留在内存中,除非它们管理的设备已打开。 但是,只要存在活动连接或端口,管理该端口的驱动程序代码的某些部分必须驻留才能为设备提供服务。 如果未使用端口或连接,则不需要驱动程序代码。 相比之下,包含系统代码、应用程序代码或系统分页文件的磁盘驱动程序必须始终驻留在内存中,因为驱动程序会不断在其设备和系统之间传输数据。

偶尔使用的设备 ((例如调制解调器) )的驱动程序可以在其管理的设备未处于活动状态时释放系统空间。 如果将必须驻留的代码置于单个部分中,以便为活动设备提供服务,并且驱动程序在使用设备时将代码锁定在内存中,则可以将此部分指定为可分页。 当驱动程序的设备打开时,操作系统会将可分页部分引入内存,驱动程序会将其锁定到内存中,直到不再需要。

系统 CD 音频驱动程序代码使用此技术。 驱动程序的代码根据 CD 设备的制造商分组到可分页部分。 某些品牌可能永远不会出现在给定的系统上。 此外,即使系统中存在 CD-ROM,也可能不经常访问它,因此,按 CD 类型将代码分组到可分页部分可确保永远不会加载特定计算机上不存在的设备的代码。 但是,当访问设备时,系统会加载相应 CD 设备的代码。 然后,驱动程序调用 MmLockPagableCodeSection 例程(如下所述),以在使用其设备时将其代码锁定到内存中。

若要将可分页代码隔离到命名节中,请使用以下编译器指令对其进行标记:

#pragma alloc_text (PAGE*Xxx,*RoutineName)

可分页代码节的名称必须以四个字母“PAGE”开头,后跟最多四个字符, (此处表示为 Xxx) 以唯一标识该节。 节名称的前四个字母 (即“PAGE”) 必须大写。 RoutineName 标识要包含在可分页节中的入口点。

驱动程序文件中可分页代码节的最短有效名称为 PAGE。 例如,以下代码示例中的 pragma 指令将 标识 RdrCreateConnection 为名为 PAGE 的可分页代码节中的入口点。

#ifdef  ALLOC_PRAGMA 
#pragma alloc_text(PAGE, RdrCreateConnection) 
#endif 

若要使可分页驱动程序代码驻留并在内存中锁定,驱动程序调用 MmLockPagableCodeSection,传递地址 (通常是驱动程序例程) 的入口点,该入口点位于可分页代码节中。 MmLockPagableCodeSection 锁定包含调用中引用的例程的节的全部内容。 换句话说,此调用使与同一 PAGEXxx 标识符关联的每个例程驻留在内存中并锁定。

MmLockPagableCodeSection 返回一个句柄,该句柄在通过调用 MmUnlockPagableImageSection 例程) 解锁节 (,或者驱动程序必须从其代码中的其他位置锁定该节时使用。

驱动程序还可以将很少使用的数据视为可分页数据,以便也可以将其分页,直到它支持的设备处于活动状态。 例如,系统混音器驱动程序使用可分页数据。 混音器设备没有与之关联的异步 I/O,因此此驱动程序可以使其数据可分页。

可分页数据节的名称必须以四个字母“PAGE”开头,后跟最多四个字符以唯一标识该节。 节名称的前四个字母 (即“PAGE”) 必须大写。

避免为代码和数据部分分配相同的名称。 为了使源代码更具可读性,驱动程序开发人员通常会将名称 PAGE 分配给可分页代码部分,因为此名称较短,并且它可能出现在许多alloc_text杂注指令中。 然后将较长的名称分配给驱动程序可能需要的任何可分页数据节 (例如,data_seg的 PAGEDATA、bss_seg的 PAGEBSS 等) 。

例如,以下代码示例中的前两个 pragma 指令定义两个可分页数据部分:PAGEDATA 和 PAGEBSS。 PAGEDATA 是使用 data_seg 杂注指令声明的,其中包含初始化的数据。 PAGEBSS 是使用 bss_seg pragma 指令声明的,并且包含未初始化的数据。

#pragma data_seg("PAGEDATA")
#pragma bss_seg("PAGEBSS")

INT Variable1 = 1;
INT Variable2;

CHAR Array1[64*1024] = { 0 };
CHAR Array2[64*1024];

#pragma data_seg()
#pragma bss_seg()

在此代码示例中, Variable1Array1 被显式初始化,因此放置在 PAGEDATA 节中。 Variable2Array2 是隐式零初始化的,并放置在 PAGEBSS 节中。

将全局变量隐式初始化为零可减小磁盘上可执行文件的大小,并且首选显式初始化为零。 除非需要显式零初始化才能将变量置于特定的数据节中,否则应避免显式零初始化。

若要使数据节在内存中驻留并锁定它,驱动程序会调用 MmLockPagableDataSection,传递显示在可分页数据节中的数据项。 MmLockPagableDataSection 返回用于后续锁定或解锁请求的句柄。

若要还原锁定分区的可分页状态,请调用 MmUnlockPagableImageSection,并根据需要传递 MmLockPagableCodeSectionMmLockPagableDataSection 返回的句柄值。 驱动程序的 Unload 例程必须调用 MmUnlockPagableImageSection 来释放它为可锁定代码和数据部分获取的每个句柄。

锁定节是一项成本高昂的操作,因为内存管理器必须先搜索其加载的模块列表,然后才能将页面锁定到内存中。 如果驱动程序在其代码中锁定多个位置的分区,则应在首次调用 MmLockPagable Xxx Section 后使用更高效的 MmLockPagableSectionByHandle

传递给 MmLockPagableSectionByHandle 的句柄是由之前调用 MmLockPagableCodeSection 或 MmLockPagableDataSection 返回 的句柄

内存管理器维护每个节句柄的计数,并在驱动程序每次为该节调用 MmLockPagableXxx 时递增此计数。 调用 MmUnlockPagableImageSection 会减少计数。 虽然任何节句柄的计数器都是非零的,但该节仍锁定在内存中。

只要加载了节的驱动程序,该节的句柄就有效。 因此,驱动程序只应调用 MmLockPagableXxxSection 一次。 如果驱动程序需要其他锁定调用,则应使用 MmLockPagableSectionByHandle

如果驱动程序为已锁定的节调用任何 MmLockPagableXxx 例程,内存管理器将递增该节的引用计数。 如果在调用锁例程时将节分页,则内存管理器将分页,并将其引用计数设置为 1。

使用此技术可将驱动程序对系统资源的影响降到最低。 驱动程序运行时,它可以将必须驻留的代码和数据锁定到内存中。 当设备没有未完成的 I/O 请求时, (,即关闭设备或从未) 打开设备时,驱动程序可以解锁相同的代码或数据,使其可供分页。

但是,在驱动程序发生连接中断后,可在中断处理期间调用的任何驱动程序代码都必须始终驻留在内存中。 虽然某些设备驱动程序可以按需进行分页或锁定到内存中,但此类驱动程序的代码和数据的某些核心集必须永久驻留在系统空间中。

请考虑以下用于锁定代码或数据部分的实现准则。

  • Mm (Un) LockXxx 例程的主要用途是使正常非分页代码或数据可分页并作为非分页代码或数据引入。 串行驱动程序和并行驱动程序等驱动程序是一个很好的示例:如果此类驱动程序管理的设备没有打开句柄,则不需要部分代码,并且可以保持分页状态。重定向人和服务器也是可以使用此方法的驱动程序的不错示例。 如果没有活动连接,则可以分页出这两个组件。

  • 整个可分页部分锁定在内存中。

  • 一个部分用于代码,一个部分用于每个驱动程序的数据是高效的。 许多命名的可分页分区通常效率低下。

  • 将纯可分页分区和分页但锁定的按需分区分开。

  • 请记住,不应频繁调用 MmLockPagableCodeSectionMmLockPagableDataSection 。 当内存管理器加载 节时,这些例程可能会导致大量 I/O 活动。 如果驱动程序必须锁定代码中多个位置的分区,则应使用 MmLockPagableSectionByHandle