Linux 的select、poll、epoll

    select、poll、epoll都是用来监听文件描述符。都是I/O多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个文件描述符,一旦某个文件描述符就绪(读就绪或写就绪),能够通知程序进行相应的读写操作

    select、poll、epoll本质还是同步I/O(I/O多路复用本身就是同步I/O)的范畴,因为它们都需要在读写事件就绪后线程自己进行读写,读写的过程阻塞的。而异步I/O的实现是系统会负责把数据从内核空间拷贝到用户空间,无须线程自己再进行阻塞的读写,内核已经准备完成


文件描述符

    在Linux系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述服就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符,总的来说就是打开文件的一种资源描述

    文件描述符、文件、进程间的关系

        1.每个文件描述符会与一个打开的文件相对应

        2.不同的文件描述符也可能指向同一个文件

        3.相同的文件可以被不同的进程打开,也可以在同一个进程被多次打开


Select机制

    API简介

        linux系统中的 /usr/include/sys/select.h 文件中对 select方法的定义如下:

/* fd_set for select and pselect.  */
typedef struct
  { 
    /* XPG4.2 requires this member name.  Otherwise avoid the name
       from the global namespace.  */
    #ifdef __USE_XOPEN
        __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
    # define __FDS_BITS(set) ((set)->fds_bits)
    #else
        __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
    # define __FDS_BITS(set) ((set)->__fds_bits)
    #endif
  } fd_set;

/* Check the first NFDS descriptors each in READFDS (if not NULL) for read
   readiness, in WRITEFDS (if not NULL) for write readiness, and in EXCEPTFDS
   (if not NULL) for exceptional conditions.  If TIMEOUT is not NULL, time out
   after waiting the interval specified therein.  Returns the number of ready
   descriptors, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int select (int __nfds, fd_set *__restrict __readfds,
                   fd_set *__restrict __writefds,
                   fd_set *__restrict __exceptfds,
                   struct timeval *__restrict __timeout);

int __nfds是 fd_set 中最大的文件描述符 + 1,当调用 select 时,内核态会判断 fd_set 中描述符是否就绪,__nfds会告诉内核最多判断到哪一个描述符

__readfds、__writefds、__exceptfds都是结构体 fd_set,fd_set可以看做是一个描述符的集合。select函数中存在三个fd_set集合,分别代表三种事件,readfds表示读描述符集合,writefds表示写描述符集合,exceptfds表示异常描述符集合,当对应的fd_set = NULL 时,表示不监听该类描述符

timeval __timeout用来指定select的工作方式,即当文件描述符尚未就绪时,select是永远等待下去,还是等待一定时间,或者是直接返回

函数返回值int表示:就绪描述符的数量,如果为-1表示产生错误

    运行机制

        当用户调用process调用select的时候,select会将需要监控的readfds集合拷贝到内核空间(假设监控的仅仅是socket可读),然后遍历自己监控的socket sk,挨个调用sk的poll逻辑以便检查该sk是否有可读事件,遍历完所有的sk后,如果没有任何一个sk可读,那么select会调用schedule_timeout进行schedule循环,使得process进入睡眠。如果在timeout时间内某个sk上有数据可读了,或者等待timeout了,则调用select的process会被唤醒,接下来select就是遍历监控的sk集合,挨个收集可读事件并返回被用户了(会从readfds集合中进行删除)

    Select缺陷机制

        1.select支持的文件描述符数量有限,默认是1024

        2.每次调用select,都需要把fd集合从用户态拷贝到内核态,fd越多开销越大

        3.被监控的fds集合中,只要有一个数据可读,整个socket集合都会被遍历一次调用sk的poll函数收集可读事件


Poll机制

    API简介

        linux系统中/usr/include/sys/poll.h文件中对poll方法的定义如下:

/* Data structure describing a polling request.  */
struct pollfd
  {
    int fd;                     /* File descriptor to poll.  */
    short int events;           /* Types of events poller cares about.  */
    short int revents;          /* Types of events that actually occurred.  */
  };

/* Poll the file descriptors described by the NFDS structures starting at
   FDS.  If TIMEOUT is nonzero and not -1, allow TIMEOUT milliseconds for
   an event to occur; if TIMEOUT is -1, block until an event occurs.
   Returns the number of file descriptors with events, zero if timed out,
   or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);

__fds参数时Poll机制中定义的结构体 pollfd, 用来指定一个需要监听的描述符。结构体中fd为需要监听的文件描述符,events为需要监听的事件类型,而revents为经过poll调用之后返回的事件类型,在调用poll的时候,一般会传入一个pollfd的结构体数组,数组的元素个数表示监控的描述符个数

__nfds__timeout参数都和Select机制中的同名参数含义类似

    运行机制

        poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用 pollfd 结构代替select的fd_set结构,其他的本质上都差不多。所以Poll机制突破了Select机制中的文件描述符数量最大为1024的限制(指针传进去之后内核的确是把数组做成链表了,这样就能解决1024的限制)

    Poll的缺陷

        Poll虽然解决了Select机制的最大文件描述符限制1024,但是其它两个缺点还是没有得到解决

        1.每次调用poll,都需要把fd集合从用户态拷贝到内核态,fd越多开销越大

        2.每次调用poll,都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大


Epoll机制

    epoll是基于事件驱动的I/O方式。相对于select来说,epoll没有描述符个数限制;使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件放到内核的一个事件表中,通过直接物理内存映射,使其在用户空间也可直接访问,省去了拷贝带来的资源消耗

    API简介

        linux系统中 /usr/include/sys/epoll.h 文件有如下方法:

/* Creates an epoll instance.  Returns an fd for the new instance.
   The "size" parameter is a hint specifying the number of file
   descriptors to be associated with the new instance.  The fd
   returned by epoll_create() should be closed with close().  */
extern int epoll_create (int __size) __THROW;

/* Manipulate an epoll instance "epfd". Returns 0 in case of success,
   -1 in case of error ( the "errno" variable will contain the
   specific error code ) The "op" parameter is one of the EPOLL_CTL_*
   constants defined above. The "fd" parameter is the target of the
   operation. The "event" parameter describes which events the caller
   is interested in and any associated user data.  */
extern int epoll_ctl (int __epfd, int __op, int __fd,
                      struct epoll_event *__event) __THROW;

/* Wait for events on an epoll instance "epfd". Returns the number of
   triggered events returned in "events" buffer. Or -1 in case of
   error with the "errno" variable set to the specific error code. The
   "events" parameter is a buffer that will contain triggered
   events. The "maxevents" is the maximum number of events to be
   returned ( usually size of "events" ). The "timeout" parameter
   specifies the maximum wait time in milliseconds (-1 == infinite).

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int epoll_wait (int __epfd, struct epoll_event *__events,
                       int __maxevents, int __timeout);

epoll_create函数:创建一个epoll实例并返回,该实例可以监控__size个文件描述符

epoll_ctl函数:向epoll中注册事件,该函数如果调用成功返回0,否则返回-1

  • __epfd为epoll_create返回的epoll实例
  • __op表示要进行的操作
  • __fd为要进行监控的文件描述符
  • __event要监控的事件

epoll_wait函数:类似与select机制中的select函数,poll机制中的poll函数,等待内核返回监控描述符的事件产生。该函数返回已经就绪的事件的数量,如果为-1表示出错

  • __epfd为epoll_create返回的epoll实例
  • __events数组为 epoll_wait要返回的已经产生的事件集合
  • __maxevents为希望返回的最大的事件数量(通常为__events的大小)
  • __timeout和select、poll机制中的同名参数含义相同

    fd集合拷贝问题的解决

对于IO多路复用,有两件事是必须要做的(对于监控可读事件而言):

    1.准备好要监控的fds集合

    2.探测并返回fds集合中哪些fd可读

select或poll的函数原型,每次调用select或poll都在重复地准备(集中处理)整个需要监控的fds集合。对于然而对于频繁调用的select或poll而言,fds集合的变化频率要低得多,我们没必要每次都重新准备(集中处理)整个fds集合

于是,epoll引入了epoll_ctl系统调用,将高频调用的epoll_wait和低频的epoll_ctl隔离开。同时,epoll_ctl通过(EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL)三个操作来分散对需要监控的fds集合修改,做到了有变化才变更,将select或poll高频、大块内存拷贝(集中处理)变成epoll_ctl的低频、小块内存的拷贝(分散处理),避免了大量的内存拷贝。同时,对于高频epoll_wait的可读就绪的fd集合返回的拷贝问题,epoll通过内核与用户空间mmap(内存映射)同一块内存解决。mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址,使得这块物理内存对用户和内核都可见,减少用户态和内核态之间的数据交换。

另外,epoll通过epoll_ctl来对监控的fds集合来进行增,删,改。那么就必须涉及到fd的快速查找问题。于是,一个低时间复杂度的增,删,改,查的数据结构来组织被监控的fds集合是必不可少的。在linux 2.6.8之前的内核,epoll使用hash来组织fds集合,于是在创建epoll fd的时候,epoll需要初始化hash的大小。于是epoll_create(int __size)有一个参数size,以便内核根据size的大小来分配hash的大小。在linux 2.6.8以后的内核中,epoll使用红黑树来组织监控的fds集合,于是epoll_create(int size)的参数size实际上已经没有意义了

    按需遍历就绪的fds集合

epoll的解决方案不像select或poll一样每次都把当前线程轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把当前线程挂一遍(这一遍必不可少),并为每个fd指定一个回调函数。当设备就绪时,唤醒等待队列上的等待者,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表epoll轮询的是就绪链表是否为空,那么我们调用epoll_wait时,epoll_wait只需要检查链表中存在就绪的fd即可,效率非常可观

    工作模式

相较于Select和Poll,Epoll内部还分为两种工作模式:LT水平触发(level trigger) 和 ET边缘触发(edge trigger)

  • LT模式:默认的工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;事件会被放回到就绪链表中,下次调用epoll_wait时,会再次通知此事件。
  • ET模式: 当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应并通知该事件

由于上述两种工作模式的区别,LT模式同时支持block和no-block socket两种,而ET模式下仅支持no-block socket。即epoll工作在ET模式的时候,必须使用非阻塞套接字接口,以避免由于一个fd的阻塞I/O操作把多个处理其它文件描述符的任务饿死。ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高

    Epoll的优点

  • 使用内存映射技术,节省了用户态和内核态之间数据拷贝的资源消耗;
  • 通过每个fd定义的回调函数来实现的,只有就绪的fd才会执行回调函数。I/O的效率不会随着监视fd的数量的增长而下降;
  • 文件描述符数量不再受限;

总结

posted @ 2021-04-05 17:18  半分、  阅读(737)  评论(0编辑  收藏  举报