Windows基础知识
+ -

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

2022-08-31 246 0
原文转自:https://blog.csdn.net/lioqio/article/details/2140453

译自:The NT Insider November-December 2007 Volume 14 Issue 4
译者:lioqio
[编辑提示:《The NT Insider》将陆续对以前文章进行维护修正,以便确认这些文章针对Windows的大多数当前版本是正确的,并且反映了最好的工程实践。这篇文章是OSR在线上最频繁被引用的文章之一,作为该系列的第一篇]

在不同的场合,很多驱动编写人员需要在驱动和用户模式程序间共享内存。当然大多数这种事情,有很多方法完成目标。其中一些方案是正确的,而一些被确定是错误的。两种最容易的技术是:

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

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

考虑到篇幅,我们将讨论内容限制在这两种直接技术上。其他可接受的好技术包括共享一个由换页文件或者内存映射文件后台支持的命名段。我们可能在将来的文章中讨论这些技术。同样注意,这篇文章并不是特指驻留在设备上的共享内存。尽管很多概念都是一样的,与一个用户模式程序共享设备内存也将带来一系列特定的挑战。

使用IOCTL共享Buffer

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

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

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

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

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

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

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

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

  • 当应用程序从IOCTL调用中得到ERROR_IO_PENDING的返回结果时,知道Buffer被映射了。并且知道什么时候IOCTL最终完成并将Buffer取消映射。
  • 通过取消例程(WDM)或者一个EvtIoCancelOnQueue事件处理回调例程,驱动能够在应用程序退出或者取消I/O命令时得到通知,所以它可以执行必要的操作来完成IOCTL,因而有MDL位置用于内存取消映射操作。

分配并且映射页

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

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

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

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

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

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

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

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

PVOID CreateAndMapMemory(PMDL* PMemMdl,PVOID* UserVa)
{
    PMDL mdl;
    PVOID UserVAToReturn;
    PHYSICAL_ADDRESS lowAddress;
    PHYSICAL_ADDRESS highAddress;
    SIZE_T totalBytes;

    //Initialize the Physical addresses need for MmAllocatePagesForMdl

    lowAddress.QuadPart = 0;
    MAX_MEM(highAddress.Quadpart);
    totalBytes.Quadpart = PAGE_SIZE;

    //Allocate a 4k buffer to share with the application
    mdl = MmAllocatePagesForMdl(lowAddress,highAddress,lowAddress,totalBytes);

    if(!mdl){
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    //The preferred way to map the buffer into user space

    userVAToReturn=
        MmMapLockedPagesSpecifyCache(mdl,        //MDL
                                     UserMode,   //Mode
                                     MmCached,   //Caching
                                     NULL,       //Address
                                     FALSE,      //Bugcheck
                                     NormalPagePriority);//Priority

    //If we get NULL back,the request didn’t work
    //I’m thinkin’ that’s better than a bug check anyday
    if(!userVAToReturn){
        MmFreePagesFromMdl(mdl);
        IoFreeMdl(mdl);
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    //Return the allocated pointers
    *UserVa = userVAToReturn;
    *PMemMdl = mdl;
    DbgPrint(“UserVA = 0x%0x/n”,userVAToReturn);
    return STATUS_SUCCESS;
}

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

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

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

void UnMapAndFreeMemory(PMDL PMdl,PVOID UserVa)
{

    //Make sure we have an MDL to free
    if(!PMdl) {
        return;
    }

    //return the allocated resource

    MmUnMapLockedPages(UserVa,PMdl);
    MmFreePagesFromMdl(PMdl);
    IoFreeMdl(PMdl);
}

其它挑战

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

总结

我们观察了两种在驱动和用户模式应用程序共享内存的方法:使用用户程序创建的Buffer并且通过IOCTL传给驱动;在驱动中使用MmAllocatePageForMdl分配内存页,然后使用MmMapLockedPagesSpecifyCache函数映射内存到应用程序的地址空间。

与本文配套的MemDrv例子驱动和测试程序演示了这两种技术的实现,两种方法都相对比较简单,主要你遵循很少的规则。祝玩得开心!

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

0 篇笔记 写笔记

在驱动程序和应用程序间共享内存
译自:The NT Insider November-December 2007 Volume 14 Issue 4译者:lioqio[编辑提示:《The NT Insider》将陆续对以前文章进行维护修正,以便确认这些文章针对Windows的大多数当前版本是正确的,并且反映了最好的工程实践。这篇文......
Windows应用层创建共享内存,内核层使用ZwOpenSection打开
应用层创建共享内存,并使用RefreshBuffer通知内核层刷新数据:typedef struct __SHAREDBUFFER_HEADER { DWORD dwFrameType; /* compression hex (Data1 part......
作者信息
我爱内核
Windows驱动开发,网站开发
好好学习,天天向上。
取消
感谢您的支持,我会继续努力的!
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

您的支持,是我们前进的动力!