IO模型及select,poll,epoll和kqueue的区别

IO模型及select,poll,epoll和kqueue的区别,第1张

(一)首先,介绍几种常见的I/O模型及其区别,如下:

blocking I/O

nonblocking I/O

I/O multiplexing (select and poll)

signal driven I/O (SIGIO)

asynchronous I/O (the POSIX aio_functions)—————异步IO模型最大的特点是 完成后发回通知。

阻塞与否,取决于实现IO交换的方式。

异步阻塞是基于select,select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄.

异步非阻塞直接在完成后通知,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。

1 blocking I/O

这个不用多解释吧,阻塞套接字。下图是它调用过程的图示:

重点解释下上图,下面例子都会讲到。首先application调用 recvfrom()转入kernel,注意kernel有2个过程,wait for data和copy data from kernel to user。直到最后copy complete后,recvfrom()才返回。此过程一直是阻塞的。

2 nonblocking I/O:

与blocking I/O对立的,非阻塞套接字,调用过程图如下:

可以看见,如果直接操作它,那就是个轮询。。直到内核缓冲区有数据。

3 I/O multiplexing (select and poll)

最常见的I/O复用模型,select。

select先阻塞,有活动套接字才返回。与blocking I/O相比,select会有两次系统调用,但是select能处理多个套接字。

4 signal driven I/O (SIGIO)

只有UNIX系统支持,感兴趣的课查阅相关资料

与I/O multiplexing (select and poll)相比,它的优势是,免去了select的阻塞与轮询,当有活跃套接字时,由注册的handler处理。

5 asynchronous I/O (the POSIX aio_functions)

很少有*nix系统支持,windows的IOCP则是此模型

完全异步的I/O复用机制,因为纵观上面其它四种模型,至少都会在由kernel copy data to appliction时阻塞。而该模型是当copy完成后才通知application,可见是纯异步的。好像只有windows的完成端口是这个模型,效率也很出色。

6 下面是以上五种模型的比较

可以看出,越往后,阻塞越少,理论上效率也是最优。

=====================分割线==================================

5种模型的比较比较清晰了,剩下的就是把select,epoll,iocp,kqueue按号入座那就OK了。

select和iocp分别对应第3种与第5种模型,那么epoll与kqueue呢?其实也于select属于同一种模型,只是更高级一些,可以看作有了第4种模型的某些特性,如callback机制。

为什么epoll,kqueue比select高级?

答案是,他们无轮询。因为他们用callback取代了。想想看,当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

windows or *nix (IOCP or kqueue/epoll)?

诚然,Windows的IOCP非常出色,目前很少有支持asynchronous I/O的系统,但是由于其系统本身的局限性,大型服务器还是在UNIX下。而且正如上面所述,kqueue/epoll 与 IOCP相比,就是多了一层从内核copy数据到应用层的阻塞,从而不能算作asynchronous I/O类。但是,这层小小的阻塞无足轻重,kqueue与epoll已经做得很优秀了。

提供一致的接口,IO Design Patterns

实际上,不管是哪种模型,都可以抽象一层出来,提供一致的接口,广为人知的有ACE,Libevent(基于reactor模式)这些,他们都是跨平台的,而且他们自动选择最优的I/O复用机制,用户只需调用接口即可。说到这里又得说说2个设计模式,Reactor and Proactor。见:Reactor模式--VS--Proactor模式。Libevent是Reactor模型,ACE提供Proactor模型。实际都是对各种I/O复用机制的封装。

Java nio包是什么I/O机制?

现在可以确定,目前的java本质是select()模型,可以检查/jre/bin/nio.dll得知。至于java服务器为什么效率还不错。。我也不得而知,可能是设计得比较好吧。。-_-。

=====================分割线==================================

总结一些重点:

只有IOCP是asynchronous I/O,其他机制或多或少都会有一点阻塞。

select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善

epoll, kqueue、select是Reacor模式,IOCP是Proactor模式。

java nio包是select模型。。

(二)epoll 与select的区别

1. 使用多进程或者多线程,但是这种方法会造成程序的复杂,而且对与进程与线程的创建维护也需要很多的开销。(Apache服务器是用的子进程的方式,优点可以隔离用户) (同步阻塞IO)

2.一种较好的方式为I/O多路转接(I/O multiplexing)(貌似也翻译多路复用),先构造一张有关描述符的列表(epoll中为队列),然后调用一个函数,直到这些描述符中的一个准备好时才返回,返回时告诉进程哪些I/O就绪。select和epoll这两个机制都是多路I/O机制的解决方案,select为POSIX标准中的,而epoll为Linux所特有的。

区别(epoll相对select优点)主要有三:

1.select的句柄数目受限,在linux/posix_types.h头文件有这样的声明:#define __FD_SETSIZE1024 表示select最多同时监听1024个fd。而epoll没有,它的限制是最大的打开文件句柄数目。

2.epoll的最大好处是不会随着FD的数目增长而降低效率,在selec中采用轮询处理,其中的数据结构类似一个数组的数据结构,而epoll是维护一个队列,直接看队列是不是空就可以了。epoll只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数(把这个句柄加入队列),其他idle状态句柄则不会,在这点上,epoll实现了一个"伪"AIO。但是如果绝大部分的I/O都是“活跃的”,每个I/O端口使用率很高的话,epoll效率不一定比select高(可能是要维护队列复杂)。

3.使用mmap加速内核与用户空间的消息传递。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。

关于epoll工作模式ET,LT

epoll有两种工作方式

ET:Edge Triggered,边缘触发。仅当状态发生变化时才会通知,epoll_wait返回。换句话,就是对于一个事件,只通知一次。且只支持非阻塞的socket。

LT:Level Triggered,电平触发(默认工作方式)。类似select/poll,只要还有没有处理的事件就会一直通知,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll.支持阻塞和不阻塞的socket。

三 Linux并发网络编程模型

1 Apache 模型,简称 PPC ( Process Per Connection ,):为每个连接分配一个进程。主机分配给每个连接的时间和空间上代价较大,并且随着连接的增多,大量进程间切换开销也增长了。很难应对大量的客户并发连接。

2 TPC 模型( Thread Per Connection ):每个连接一个线程。和PCC类似。

3 select 模型:I/O多路复用技术。

.1 每个连接对应一个描述。select模型受限于 FD_SETSIZE即进程最大打开的描述符数linux2.6.35为1024,实际上linux每个进程所能打开描数字的个数仅受限于内存大小,然而在设计select的系统调用时,却是参考FD_SETSIZE的值。可通过重新编译内核更改此值,但不能根治此问题,对于百万级的用户连接请求 即便增加相应 进程数, 仍显得杯水车薪呀。

.2select每次都会扫描一个文件描述符的集合,这个集合的大小是作为select第一个参数传入的值。但是每个进程所能打开文件描述符若是增加了 ,扫描的效率也将减小。

.3内核到用户空间,采用内存复制传递文件描述上发生的信息。

4 poll 模型:I/O多路复用技术。poll模型将不会受限于FD_SETSIZE,因为内核所扫描的文件 描述符集合的大小是由用户指定的,即poll的第二个参数。但仍有扫描效率和内存拷贝问题。

5 pselect模型:I/O多路复用技术。同select。

6 epoll模型:

.1)无文件描述字大小限制仅与内存大小相关

.2)epoll返回时已经明确的知道哪个socket fd发生了什么事件,不用像select那样再一个个比对。

.3)内核到用户空间采用共享内存方式,传递消息。

四 :FAQ

1、单个epoll并不能解决所有问题,特别是你的每个操作都比较费时的时候,因为epoll是串行处理的。 所以你有还是必要建立线程池来发挥更大的效能。

2、如果fd被注册到两个epoll中时,如果有时间发生则两个epoll都会触发事件。

3、如果注册到epoll中的fd被关闭,则其会自动被清除出epoll监听列表。

4、如果多个事件同时触发epoll,则多个事件会被联合在一起返回。

5、epoll_wait会一直监听epollhup事件发生,所以其不需要添加到events中。

6、为了避免大数据量io时,et模式下只处理一个fd,其他fd被饿死的情况发生。linux建议可以在fd联系到的结构中增加ready位,然后epoll_wait触发事件之后仅将其置位为ready模式,然后在下边轮询ready fd列表。

windows

select模型

在widows下提供了众多非阻塞的I/O模型,如select、WSAAsyncSelect、WSAEventSelect、overlapped、completion

port,其中IO

completion

port(IOCP)提供了较好的伸缩性,在windows应用比较广泛

说明:而select模型主要是解决在单一线程模式下只能处理一个套接字的问题,这样可以避免线程膨胀问题,但是。。。下面看完原型再讲起不足之处

select模型:winsock库主要有两个版本,这里主要以winsock2版本为说明,select模型

int

select(int

nfds,fd_set*

readfds,fd_set*

writefds,fd_set*

exceptfds,const

struct

timeval*

timeout);

其实主要是两者采用的标准一致,所以接口基本跟linux一致,不过nfds在windows其实是没有意义的,主要是为了兼容其他版本

处理过程:假设以read为例,在这里windows主要是先将套接字s添加到readfds集合中,然后等待select函数返回,在select函数里面会移除没有未决的

I/O操作的套接字句柄,即已经处理过的IO套接字句柄,然后看s是否认仍然还是readfs集合中,在就说明s可读了,但是这里面可读,不一定有数据

有几种情况都会引发:数据可读、连接关闭/重启/中断、监听套接字被调用,此时还有连接未决,accept函数接受新的套接字成功

不足:其实添加到fd_set套接字数量是有限制的,winsock2.h定义的64,自定义也不超过1024,因为值太大,会对服务器的性能有影响,更高的就是可伸缩的IOCP

linux

select模型

其实原理跟windows是差不多的,只是处理过程在底层上有点区别

模型:int

select(int

maxfd,fd_set*readfds,fd_set*

writefds,fd_set*exceptfds,const

struct

timeval*timeout)

这里主要是maxfd,文件描述符的范围,比待检测的最大文件描述符大1

处理过程:也是先将监控的文件添加到文件描述符集合中,调用select监控,判断文件是否发生变化,但是在底层调用的确是poll方法;首先使用poll_wait将等待队列添加到poll_table中,返回描述符的掩码

poll原型:unsigned

int

(*poll)(struct

file*filp,poll_wait*

wait)

看如下一个简单的处理过程

unsigned

int

mem_poll(struct

file

*filp,

poll_table

*wait)

{

struct

mem_dev

*dev

=

filp->private_data

unsigned

int

mask

=

0

/*将等待队列添加到poll_table

*/

poll_wait(filp,

&dev->inq,

wait)

if

(have_data)

mask

|=

POLLIN

|

POLLRDNORM

/*

readable

*/

return

mask

}

在这里只是添加队列,返回可读可写的掩码,真正阻塞的不是这里,是在do_select(...)函数中,在linux内核fs/select.c里面

int

do_select(int

n,

fd_set_bits

*fds,

struct

timespec

*end_time)这个函数

if

(file)

{

f_op

=

file->f_op

mask

=

DEFAULT_POLLMASK

if

(f_op

&&

f_op->poll)

{

wait_key_set(wait,

in,

out,

bit)

mask

=

(*f_op->poll)(file,

wait)

}

这里面是先判断文件存在,然后读取你自己定义的操作设备I/O的f_op函数,这里有一个默认的mask,接着才判断然后返回描述符mask

=

(*f_op->poll)(file,

wait)用于区分当前哪个集合被触发了;接着判断f_op&f_op->poll在这里我们默认定义了poll函数,所以这里会进入此判断语句,mask

=

(*f_op->poll)(file,

wait)这个就是调用默认的poll函数进行处理,关键的是如何区分不同的读、写、异常过程?

(mask

&

POLLIN_SET)

&&

(in

&

bit),这里面就是对当前的可读、写、异常的&&过程,就是为了判断和区分当前的套接字只是某一个具体的fd_set集合下;当然某一个套接字也可能同时在可读可写里面,这时候两个会进行判断。

if

(retval

||

timed_out

||

signal_pending(current))

break

上面的retval如果为0,且其他也不满足就会导致空循环状态,就处于阻塞状态了

异步选择(WSAAsyncSelect)模型是一个有用的异步 I/O 模型。利用这个模型,应用程序可在一个套接字上,

接收以 Windows 消息为基础的网络事件通知。具体的做法是在建好一个套接字后,调用WSAAsyncSelect函数。

该模型的核心即是WSAAsyncSelect函数。

█ 要想使用 WSAAsyncSelect 模型,在应用程序中,首先必须用CreateWindow函数创建一个窗口,再为该窗口提供一个窗口例程函数(WinProc)。

█ WSAAsyncSelect 的函数原型如下:

[cpp] view plain copy

int WSAAsyncSelect(

__in SOCKET s,

__in HWND hWnd,

__in unsigned int wMsg,

__in long lEvent

)

● s 参数指定的是我们感兴趣的那个套接字。

● hWnd 参数指定一个窗口句柄,它对应于网络事件发生之后,想要收到通知消息的那个窗口。

● wMsg 参数指定在发生网络事件时,打算接收的消息。该消息会投递到由hWnd窗口句柄指定的那个窗口。

(通常,应用程序需要将这个消息设为比Windows的WM_USER大的一个值,避免网络窗口消息与系统预定义的标准窗口消息发生混淆与冲突)

● lEvent 参数指定一个位掩码,对应于一系列网络事件的组合,大多数应用程序通常感兴趣的网络事件类型包括:

FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT、FD_CLOSE。当然,到底使用FD_ACCEPT,还是使用FD_CONNECT类型,

要取决于应用程序的身份是客户端,还是服务器。如应用程序同时对多个网络事件有兴趣,只需对各种类型执行一次简单的按位OR(或)运算,

然后将它们分配给lEvent就可以了,例如:

WSAAsyncSeltct(s, hwnd, WM_SOCKET, FD_CONNECT | FD_READ | FD_WRITE | FD_CLOSE)

解释说明:我们的应用程序以后便可在套接字s上,接收到有关连接、发送、接收以及套接字关闭这一系列网络事件的通知。

█ 注意 ①:

多个事件务必在套接字上一次注册!

另外还要注意的是,一旦在某个套接字上允许了事件通知,那么以后除非明确调用closesocket命令,

或者由应用程序针对那个套接字调用了WSAAsyncSelect,从而更改了注册的网络事件类型,否则的话,

事件通知会永远有效!若将lEvent参数设为0,效果相当于停止在套接字上进行的所有网络事件通知。

█ 注意 ②:

若应用程序针对一个套接字调用了WSAAsyncSelect,那么套接字的模式会从“锁定”变成“非锁定”。

这样一来,如果调用了像WSARecv这样的Winsock函数,但当时却并没有数据可用,那么必然会造成调用的失败,并返回WSAEWOULDBLOCK错误。

为防止这一点,应用程序应依赖于由WSAAsyncSelect的uMsg参数指定的用户自定义窗口消息,来判断网络事件类型何时在套接字上发生;而不应盲目地进行调用。

FD_READ 应用程序想要接收有关是否可读的通知,以便读入数据

FD_WRITE 应用程序想要接收有关是否可写的通知,以便写入数据

FD_ACCEPT 应用程序想接收与进入连接有关的通知

FD_CONNECT 应用程序想接收与一次连接完成的通知

FD_CLOSE 应用程序想接收与套接字关闭的通知

█ 应用程序在一个套接字上成功调用了WSAAsyncSelect之后,会在与hWnd窗口句柄对应的窗口例程中,以Windows消息的形式,接收网络事件通知。

窗口例程通常定义如下:

[cpp] view plain copy

LRESULT CALLBACK WindowProc(

HWND hwnd,

UINT uMsg,

WPARAM wParam,

LPARAM lParam

)

● hWnd 参数指定一个窗口的句柄,对窗口例程的调用正是由那个窗口发出的。

● uMsg 参数指定需要对哪些消息进行处理。这里我们感兴趣的是WSAAsyncSelect调用中定义的消息。

● wParam 参数指定在其上面发生了一个网络事件的套接字。假若同时为这个窗口例程分配了多个套接字,这个参数的重要性便显示出来了。

● lParam参数中,包含了两方面重要的信息。其中, lParam的低字(低位字)指定了已经发生的网络事件,而lParam的高字(高位字)包含了可能出现的任何错误代码。

█ 步骤:网络事件消息抵达一个窗口例程后,应用程序首先应检查lParam的高字位,以判断是否在网络错误。

这里有一个特殊的宏: WSAGETSELECTERROR,可用它返回高字位包含的错误信息。

若应用程序发现套接字上没有产生任何错误,接着便应调查到底是哪个网络事件类型,具体的做法便是读取lParam低字位的内容。

此时可使用另一个特殊的宏:WSAGETSELECTEVENT,用它返回lParam的低字部分。

█ 注意 ③:应用程序如何对 FD_WRITE 事件通知进行处理。

只有在三种条件下,才会发出 FD_WRITE 通知:

■ 使用 connect 或 WSAConnect,一个套接字首次建立了连接。

■ 使用 accept 或 WSAAccept,套接字被接受以后。

■ 若 send、WSASend、sendto 或 WSASendTo 操作失败,返回了 WSAEWOULDBLOCK 错误,而且缓冲区的空间变得可用。

因此,作为一个应用程序,自收到首条 FD_WRITE 消息开始,便应认为自己必然能在一个套接字上发出数据,

直至一个send、WSASend、sendto 或 WSASendTo 返回套接字错误 WSAEWOULDBLOCK。

经过了这样的失败以后,要再用另一条 FD_WRITE 通知应用程序再次发送数据。

用例:

[cpp] view plain copy

WSAAsyncSelect(pThis->m_SockListen, pThis->GetSafeHwnd(), WM_SOCKET, FD_ACCEPT | FD_CLOSE)//WM_SOCKET为自定义消息#define WM_SOCKET WM_USER+100

[cpp] view plain copy

//override窗口过程函数

LRESULT CServerDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)

{

switch(message)

{

case WM_SYSCOMMAND:

if (wParam == SC_CLOSE) {

closesocket(m_SockClient)

closesocket(m_SockListen)

WSACleanup()

}

break

case WM_SOCKET:

if (WSAGETSELECTERROR(lParam)) {

--m_ClientNums

closesocket(wParam)

break

}

switch(WSAGETSELECTEVENT(lParam))

{

case FD_ACCEPT:

{

if (m_ClientNums >= 1) {

break

}

m_SockClient = accept(wParam, NULL, NULL)

WSAAsyncSelect(m_SockClient, m_hWnd, WM_SOCKET, FD_READ|FD_WRITE|FD_CLOSE)

++m_ClientNums

break

}

case FD_READ:

{

TCHAR szBuf[MAX_BUF_SIZE] = {0}

recv(wParam, (char *)szBuf, MAX_BUF_SIZE, 0)

ShowMsg(szBuf)

break

}

case FD_WRITE:

wParam = wParam

break

case FD_CLOSE:

--m_ClientNums

closesocket(wParam)

break

}

default:break

}

return CDialog::WindowProc(message, wParam, lParam)

}


欢迎分享,转载请注明来源:夏雨云

原文地址:https://www.xiayuyun.com/zonghe/346167.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-05-07
下一篇2023-05-07

发表评论

登录后才能评论

评论列表(0条)

    保存