Epoll 与 Select

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()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你

此可见,水平触发时如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率,而边缘触发,则不会充斥大量你不关心的就绪文件描述符,从而性能差异,高下立见。)

你可能感兴趣的:(linux)