设想一个场景:有100万用户同时与一个进程保持着TCP连接,,也就是说在每一时刻进程只需要处理这100万连接中的一小部分连接。那么,如何才能高效的处理这种场景呢?进程是否在每次询问操作系统收集有事件发生的TCP连接时,把这100万个连接告诉操作系统,然后由操作系统找出其中有事件发生的几百个连接呢?实际上,在Linux2.4版本以前,那时的select或者poll事件驱动方式是这样做的。
这里有个非常明显的问题,即在某一时刻,进程收集有事件的连接时,。因此如果每次收集事件时,都把100万连接的套接字传给操作系统(这首先是用户态内存到内核态内存的大量复制),。而epoll不这样做,它在Linux内核中申请了一个简易的文件系统,把原先的一个select或poll调用分成了3部分:
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建立一个epoll对象(在epoll文件系统中给这个句柄分配资源);
-
调用epoll_ctl向epoll对象中添加这100万个连接的套接字;
-
调用epoll_wait收集发生事件的连接。
这样只需要在进程启动时建立1个epoll对象,并在需要的时候向它添加或删除连接就可以了,因此,在实际收集事件时,epoll_wait的效率就会非常高,
Epoll流程
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关,如下所示:
struct eventpoll {
...
/*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,
也就是这个epoll监控的事件*/
struct rb_root rbr;
/*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/
struct list_head rdllist;
...
};
- 我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个用于存储以后epoll_ctl传来的socket外,还会再建立一个,用于存储准备就绪的事件
- 执行epoll_ctl()时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,
- 当epoll_wait调用时,仅仅观察里有没有数据即可。有数据就返回,并将事件复制到用户态内存(使用共享内存提高效率)。没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。