在驱动和应用程序间共享内存

简介: <p class="p0" style="margin-top:0pt; text-indent:21pt; margin-bottom:0pt"><span style="font-family:黑体"><span style="font-size:10.5pt">在不同的场合,很多驱动编写人员需要在驱动和用户程序间共享内存。两种最容易的技术是:</span><span style="f

在不同的场合,很多驱动编写人员需要在驱动和用户程序间共享内存。两种最容易的技术是: 

 

l 应用程序发送IOCTL给驱动程序,提供一个指向内存的指针,之后驱动程序和应用程序就可以共享内存。(应用程序分配共享内存 

 

l 由驱动程序分配内存页,并映射这些内存页到指定用户模式进程的地址空间,并且将地址返回给应用程序。(驱动程序分配共享内存 

 

使用IOCTL共享Buffer: 

 

使用一个IOCT描述的Buffer,在驱动和用户程序间共享内存是内存共享最简单的实现形式。毕竟,IOCTL也是驱动支持其他I/O请求最经典的方法。应用程序调用Win32函数DeviceIoControl(),要被共享的Buffer的基地址和长度被放入OutBuffer参数中。对于使用这种Buffer共享方式的驱动编写者需要确定的事情就是对于特定的IOCTL采取哪种Buffer method。既可以使用METHOD_XXX_DIRECT,也可以使用METHOD_NEITHER。 

 

(PS:在METHOD_XXX_DIRECT模式下,IO管理器为应用层指定的输出缓冲区(OutputBuffer)创建一个MDL锁住该应用层的缓冲区内存,然后,我们可以在内核层中使用MmGetSystemAddressForMdlSafe获得应用层输出缓冲区所对应的内核层地址。MDL地址被放在了Irp->MdlAddress中)。 

 

如果采用METHOD_XXX_DIRECT方式,那用户Buffer将被检查是否正确存取,检查过后用户Buffer将被锁进内存。驱动需要调用MmGetSystemAddressForMdlSafe将前述Buffer映射到内核地址空间。这种方式的一个优点就是驱动可以在任意进程上下文、任意IRQL优先级别上存取共享内存Buffer。如果只需要将数据传给驱动则使用METHOD_IN_DIRECT方式。如果从驱动返回数据给应用程序或者做双向数据交换则使用METHOD_OUT_BUFFER。 

 

(PS:这些检查都将由IO管理器来负责,并且,此时IO管理器将为用户层的缓冲区创建MDL。因为此时还未到设备驱动层,当前上下文还属于当前发起 DeviceIo调用的进程,用户模式缓冲区的内存有效。但是,经过IO管理器发送IRP到下层驱动时,就不能保证当前上下文,幸而有IO管理器为我们创建MDL。这样,我们就可以在内核层获得对应的内核地址,并且自由写入数据。) 

 

使用METHOD_NEITHER方式描述一个共享内存Buffer存在许多固有的限制和需要小心的地方。(基本上,在任何时候一个驱动使用这种方式都是一样的)。其中最主要的规则是驱动只能在发起请求进程的上下文中存取Buffer。这是因为要通过Buffer的用户虚拟地址存取共享内存Buffer。这也就意味着驱动必须要在设备栈的顶端,被用户应用程序经由IO Manager直接调用。期间不能存在中间层驱动或者文件系统驱动在我们的驱动之上。在实际情况下,WDM驱动将严格限制在其Dispatch例程中存储用户Buffer。而KMDF驱动则需要在EvtIoInCallerContext事件回调函数中使用。 

 

另外一个重要的固有限制就是使用METHOD_NEITHER方式的驱动要存取用户Buffer必须在PASSIVE_LEVEL的IRQL级别。这是因为 IO Manager没有把Buffer锁在内存中,因此驱动程序想要存取共享Buffer时,内存可能被换出去了。如果驱动不能满足这个要求,就需要驱动创建一个mdl,然后将其共享Buffer锁进到内存中。 

 

(PS: METHOD_NEITHER不建议使用,还是使用直接IO好。) 

 

另外,考虑到传输类型的选择,对于这种方式可能的非直接明显的限制是对于共享的内存必须被用户模式应用程序分配。如果考虑到配额限制,能够被分配的内存数量是有限的。另外,用户应用程序不能分配物理连续的内存和Non-cache内存。当然,如果驱动和用户模式应用所有要做的就是使用合理大小的数据 Buffer将数据传入和传出,这个技术可能是最简单和实用的。 

 

和它的简易一样,使用IOCTL在驱动和用户模式应用之间共享内存的也是最常被误解的方案。一个使用这种方案的新Windows驱动开发者常犯的错误就是当驱动已经查询到了Buffer的地址后就通知结束IOCTL。这是一个非常坏的事情。为什么?如果应用程序突然退出了,比如有一个意味,会发生什么情况。另外一个问题就是当使用METHOD_XXX_DIRECT,如果带有MDL的IRP被完成,Buffer将不再被映射到系统内核地址空间,一次试图对以前有效的内核虚拟地址空间的存取(MmGetSystemAddressForMdlSafe获取)将使系统崩溃。这通常要避免。 

 

一个针对这个问题的方案是应用程序使用FILE_FLAG_OVERLAPPED打开设备并且考虑IOCTL使用一个OVERLAPPED结构。一个驱动可以针对IRP设置cancel例程(使用IoSetCancelRoutine),将IRP标记为挂起(使用IoMakeIrpPending),并且返回给调用者STATUS_PENGDING前将IRP放进内部队列。当然,KMDF驱动对这类问题可以放心,只需要将请求设置为进行中并且可取消,就像 WDFQUEUE。 

 

(PS: 要小心使用MDL,防止应用层程序意外退出而造成MDL所描述的虚拟内存无效。) 

 

使用这种方法有两个优点: 

 

1、当应用程序从IOCTL调用中得到ERROR_IO_PENDING的返回结果时,知道Buffer被映射了。并且知道什么时候IOCTL最终完成并将Buffer取消映射。 

 

2、通过取消例程(WDM)或者一个EvtIoCancelOnQueue事件处理回调例程,驱动程序成功在应用程序退出或者取消IO命令时得到通知,所以它可以执行必要的操作来完成IOCTL。因而有MDL位置用于内存取消映射操作。

分配并且映射页:

 

 

现在剩下了前面提到的第二种方法:分配内存页并且映射这些页到特定进程的用户虚拟地址空间上。使用大多数Windows驱动编写者常见的API,这个方法令人惊讶的容易,同时也允许驱动对分配内存的类型具有最大的控制能力。 

 

驱动无论使用什么标准方法,都是希望分配内存来共享。例如,如果驱动需要一个适当的设备(逻辑)地址作DMA,就像内存块的内核虚拟地址,它能够使用 AllocateCommonBuffer来分配内存。如果没有要求特定的内存特性,要被共享的内存大小也是适度的,驱动可以将0填充、非分页物理内存页分配给Buffer。 

 

从主内存分配0填充、非分页的页面,使用MmAllocatePagesForMDL或者MmAllocatePagesForMdlEx。这些函数返回一个MDL描述内存的分配。驱动使用函数MmGetSystemAddressForMdlSafe映射MDL描述的页到内核虚拟地址空间。从主内存分配页比使用分页内存池或者非分页内存池得到的内存更加安全,后者不是一个好主意。 

 

PS:这种方式是内核来分配内存空间,但是是使用MmAllocatePagesForMDL从主内存池中分配,返回得到一个MDL,对于驱动如何使用该共享内存,采用MmGetSystemAddressForMdlSafe得到其内核地址。对于应用层使用该共享内存,采用 MmMapLockedPagesSpecifyCache映射到应用层进程地址空间中,返回用户层地址空间的起始地址,将其放在IOCTL中返回给用户应用程序。 

 

借助一个用来描述共享内存的MDL,驱动现在准备映射这些页到用户进程地址空间。这可以使用函数MmMapLockedPagesSpecifyCache来实现。你需要知道调用这个函数的窍门是: 

你必须在你希望映射Buffer的进程上下文中调用这个函数。 

 

PS:如果是在别的进程上下文中调用,就变成了映射到其他进程上下文中了,但是我如何保证在我希望映射Buffer的进程上下文调用呢? 

 

设定AccessMode参数为UserMode。对MmMapLockedPagesSpecifyCache函数调用返回值是MDL描述内存页映射的用户虚拟地址空间地址。驱动可以将其放在对应IOCTL的缓存中给用户应用程序 。

 

你需要有一个方法,在不需要时将分配的内存清除掉。换句话说,你需要调用MmFreePageFromMdl来释放内存页。并且调用IoFreeMdl来释放由MmAllocatePageForMdl(Ex)创建的MDL。你几乎都是在你驱动的IRP_MJ_CLEANUP处理例程(WDM)或者 EvtFileCleanup事件处理回调(KMDF中作这个工作)。

 

这是所要做的,综合起来,完成这个过程的代码见下面。

 

PVOID CreateAndMapMemory(OUT PMDL* PMemMdl,

OUT PVOID* UserVa)

{

PMDL Mdl;

PVOID UserVAToReturn;

PHYSICAL_ADDRESS LowAddress;

PHYSICAL_ADDRESS HighAddress;

SIZE_T TotalBytes;

 

// 初始化MmAllocatePagesForMdl需要的Physical Address

LowAddress.QuadPart = 0;

MAX_MEM(HighAddress.QuardPart);

TotalBytes.QuadPart = PAGE_SIZE;

 

// 分配4K的共享缓冲区

Mdl = MmAllocatePagesForMdl(LowAddress,

HighAddress,

LowAddress,

TotalBytes);

if(!Mdl)

{

Return STATUS_INSUFFICIENT_RESOURCES;

}

 

// 映射共享缓冲区到用户地址空间

UserVAToReturn = MmMapLockedPagesSpecifyCache(Mdl,

UserMode,

MmCached,

NULL,

FALSE,

NormalPagePriority);

 

if(!UserVAToReturn)

{

MmFreePagesFromMdl(Mdl);

IoFreeMdl(Mdl);

Return STATUS_INSUFFICIENT_RESOURCE;

}

 

// 返回,得到MDL和用户层的虚拟地址

*UserVa = UserVAToReturn;

*PMemMdl = Mdl;

 

return STATUS_SUCCESS;

}

 

当然,这种方法也有缺点,调用MmMapLockedPagesSpecifyCache必须在你希望内存页被映射的进程上下文来做。较之使用 METHOD_NEITHER的IOCTL方法,该方法表现出不必其更多的灵活性。然而,不像前者,后者只需一个函数(MmMapLockerPagesSpecifyCache)在目标上下文被调用。由于很多OEM设备驱动在设备栈中只有一个且直接基于总线的(也就是在其上没有别的设备,除了总线驱动其下没有别的驱动),这个条件很容易满足。对于那些少量的设备驱动,处于设备栈的深处并且需要和用户模式应用直接共享 Buffer的,一个企业级的驱动编写者可能能找到一个安全的地方在请求的进程上下文中调用。

 

在页面被映射以后,共享内存就可以象使用METHOD_XXX_DIRECT的IOCTL方法一样,能够在任意的进程上下文被存取,也可以在高IRQL上存取(因为共享内存来之非分页内存)。

 

PS:需要我们确定的一点就是何时调用MmMapLockedPagesSpecifyCache安全的映射到指定进程的上下文中。还有一点,就是该共享内存处于非分页内存中,所以可以在搞IRQL上存取。

 

 

如果你使用这种方法,有一个决定性的事情一直要记者:你必须确信你的驱动要提供方法,在任何时候用户进程退出的时候,能够将你映射到用户空间的页面作取消映射的操作。这件事情的失败会导致系统在应用层退出的时候崩溃。我们找到一个简单方法就是无论何时应用层关闭设备句柄,则对这些页面作取消映射操作。由于应用层关闭句柄,出现意外或者其他情况,驱动将收到对应于该应用层打开的设备文件对象的一个IRP_MJ_CLEANUP,你可以确信这是工作的。你将在 CLEANUP使执行这些操作,而不是CLOSE,因为你可以保证在请求线程的上下文中得到Cleanup IRP。下面代码可以看见分配资源的释放。

 

VOID UnMapAndFreeMemory(PMDL PMdl,PVOID UserVa)

{

if(!PMdl)

{ return ;}

 

// 解除映射

MmUnMapLockerPages(UserVa,PMdl);

// 释放MDL锁定的物理页

MmFreePagesFromMdl(PMdl);

// 释放MDL

IoFreeMdl(PMdl);

}

 

其他挑战:

 

无论使用哪种机制,驱动和应用程序将需要支持同步存取共享内存的通用方式,这可以通过很多许多方法来做。可能最简单的机制是共享一个或者多个命名事件。应用和驱动共享事件的最简单方法就是应用层生成事件,然后将事件句柄传递给驱动层驱动然后从应用层的上下文中Reference事件句柄如果你使用这种方法,请不要忘记在驱动的Cleanup处理代码中Dereference这个句柄

 

PS:一定要注意解引用来自应用层的事件对象。

 

总结:

 

我们观察了两种在驱动和用户模式应用程序共享内存的方法:

1、用户层创建缓冲区并且通过IOCTL传递给驱动

2、在驱动中使用MmAllocatePagesForMdl分配内存页,得到MDL,然后将该MDL所描述的内存映射到用户层地址空间(MmMapLockedPagesSpecifyCache)。得到用户地址空间的起始地址,并通过IOCTL返回给用户层。

 

译者注:

 

在使用命名事件来同步驱动和应用程序共享缓冲区时,一般不要使用驱动程序创建命名事件,然后根据应用程序名称打开的方法。这种方法虽然可以使得驱动激活事件后,所有相关应用程序都能够被唤醒,方便程序的开发,但是他有两个问题:一是命名事件只有在WIN32子系统起来后才能正确创建,这会影响到驱动程序开发。最严重的问题是在驱动中创建的事件其存取权限要求比较高,在WinXP下要求具有Administrator组权限的用户创建的应用程序才能够存取该事件。在Vista系统下由于安全功能的强化,这方面的问题更加严重。因此尽量使用应用程序创建的事件,或者通过其他同步方式

相关文章
|
12月前
|
SQL 缓存 安全
深度理解 Java 内存模型:从并发基石到实践应用
本文深入解析 Java 内存模型(JMM),涵盖其在并发编程中的核心作用与实践应用。内容包括 JMM 解决的可见性、原子性和有序性问题,线程与内存的交互机制,volatile、synchronized 和 happens-before 等关键机制的使用,以及在单例模式、线程通信等场景中的实战案例。同时,还介绍了常见并发 Bug 的排查与解决方案,帮助开发者写出高效、线程安全的 Java 程序。
588 0
|
存储 缓存 监控
如何使用内存监控工具来优化 Node.js 应用的性能
需要注意的是,不同的内存监控工具可能具有不同的功能和特点,在使用时需要根据具体工具的要求和操作指南进行正确使用和分析。
814 158
|
JavaScript
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
753 159
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
755 174
|
存储 缓存 JavaScript
如何优化Node.js应用的内存使用以提高性能?
通过以上多种方法的综合运用,可以有效地优化 Node.js 应用的内存使用,提高性能,提升用户体验。同时,不断关注内存管理的最新技术和最佳实践,持续改进应用的性能表现。
861 62
|
开发框架 .NET PHP
网站应用项目如何选择阿里云服务器实例规格+内存+CPU+带宽+操作系统等配置
对于使用阿里云服务器的搭建网站的用户来说,面对众多可选的实例规格和配置选项,我们应该如何做出最佳选择,以最大化业务效益并控制成本,成为大家比较关注的问题,如果实例、内存、CPU、带宽等配置选择不合适,可能会影响到自己业务在云服务器上的计算性能及后期运营状况,本文将详细解析企业在搭建网站应用项目时选购阿里云服务器应考虑的一些因素,以供参考。
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
694 1
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
435 5
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。