设备io的(同步异步io,iocp/epool等),以及iocp 和socket操作(求批判|讨论)

iocp/epool这类东西是用于与设备通信时获取通知。

 

这里的设备在访问硬盘上的文件就是和文件系统驱动打交道,访问socket时则是和网络驱动(软件->硬件)打交道。

 

os驱动运行模式一般是发送命令,等待命令完成,获取结果。

 

层次类似这样

user

-----------------------

kernel <=> driver

 

driver下面可以挂很多个,其具体实现跟用户没什么关系, kernel会知道什么时候可以认为操作实际完成。

历史悠久的2种访问方式

 

A. 阻塞访问时(read/send),每次api call后,会等待操作完成再返回,此时用户线程处于挂起状态。白白浪费了处理器时间。

其工作模式(实际的通知模式等可能跟描述有差异,但流程上是这样)

user->api -> 发送请求至kernel 挂起用户线程

请求完成时

driver发送请求结果通知kernel,kernel 返回结果至api,恢复用户线程。

 

B.异步访问:

user->api 根据用户操作 查询设备读写状态,根据状态返回结果(成功返回数据,或失败需要block)

 

这里比较特殊的是读。

例如访问磁盘文件,第一次读请求肯定是没数据的,因为kernel不知道你请求的是读哪块数据,不可能说打开文件,系统就马上把文件读取到缓存里面。

所以读文件的第一次访问,实际操作是发送读取命令到driver,然后返回would block。

kernel收到返回数据后下次读操作可以立刻完成。

 

对于可以主动产生数据的设备(socket/串口之类),第一次读就有可能立即完成。

 

-----------------------------------------------------------------------------------------------------------------------------------------

在设备密集情况下:

同步模式显然是很低效的,大部分cpu时间在等待设备完成操作。

异步模式要稍微好点,但是需要大量时间轮询,设备很多,io少时cpu消耗在查询上面很大。

 

于是出现了selecl/pool模式用于查询一堆设备的读写状态。然后标记可读,或可写的装备供用户操作。

 

在select/pool时每次需提交大量的设备句柄(r/w/e句柄数组),而可能只返回其中一个,操作效率太低。

于是epool出现。

 

epool和select/pool不同在于,其关注的设备列表是由系统内核维护 用epool_ctrl来增减修改关注设备或需要查询的状态。

因此每次查询时无需提交设备列表。效率上面大大增加了。

不过epool

 

iocp使用了另外一种完全不同的通知方式。

通过iocp访问设备时,直接提交命令+用户缓冲区至内核驱动,然后立即返回。此时用户线程可以继续运行做其他的事情。

用户想知道命令完成状态时,通过GetQueuedCompletionStatus来获取完成的请求和数据。

所以用户可以先提交一大堆io请求,然后慢慢处理返回。

 

iocp比epoll高效的原因在于,epool每次查询后,返回的还是一个状态数组,很可能大部分数据是没用的,但是应用程序还是的循环来找有用的状态。

而iocp每次返回的东西必定是有用的,用户可以直接使用结果,减少了寻找这一步,所以对于大量设备时效率比epool高

 

其他io通知方式:

kqueue 因为没有用过,不敢妄加评论。

 

----------------------------------------------------------------------------------------------------------------------------------------------

关于iocp在socket服务器上应用

 

首先,iocp这东西跟线程没什么关系,他只和设备打交道。

所谓的iocp work thread说法本来就是有问题的,应该是thread worked with iocp, 也就是io+逻辑线程。

NumberOfConcurrentThreads 是指最多有多少个线程可以同时查询这个iocp的状态,多余此数字的线程查询时全部挂起在GetQueuedCompletionStatus。

 

其次,io速度和处理速度的平衡性。

假设一个服务器程序,每秒能接受 1m字节的数据请求,但是其cpu上运算过程只能处理512k的数据运算,

那么单纯的提高io效率最终结果就是buffer溢出,服务器崩溃。

 

现在来看看iocp socket在服务器上的的2种使用方法。

A. 1个iocp对象,NumberOfConcurrentThreads =nCpu*2, threads workd with iocp=nCpu*2。

msdn里面iocp推荐的做法,code project上的实现也是这种。

这种模式本身是没有什么问题的,但是因为误解和错误的用法,会引起效率损失。

 

msdn里面例子上面,iocp是对文件操作,这和socket操作有细微的不同。

比如做文件加密算法。文件可以划分为等大小的小块,然后提交读取请求,获取数据后计算,提交写请求。这些都在 threads workd with iocp里面完成。

因为其数据没有前后相关性,thread可以完全的并发处理,此时最为高效。而且自动的平衡了io和处理速度。

同时在io速度低时,被唤醒的thread 可能永远是第一个(iocp内部跟踪了线程标志,知道哪个是最近被唤醒的线程)。线程切换开销最小。

此为理想状态下的服务器情况。

而现实是残酷的,对于socket操作来说,因为收到数据有很大程度上的前后相关性,可能2个线程分别获取了同一个连接的2段读取数据,这样的话,后一个线程只能等待前面一个线程完成操作,然后锁定连接再做处理。

同时,每次到达的数据也可能不会是一个完整的可以处理数据块,此时还的另外发起读请求。直到获取到完整数据段拼包处理。

此类模型代码复杂,如果应用方式正确的话是个不错的选择。

 

 

错误的使用方法:

某些应用方法是把这整个模型作为一个io线程来用,然后单独的一个或几个逻辑线程处理输入缓冲,那么造成的问题是 逻辑处理时间为0,io效率最大化。 假设逻辑线程跟不上io速度的话,server 最终就是crash。(缓冲区一直增加)

 

B. n个iocp对象,n个线程,socket平均绑定至各iocp对象

n=1时,就是select/pool/epool的工作方式了。单线程,不会有包的前后顺序问题,效率受逻辑处理速度影响。

n>1时,密集io情况下,少做了包顺序检查看起来比A方式效率要高些,但是如果锁定操作比较多的话,反倒降低效率。只有一个线程,等待的话其他io也没法处理了。

同时在请求较少时,线程切换开销很大。

 

此模型最好的用法是作为单独的io线程,只使用1个线程做专门的io, 其他线程做逻辑处理数据。

此时io线程逻辑处理时间基本没有,io效率得以最大化。

 

另外对于简单的如echo/socks这类协议的做server也很实用。

 

现在总结下2种方式:

A.n个(iocp+逻辑线程)

B.iocp io线程1个 +      n个逻辑线程

 

测试:

测试环境

server和client一样: win7 64 +8g ,100m网络。cpu i5 2500k

server端是一个简单的登陆+echo协议。登陆请求成功后echo客户端的请求内容。

client端登陆成功后,每100ms发送20byte请求内容,其中4byte是自定义协议的包头。

2边都使用了tcp_nodely 1,用来模拟大量的ip包。

5k个连接。

初步估计单向流量为(20+20)*10*连接数。=2MB  16mbits

实际流量为56mbit

server端:

A方式实现,4线程                  cpu消耗在20%左右,其中约3/4为kernel时间                                                       

B方式单线程实现(io和逻辑一起一个线程)   与上面差不多,看起来cpu消耗似乎低点,应该是跟线程切换有关

 

也就是说,如果用单独的io线程来使用iocp,1个线程就是最优解,其他方法只会降低效率。

对于io线程的读请求,应该由逻辑线程控制,io线程只控制需要组包时的读请求。 避免缓冲溢出。

写请求可以由io线程维护队列(或者只计数)避免缓冲区溢出。

 

补充些iocp容易犯的错误:

1.acceptex 

acceptEx不要传递额外的读取buffer(recvdatalength=0),避免拒绝服务攻击

(恶意客户端连接上不发送数据,此时io未完成,无法投递新accpetEx,耗完后其他客户端无法连接)

2.send/recv/read/write直接返回成功的时候,不要回收缓冲区contex,成功的调用iocp会有通知的。

只有在io调用失败时才需要自己回收,其他的都在iocp result里面处理。

3.overlapped和wsbuf有些说法是不要放在一起,会出现nobuf的error,经测试,这种说法没有道理。

wsae_nobuf是因为太多挂起的io调用,把系统内存耗完了,跟内存锁定没关系。

唯一可能优化就是内存页对齐的地方。

 

 

 

你可能感兴趣的:(网络)