分享

《揭秘Windows内核》1

 金刚光 2020-02-03

最近在做图像处理(其实是利用摄像机采回来的Image进行分析,从而得到目标的运动信息),既然是图像处理那就得有图像Source,没错那就是Camera,实验室的这个Camera还挺高级的,是千兆以太网接口的,呵呵,管他是什么接口——反正我们的任务就是将数据采集到指定的缓冲区内,然后再把缓冲区内的数据“显示出来”或者“分析分析”。按照常规的做法,你可能会这样申请一段缓冲区,估计编写过Windows程序得人10个得有9个会这么开辟一段“内存”:

(1)LPVOID pbuf = VirtualAlloc(....)

或者

(2)LPVOID pbuf= malloc(....)

或者

(3)LPVOID pbuf = new[...]

好的,到这里我再回顾一下我们的目的:将数据从千兆以太网接口取回来然后存到pbuf所指向的缓冲区内。

恩!没错,既然是和物理设备进行数据交换,那么我们就希望数据交换具备实时性,那靠什么保障呢?在普通的PC平台下,这只能依靠申请纯物理内存来做到这一点,因为对于开辟在物理内存——DDR3内存条上的缓冲区而言,其访问时间会很短,至于为什么相信玩过一些高端FPGA电路板的人会很清楚,如果没有接触过这类集成电路,那么你去查看DDR3的手册,你会发现DDR3的总线速率不过零点几个纳秒,因此一次读/写周期(相当于数个总线周期)也就2~3个纳秒。

至此,有些看客可能会说,你到底想说什么呢?你这不是废话吗?你的pbuf不是已经通过上述方式申请了内存了吗?

我要澄清的是,事情远没有那么简单!MicroSoft也远没有那么肤浅!事实上学过计算机的人应该会很清楚这个问题:Windows在用户模式(Ring3)采用的是虚拟内存机制,简而言之:每个进程(Process)都有自己独立的4个G的虚拟内存空间。然而,你可以看看你的系统配置,你最多也不过4个G的内存,那么如果我运行10个进程岂不是需要40个G的虚拟内存空间?这不矛盾了。。。

恩,要解释清这一点,可以参考下MS的相关资料,其实原理很好理解,就是用硬盘来代替内存。将当前需要运行的“可执行片段”(请不要笑话我幼稚的理解,我实在找不出什么好词了--!)提交到内存,同时将那些在内存中不经常使用的数据换出到硬盘上。我为了叙述的简单,以后就将真正的物理存储资源(我们希望将缓冲区开辟到这里)称为非分页资源,而将映射到硬盘上的虚拟内存称为分页资源。

Windows访问非分页资源其实就是CPU访问物理存储器,就是我们所能够想象到的CPU通过其总线对内存条的访问。然而当Windows访问分页资源的时候,首先会触发一个异常(Exception),然后会通过该异常执行一些服务代码,这些服务代码,会将硬盘上的数据再换回到内存中去。

通过上述阐述,你应该理解一个道理:在用户模态(Ring3层)所有的内存分配函数是针对虚拟内存而言的,换句话说,就是无法保证你开辟的缓冲区到底是分页资源还是非分页资源。如果你通过方法(1),(2)或者(3)所分配的内存恰好是分页资源。这将意味着,你将无法以预期的实时性能来和千兆以太网接口进行数据交换,原因已经说过了:你其实是在和硬盘打交道而不是真正的内存。

那么我们的任务就来了:如何实现再用户模式(Ring3层)确保所分配的内存确实是物理内存——非分页资源呢?

方法其实也不难:就是写一个驱动(Driver),呵呵,驱动运行在Ring0因此很容易申请到物理内存!

其实在这个驱动程序中,你的目的不是为了访问某个硬件设备,而是单纯的开辟一段物理内存并“返回”至用户模式的应用程序中。

在这样的一个驱动程序中,你没有必要实现所有的派遣函数(Dispatch Routine),事实上,你只要实现IRP_MJ_DEVICE_CONTROL(在DDK中定义:wdm.h)所对应的派遣函数就够了,当然你也肯定也得实现[IRP_MJ_PNP, IRP_MN_REMOVE_DEVICE]要不然你怎么卸载你的驱动啊?呵呵,不要偷懒,要不然Windows不会让你有好果子吃的。

言归正传,IRP_MJ_DEVICE_CONTROL的会被应用层的DeviceIoControl这个API函数所触发,嘿嘿,技巧就在这里:DeviceIoControl的定义如下,

BOOL DeviceIoControl(

HANDLE hDevice,

DWORD dwIoControlCode,

LPVOID lpInBuffer,

DWORD nInBufferSize,

LPVOID lpOutBuffer,

DWORD nOutBufferSize,

LPDWORD lpBytesReturned,

LPOVERLAPPED lpOverlapped

);

我们着重看一下黑色字体的参数,这两个参数,代表了驱动程序所返回的“内容”。在调用这个函数的时候,的确由用户模式的应用程序分配lpOutBuffer,但是如果dwioControlCode指定了操作方式为METHOD_IN_DIRECT或者METHOD_OUT_DIRECT的话(具体请参照DDK说明,这里仅仅是提供一个思路),那么就可以在驱动里面做这样我个人认为很邪门的事情以保证lpOutBuffer确实开辟到了物理内存上面去:

// Map the user-mode buffer to the real physical memory

UCHAR * pOBuffer = (UCHAR*)MmGetSystemAddressForMdlSafe(Irp->MdlAddress,

NormalPagePriority);

对,原理就是把用户模式的虚拟地址映射到真正的物理地址上去!其中Irp就是所谓的IRP(I/O Request Package)结构体指针,Irp->MdlAddress包含了Ring3层你通过方法(1),(2)或者(3)所分配的虚拟内存首地址,pOBuffer就是真实的物理地址。换言之经过DeviceIoControl的调用以及相应驱动的配合,用户态的虚拟内存(确切地说是用户态虚拟内存中的分页资源成分)就被牢牢地锁在了物理内存上。只要你不显式的通过VirtualFree等函数进行释放,那么你在用户层(Ring3)对你所申请的虚拟内存进行操作,实际上就是对在驱动程序中重新映射后的物理内存进行操作,因而极大地提高了存储效率!

为了验证这一点,我进行了如下操作:

(A)开辟用户态的缓冲区(不确定到底是分页的还是非分页的)

(B)调用DeviceIoControl尝试将所开辟的缓冲区锁定到物理内存上去

(C)在应用程序中:memset(pbuf, 0xaa, buf_size)

(D)再次调用DeviceIoControl打印出上一次所分配的物理内存中的内容。

如果打印出0xAA的话就证明我的设想是成立的,结果表明此想法是完全行的通的!

具体验证代结果如下:

可以看出,所分配的用户模式缓冲区首地址为:0x04be0000,然而这段地址其实被映射到了Kernel mode下的0xa6a6a000为首地址的非分页内存上去了,因而无论是驱动对0xa6a6a000进行操作还是应用程序对0x04be0000进行操作,其实都是访问的同一段物理内存,这就达到了我们最终的目的了!

————————————————

版权声明:本文为CSDN博主「LnTigerLn」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文及本声明。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多