前面描述了DMA技术中适配器相关的部分以及DMA的分类,接下来看一下系统具体在支持两种DMA时候的操作的细微差别。
此处解释一下***Scatter/Gather,***这个也翻译为散点/收集,是指指示设备能够读取或写入内存中的任何区域,而不仅仅是特定范围。在早期,DMA读写的区域必须是一个整页面,但是这种要求其实对于内核来说,不是每时每刻都可以满足的,故和MDL类似,系统也支持将不在同一个页面上N个数据块映射为一整个页面。
使用基于数据包的系统 DMA
使用基于数据包的 DMA 的从属设备的驱动程序在处理请求 DMA 传输的 IRP 时调用以下常规支持例程序列:
- 尝试分配系统 DMA 控制器 ;
- 当驱动程序准备好针对 DMA 对设备进行编程并需要系统 DMA 控制器调用AllocateAdapterChannel,AllocateAdapterChannel 依次调用驱动程序的 AdapterControl 例程。
- MmGetMdlVirtualAddress 获取 MDL 中的索引,在对 MapTransfer 的初始调用中需要作为参数MapTransfer 为传输操作对系统 DMA 控制器进行编程
- 驱动程序可能需要多次调用 MapTransfer 来传输所有请求的数据;
- FlushAdapterBuffers ,就在每个 DMA 传输操作与从属设备之间
- 如果驱动程序必须多次调用 MapTransfer 以传输所有请求的数据,则它必须调用 FlushAdapterBuffers 的次数与调用 MapTransfer 的次数一样多。
- FreeAdapterChannel 只要传输了所有请求的数据,或者驱动程序因设备 I/O 错误导致 IRP 失败
IoGetDmaAdapter 返回的适配器对象指针是每个例程的必需参数,KeFlushIoBuffers 和 MmGetMdlVirtualAddress 除外,后者需要指向在 Irp->MdlAddress 传递的 MDL 的指针。
各个驱动程序在不同点调用此支持例程序列,具体取决于实现每个驱动程序以为其设备提供服务的方式。 例如,一个驱动程序的 StartIo 例程可能会调用 AllocateAdapterChannel,另一个驱动程序可能会从从驱动程序创建的互锁队列中删除 IRP 的例程发出此调用,另一个驱动程序可能在其从属 DMA 设备指示它已准备好传输数据时发出此调用。
分配适配器通道
为了准备基于数据包的系统 DMA,驱动程序在收到IRP_MJ_READ或IRP_MJ_WRITE请求后调用 KeFlushIoBuffers 和 AllocateAdapterChannel。
在驱动程序调用这些例程之前,其 DispatchRead 或 DispatchWrite 例程 (或其他处理 DMA 传输) 的调度例程应已检查 IRP 参数的有效性。 调度例程还可能已将 IRP 排到另一个驱动程序例程进行进一步处理。
调用 AllocateAdapterChannel 的 驱动程序例程必须在 IRQL=DISPATCH_LEVEL执行。 除了指向 IoGetDmaAdapter 返回的适配器对象的指针之外,驱动程序还必须在调用 AllocateAdapterChannel 时提供以下内容:
- 指向目标设备对象的指针
- 其 AdapterControl 例程的入口点
- 指向 AdapterControl 例程将使用的任何驱动程序确定的上下文信息的指针
AllocateAdapterChannel 将驱动程序的 AdapterControl 例程排队,该例程在系统 DMA 控制器分配给此驱动程序时运行,并且已为驱动程序的 DMA 操作分配了一组 映射寄存器。
输入时,AdapterControl 例程接收在对 AllocateAdapterChannel 的调用中传递的 DeviceObject 和 Context 指针,以及分配的映射寄存器 (MapRegisterBase) 句柄。
如果驱动程序具有 StartIo 例程,AdapterControl 例程还会收到指向 DeviceObject->CurrentIrp 的指针。 如果驱动程序管理自己的 IRP 队列,而不是使用StartIo 例程,则驱动程序应包含指向当前 IRP 的指针,作为它在调用 AllocateAdapterChannel 时传递的上下文的一部分。
AdapterControl 例程通常执行以下操作:
- 保存或初始化驱动程序维护的有关 DMA 操作的任何上下文。 上下文可能包括驱动程序必须传递给 MapTransfer 和 FlushAdapterBuffers 的输入 MapRegisterBase 句柄,以及从 IRP 中的 I/O 堆栈位置请求的传输的长度。
- 调用 MmGetMdlVirtualAddress 和 MapTransfer。
- 设置从属设备以启动传输操作;
- 返回值 KeepObject;
每个 AdapterControl 例程都必须返回 IO_ALLOCATION_ACTION类型的系统定义值。 对于使用系统 DMA 的驱动程序, AdapterControl 例程必须返回值 KeepObject。 这允许驱动程序保留系统 DMA 控制器和分配的映射寄存器的所有权,直到传输了所有请求的数据。
由于 AdapterControl 例程无法等待从属设备执行 DMA 操作,因此每个 AdapterControl 例程至少必须执行以下操作:
- 将上下文信息(尤其是 MapRegisterBase 句柄)保存在驱动程序的设备扩展、控制器扩展或其他驱动程序可访问的驻留存储区域中 (驱动程序 分配的非分页池中);
- 返回 KeepObject;
另一个驱动程序例程 (可能是 DpcForIsr 例程) 必须在每个 DMA 传输操作完成时调用 FlushAdapterBuffers 。 如果需要多次设置 DMA 控制器以满足当前 IRP 的传输请求,此例程还必须再次调用 MapTransfer 和 FlushAdapterBuffers 。
当驱动程序满足当前 IRP 的请求时,它必须调用 FreeAdapterChannel。 此支持例程应在上次调用当前 IRP 的 FlushAdapterBuffers 之后立即调用,以便系统 DMA 控制器可供任何驱动程序 迅速满足其他传输请求。
具有Scatter/Gather功能的从属设备的驱动程序还应从其 AdapterControl 例程返回 KeepObject。 当驱动程序必须拆分给定 DMA 请求时,当系统 DMA 控制器在 DMA 操作之间重新编程时,设备必须能够等待。 在某些 Windows 平台上,此类设备每次 DMA 操作最多可以传输一页数据,因为 HAL 只能将单个映射寄存器分配给该设备的驱动程序。
设置系统DMA
当 AllocateAdapterChannel 将控制权转移到驱动程序的 AdapterControl 例程时,驱动程序将"拥有"系统 DMA 控制器和一组映射寄存器。 然后,驱动程序必须为传输操作设置 DMA 控制器,如下图所示:
如果驱动程序具有 StartIo 例程,则 AllocateAdapterChannel 会将 PIrp 参数中指向 DeviceObject-CurrentIrp> 的指针传递到 AdapterControl 例程。 但是,如果驱动程序管理自己的 IRP 队列,则驱动程序应包含指向当前 IRP 的指针,作为它传递给 AdapterControl 的上下文的一部分。
如上图所示,驱动程序的 AdapterControl 例程设置 DMA 传输,如下所示:
1. AdapterControl 例程获取开始传输的地址。 对于满足 IRP 所需的初始传输, AdapterControl 例程调用 MmGetMdlVirtualAddress,将指针传递到 Irp->MdlAddress 处的 MDL,该指针描述此 DMA 传输的缓冲区;MmGetMdlVirtualAddress 返回一个虚拟地址,驱动程序可以使用该地址作为应开始传输的系统物理地址的索引;如果 IRP 需要多个传输操作,驱动程序将计算更新的起始地址,如后面所述。
2. AdapterControl 例程保存 MmGetMdlVirtualAddress 返回或在步骤 1 中计算的地址。 此地址是 MapTransfer (CurrentVa) 的必需参数。
3. AdapterControl 例程调用 MapTransfer 来设置系统 DMA 控制器,并提供以下参数:
- IoGetDmaAdapter 返回的适配器对象指针;
- 指向当前 IRP 的 Irp-> MdlAddress处的 MDL (Mdl 的指针),通过 AllocateAdapterChannel 传递给驱动程序的 AdapterControl 例程的 MapRegisterBase 句柄;
- 如果这是第一次调用 IRP 的 MapTransfer,则 mmGetMdlVirtualAddress 返回的 Current) Va 值 ;否则,驱动程序会提供更新的 CurrentVa 值,指示下一个传输操作应在缓冲区中的哪个位置启动;
- 指向变量的指针 (Length) 指示此传输的字节数;
- 如果驱动程序可以通过一次调用 MapTransfer 来传输请求的所有数据,并且对其 DMA 操作没有特定于设备的约束,则可以在驱动程序的 I/O 堆栈位置的 IRP 中将 Length 设置为 Length 值。 最多可以 (PAGE_SIZE * IoGetDmaAdapter) 返回的 NumberOfMapRegisters,以字节为单位。 否则,驱动程序必须拆分请求(如拆分传输请求中所述),并且必须在对当前 IRP 的 MapTransfer 的后续调用中更新 Length 值;
- 一个布尔值 (WriteToDevice) ,指示传输操作的方向, TRUE 将数据从系统内存传输到设备;反之亦然;
4. MapTransfer 返回逻辑地址,使用系统 DMA 的驱动程序必须忽略此值;
5. AdapterControl 例程为 DMA 操作设置设备;
6. AdapterControl 例程返回 KeepObject;
当设备指示其当前 DMA 操作已完成时,驱动程序应调用 FlushAdapterBuffers,通常是从驱动程序的 DpcForIsr 例程调用。
完成 DMA 操作的 DpcForIsr 例程或其他驱动程序例程调用 FlushAdapterBuffers,以确保将系统 DMA 控制器中缓存的任何数据读入系统内存或写出到设备。 如果需要重新编程系统 DMA 控制器以便为当前 IRP 传输更多数据,则同一例程还必须再次调用 MapTransfer ;同样它必须在每次传输操作后再次调用 FlushAdapterBuffers 。
如果驱动程序必须多次为当前 IRP 调用 MapTransfer ,它将在每次调用中提供相同的适配器对象指针、 Mdl 指针、 MapRegisterBase 句柄和传输方向。 但是,驱动程序必须先更新 CurrentVa 和 Length 参数,然后才能对 MapTransfer 进行第二次和任何后续调用。 若要计算其中每个参数的更新值,请使用以下公式:
- CurrentVa = 在对 MapTransfer的前面调用中请求的 CurrentVa + Length;
- Length = 最小剩余要传输的长度, (PAGE_SIZE * IoGetDmaAdapter) ) 返回的 NumberOfMapRegisters;
每个驱动程序应维护的有关其 DMA 传输的上下文信息取决于其特定设备的需求。 典型上下文可能包括 MDL (CurrentVa) 中的当前虚拟地址、到目前为止传输的字节数、要传输的剩余字节数、可能指向当前 IRP 的指针,以及驱动程序编写者认为有用的任何其他信息。
当请求的传输完成时,或者如果驱动程序必须返回 IRP 的错误状态,则驱动程序应立即调用 FreeAdapterChannel ,以释放系统 DMA 控制器以供其他驱动程序和此驱动程序使用。