前言
关于epoll和select还有poll这三个东西,一直经常听到,自己也去弄过他们的原理,但是大都没有总结,就着红黑树的学习,再一次去理解epoll底层的一些实现,现在就来总结一下这三个到底是个啥。
概念
epoll:epoll到底是什么,epoll其实就是一种I/O事件通知机制,是Linux实现I/O多路复用的一种手段,(好吧我知道这就话说完基本就蒙蔽了,不用怕继续看)。
I/O:就是input和output,在计算机中,所有的操作基本就是在进行输入输出操作,比如对文件I/O,网络之间通信也是I/O,进程之间的管道等等等都是I/O。
I/O多路复用就是一个操作想要同时管着多个输入输出,这个就是多路复用,比如Linux中通过文件描述符(fd)去管理I/O,一个文件描述符就是一个I/O,多路复用也就是管理多个文件描述符。是不是有那么点意思。
**那么epoll就是实现了管理多个文件描述的操作,也就是I/O多路复用技术喽。**应该是能理解了。
**事件通知机制:**事件通知机制就是说,当有事件发生的时候,就发出一个通知信号去通知一下,而它的反面就是轮询机制,也就是不断主动询问有没有事件发生,好了一想就知道通知更好,因为轮询太折腾了!
epoll它的核心就是三个函数两个数据结构,哈哈哈有一个就是我最近研究的红黑树,上一张图吧:
这个就是核心喽,来看看到底是何方神圣。我们先来看一下这三个函数:
epoll_create:这个函数就是创建一个epoll对象,原型是这样的int epoll_create(int size)
,返回值是一个文件描述符后面两个函数都会以它为核心,说白了就是建立一个联系,size就是文件描述符最大值,现在已经被弃用,不要传0就行,调用这个函数的时候内核会帮我们创建一个epoll实例数据结构,就是一个用于存放fd的红黑树和一个用于存储就绪事件的链表,这些是在内核中存放了一下块缓存中,现象一下,缓存意味着什么,缓存当然意味着快快快啦(勿开车),epoll就是典型的以空间换取时间思想。
epoll_ctl:这个函数就是负责管理fd的增加和删除的,也就是在红黑树上进行删除和增加。原型:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
,epfd就是上面那个创建返回的fd,op就是操作的类型就是:
- EPOLL_CTL_ADD:向interest list添加一个需要监视的描述符
- EPOLL_CTL_DEL:从interest list中删除一个描述符
- EPOLL_CTL_MOD:修改interest list中一个描述符
fd就是文件描述符喽,
typedef union epoll_data {
void *ptr; /* 一般就是事件的回调函数,当有事件发生时通过回调函数将事件添加到list上 */
int fd; /* 注册的文件描述符 */
uint32_t u32; /* 32-bit integer */
uint64_t u64; /* 64-bit integer */} epoll_data_t;
struct epoll_event {
uint32_t events; /* 描述epoll事件 */
epoll_data_t data; /* 见上面的联合体 */
};
最后一个就是上面这个结构体,存储了事件的信息。这也正是epoll的一个关键,存储事件的信息。
events域是bit mask,常用的,是可以多选的:
- EPOLLIN:描述符处于可读状态
- EPOLLOUT:描述符处于可写状态
- EPOLLET:将epoll event通知模式设置成edge triggered
- EPOLLONESHOT:第一次进行通知,之后不再监测
- EPOLLHUP:本端描述符产生一个挂断事件,默认监测事件
- EPOLLRDHUP:对端描述符产生一个挂断事件
- EPOLLPRI:由带外数据触发
- EPOLLERR:描述符产生错误时触发,默认检测事件
最后一个函数就是int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
,它是用来阻塞等待注册事件发生的,返回值是发生事件的数目,并且把事件写入到events数组中,maxevents是每次返回的最大事件个数,timeout是阻塞事件,-1为一直阻塞,0为不阻塞,剩下的就是持续的时间。
注意一下:events数组从readylist中复制时间的时候,一次最多能复制maxevents个,当小于这个就全部复制过来,否则只能复制maxevents个。
epoll两种事件触发机制:(LT)(ET)
事件触发机制,就是注册的那些事件在什么时候被触发,也就是说什么时候调用回调函数把事件加入到list中去。epoll默认使用LT:叫做水平出发机制,什么意思呢?
触发时机:
- 对于读操作,只要缓冲内容不为空,LT模式返回读就绪。
- 对于写操作,只要缓冲区还不满,LT模式会返回写就绪。当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你。如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。
这个理解起来是比较容易的,就是说这个事件,在满足这些条件就触发。
那么ET就是边沿触发,什么叫边沿触发:
- 对于读操作当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。当有新数据到达时,即缓冲区中的待读数据变多的时候。当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。
- 对于写操作当缓冲区由不可写变为可写时。当有旧数据被发送走,即缓冲区中的内容变少的时候。当缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时。当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。
你可以读一下再看我这段:就是说水平触发方式,epoll就只要这个事件发生了,每一次都会通知你,你必须处理了才算完,而边沿触发就是我只通知你一次,你不处理下一次我就不通知你了,除非事件发生变化了,其实这个源于信号理论,再结合信号上的理论:当电压从0-》1或者从1-》0就会产生上升沿或者下降沿,这时候触发的就是边沿触发,而水平触发就是电压一直为1就一直触发。应该可以理解了。
epoll中使用红黑树
红黑树在epoll中就保存在linux内核中的一块cache,然后通过在这个cache来进行文件描述符的插入删除等操作,由于红黑树的插入删除速度比较良好,查找效率也比较优秀,所以epoll性能提升的很大一个原因就在于此。红黑树只是数据的一个载体:当我们需要平凡的对数据进行插入删除而且还需要保证查找效率的时候,就应该想到使用红黑树。
epoll和select还有poll的对比
没有对比就没有伤害!通过四个方面来对比一下他们就能很明显的知道为啥epoll这么优秀了!
**tips:**轮询是指,想要知道操作系统内核中哪些fd发生了读写事件需要不断地去询问操作系统,也就是循环的询问。
- 用户态将文件描述符传入内核的方式
- select:创建三个文件描述符集,分别监听读、写、异常动作,这里单个进程文件描述最大个数为1024.
- poll:将传入的struct pollfd数组传入内核,让内核监听
- epoll:不用传入内核,而是通过在内核的高速cache中维护一颗红黑树去监听,然后再维护一个就绪链表去存储已经触发的事件。可以往红黑树上增加删除节点
- 内核检测文件描述符状态的方式
- select:内核采用轮询的方式,遍历前面出入的文件描述符,然后返回一个描述符读写操作是否就绪的mask掩码,根据这个掩码给fd_set赋值。
- poll:也是采用轮询机制,内核轮询刚才传入的数组,当数组中fd就绪,就加入到等待队列中,然后继续遍历
- epoll:采用回调机制,每一个事件都注册了回调函数,当内核检测到某个fd可读可写时就会调用回调函数把事件添加到就绪链表中去,由于红黑树就是在内核中的,所以没有了拷贝操作,也不需要轮询,内核直接去操作红黑树上的fd就行。
- 文件描述符传递给用户的方式
- select:同样的这里也要把文件描述符集拷贝回去,返回哪些文件描述符处于就绪态了,但是用户态依然不知道哪个就绪了,所以需要来遍历判断。
- poll:将之前传入的数组拷贝回给用户态,并返回就绪的个数,用户也不知道哪些是就绪的,也需要遍历数组来判断。
- epoll:epoll就不用这样了,它是通过把就绪状态的文件描述符记录在了就绪链表中,所以放在传入的数组就行了,然后用户只要遍历这个数组处理事件就行了,不需要再判断是否发生了读写操作。而且这里是通过mmap让内核和用户共享一块数据,避免了不必要的拷贝操作!
- 重复监听的方式,I/O多路复用
- select:需要将新的文件描述符集合拷贝给内核然后重复以上操作。
- poll:将新的数组拷贝到内核中,然后重复以上步骤。
- epoll:epoll只需要直接向红黑树中增加删除即可,红黑树就是在内核中的。
epoll更加高性能:
- select和poll的动作基本一致,只是poll采用链表来进行文件描述符的存储,而select采用fd标注位来存放,所以select会受到最大连接数的限制,而poll不会。
- select、poll、epoll虽然都会返回就绪的文件描述符数量。但是select和poll并不会明确指出是哪些文件描述符就绪,而epoll会。造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听的整个文件描述符找到是谁处于就绪,而epoll则直接处理即可。
- select、poll都需要将有关文件描述符的数据结构拷贝进内核,最后再拷贝出来。而epoll创建的有关文件描述符的数据结构本身就存于内核态中,系统调用返回时利用mmap()文件映射内存加速与内核空间的消息传递:即epoll使用mmap减少复制开销。
- select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制。造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大影响,除非活跃的socket很多。
- epoll的边缘触发模式效率高,系统不会充斥大量不关心的就绪文件描述符。
总之epoll性能是最好的,但是注意,在不需要大量用户访问的时候,epoll可能不如select和poll,因为通知机制需要函数的回调!