<p>在不一样的场合,不少驱动编写人员须要在驱动和用户程序间共享内存。两种最容易的技术是:</p> <p>l 应用程序发送IOCTL给驱动程序,提供一个指向内存的指针,以后驱动程序和应用程序就能够共享内存。(应用程序分配共享内存)</p> <p>l 由驱动程序分配内存页,并映射这些内存页到指定用户模式进程的地址空间,而且将地址返回给应用程序。(驱动程序分配共享内存)</p> <p>使用IOCTL共享Buffer:</p> <p>使用一个IOCT描述的Buffer,在驱动和用户程序间共享内存是内存共享最简单的实现形式。毕竟,IOCTL也是驱动支持其余I/O请求最经典的方法。应用程序调用Win32函数DeviceIoControl(),要被共享的Buffer的基地址和长度被放入OutBuffer参数中。对于使用这种Buffer共享方式的驱动编写者须要肯定的事情就是对于特定的IOCTL采起哪一种Buffer method。既可使用METHOD_XXX_DIRECT,也可使用METHOD_NEITHER。</p> <p>(PS:在METHOD_XXX_DIRECT模式下,IO管理器为应用层指定的输出缓冲区(OutputBuffer)建立一个MDL锁住该应用层的缓冲区内存,而后,咱们能够在内核层中使用MmGetSystemAddressForMdlSafe得到应用层输出缓冲区所对应的内核层地址。MDL地址被放在了Irp->MdlAddress中)。</p> <p>若是采用METHOD_XXX_DIRECT方式,那用户Buffer将被检查是否正确存取,检查事后用户Buffer将被锁进内存。驱动须要调用MmGetSystemAddressForMdlSafe将前述Buffer映射到内核地址空间。这种方式的一个优势就是驱动能够在任意进程上下文、任意IRQL优先级别上存取共享内存Buffer。若是只须要将数据传给驱动则使用METHOD_IN_DIRECT方式。若是从驱动返回数据给应用程序或者作双向数据交换则使用METHOD_OUT_BUFFER。</p> <p>(PS:这些检查都将由IO管理器来负责,而且,此时IO管理器将为用户层的缓冲区建立MDL。由于此时还未到设备驱动层,当前上下文还属于当前发起 DeviceIo调用的进程,用户模式缓冲区的内存有效。可是,通过IO管理器发送IRP到下层驱动时,就不能保证当前上下文,幸而有IO管理器为咱们建立MDL。这样,咱们就能够在内核层得到对应的内核地址,而且自由写入数据。)</p> <p>使用METHOD_NEITHER方式描述一个共享内存Buffer存在许多固有的限制和须要当心的地方。(基本上,在任什么时候候一个驱动使用这种方式都是同样的)。其中最主要的规则是驱动只能在发起请求进程的上下文中存取Buffer。这是由于要经过Buffer的用户虚拟地址存取共享内存Buffer。这也就意味着驱动必需要在设备栈的顶端,被用户应用程序经由IO Manager直接调用。期间不能存在中间层驱动或者文件系统驱动在咱们的驱动之上。在实际状况下,WDM驱动将严格限制在其Dispatch例程中存储用户Buffer。而KMDF驱动则须要在EvtIoInCallerContext事件回调函数中使用。</p> <p>另一个重要的固有限制就是使用METHOD_NEITHER方式的驱动要存取用户Buffer必须在PASSIVE_LEVEL的IRQL级别。这是由于 IO Manager没有把Buffer锁在内存中,所以驱动程序想要存取共享Buffer时,内存可能被换出去了。若是驱动不能知足这个要求,就须要驱动建立一个mdl,而后将其共享Buffer锁进到内存中。</p> <p>(PS: METHOD_NEITHER不建议使用,仍是使用直接IO好。)</p> <p>另外,考虑到传输类型的选择,对于这种方式可能的非直接明显的限制是对于共享的内存必须被用户模式应用程序分配。若是考虑到配额限制,可以被分配的内存数量是有限的。另外,用户应用程序不能分配物理连续的内存和Non-cache内存。固然,若是驱动和用户模式应用全部要作的就是使用合理大小的数据 Buffer将数据传入和传出,这个技术多是最简单和实用的。</p> <p>和它的简易同样,使用IOCTL在驱动和用户模式应用之间共享内存的也是最常被误解的方案。一个使用这种方案的新Windows驱动开发者常犯的错误就是当驱动已经查询到了Buffer的地址后就通知结束IOCTL。这是一个很是坏的事情。为何?若是应用程序忽然退出了,好比有一个意味,会发生什么状况。另一个问题就是当使用METHOD_XXX_DIRECT,若是带有MDL的IRP被完成,Buffer将再也不被映射到系统内核地址空间,一次试图对之前有效的内核虚拟地址空间的存取(MmGetSystemAddressForMdlSafe获取)将使系统崩溃。这一般要避免。</p> <p>一个针对这个问题的方案是应用程序使用FILE_FLAG_OVERLAPPED打开设备而且考虑IOCTL使用一个OVERLAPPED结构。一个驱动能够针对IRP设置cancel例程(使用IoSetCancelRoutine),将IRP标记为挂起(使用IoMakeIrpPending),而且返回给调用者STATUS_PENGDING前将IRP放进内部队列。固然,KMDF驱动对这类问题能够放心,只须要将请求设置为进行中而且可取消,就像 WDFQUEUE。</p> <p>(PS: 要当心使用MDL,防止应用层程序意外退出而形成MDL所描述的虚拟内存无效。)</p> <p>使用这种方法有两个优势:</p> <p>一、当应用程序从IOCTL调用中获得ERROR_IO_PENDING的返回结果时,知道Buffer被映射了。而且知道何时IOCTL最终完成并将Buffer取消映射。</p> <p>二、经过取消例程(WDM)或者一个EvtIoCancelOnQueue事件处理回调例程,驱动程序成功在应用程序退出或者取消IO命令时获得通知,因此它能够执行必要的操做来完成IOCTL。于是有MDL位置用于内存取消映射操做。</p> <p>分配而且映射页:</p> <p>如今剩下了前面提到的第二种方法:分配内存页而且映射这些页到特定进程的用户虚拟地址空间上。使用大多数Windows驱动编写者常见的API,这个方法使人惊讶的容易,同时也容许驱动对分配内存的类型具备最大的控制能力。</p> <p>驱动不管使用什么标准方法,都是但愿分配内存来共享。例如,若是驱动须要一个适当的设备(逻辑)地址做DMA,就像内存块的内核虚拟地址,它可以使用 AllocateCommonBuffer来分配内存。若是没有要求特定的内存特性,要被共享的内存大小也是适度的,驱动能够将0填充、非分页物理内存页分配给Buffer。</p> <p>从主内存分配0填充、非分页的页面,使用MmAllocatePagesForMDL或者MmAllocatePagesForMdlEx。这些函数返回一个MDL描述内存的分配。驱动使用函数MmGetSystemAddressForMdlSafe映射MDL描述的页到内核虚拟地址空间。从主内存分配页比使用分页内存池或者非分页内存池获得的内存更加安全,后者不是一个好主意。</p> <p>PS:这种方式是内核来分配内存空间,可是是使用MmAllocatePagesForMDL从主内存池中分配,返回获得一个MDL,对于驱动如何使用该共享内存,采用MmGetSystemAddressForMdlSafe获得其内核地址。对于应用层使用该共享内存,采用 MmMapLockedPagesSpecifyCache映射到应用层进程地址空间中,返回用户层地址空间的起始地址,将其放在IOCTL中返回给用户应用程序。</p> <p>借助一个用来描述共享内存的MDL,驱动如今准备映射这些页到用户进程地址空间。这可使用函数MmMapLockedPagesSpecifyCache来实现。你须要知道调用这个函数的窍门是:</p> <p>你必须在你但愿映射Buffer的进程上下文中调用这个函数。</p> <p>PS:若是是在别的进程上下文中调用,就变成了映射到其余进程上下文中了,可是我如何保证在我但愿映射Buffer的进程上下文调用呢?</p> <p>设定AccessMode参数为UserMode。对MmMapLockedPagesSpecifyCache函数调用返回值是MDL描述内存页映射的用户虚拟地址空间地址。驱动能够将其放在对应IOCTL的缓存中给用户应用程序 。</p> <p>你须要有一个方法,在不须要时将分配的内存清除掉。换句话说,你须要调用MmFreePageFromMdl来释放内存页。而且调用IoFreeMdl来释放由MmAllocatePageForMdl(Ex)建立的MDL。你几乎都是在你驱动的IRP_MJ_CLEANUP处理例程(WDM)或者 EvtFileCleanup事件处理回调(KMDF中做这个工做)。</p> <p>这是所要作的,综合起来,完成这个过程的代码见下面。</p> <div id="scid:9D7513F9-C04C-4721-824A-2B34F0212519:3c8500d5-d6c2-48ac-82a8-27f784a03119" class="wlWriterEditableSmartContent" style="float: none; padding-bottom: 0px; padding-top: 0px; padding-left: 0px; margin: 0px; display: inline; padding-right: 0px"><pre class="brush: cpp; gutter: true; first-line: 1; tab-size: 4; toolbar: false; width: 497px; height: 410px;" style=" width: 497px; height: 410px;overflow: auto;">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;
}spa
</pre><!-- Code inserted with Steve Dunn's Windows Live Writer Code Formatter Plugin. http://dunnhq.com --></div>线程
<p>固然,这种方法也有缺点,调用MmMapLockedPagesSpecifyCache必须在你但愿内存页被映射的进程上下文来作。较之使用 METHOD_NEITHER的IOCTL方法,该方法表现出没必要其更多的灵活性。然而,不像前者,后者只需一个函数(MmMapLockerPagesSpecifyCache)在目标上下文被调用。因为不少OEM设备驱动在设备栈中只有一个且直接基于总线的(也就是在其上没有别的设备,除了总线驱动其下没有别的驱动),这个条件很容易知足。对于那些少许的设备驱动,处于设备栈的深处而且须要和用户模式应用直接共享 Buffer的,一个企业级的驱动编写者可能能找到一个安全的地方在请求的进程上下文中调用。</p>指针
<p>在页面被映射之后,共享内存就能够象使用METHOD_XXX_DIRECT的IOCTL方法同样,可以在任意的进程上下文被存取,也能够在高IRQL上存取(由于共享内存来之非分页内存)。</p>code
<p>PS:须要咱们肯定的一点就是什么时候调用MmMapLockedPagesSpecifyCache安全的映射到指定进程的上下文中。还有一点,就是该共享内存处于非分页内存中,因此能够在搞IRQL上存取。</p>orm
<p>若是你使用这种方法,有一个决定性的事情一直要记者:你必须确信你的驱动要提供方法,在任什么时候候用户进程退出的时候,可以将你映射到用户空间的页面做取消映射的操做。这件事情的失败会致使系统在应用层退出的时候崩溃。咱们找到一个简单方法就是不管什么时候应用层关闭设备句柄,则对这些页面做取消映射操做。因为应用层关闭句柄,出现意外或者其余状况,驱动将收到对应于该应用层打开的设备文件对象的一个IRP_MJ_CLEANUP,你能够确信这是工做的。你将在 CLEANUP使执行这些操做,而不是CLOSE,由于你能够保证在请求线程的上下文中获得Cleanup IRP。下面代码能够看见分配资源的释放。</p>对象
<div id="scid:9D7513F9-C04C-4721-824A-2B34F0212519:898ad414-41d5-4703-8b70-801b06b0f32a" class="wlWriterEditableSmartContent" style="float: none; padding-bottom: 0px; padding-top: 0px; padding-left: 0px; margin: 0px; display: inline; padding-right: 0px"><pre class="brush: cpp; gutter: true; first-line: 1; tab-size: 4; toolbar: false; width: 497px; height: 410px;" style=" width: 497px; height: 410px;overflow: auto;">VOID UnMapAndFreeMemory(PMDL PMdl,PVOID UserVa)队列
{
if(!PMdl) { return ;} // 解除映射 MmUnMapLockerPages(UserVa,PMdl); // 释放MDL锁定的物理页 MmFreePagesFromMdl(PMdl); // 释放MDL IoFreeMdl(PMdl);
}
</pre><!-- Code inserted with Steve Dunn's Windows Live Writer Code Formatter Plugin. http://dunnhq.com --></div>
<p>其余挑战:</p>
<p>不管使用哪一种机制,驱动和应用程序将须要支持同步存取共享内存的通用方式,这能够经过不少许多方法来作。可能最简单的机制是共享一个或者多个命名事件。应用和驱动共享事件的最简单方法就是应用层生成事件,而后将事件句柄传递给驱动层。驱动而后从应用层的上下文中Reference事件句柄。若是你使用这种方法,请不要忘记在驱动的Cleanup处理代码中Dereference这个句柄。</p>
<p>PS:必定要注意解引用来自应用层的事件对象。</p>
<p>总结:</p>
<p>咱们观察了两种在驱动和用户模式应用程序共享内存的方法:</p>
<p>一、用户层建立缓冲区而且经过IOCTL传递给驱动</p>
<p>二、在驱动中使用MmAllocatePagesForMdl分配内存页,获得MDL,而后将该MDL所描述的内存映射到用户层地址空间(MmMapLockedPagesSpecifyCache)。获得用户地址空间的起始地址,并经过IOCTL返回给用户层。</p>
<p>译者注:</p>
<p>在使用命名事件来同步驱动和应用程序共享缓冲区时,通常不要使用驱动程序建立命名事件,而后根据应用程序名称打开的方法。这种方法虽然可使得驱动激活事件后,全部相关应用程序都可以被唤醒,方便程序的开发,可是他有两个问题:一是命名事件只有在WIN32子系统起来后才能正确建立,这会影响到驱动程序开发。最严重的问题是在驱动中建立的事件其存取权限要求比较高,在WinXP下要求具备Administrator组权限的用户建立的应用程序才可以存取该事件。在Vista系统下因为安全功能的强化,这方面的问题更加严重。所以尽可能使用应用程序建立的事件,或者经过其余同步方式</p>