WSASend 异步IO发送数据的机理浅析

 

    最近在摸索WSASend函数在IOCP网络模型中的发送机制, 首先当我们使用Overlapped的Socket的时候, 其实已经就是在异步使用该Socket了, 这就有一个疑问, WSASend到底是如何发送数据, 在应用层又是如何处理发送的内存的呢, 带着这个疑问查阅了Reactos的代码, 终于有了一些答案, 针对网上一直说关于WSASend会锁定内存的说法也有了一个比较清晰的答案, 虽然网上一直存在这个说法主要是源于国外的一篇高性能Socket的一篇译文, 但是出到底锁定的是什么基本没人能完整的描述出来.

 

首先WSASend函数调用后, 是通过向驱动层发送IO指令, 提交irp请求, 驱动层核心代码检查如果当前的内存如果足以缓冲用户提交的数据, 便直接Copy用户的数据, 这个部分的数据会被安排在内核的非分页缓冲池中, 因为非分页缓冲池是有大小限制, 所以说并不是用户发送多大的内存, 核心驱动都直接Copy的, 一旦Copy成功就代表IO的请求正确的提交, 也就是说只要WSASend返回的是0 或者是-1(GetLastError 为IO_PENDDING)我们就可以认为用户层代码提交的数据理论上是会正确被发送到客户端的, 为什么说是理论呢, 因为那些异常情况可能包含网络发送中断线, 对方关闭连接等等异常的情况, 还有对方拒收信息等, 除此之外我们都可以认为系统会安全的帮我们缓存这些数据直到发送完毕这些数据, 那么如果非分页缓冲池不足以提供和用户提交的数据等量大小的内存, 那么就会启用另外一种模式, 也就是网上一直说的”锁定内存”, 这里不得不提一下驱动层关于I/O管理内存的3种方案, 这里我就不再提驱动层为什么不能直接访问用户虚拟内存的原因了, 这个网上驱动开发的资料很多.

 

这三种方式, 第一种就是我们刚才说的直接Copy, 在驱动层这个叫BufferedIO方式, 另外一直就是直接(Direct), 最后一种叫Neither故名思议就是直接使用用户的虚拟内存, 这个是不安全, 主要是一些不需要提交用户数据的情况下使用, 当驱动层非分页缓冲池不足缓冲用户数据时, 驱动便使用MDL(Memory Descriptor List)建立内核虚拟内存映射, 这种方式就是所谓的直接方式, 首先驱动会申请一片内存用于存放MDL结构和用户虚拟内存的页表, MDL结构主要描述的是内存的大小, 起始位置, 偏移量等等信息, 因为内存总是页式管理的, 所以页表里填充的都是用户的虚拟内存对应的页码, 下一步便是填充这些页码, 通过MDL中提供的起始位置和大小计算和填充页表中的虚拟内存页码, 再通过遍历这些页码将它们转换成物理内存的页码, 再这个过程中系统会做2件很重要的事, 第一个是检查用户的虚拟内存, 如果虚拟内存的页面对应的内存不在物理内存里, 那么就得不到物理内存页, 这个时候需要将虚拟内存页面文件中的内容强制交换回物理内存中, 同时将这块物理内存打叫一个标记(既锁定)告诉操作系统无论内存够不够, 这块物理页面都不可以被置换回虚拟内存页面文件中, 另外一个操作即增加该物理内存的引用计数, 防止用户层释放虚拟内存时导致这块物理内存也被释放, 有了这2层保障, 我们可以认为这块物理内存已经被锁定给内核使用了, 有兴趣的朋友可以通过实验进行验证, 当然我已经测试过了.

 

这就是网上所说的锁定内存的机制, 实际上当这些内存被锁定时, 用户层调用完Wsasend后完全可以释放掉这些内存, 后面还会讲到这样做得有一个先决条件, 所谓用户释放掉这些内存不是代表物理内存也被释放了, 因为物理内存页都有引用计数的概念, 用户层虽然释放了内存, 但是这块物理内存依然有其他对象访问(即引用计数不为0), 所以不会直接就释放了, 核心层在锁定这块物理内存后, 不是马上将这块内存映射为虚拟内存地址, 而是在要使用的时候才进行映射, 当内核发送数据的时候实际上是访问内核虚拟内存地址, MMU通过虚拟地址访问到相应的物理内存, 这块内存也就是用户之前发送的数据(锁定的物理内存页), 有没有发现这个步骤实际上已经少了用户层Copy数据到内核层的步骤, 可以从下图中了解 用户提交的数据是如何被内核层直接访问的.

 

   

 

其实可以把这个想象成内存共享文件FileMapping的概念, 虚拟内存实际是连续的, 但是物理内存大部分情况下不是连续的.

 

    前面我讲到WSASend发送完数据后理论上是可以释放掉这块用户层内存的, 但是这么做其实没有什么价值,因为这块内存释放掉了, 不代表物理内存回收了, 由于物理内存被内核引用了, 系统只会回收销毁该进程的虚拟内存, 由于进程的虚拟内存可以申请到的大小限制是2G的空间(32位Windows), 如果是因为这个理由防止应用程序虚拟空间不够用倒没什么, 我觉得释放没啥问题, 但是问题是另外一种情况, 当我们发送的数据是小内存块的时候, 由于应用程序一般会有自己的内存管理器, 这种情况就危险了, 当我们发送数据后调用FreeMem的时候, 应用程序内存管理器(如delphi使用的是fastmm)会自动回收这个内存块, 但是不会调用系统的VirtualFree释放掉这块内存, 也就是说这块内存实际上没有真正的销毁, 如果这块内存与物理还保持着映射(我们这里的假设是没有被置换到虚拟内存页面文件上), 那么下次再分配小内存块的时候很可能依然得到同样的虚拟地址, 重要的是这个地址指向的物理地址和上次发送数据的物理地址是同一个, 那么问题就来了, 如果我们改写这块数据, 必然会影响到系统正在发送中的数据, 虽然这种可能性极小, 但这要依赖应用程序当前使用的内存管理器是如何处理内存分配的, 为了保证安全性, 我们还是老老实实的等到Iocp的通知之后再去释放这块内存.

posted on 2012-04-26 00:58  Ryan Huang  阅读(4698)  评论(2编辑  收藏  举报