先来看一下函数原型:
#include <sys/epoll.h>
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是多路复用的一种,什么是多路复用?
IO复用表示多个IO操作复用一个线程。操作系统为你提供一种功能,当某个socket上有事件发生时,操作系统会给你一个通知。然后我们使用select,poll, epoll去get这些通知并逐个处理,这就是简单的多路复用思想。
网络IO模型有很多,IO多路复用只是其中一种,还包括:
- 阻塞IO(read()或write()阻塞等待完成)
- 非阻塞IO (设置套接字非阻塞,循环read()直到完成)
- IO多路复用(select ,poll, epoll这些系统调用函数)
- 信号驱动IO(通过fcntl设置使得IO事件时进程收到SIGIO信号)
- 异步IO(aio操作,与上面最大的不同就是异步IO通知的是读完成事件,即IO操作不在用户进程内执行,由内核带手)
上面这些性能由低到高,但是像异步IO,信号驱动IO的应用操作实现都比较难,多路复用属于性能颇好实现难度又不太高的那种,所以貌似用得比较广泛。
那么三种IO多路复用效率比较呢?
这里不做函数的实现细节解释。先来看select和poll缺点:
- 每次调用select()或poll(),内核都必须检查所有被指定的文件描述符。随文件描述符增加性能降低。
- 调用select()或poll(),程序都必须传递一个表示所有需要检查的文件描述符的数据结构到内核。随文件描述符增多,拷贝数据结构占用时间增多。
- 调用select()或poll()返回后,需要检查返回数据结构中每个元素,同上缺点。时间复杂度O(n)。
然后是epoll优点:
- 支持ET和LT,而select和poll只支持LT,信号驱动IO只支持ET。
- 相比信号驱动IO性能更好,避免复杂的信号处理流程。
- 灵活性,可指定希望检查的事件类型。
- 记录了在进程中声明过的感兴趣描述符表。
- 维护了处于IO就绪态的文件描述符列表。
几个系统宏定义:
EPOLLONESHORT是什么
即使用ET模式,一个socket上的某个事件还是可能被触发多次.这在并发程序中会引发一个问题。
比如一个线程在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中,该socket上又有新数据可读(EPOLLIN再次被触发),此时另外一个线程被唤醒来读取这些新的数据.于是出现了两个线程同时操作一个socket的局面.我们期望的是一个socket连接在任何一个时刻只能被一个线程处理.
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多只触发其上注册的一个可读,可写,异常事件,且只触发一次.除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件.
EAGAIN一流
EAGAIN、EWOULDBLOCK、EINTR用于非阻塞 长连接
EWOULDBLOCK用于非阻塞模式,不需要重新读或者写
EINTR指操作被中断唤醒,需要重新读/写
再来看看人家的边缘触发和水平触发
epoll默认是水平触发,但还能以边缘触发方式通知,通过
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev);
来设置。
举个例子来区分两种情况:
fd上有可读事件发生,调用非阻塞read(),一次没读完。socket缓冲区还有数据,水平触发第二次再次触发EPOLLIN事件,边缘触发第二次不触发。但数据还在,下次依旧能读到。
采用边缘触发的基本设计框架:
- 让所有待监视文件描述符成为非阻塞。
- 通过epoll_ctl()构建epoll感兴趣的事件。
- 通过epoll_wait()获得就绪事件,针对每一个就绪文件描述符进行IO处理直到相关系统调用返回EAGAIN或EWOULDBLOCK。
采用边缘触发是为了减少触发次数,但也要避免出现文件描述符饿死现象,比如现在处理就绪文件描述符上的IO事件,采用while(1) read(0读取,如果此描述符存在不间断的输入流的情况,那后序的文件描述符都要等待很长一段时间。
解决:将就绪文件描述符添加到应用程序维护的列表中,轮询的非阻塞处理,直到EWOULDBLOCK将描述符移除。