在早期,是按照基于包或者基于流的方式来描述DMA的,不过这个描述可能不准确,故在Vista之后修改为使用数据包/使用公共缓冲区的系统DMA。
简单的解释一下基于包和基于流的说法的原因,数据包是指一个个基于一定大小的数据块,例如设定4096大小的内存页面,如果我们使用基于包的方式传输5000个字节,那么就是两个数据包,第一个数据包为4096字节,第二个数据包是904字节,实际传输两个数据包;如果使用基于流的,那么流传输每次回来1个数据包,缓冲区会根据自身的大小从里面拷贝数据,那么系统缓冲区先从第一个数据包中拷贝4096个字节,再从第二个数据包中拷贝906个字节。
在上面的描述中,我们会发现基于流的数据包对于数据利用率和容忍度更高一些。
使用公用缓冲区系统 DMA
使用系统 DMA 控制器的自动初始化模式的驱动程序必须为可以执行 DMA 传输的缓冲区分配内存。驱动程序调用 AllocateCommonBuffer 来获取此缓冲区,通常来自处理IRP_MN_START_DEVICE请求的 DispatchPnP 例程。 下图显示了驱动程序如何分配缓冲区并将其虚拟地址范围映射到系统物理内存。
如上图所示,驱动程序执行以下步骤为系统 DMA 分配缓冲区:
1. 驱动程序调用 AllocateCommonBuffer,将指针传递到 IoGetDmaAdapter 返回的适配器对象,以及为其缓冲区请求的长度(以字节为单位)。 若要以经济方式使用内存,缓冲区的输入 Length 值应小于或等于 PAGE_SIZE或应是PAGE_SIZE的整数倍数;
2. 如果 AllocateCommonBuffer 返回 NULL 指针,驱动程序应释放已声明的任何系统资源,并返回STATUS_INSUFFICIENT_RESOURCES以响应 IRP_MN_START_DEVICE 请求;否则, AllocateCommonBuffer 在系统虚拟地址空间中分配请求的内存量,并返回指向该缓冲区的两种不同类型的指针:
- 上图中缓冲区的 LogicalAddress (BufferLogicalAddress) ,驱动程序必须为此提供存储,但随后应忽略存储
- 上图中缓冲区虚拟地址 (BufferVirtualAddress) ,驱动程序还必须存储该地址,以便它可以生成描述其缓冲区的 MDL 以执行 DMA 操作
驱动程序应将这些指针存储在设备扩展或其他驱动程序分配的驻留内存中;
3. 驱动程序调用 IoAllocateMdl 为缓冲区分配 MDL。 驱动程序传递 AllocateCommonBuffer 返回的缓冲区的 VirtualAddress 及其缓冲区的 Length 以分配 MDL;
4. 驱动程序使用 IoAllocateMdl 返回的指针调用 MmBuildMdlForNonPagedPool,以将其驻留缓冲区的虚拟地址范围映射到系统物理内存;
分配公共缓冲区并映射其虚拟地址范围后,从属设备的驱动程序可以开始处理请求 DMA 传输的 IRP。 为此,驱动程序调用以下常规支持例程序列:
- 由驱动程序编写者自行决定, RtlMoveMemory 将数据从锁定的用户缓冲区复制到驱动程序分配的公共缓冲区,以便传输到设备;
- 当驱动程序准备好针对 DMA 对设备进行编程并需要系统 DMA 控制器时,AllocateAdapterChannel;
- MapTransfer,使用描述驱动程序分配的公共缓冲区的 MDL,为传输操作设置系统 DMA 控制器,请注意,驱动程序仅调用 MapTransfer 一次,以将系统 DMA 控制器设置为使用其公共缓冲区。 在传输期间,驱动程序可以调用 ReadDmaCounter 来确定剩余要传输的字节数,并在必要时调用 RtlMoveMemory 以向用户缓冲区或从用户缓冲区复制更多数据;
- 当驱动程序完成从属设备的 DMA 传输时调用FlushAdapterBuffers;
- 只要传输了所有请求的数据,或者驱动程序必须因设备 I/O 错误而使 IRP 失败,则 FreeAdapterChannel;
IoGetDmaAdapter 返回的适配器对象指针是除 RtlMoveMemory 以外的每个支持例程的必需参数。
各个驱动程序在不同点调用此支持例程序列,具体取决于实现每个驱动程序以为其设备提供服务的方式。 例如,一个驱动程序的 StartIo 例程可能会调用 AllocateAdapterChannel,另一个驱动程序可能会从从驱动程序创建的互锁队列中删除 IRP 的例程发出此调用,另一个驱动程序可能在其从属 DMA 设备指示它已准备好传输数据时发出此调用。
分配适配器通道
驱动程序在其 DispatchRead 或 DispatchWrite 例程 (或任何其他处理 DMA 传输的调度例程之后调用 AllocateAdapterChannel之前) 需要检查 IRP 参数的有效性,可能已将一个或多个 IRP 排队到另一个驱动程序例程进行进一步处理,如果适用可能加载其公共缓冲区以及要传输的数据。
调用 AllocateAdapterChannel 的 驱动程序例程必须在 IRQL=DISPATCH_LEVEL 执行。 AllocateAdapterChannel 例程将驱动程序的 AdapterControl 例程排入队列,该例程在系统 DMA 控制器分配给此驱动程序后运行,并为驱动程序的 DMA 操作分配了一组映射寄存器。
输入时, 向 AdapterControl 例程提供指向设备对象的指针和在 调用 AllocateAdapterChannel 中传递的上下文,以及分配的映射寄存器的句柄。 如果驱动程序具有 StartIo 例程,还会为 AdapterControl 例程提供指向 DeviceObject->CurrentIrp 的指针。 如果驱动程序管理自己的 IRP 队列而不是 StartIo 例程,则驱动程序应包含指向当前 IRP 的指针,作为它在调用 AllocateAdapterChannel 时传递的上下文数据的一部分。
AdapterControl 例程通常执行以下操作:
- 保存或初始化驱动程序维护的有关 DMA 操作的任何上下文。 上下文可能包括驱动程序必须传递给 MapTransfer 和 FlushAdapterBuffers 的输入 MapRegisterBase 句柄,以及从 IRP 中的 I/O 堆栈位置请求的传输的长度;
- 设置从属设备以启动传输操作;
- 返回值 KeepObject;
对于使用系统 DMA 控制器的自动初始化模式的驱动程序, AdapterControl 例程必须返回值 KeepObject, 这允许驱动程序保留系统 DMA 控制器的"所有权"和分配的映射寄存器 ,直到传输所有数据。
由于 AdapterControl 例程不能等待从属设备执行 DMA 操作, 因此 AdapterControl 例程必须至少执行以下操作:
- 将上下文信息(尤其是 MapRegisterBase 句柄)保存在驱动程序的设备扩展、控制器扩展或其他驱动程序可访问的常驻存储区域中,驱动程序分配的非分页池。
- 返回 KeepObject。
另一个驱动程序例程 (很可能是 DpcForIsr 例程) 必须在 DMA 传输操作完成时调用 FlushAdapterBuffers 和 FreeAdapterChannel 。
设置系统DMA
当 AllocateAdapterChannel 将控制权转移到驱动程序的 AdapterControl 例程时,驱动程序将"拥有"系统 DMA 控制器和一组映射寄存器。 然后,驱动程序必须调用 MapTransfer ,以将系统 DMA 控制器设置为使用驱动程序分配的公共缓冲区,然后驱动程序为传输操作设置其设备。
驱动程序向 MapTransfer 提供以下参数:
- IoGetDmaAdapter 返回的适配器对象指针;
- 指向描述驱动程序分配的公共缓冲区的 MDL 的指针;
- 通过 AllocateAdapterChannel 传递给驱动程序的 AdapterControl 例程的 MapRegisterBase 句柄;
- 指向变量的指针 (Length) 指示驱动程序分配的公共缓冲区的大小,以字节为单位;
- 一个布尔值,指示传输操作的方向,请求从系统内存传输到设备;
MapTransfer 返回一个逻辑地址,使用系统 DMA 的驱动程序必须忽略该地址。 当 MapTransfer 返回控件时,驱动程序应为 DMA 操作设置其设备。 驱动程序只调用 MapTransfer 一次,但会继续在其公共缓冲区和锁定的用户缓冲区之间复制数据,直到完成请求的传输。
驱动程序可以调用 ReadDmaCounter 来确定当前在公共缓冲区中要传输的字节数,然后驱动程序可以继续使用用户数据填充其公共缓冲区,或将数据从其公共缓冲区复制到用户缓冲区,具体取决于 DMA 操作的方向。
传输完成或驱动程序必须返回 IRP 的错误状态时,驱动程序将调用 FlushAdapterBuffers ,以确保将系统 DMA 控制器中缓存的任何数据读入系统内存或写出到设备。 然后,驱动程序应立即调用 FreeAdapterChannel ,以释放系统 DMA 控制器供任何驱动程序 (包括自身) 使用。