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

简介: <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系统下由于安全功能的强化,这方面的问题更加严重。因此尽量使用应用程序创建的事件,或者通过其他同步方式

相关文章
|
27天前
|
存储 Java C++
C++ 引用和指针:内存地址、创建方法及应用解析
C++中的引用是现有变量的别名,创建时需用`&`运算符,如`string &meal = food;`。指针存储变量的内存地址,使用`*`创建,如`string* ptr = &food;`。引用必须初始化且不可为空,而指针可初始化为空。引用在函数参数传递和提高效率时有用,指针适用于动态内存分配和复杂数据结构操作。选择使用取决于具体需求。
40 9
|
1月前
|
存储 编译器 C语言
深入探索C语言动态内存分配:释放你的程序潜力
深入探索C语言动态内存分配:释放你的程序潜力
30 0
|
7天前
|
缓存 安全 Android开发
构建高效Android应用:采用Kotlin进行内存优化
【5月更文挑战第1天】随着移动设备的普及,用户对应用程序的性能要求越来越高。特别是对于Android开发者来说,理解并优化应用的内存使用是提升性能的关键。本文将探讨使用Kotlin语言在Android开发中实现内存优化的策略和技术。我们将深入分析Kotlin特有的语言特性和工具,以及它们如何帮助开发者减少内存消耗,避免常见的内存泄漏问题,并提高整体应用性能。
|
8天前
|
机器学习/深度学习 自动驾驶 安全
深入理解操作系统内存管理:策略与实现基于深度学习的图像识别技术在自动驾驶系统中的应用
【4月更文挑战第30天】 在现代计算机系统中,操作系统的内存管理是确保系统高效、稳定运行的关键组成部分。本文将深入探讨操作系统中内存管理的多种策略及其实现机制,包括但不限于分页、分段和段页式结合等技术。我们将剖析内存分配的原理,讨论虚拟内存技术的实现以及它如何提供更大的地址空间并允许内存的交换。同时,我们还会涉及内存保护机制,它们是如何防止程序访问未授权的内存区域。最后,文中将对现代操作系统如Linux和Windows中的内存管理实践进行比较分析,以期给读者提供全面而深入的理解和参考。 【4月更文挑战第30天】 随着人工智能技术的飞速发展,深度学习已经
|
8天前
|
安全 网络安全 Android开发
云端防御策略:融合云服务与网络安全的未来构建高效的Android应用:从内存优化到电池寿命
【4月更文挑战第30天】 随着企业加速向云计算环境转移,数据和服务的云端托管成为常态。本文探讨了在动态且复杂的云服务场景下,如何构建和实施有效的网络安全措施来保障信息资产的安全。我们将分析云计算中存在的安全挑战,并展示通过多层次、多维度的安全框架来提升整体防护能力的方法。重点关注包括数据加密、身份认证、访问控制以及威胁检测与响应等关键技术的实践应用,旨在为读者提供一种结合最新技术进展的网络安全策略视角。 【4月更文挑战第30天】 在竞争激烈的移动市场中,Android应用的性能和资源管理已成为区分优秀与平庸的关键因素。本文深入探讨了提升Android应用效率的多个方面,包括内存优化策略、电池
|
8天前
|
算法 安全 Android开发
深入理解操作系统的内存管理机制构建高效Android应用:Kotlin的协程优势
【4月更文挑战第30天】 在现代计算机系统中,操作系统的内存管理是确保系统高效、稳定运行的关键。本文将探讨操作系统内存管理的核心技术,包括内存分配、虚拟内存、分页和分段等概念,以及它们是如何协同工作以提高内存利用率和系统性能的。通过对这些技术的详细分析,我们可以更好地理解操作系统背后的原理,并评估不同内存管理策略对系统行为的影响。 【4月更文挑战第30天】 在移动开发领域,尤其是针对Android平台,性能优化和流畅的用户体验始终是开发者追求的核心目标。随着Kotlin语言的普及,协程作为其在异步编程领域的杀手锏特性,已经逐渐成为提高应用性能和简化代码结构的重要工具。本文将深入探讨Kotli
|
8天前
|
缓存 监控 Android开发
构建高效Android应用:从内存优化到电池续航
【4月更文挑战第30天】 在移动开发领域,性能优化是一个永不过时的话题。对于Android应用而言,实现流畅的用户体验和延长设备电池寿命是至关重要的。本文将深入探讨Android平台特有的内存管理和电池使用策略,并提出一系列切实可行的优化措施。通过智能管理应用的生命周期、合理利用系统资源和调整后台任务执行策略,开发者可以显著提升应用性能并减少能源消耗。文章最后还将讨论如何利用Android Studio内置工具进行性能分析与监控,确保应用在发布前达到最优状态。
|
9天前
|
存储 缓存 数据库
构建高效Android应用:内存优化策略深度剖析
【4月更文挑战第29天】 在移动开发领域,性能一直是衡量应用质量的关键指标之一。特别是对于Android平台,由于设备硬件配置的多样性,内存管理成为开发者面临的一大挑战。本文将深入探讨Android应用内存优化的有效策略,旨在帮助开发者提升应用性能,减少内存消耗,避免常见的内存泄漏问题。通过对Android内存管理机制的分析与实际案例的结合,我们将提供一系列实用的优化技巧,助力应用在竞争激烈的市场中脱颖而出。
|
10天前
|
运维 Serverless API
Serverless 应用引擎产品使用之在阿里云函数计算中,容器运行过程中的最大内存使用量获取如何解决
阿里云Serverless 应用引擎(SAE)提供了完整的微服务应用生命周期管理能力,包括应用部署、服务治理、开发运维、资源管理等功能,并通过扩展功能支持多环境管理、API Gateway、事件驱动等高级应用场景,帮助企业快速构建、部署、运维和扩展微服务架构,实现Serverless化的应用部署与运维模式。以下是对SAE产品使用合集的概述,包括应用管理、服务治理、开发运维、资源管理等方面。
32 2
|
10天前
|
运维 JavaScript Java
Serverless 应用引擎产品使用之Nacos 在集中发版时遇到老年代暂满,并且频繁进行 Full GC,但是 GC 后内存没有降下来如何解决
阿里云Serverless 应用引擎(SAE)提供了完整的微服务应用生命周期管理能力,包括应用部署、服务治理、开发运维、资源管理等功能,并通过扩展功能支持多环境管理、API Gateway、事件驱动等高级应用场景,帮助企业快速构建、部署、运维和扩展微服务架构,实现Serverless化的应用部署与运维模式。以下是对SAE产品使用合集的概述,包括应用管理、服务治理、开发运维、资源管理等方面。
19 0