首先要明白的是: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都有使用这个,所以他们才能如此高效。