epoll: epoll的优势在于它不会随着监听fd数目的增长而降低效率。
select: 在内核中select采用轮询方式来处理,轮询的fd数目越多,耗时就越多。
在linux/posix_types.h头文件有这样的声明:
#define __FD_SETSIZE 1024
表示 select 最多同时监听1024个fd,可以通过修改头文件在重新编译内核来扩大这个数目,但这似乎并不治本。
一 IO 多路复用的 select
IO 多路复用相对于阻塞式和非阻塞式的优势在于它可以监听多个socket,并且不会消耗过多资源。当用户进程调用select时,它会监听其中所有socket知道有一个或多个socket数据已经准备好,否则就一直处于阻塞式状态。select的劣势在于单个进程能够监视的文件描述符的数量存在最大限制,select() 所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络相应时间的延迟使得大量TCP链接处于非常活跃状态,但调用select()会对所有的select进行一次线性扫描,所以这也浪费了一定的开销。优势在于它具有跨平台特性。
二 Epoll
epoll 的ET 是必须对非阻塞的socket 才能工作,LT对阻塞与非阻塞的socket 都可以。
所有I/O 多路复用操作都是同步的,涵盖select/poll。
阻塞/非阻塞是相对同步I/O来说的,与异步I/O无关。
select/poll/epoll 本身是同步的,可以阻塞与可以不阻塞。(其中,阻塞与非阻塞 与 同步不同步不同;阻塞与否是自身,异步与否是与外部协作的关系)
skater:无论是阻塞I/O、非阻塞I/O,还是基于非阻塞I/O的多路复用都是同步调用。因为他们在read调用时,内核将数据从内核空间拷贝至应用程序空间(epoll应该是从mmap),过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read调用就会在这个同步过程中等待比较长的时间。
epoll事件:
EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT: 表示对应的文件描述符可以写;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
epoll高效的核心是:1 用户态和内核态共享内存mmap。2 数据到来采用事件通知机制(不需要轮询)。
epoll 的api:
epoll - I/O event notification facility
#include
int epoll_create(int size);
int epoll_create1(int flags);
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);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
①
int epoll_create(int size);
int epoll_create1(int flags);
epoll_create 返回一个句柄,之后epoll的使用都将依靠这个句柄来标识。参数size来说明epoll最大处理的事件个数。当不再使用epoll时,需要调用close函数来关闭这个句柄。
(注意:size参数只是告诉内核这个epoll对象会处理的事件大概个数,而不是能够处理的事件的最大个数;在Linux最新的一些内核版本中,size参数没有任何意义)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl 向 epoll 对象中添加、修改或者删除感兴趣的事件,返回0表示成功,否则返回–1,此时需要根据errno错误码判断错误类型。epoll_wait方法返回的事件必然是通过 epoll_ctl添加到 epoll中的。
参数:
epfd: epoll_create返回的句柄,
op:的意义见下表:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
fd:需要监听的socket句柄fd,
event:告诉内核需要监听什么事的结构体,struct epoll_event结构如下:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
__uint32_t events就要监听的事件(感兴趣的事件):
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
收集在 epoll监控的事件中已经发生的事件,如果 epoll中没有任何一个事件发生,则最多等待timeout毫秒后返回。epoll_wait的返回值表示当前发生的事件个数,如果返回0,则表示本次调用中没有事件发生,如果返回–1,则表示出现错误,需要检查 errno错误码判断错误类型。
epfd:epoll的描述符。
events:分配好的 epoll_event结构体数组,epoll将会把发生的事件复制到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)。
maxevents:表示本次可以返回的最大事件数目,通常 maxevents参数与预分配的events数组的大小是相等的。
timeout:表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout为0,则表示 epoll_wait在 rdllist链表中为空,立刻返回,不会等待。
epoll有两种工作模式:LT(水平触发)模式和ET(边缘触发)模式。
默认情况下,epoll采用 LT模式工作,这时可以处理阻塞和非阻塞套接字,而上表中的 EPOLLET表示可以将一个事件改为 ET模式。ET模式的效率要比 LT模式高,它只支持非阻塞套接字。
(水平触发LT:当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上次没读写完的文件描述符上继续读写
边缘触发ET:当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你
此可见,水平触发时如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率,而边缘触发,则不会充斥大量你不关心的就绪文件描述符,从而性能差异,高下立见。)