Linux-epoll

在Linux网络编程中,Linux内核2.6版本之前大多都是用 select() 作为非阻塞的事件触发模型,但是效率低,使用受限已经很明显的暴露了select()(包括poll)的缺陷,为了解决这些缺陷,epoll作为linux新的事件触发模型被创造出来。

一、epoll() 相对于 select() 的优点:

1、支持一个进程socket描述符(FD)的最大数目:

在select() 中,一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置(linux/posix_types.h 中定义:#define __FD_SETSIZE 1024)。表示 select() 最多同时监听1024个fd。对于那些需要支持上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样 会带来网络效率的下降;二是可以选择多进程的解决方案(传统的Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加 上进程间数据同步远比不上线程间同步高效,所以这也不是一种完美的方案。而 epoll() 支持的数目很大,等于系统最大打开的文件描述符数,这个文件描述符数跟内存有一定关系。

2、IO效率不随FD数目增加而线性下降:

select() 对事件的扫描是针对于所有创建的socket描述符进行的,也就是说,有多少个socket描述符,就需要遍历多少个句柄,所以IO效率是随描述符增加线性下降的;而epoll只遍历活跃的 socket 描述符,这是因为在内核实现中 epoll() 是根据每个 fd 上面的callback函数实现的。那么,只有“活跃”的socket才会主动的去调用 callback 函数,其他 idle 状态 socket 则不会。比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

3、使用 mmap 加速内核与用户空间的消息传递

select() 事件触发后会将信息从内核拷贝到用户空间,这种拷贝就影响了效率。而 mmap 将内核与用户空间的内存映射到一块内存上,内核将消息捕获后放入该内存空间,用户无需拷贝直接可以访问,减少了拷贝次数,提高了效率。(mmap 将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。)

二、epoll() 工作模型:ET、LT

LT(level triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。

ET (edge-triggered) 是高速工作方式,只支持no-block socket。 在这种模式下,当描述符从未就绪变为就绪时,内核就通过epoll告诉你,然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的 就绪通知,直到你做了某些操作而导致那个文件描述符不再是就绪状态(比如 你在发送,接收或是接受请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核就不会发送更多的通知(only once)。不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。效率非常高,在并发,大流量的情况下,会比LT少很多 epoll() 的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。

三、epoll() 的使用

1、epoll() 用到的所有函数都是在头文件 sys/epoll.h 中声明的

2、epoll 的三大函数:

  • int epoll_create(int size);
    创建一个 epoll 的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
  •  int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    epoll 的事件注册函数,它不同与 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
    参数1 是 epoll_create() 的返回值;
    参数2 表示动作,用三个宏来表示:
      EPOLL_CTL_ADD   注册新的 fd 到 epfd 中
      EPOLL_CTL_MOD  修改已经注册的 fd 的监听事件
      EPOLL_CTL_DEL    从 epfd 中删除一个 fd 
    参数3 是需要监听的 fd ;
    参数4 是告诉内核需要监听什么事,struct epoll_event结构如下:
    struct epoll_event {
    
        __uint32_t events;  /* Epoll events */
    
        epoll_data_t data;  /* User data variable */
    
    };
    typedef union epoll_data {
    
        void *ptr;
    
        int fd;
    
        __uint32_t u32;
    
        __uint64_t u64;
    
    } epoll_data_t;
    结构体 epoll_event 被用于注册所感兴趣的事件和回传所发生待处理的事件,而epoll_data 联合体用来保存触发事件的某个文件描述符相关的数据。例如一个client连接到服务器,服务器通过调用 accept 函数可以得到于这个client对应的socket文件描述符,可以把这文件描述符赋给epoll_data的fd字段,以便后面的读写操作在这个文件描述符上进行。epoll_event 结构体的events字段是表示感兴趣的事件和被触发的事件,可以是以下几个宏的集合:
      EPOLLIN: 表示对应的文件描述符可以读;
      EPOLLOUT:表示对应的文件描述符可以写;
      EPOLLPRI:表示对应的文件描述符有紧急的数据可读;
      EPOLLERR:表示对应的文件描述符发生错误;
      EPOLLHUP:表示对应的文件描述符被挂断;
      EPOLLET: 表示对应的文件描述符有事件发生;
      EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。 
    epoll_ctl() 如果调用成功则返回0,不成功则返回-1。
  • int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    等待事件的产生,类似于 select() 调用。参数 event
    s 用来从内核得到事件的集合,maxevents 表示每次能处理的事件数,这个 maxevents 的值不能大于创建 epoll_create() 时的size,参数 timeout 是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。最后 epoll_wait 返回发生事件数

你可能感兴趣的:(linux)