内核里也需要访问用户应用程序内存,那么有什么方法呢?在ReactOS主要有两种方法:一种是使用缓冲I/O的方法,在驱动程序运行前,I/O管理器把写数据复制到这个缓冲区,并在请求完成时把读数据复制回到用户空间;另一种是使用直接I/O,这是优先的技术,因为它减少数据复制。这是通过I/O管理器传递一个内存描述符列表(MDL-- Memory descriptor list)来实现的,这个描述符列表是描述用户空间缓冲区。
MDL的实现代码如下:
#001 PMDL
#002 NTAPI
#003 IoAllocateMdl(IN PVOID VirtualAddress,
#004 IN ULONG Length,
#005 IN BOOLEAN SecondaryBuffer,
#006 IN BOOLEAN ChargeQuota,
#007 IN PIRP Irp)
#008 {
函数IoAllocateMdl前两个参数定义了虚拟地址和内存区的大小,是建立MDL所必须的。如果MDL不与IRP相关联,则第三个参数就为FALSE。第四个参数定义是否需要减少进程的份额,并只用于位于驱动程序链最上层的驱动程序或是单层的驱动程序(。每一个进程都要获得一定份额的系统资源。当进程为自己分配资源时,这个份额就会减小。如果份额用完,就不能再为其分配相应的资源。最后一个参数定义了一个非必要的指向IRP的指针,通过这个指针MDL可以与IRP关联。例如,对于直接I/O,I/O管理器为用户缓冲区建立MDL,并将其地址送至IRP.MdlAddress。
#009 PMDL Mdl = NULL, p;
#010 ULONG Flags = 0;
#011 ULONG Size;
#012
如果申请的内存超过2G,就返回失败。
#013 /* Fail if allocation is over 2GB */
#014 if (Length & 0x80000000) return NULL;
#015
计算需要使用多少页内存。
#016 /* Calculate the number of pages for the allocation */
#017 Size = ADDRESS_AND_SIZE_TO_SPAN_PAGES(VirtualAddress, Length);
#018 if (Size > 23)
#019 {
大于23页,就计算实际使用的大小页。
#020 /* This is bigger then our fixed-size MDLs. Calculate real size */
#021 Size *= sizeof(PFN_NUMBER);
#022 Size += sizeof(MDL);
#023 if (Size > MAXUSHORT) return NULL;
#024 }
#025 else
#026 {
如果小于等于23页,就直接使用23页的大小。
#027 /* Use an internal fixed MDL size */
#028 Size = (23 * sizeof(PFN_NUMBER)) + sizeof(MDL);
#029 Flags |= MDL_ALLOCATED_FIXED_SIZE;
#030
从后备列表里找到合适的内存。
#031 /* Allocate one from the lookaside list */
#032 Mdl = IopAllocateMdlFromLookaside(LookasideMdlList);
#033 }
#034
如果前面没有找到相应的MDL内存,就重新分配一个。
#035 /* Check if we don't have an mdl yet */
#036 if (!Mdl)
#037 {
#038 /* Allocate one from pool */
#039 Mdl = ExAllocatePoolWithTag(NonPagedPool, Size, TAG_MDL);
#040 if (!Mdl) return NULL;
#041 }
#042
通过内存管理器的函数MmInitializeMdl初始化MDL。
#043 /* Initialize it */
#044 MmInitializeMdl(Mdl, VirtualAddress, Length);
#045 Mdl->MdlFlags |= Flags;
#046
检查IRP是否存在,如果存在就把MDL的地址放到IRP包里。
#047 /* Check if an IRP was given too */
#048 if (Irp)
#049 {
#050 /* Check if it came with a secondary buffer */
#051 if (SecondaryBuffer)
#052 {
#053 /* Insert the MDL at the end */
#054 p = Irp->MdlAddress;
#055 while (p->Next) p = p->Next;
#056 p->Next = Mdl;
#057 }
#058 else
#059 {
#060 /* Otherwise, insert it directly */
#061 Irp->MdlAddress = Mdl;
#062 }
#063 }
#064
最后返回MDL的地址。
#065 /* Return the allocated mdl */
#066 return Mdl;
#067 }
在MDL描述符表里,还有这样的需求,当作一个MDL表已经映射过一次MDL了,那么当用户再次想去分配这个MDL时,就需要使用函数IoBuildPartialMdl来再次映射MDL了。其实出现这种情况,就是当驱动程序使用了一个MDL的IRP包发送给另外一个驱动程序,然后这个驱动程序又需要从IRP包里的MDL再分配一个MDL出来。这个函数的实现代码如下:
#001 /*
#002 * @implemented
#003 */
#004 VOID
#005 NTAPI
#006 IoBuildPartialMdl(IN PMDL SourceMdl,
#007 IN PMDL TargetMdl,
#008 IN PVOID VirtualAddress,
#009 IN ULONG Length)
#010 {
取目标MDL地址。
#011 PPFN_NUMBER TargetPages = (PPFN_NUMBER)(TargetMdl + 1);
取源MDL地址。
#012 PPFN_NUMBER SourcePages = (PPFN_NUMBER)(SourceMdl + 1);
#013 ULONG Offset;
#014 ULONG FlagsMask = (MDL_IO_PAGE_READ |
#015 MDL_SOURCE_IS_NONPAGED_POOL |
#016 MDL_MAPPED_TO_SYSTEM_VA |
#017 MDL_IO_SPACE);
#018
计算偏移位置。
#019 /* Calculate the offset */
#020 Offset = (ULONG)((ULONG_PTR)VirtualAddress -
#021 (ULONG_PTR)SourceMdl->StartVa) -
#022 SourceMdl->ByteOffset;
#023
计算源MDL是否有足够的长度。
#024 /* Check if we don't have a length and calculate it */
#025 if (!Length) Length = SourceMdl->ByteCount - Offset;
#026
设置进程、虚拟地址和需要内存的大小。
#027 /* Write the process, start VA and byte data */
#028 TargetMdl->StartVa = (PVOID)PAGE_ROUND_DOWN(VirtualAddress);
#029 TargetMdl->Process = SourceMdl->Process;
#030 TargetMdl->ByteCount = Length;
#031 TargetMdl->ByteOffset = BYTE_OFFSET(VirtualAddress);
#032
重新计算页面的空间。
#033 /* Recalculate the length in pages */
#034 Length = ADDRESS_AND_SIZE_TO_SPAN_PAGES(VirtualAddress, Length);
#035
设置MDL的标志。
#036 /* Set the MDL Flags */
#037 TargetMdl->MdlFlags &= (MDL_ALLOCATED_FIXED_SIZE | MDL_ALLOCATED_MUST_SUCCEED);
#038 TargetMdl->MdlFlags |= SourceMdl->MdlFlags & FlagsMask;
#039 TargetMdl->MdlFlags |= MDL_PARTIAL;
#040
#041 /* Set the mapped VA */
#042 TargetMdl->MappedSystemVa = (PCHAR)SourceMdl->MappedSystemVa + Offset;
#043
开始从源MDL里拷贝数据到新的MDL。
#044 /* Now do the copy */
#045 Offset = ((ULONG_PTR)TargetMdl->StartVa - (ULONG_PTR)SourceMdl->StartVa) >>
#046 PAGE_SHIFT;
#047 SourcePages += Offset;
#048 RtlCopyMemory(TargetPages, SourcePages, Length * sizeof(PFN_NUMBER));
#049 }
前面都是创建MDL的,下面这个函数就是删除MDL所占用的资源,实现如下:
#001 VOID
#002 NTAPI
#003 IoFreeMdl(PMDL Mdl)
#004 {
让内存管理器删除所有MDL。
#005 /* Tell Mm to reuse the MDL */
#006 MmPrepareMdlForReuse(Mdl);
#007
检查是否重新分配的内存,如果是就需要删除掉,否则就放回到后备列表,以便下一次使用。
#008 /* Check if this was a pool allocation */
#009 if (!(Mdl->MdlFlags & MDL_ALLOCATED_FIXED_SIZE))
#010 {
#011 /* Free it from the pool */
#012 ExFreePoolWithTag(Mdl, TAG_MDL);
#013 }
#014 else
#015 {
#016 /* Free it from the lookaside */
#017 IopFreeMdlFromLookaside(Mdl, LookasideMdlList);
#018 }
#019 }
#020