一文搞懂Linux高性能技术--epoll
Stream 流
流可以是文件、socket、pipe等等可以支持io操作的对象。流有两端,两端可以进行读写。如果我们此时读,流另一端没有数据传来,该怎么办。处理的方式的这样几种。
简单的操作是阻塞,读操作将进入阻塞状态等待数据到来。这样实现简单,但是阻塞时线程无法工作,性能太低。阻塞的实现需要依靠缓冲区。缓冲区的引入是为了减少频繁的io操作而引起频繁的Io调用。
另一种操作是非阻塞忙轮询,不断地检查是否有数据到来,虽然线程不会进入阻塞状态,但是这样会占用太多CPU资源用来做无用的询问,这是无法接受的。
epoll
epoll在Linux内核实现异步操作中被提出,广泛应用于高并发的web服务器中。可以同时对多个服务提供服务的技术称为io多路复用。io复用的基本思想是事件驱动,服务端同时保持有多个客户端IO连接,但某个客户端在请求某项服务时,服务器响应服务。Linux中使用select、poll和epoll实现。epoll是所有该并发服务器的基础。
epoll原理
再介绍epoll原理之前先介绍一下多路复用IO模型。网络IO的本质是对socket的读取,需要等到流数据准备就绪和内核向进程复制数据。如果是非阻塞调用,进程会一直保持运行态,一直轮询,如果能将这个轮询的过程外包给另外的进程,负责在数据到来的时候唤醒等待的进程,这就是多路复用IO模型。
多路复用有两个特殊的调用:select和poll。select调用是内核级别,select会监听多个socket,但其中一个socket的数据准备好了的时候,唤醒进程进行recvfrom,接收数据的过程是阻塞的,要一点一点读。
函数详解:
- select 函数 TODO:
- poll 函数 TODO:
- epoll 函数 函数原型:
1 2 3 4 5 6 7 8 9 10 11 12
//epoll_data保存触发事件相关的数据。(数据类型与具体使用方式有关) typedef union epoll_data{ void* ptr; int fd; _uint32_t u32; _uint64_t u64; } epoll_data_t; //保存感兴趣的事件和被触发的事件 struct epoll_event{ _uint32_t events; epoll_data_t data; };
events是枚举类型,是一系列事件类型的集合。
- EPOLLIN : 表示关联的fd可以进行读操作
- EPOLLOUT :表示关联的fd可以进行写操作
- EPOLLRDHUP(2.6.17之后):表示套接字关闭了连接,或关闭了正写的一半的连接
- EPOLLPRI : 表示关联的fd有紧急优先事件可以进行读操作。
- EPOLLERR : 表示关联的fd发生了错误,epoll_wait会一直等待这个事件,所以一般没有必要设置这个属性
- EPOLLHUP : 表示关联的fd被挂起,epoll_wait会一直等待这个事件,所以一般没有必要设置这个属性
- EPOLLET : 设置关联的fd为ET的工作方式,即边缘触发
- EPOLLONESHOT : 设置关联的fd为one-shot工作方式,表示只监听一次事件,如果要再次监听,需要把socket放入到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 : 创建一个epoll句柄,注意创建epoll句柄会占用一个文件描述符,在使用完之后需要关闭。否则可能会导致文件描述符耗尽。
- size : size为最大的监听文件描述符数,监听的文件描述符的个数不能超过size可以手动指定,但是这个数值可以达到系统可以开的最大的文件描述符数。
- epoll_ctl : epoll的事件注册函数,它不同于select的是,它不是在监听事件的时候告诉内核要监听什么类型的时间,而是先注册要监听的事件类型。
- epfd : epoll文件描述符,即epoll_ create的返回值,表示该epoll描述符注册事件
- op : 注册事件的类型包括以下三类。
- EPOLL_CTL_ADD : 注册行的fd到epfd中
- EPOLL_CTL_MOD : 修改已经注册的fd的事件类型
- EPOLL_CTL_DEL : 删除已经注册的fd
- fd : 注册的文件描述符
- event : 注册的时间的类型,告诉内核需要监听什么事件,类型包括上面几种。
- epoll_wait : 收集epoll监控的时间中已经就绪的事件,若调用成功,返回就绪的文件描述符的个数,返回0表示超时。
- epfd : epoll的文件描述符
- events : 已经就绪的事件集合.内核不分配内存,需要程序自己分配内存传给内核,内核只负责将书复制到这里
- maxevents : events数组的大小。
- timeout : 超时时间。
水平触发(LT)和边缘触发(ET)
LT: 当被监控的文件描述符上有可读可写的事件时,epoll_wait()会通知用户处理程序去读写。如何程序没有一次性把缓冲区的数据读完或写完,那么下次调用epoll_wait时仍然通知可读写,如果一直不写,那么会一直通知你,进而降低效率。
ET:和LT的区别就是缓冲区如果在一次wait通知有读写后没完成,之后调用wait就不会通知你,知道有下一个可读写事件的到来。
介绍完epoll的使用,该介绍下epoll的底层实现。
epoll在内核初始化的时候会向内核注册个文件系统,用户储存被监控的文件描述符的信息。同时在初始化的时候在内核开辟出一块cache,用红黑树的结构储存监听的fd信息。
epoll fd在内核中可以查找到的数据如下:
1
2
3
4
5
6
7
8
9
struct eventpoll {
spin_lock_t lock; //对本数据结构的访问
struct mutex mtx; //防止使用时被删除
wait_queue_head_t wq; //sys_epoll_wait() 使用的等待队列
wait_queue_head_t poll_wait; //file->poll()使用的等待队列
struct list_head rdllist; //事件满足条件的链表
struct rb_root rbr; //用于管理所有fd的红黑树(树根)
struct epitem *ovflist; //将事件到达的fd进行链接起来发送至用户空间
}
当你向系统中添加一个fd时,就创建一个epitem结构体,这是内核管理epoll的基本数据:
1
2
3
4
5
6
7
8
9
10
11
struct epitem {
struct rb_node rbn; //用于主结构管理的红黑树
struct list_head rdllink; //事件就绪队列
struct epitem *next; //用于主结构体中的链表
struct epoll_filefd ffd; //这个结构体对应的被监听的文件描述符信息
int nwait; //poll操作中事件的个数
struct list_head pwqlist; //双向链表,保存着被监视文件的等待队列,功能类似于select/poll中的poll_table
struct eventpoll *ep; //该项属于哪个主结构体(多个epitm从属于一个eventpoll)
struct list_head fllink; //双向链表,用来链接被监视的文件描述符对应的struct file。因为file里有f_ep_link,用来保存所有监视这个文件的epoll节点
struct epoll_event event; //注册的感兴趣的事件,也就是用户空间的epoll_event
}
用户调用cli去添加监听的fd,会放在红黑树当中