socket 由浅入深 (五)select Epoll poll 比较

1. Epoll是何方神圣?

Epoll可是当前在Linux下开发大规模并发网络程序的热门人选,Epoll Linux2.6内核中正式引入,和select相似,其实都I/O多路复用技术而已,并没有什么神秘的。

 

其实在Linux下设计并发网络程序,向来不缺少方法,比如典型的Apache模型(Process Per Connection,简称PPC),TPCThread PerConnection)模型,以及select模型和poll模型,那为何还要再引入Epoll这个东东呢?那还是有得说说的

2. 常用模型的缺点

如果不摆出来其他模型的缺点,怎么能对比出Epoll的优点呢。

2.1 PPC/TPC模型

这两种模型思想类似,就是让每一个到来的连接一边自己做事去,别再来烦我。只是PPC是为它开了一个进程,而TPC开了一个线程。可是别烦我是有代价的,它要时间和空间啊,连接多了之后,那么多的进程/线程切换,这开销就上来了;因此这类模型能接受的最大连接数都不会高,一般在几百个左右。

2.2 select模型

1. 最大并发数限制,因为一个进程所打开的FD(文件描述符)是有限制的,由FD_SETSIZE设置,默认值是1024/2048,因此Select模型的最大并发数就被相应限制了。自己改改这个FD_SETSIZE?想法虽好,可是先看看下面吧

2. 效率问题,select每次调用都会线性扫描全部的FD集合,这样效率就会呈现线性下降,把FD_SETSIZE改大的后果就是,大家都慢慢来,什么?都超时了??!!

3. 内核/用户空间内存拷贝问题,如何让内核把FD消息通知给用户空间呢?在这个问题上select采取了内存拷贝方法。

2.3 poll模型

基本上效率和select是相同的,select缺点的23它都没有改掉。

3. Epoll的提升

把其他模型逐个批判了一下,再来看看Epoll的改进之处吧,其实把select的缺点反过来那就是Epoll的优点了。

3.1. Epoll没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于2048, 一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。

3.2. 效率提升,Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于selectpoll

3.3. 内存拷贝,Epoll在这点上使用了“共享内存”,这个内存拷贝也省略了。

 

4. Epoll为什么高效

Epoll的高效和其数据结构的设计是密不可分的,这个下面就会提到。

首先回忆一下select模型,当有I/O事件到来时,select通知应用程序有事件到了快去处理,而应用程序必须轮询所有的FD集合,测试每个FD是否有事件发生,并处理事件;代码像下面这样:


  1. int res = select(maxfd+1, &readfds, NULL, NULL, 120);  
  2.   
  3. if(res > 0)  
  4.   
  5. {  
  6.   
  7.     for(int i = 0; i < MAX_CONNECTION; i++)  
  8.   
  9.     {  
  10.   
  11.         if(FD_ISSET(allConnection[i],&readfds))  
  12.   
  13.         {  
  14.   
  15.             handleEvent(allConnection[i]);  
  16.   
  17.         }  
  18.   
  19.     }  
  20.   
  21. }  
  22.   
  23. // if(res == 0) handle timeout, res < 0 handle error  


 

Epoll不仅会告诉应用程序有I/0事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,因此根据这些信息应用程序就能直接定位到事件,而不必遍历整个FD集合。

  1. intres = epoll_wait(epfd, events, 20, 120);  
  2.   
  3. for(int i = 0; i < res;i++)  
  4.   
  5. {  
  6.   
  7.     handleEvent(events[n]);  
  8.   
  9. }  


5. Epoll关键数据结构

前面提到Epoll速度快和其数据结构密不可分,其关键数据结构就是:

  1. structepoll_event {  
  2.   
  3.     __uint32_t events;      // Epoll events  
  4.   
  5.     epoll_data_t data;      // User datavariable  
  6.   
  7. };  
  8.   
  9. typedefunion epoll_data {  
  10.   
  11.     void *ptr;  
  12.   
  13.    int fd;  
  14.   
  15.     __uint32_t u32;  
  16.   
  17.     __uint64_t u64;  
  18.   
  19. } epoll_data_t;  


可见epoll_data是一个union结构体,借助于它应用程序可以保存很多类型的信息:fd、指针等等。有了它,应用程序就可以直接定位目标了。

6. 使用Epoll

既然Epoll相比select这么好,那么用起来如何呢?会不会很繁琐啊先看看下面的三个函数吧,就知道Epoll的易用了。

 

intepoll_create(int size);

生成一个Epoll专用的文件描述符,其实是申请一个内核空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。size就是你在这个Epoll fd上能关注的最大socket fd数,大小自定,只要内存足够。

intepoll_ctl(int epfd, intop, int fd, structepoll_event *event);

控制某个Epoll文件描述符上的事件:注册、修改、删除。其中参数epfdepoll_create()创建Epoll专用的文件描述符。相对于select模型中的FD_SETFD_CLR宏。

intepoll_wait(int epfd,structepoll_event * events,int maxevents,int timeout);

等待I/O事件的发生;参数说明:

epfd:epoll_create() 生成的Epoll专用的文件描述符;

epoll_event:用于回传代处理事件的数组;

maxevents:每次能处理的事件数;

timeout:等待I/O事件发生的超时值;

返回发生事件数。

相对于select模型中的select函数。


 

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Epoll模型主要负责对大量并发用户的请求进行及时处理,完成服务器与客户端的数据交互。其具体的实现步骤如下:
(a) 使用epoll_create()函数创建文件描述,设定将可管理的最大socket描述符数目。
(b) 创建与epoll关联的接收线程,应用程序可以创建多个接收线程来处理epoll上的读通知事件,线程的数量依赖于程序的具体需要。
(c) 创建一个侦听socket描述符ListenSock;将该描述符设定为非阻塞模式,调用Listen()函数在套接字上侦听有无新的连接请求,在 epoll_event结构中设置要处理的事件类型EPOLLIN,工作方式为 epoll_ET,以提高工作效率,同时使用epoll_ctl()注册事件,最后启动网络监视线程。
(d) 网络监视线程启动循环,epoll_wait()等待epoll事件发生。
(e) 如果epoll事件表明有新的连接请求,则调用accept()函数,将用户socket描述符添加到epoll_data联合体,同时设定该描述符为非阻塞,并在epoll_event结构中设置要处理的事件类型为读和写,工作方式为epoll_ET.
(f) 如果epoll事件表明socket描述符上有数据可读,则将该socket描述符加入可读队列,通知接收线程读入数据,并将接收到的数据放入到接收数据的链表中,经逻辑处理后,将反馈的数据包放入到发送数据链表中,等待由发送线程发送。

 

1.不要采用一个连接一个线程的方式,而是尽量利用操作系统的事件多路分离机制
如:UNIX下的 select  linux下的epoll BSD下的kqueue
或者使用这些机制的高层API (boost.asio&&ACE Reactor)
2.尽量使用异步I/O,而不是同步
3.当事件多路分离单线程无法满足并发需求时,将事件多路分离的线程扩展成线程池  

两种方式的区别主要体现在以下几个方面:
  1. select所能控制的I/O数有限,这主要是因为fd_set数据结构是一个有大小的,相当与一个定长所数组。
  2. select每次都需要重新设置所要监控的fd_set(因为调用之后会改变其内容),这增加了程序开销。
  3. select的性能要比epoll差,具体原因会在后续内容中详细说明。
嗯,说道这个为什么select要差,那就要从这个select API说起了。这个传进去一个数组,内部实现也不知道那个有哪个没有,所以要遍历一遍。假设说我只监控一个文件描述符,但是他是1000。那么select需要遍历前999个之后再来poll这个1000的文件描述符,而epoll则不需要,因为在之前epoll_ctl的调用过程中,已经维护了一个队列,所以直接等待事件到来就可以了。
Linux中select此段相关代码为:
  1. /* 遍历所有传入的fd_set */  
  2. for (i = 0; i < n; ++rinp, ++routp, ++rexp) {  
  3.     unsigned long in, out, ex, all_bits, bit = 1, mask, j;  
  4.     unsigned long res_in = 0, res_out = 0, res_ex = 0;   
  5.     const struct file_operations *f_op = NULL;  
  6.     struct file *file = NULL;  
  7.   
  8.     in = *inp++; out = *outp++; ex = *exp++;  
  9.     all_bits = in | out | ex;  
  10.     /* 此处跳无需监控的fd, 白白的浪费时间啊…… */          
  11.     if (all_bits == 0) {   
  12.         i += __NFDBITS;  
  13.         continue;  
  14.     }   
  15.     /* 后续进行一些相关操作 */  
  16. }  


而epoll则无需进行此类操作,直接检测内部维护的一个就绪队列,如果队列有内容,说明有I/O就绪,那么直接赋值返回内容,成功返回,如果没有成功,那么睡眠,等待就绪队列非空。

通过这个两者的比较,其实两者的差距啊,大部分是因为这个API设计所决定的,select就设计成这样一个API,内部再怎么优化也只能是这么个烂样子,而epoll这样维护与等待分离,灵活多变,最后也就带来了相对的高性能,以及可扩展性。

以后的代码生涯中,还是先要设计好API,然后才能写出好代码……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值