当前位置: 首页 > 新闻中心 > 网络编程socket 结合io多路复用select epool 机制实现单线程并发tcp

网络编程socket 结合io多路复用select epool 机制实现单线程并发tcp

发布时间:2024-04-01 22:35:54

  1. I/O--多路复用的三种机制Select,Poll和Epoll对比
  2. Linux中select poll和epoll的区别
  3. IO模型及select,poll,epoll和kqueue的区别

一、I/O--多路复用的三种机制Select,Poll和Epoll对比

select、poll 和 epoll 都是 linux api 提供的 io 复用方式。

多进程和多线程技术相比,i/o多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

我们先分析一下select函数

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);

【参数说明】

int maxfdp1 指定待测试的文件描述字个数,它的值是待测试的最大描述字加1。

fd_set *readset , fd_set *writeset , fd_set *exceptset

fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄。中间的三个参数指定我们要让内核测试读、写和异常条件的文件描述符集合。如果对某一个的条件不感兴趣,就可以把它设为空指针。

const struct timeval *timeout timeout告知内核等待所指定文件描述符集合中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。

【返回值】

int 若有就绪描述符返回其数目,若超时则为0,若出错则为-1

select()的机制中提供一种fd_set的数据结构,实际上是一个long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据io状态修改fd_set的内容,由此来通知执行了select()的进程哪一socket或文件可读。

从流程上来看,使用select函数进行io请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的io请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个io请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。也就是说,poll只解决了上面的问题3,并没有解决问题1,2的性能开销问题。

下面是pll的函数原型:

poll改变了文件描述符集合的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的文件描述符集合限制远大于select的1024

【参数说明】

struct pollfd *fds fds是一个struct pollfd类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;一个pollfd结构体表示一个被监视的文件描述符,通过传递fds指示 poll() 监视多个文件描述符。其中,结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域,结构体的revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域

nfds_t nfds 记录数组fds中描述符的总数量

【返回值】

int 函数返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错;

epoll在linux2.6内核正式提出,是基于事件驱动的i/o方式,相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

linux中提供的epoll相关函数如下:

1. epoll_create 函数创建一个epoll句柄,参数size表明内核要监听的描述符数量。调用成功时返回一个epoll句柄描述符,失败时返回-1。

2. epoll_ctl 函数注册要监听的事件类型。四个参数解释如下:

epoll_event 结构体定义如下:

3. epoll_wait 函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0。

epoll是linux内核为处理大批量文件描述符而作了改进的poll,是linux下多路复用io接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统cpu利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核io事件异步唤醒而加入ready队列的描述符集合就行了。

epoll除了提供select/poll那种io事件的水平触发(level triggered)外,还提供了边缘触发(edge triggered),这就使得用户空间程序有可能缓存io状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

lt和et原本应该是用于脉冲信号的,可能用它来解释更加形象。level和edge指的就是触发点,level为只要处于水平,那么就一直触发,而edge则为上升沿和下降沿的时候触发。比如:0->1 就是edge,1->1 就是level。

et模式很大程度上减少了epoll事件的触发次数,因此效率比lt模式下高。

一张图总结一下select,poll,epoll的区别:

epoll是linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select和poll。目前流行的高性能web服务器nginx正式依赖于epoll提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞i/o方式可能性能更好。

既然select,poll,epoll都是i/o多路复用的具体的实现,之所以现在同时存在,其实他们也是不同 历史 时期的产物

二、Linux中select poll和epoll的区别

select、poll、epoll都是io多路复用的机制,但是他们的机制有很大的区别

1、select 

select机制刚开始的时候,需要把fd_set从用户空间拷贝到内核空间,并且检测的fd数是有限制的,由fd_setsize设置,一般是1024。

检测的时候,根据timeout,遍历fd_set表,把活跃的fd(可读写或者错误),拷贝到用户空间,

再在用户空间依次处理相关的fd。

这个机制是linux内核很早的版本,epool是根据select,pool基础上优化的,缺点比较多。

缺点:

1)每次调用select的时候需要把fd_set从用户空间拷贝到内存空间,比较耗性能。

2)wait时,需要遍历所有的fd,消耗比较大。

3)select支持的文件数大小了,默认只有1024,如果需要增大,得修改宏fd_setsize值,并编译内核(麻烦,并且fd_set中的文件数多的话,每次遍历的成本就很大)。

2. pool

poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多。

3. epool

epool是select和poll的改进版本,

* 先是使用int epoll_create(int size)在内存中创建一个指定size大小的事件空间,

* 再使用int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);事件注册函数,注册新的fd到epfd的epool对象空间中,并指明event(可读写啊等等),注意:在注册新事件fd的过程中,也再内核中断处理程序里注册fd对应的回调函数callback,告诉内核,一旦这个fd中断了,就把它放到ready队列里面去。

* 再使用int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);在epool对象对应的ready队列里取就绪的fd,并使用内存映射mmap拷贝到用户空间。

* 再在用户空间依次处理相关的fd。

优点:

1)支持一个进程打开大数目的socket描述符

select 一个进程打开fd是有限制的,由fd_setsize设置,默认值是1024。epool可以打开的fd数可以很大,一般1gb的内存有10万多的fd数,具体数目可以cat /proc/sys/fs/file-max查看。

2) io效率不随fd数目增加而线性下降

3) 使用mmap加速内核与用户空间的消息传递

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

1 select、poll与epoll简介

select

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

1 单个进程可监视的fd数量被限制

2 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

3 对socket进行扫描时是线性扫描

poll

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

epoll

epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。

在前面说到的复制问题上,epoll使用mmap减少复制开销。

还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知

注:水平触发(level-triggered)——只要满足条件,就触发一个事件(只要有数据没有被获取,内核就不断通知你);边缘触发(edge-triggered)——每当状态变化时,触发一个事件。

2 性能比较

由于博主并没有提供测试的机器参数,以及测试程序代码,所以这个性能测试只能够算是一个补充吧,对于epoll在大量fd情况下优势的直观展示。

表格左侧是描述符集合的大小,右侧分别表示1s对poll和epoll的调用次数,也就是性能瓶颈。

从上表可以看出当fd数量较少的时候poll略优于epoll,但是当fd增大到某个阈值时,poll性能急剧下降。而epoll始终保持的稳定的性能。

3 使用

当同事需要保持很多的长连接,而且连接的开关很频繁时,就能够发挥epoll最大的优势了。这里与服务器模型其实已经有些交集了。

同时需要保持很多的长连接,而且连接的开关很频繁,最高效的模型是非阻塞、异步io模型。而且不要用select/poll,这两个api的有着o(n)的时间复杂度。在linux用epoll,bsd用kqueue,windows用iocp,或者用libevent封装的统一接口(对于不同平台libevent实现时采用各个平台特有的api),这些平台特有的api时间复杂度为o(1)。

然而在非阻塞,异步i/o模型下的编程是非常痛苦的。由于i/o操作不再阻塞,报文的解析需要小心翼翼,并且需要亲自管理维护每个链接的状态。并且为了充分利用cpu,还应结合线程池,避免在轮询线程中处理业务逻辑。

但这种模型的效率是极高的。以知名的http服务器nginx为例,可以轻松应付上千万的空连接+少量活动链接,每个连接连接仅需要几k的内核缓冲区,想要应付更多的空连接,只需简单的增加内存(数据来源为淘宝一位工程师的一次技术讲座,并未实测)。这使得ddos攻击者的成本大大增加,这种模型攻击者只能将服务器的带宽全部占用,才能达到目的,而两方的投入是不成比例的。

注:长连接——连接后始终不断开,然后进行报文发送和接受;短链接——每一次通讯都建立连接,通讯完成即断开连接,下次通讯再建立连接。