IO多路复用中Epoll和select、poll的小随笔

 

首先要明白的是:Epoll和select以及poll没有存在谁好谁坏的情况,需要根据实际应用来决定使用哪个。

 

select和poll一样,在epoll出来以前,实现IO多路复用的方式都是监听一大个fds队列。

  eg:fds可以理解成一个大大的进程监听队列(等待IO数据ing)

  1.先遍历第一遍,如果没有进程就绪,就直接阻塞。

  2.当网卡接收到数据,并且写进内存之后,就会发送给CPU一个中断,CPU得以根据传输过来的数据将其写入socket缓冲区(通过数据中的ip和端口号判断是哪个进程的),该进程得以从系统等待队列进入到就绪队列(进程状态从阻塞变成就绪)。

  3.这个时候,第一步阻塞就得以释放,我们就可以去通过再遍历一遍判断是哪个socket就绪了。

上述过程的缺点:每次都要遍历两遍队列,并且如果fds比较大,还要将这个大队列从用户区拷贝到内核,这个操作就很费时间了,然后再去遍历,啧啧啧。

select中dfs最大为1024,一次性只能监听1024个socket,而poll的话因为是由链表实现的,所以理论上无限制(看你的内存大小了),但是poll其他方面还是和select一样,所以治标不治本(poll还有一个水平触发,就是如果socket就绪,但是你遍历没有处理,那么下次还会通知你)。

 

所以再后续epoll的设计中,针对上述的遍历两次监听数有限,且存在内存复制的问题,做出了如下优化

  1.学习CPU添加了一个就绪队列,当网卡接受到某个进程的数据并且写入内存之后,就通知CPU知行某个中断程序将该进程放入一个就绪队列中(Rdlist)

  2.监听数不用数组存储,而是像poll一样无限制(受内存限制)

  3.在内核和用户内存中划分出一个共享内存,不用再复制拷贝fds

  也就是说,将进程放入监听队列之后,如果存在进程就绪,就可以直接知道是哪个进程就绪了,不用再去遍历fds。

Epoll更加书面一点过程就是:

  1.在使用epoll的时候,先会调用epoll_create()函数创建一个eventpoll对象(小扩展:eventpoll使用红黑树去存储监听的进程)

  2.此时通过调用epoll_ctl()函数来添加需要监听的socket对象,会直接放入到第一步生成的eventpoll对象中(类似一个等待队列)

  3.当socket接收到数据之后,cpu的中断程序会直接操作eventpoll对象,而不会直接操作进程(将这两个进程存如Rdlist,表示就绪)

  4.然后调用epoll_await()开始监听,如果此时rdlist中存在进程,那么直接返回,否则,就阻塞。

 

Epoll小扩展:Epoll分为两种触发方式,水平触发(Level trigger)和边缘触发(Edge trigger)

  1.LT模式:是默认的工作方式,并且同时支持阻塞和非阻塞Socket。在这种做法中内核告诉你一个进程就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知

  2.ET模式:与之对应的就是ET模式了,只支持非阻塞Scoket,也就是说,当你第一次调用epoll_await()告诉你A进程就绪了,你没有处理,那么下一次就不会通知你了。正是因为这样,所以其实会比LT高效率一点,很大程度减少了await被重复触发的次数(因为我如果不处理某个进程,每次我调用await去监听的时候,他都会立刻返回),也正是因为这样,Epoll只支持非阻塞Socket,防止一个socket阻塞用太多时间。

 

注意:表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。

 

Epoll在实际应用中还是有很大用处,比如Redis、Nginx都有使用这个,所以他们才能如此高效。

 

 

 

上一篇:服务器编程基本框架和两种高效的事件处理方式


下一篇:Oracle SQL 精妙SQL语句讲解