epoll的原理
场景描述
有100万用户同时与一个进程保持着TCP连接,而每一时刻只有几十个或几百个TCP连接是活跃的(接收到TCP包),也就是说,在每一时刻,进程只需要处理这100万连接中的一小部分连接。那么,如何才能高效地处理这种场景呢?进程是否在每次询问操作系统收集有事件发生的TCP连接时,把这100万个连接告诉操作系统,然后由操作系统找出其中有事件发生的几百个连接呢?
select和poll如何处理
select每次收集事件时,都把这100万连接的套接字传给操作系统(这首先就是用户态内存到内核态内存的大量复制),而由操作系统内核寻找这些连接上有没有未处理的事件,将会是巨大的资源浪费,这里有个非常明显的问题,即在某一时刻,进程收集有事件的连接时,其实这100万连接中的大部分都是没有事件发生的。
epoll如何处理
它在Linux内核中申请了一个简易的文件系统,把原先的一个select或者poll调用分成了3个部分:
- 调用 epoll_create建立1个epoll对象(在epoll文件系统中给这个句柄分配资源)
- 调用 epoll_ctl向epoll对象中添加这100万个连接的套接字
- 调用 epoll_wait收集发生事件的连接。
这样,只需要在进程启动时建立1个epoll对象,并在需要的时候向它添加或删除连接就可以了,因此,在实际收集事件时,epoll_wait的效率就会非常高,因为调用epoll_wait时并没有向它传递这100万个连接,内核也不需要去遍历全部的连接。
epoll是基于回调函数的,无轮询。如果当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll(Linux)、kqueue(FreeBSD)、/dev/poll(soloris)做的。举个经典例子,假设你在大学读书,住的宿舍楼有很多间房间,你的朋友要来找你。select版宿管大妈就会带着你的朋友挨个房间去找,直到找到你为止。而epoll版宿管大妈会先记下每位同学的房间号,你的朋友来时,只需告诉你的朋友你住在哪个房间即可,不用亲自带着你的朋友满大楼找人。如果来了10000个人,都要找自己住这栋楼的同学时,select版和epoll版宿管大妈,谁的效率更高,不言自明。同理,在高并发服务器中,轮询I/O是最耗时间的操作之一,select、epoll、/dev/poll的性能谁的性能更高,同样十分明了。
关于epoll的实现原理,本文不会具体介绍,这里只是介绍epoll的工作流程,epoll的使用是三个函数:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
首先epoll_create函数会在内核中创建一块独立的内存存储一个eventpoll结构体,该结构体包括一颗红黑树和一个链表,如下图所示:
然后通过epoll_ctl函数,可以完成两件事。
- (1)将事件添加到红黑树中,这样可以防止重复添加事件;
- (2)将事件与网卡建立回调关系,当事件发生时,网卡驱动会回调ep_poll_callback函数,将事件添加到epoll_create创建的链表中。
最后,通过epoll_wait函数,检查并返回链表中是否有事件。该函数是阻塞函数,阻塞时间为timeout,当双向链表有事件或者超时的时候就会返回链表长度(发生事件的数量)。
这是如何实现的
维护了一个epitem的数据结构,他通过两种数据结构把这两件事件分开实现,也就是Nginx每次取活跃连接的时候,我们只需要去遍历一个链表,这个链表里仅仅只有活跃的的连接、这样我们速度效率就会很高
1、创建:Nginx收到80端口建立连接的请求,请求连接成功以后,这时候我要添加一个读事件,这个读事件是用来读取http消息的,这个时候我可能会添加一个新的事件、或者是写事件,这个添加我只会放到红黑树中,二叉平衡树能保证我的插入效率是logn的复杂度
2、添加:当操作系统接收到网卡中发送来一个报文的时候,这个链表就会增加一个链接
3、修改:读取一个事件的时候链表自然就没了
4、删除:如果我我不想再处理读事件和写事件,我只要从这个平衡二叉树移除一个节点
5、获取句柄:就是遍历活跃链接的链表,从内核态读取到用户态