Linux网络编程—多路复用之epoll
epoll 是多路复用select和poll的加强版,epoll到底强在了哪些地方,我们接下来就会谈到。
我们先简单说一下select和poll的不足之处
- select的缺点:
1、单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差。
2、 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销。
3、select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件。
4、select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。 - poll和select的用法非常相似,但是poll是使用链表保存文件描述符,因此没有了监视文件数量地限制,select的前三个缺点依然存在,在高并发下性能还是不够强大。
这个时候linux就进行了改进,推出了epoll模型,现在我们来说说epoll强在了哪里?
设想一下如下场景:有500万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?
在select/poll时代,服务器进程每次都把这500万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。
而epoll的是通过在Linux内核申请一个简易的文件系统,把原来的select/poll调用分为了三个部分:
1、int epfd = epoll_create(int size); 创建一个epoll句柄,size参数可以忽略。
2、int epoll_ctl(int epfd, int op, int fd, struct epoll_evnet *envent);
参数epfd就是第一个函数所创建的句柄;
参数op有以下几种:
(1)EPOLL_CTL_ADD 将fd加入到epfd句柄中进行监测
(2)EPOLL_CTL_DEL 将fd从epfd句柄中删除
(3)EPOLL_CTL_MOD 修改已经注册fd的event事件
参数fd就是要监听的fd;
参数event是一个结构体:
struct epoll_event
{
_uint32_t events; //监听fd所发生的事件类型
epoll_data_t data; //是一个联合体
};
epoll_data_t 又包含以下内容:
typedef union epoll_data
{
void *ptr;
int fd;
_uint32_t U32;
_uint64_t U64;
}
(5)events事件集合:
EPOLLIN 表示对应的文件描述符可读;
EPOLLOUT 表示对应的文件描述符可写;
EPOLLPRI:表示对应的文件描述符有紧急事件可读;
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:将EPOLL设为边缘触发模式;
EPOLLLT:将EPOLL设为水平触发模式;
EPOLLONESHOT:只监听一次事件;
3、int epoll_wait(int epfd, struct epoll_event *events, int max; int timeout);
参数epfd:
epfd:创建的epoll句柄;
参数events:
events:已分配好的epoll_events结构体,epoll会将发生的事件放到events中;
参数max:
max:events的大小(一般是一个结构体数组);
参数timeout:
timeout:想要监听的时间 (-1:没有timeout,一直阻塞等待知道某个事件发生, 大于0,则表示阻塞的时间单位是微秒);
epoll模型的优点:
(1)使用内存映射(mmap)技术,避免用户到内存的拷贝;
(2)epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait时使得到通知;
(3)监视的文件描述符数量不受限制,它所支持的fd上限是最大可以打开文件的数目;
(4)I/O的效率不会随着fd数量的增加而下降,select、poll实现需要自己不断轮询所有fd集合,指到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能睡眠和唤醒多次交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只需要遍历就绪链表是否为空就行了。这节省了大量的CPU时间。这就是回调机制的性能提升。
epoll的两种工作模式:
1、epoll工作在ET(边缘触发)模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读、阻塞写操作把处理多个文件描述符的任务饿死。
2、epoll工作在LT模式的时候,在收到多个数据的时候仍然会产生多个事件,支持阻塞和非阻塞接口,这样,内核告诉你一个文件描述符是否就绪了,然后你可以进行I/O,若你不做任何操作,内核还是会继续通知你的,错误率小。
ET与LT的区别在于,当一个新事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新事件到来时,ET模式下无法再次获取数据。但LT正好相反,只要一个事件对应的套接字缓冲区还有数据,就能够获取。
ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高
注意
每个使用ET模式的文件描述符都应该时非阻塞的。如果文件描述符是阻塞的,那么读或写操作将会因为没有后续的事件而一直处于阻塞状态(饥渴状态)。