现在的位置: 首页 > 综合 > 正文

浅谈I/O复用:select、poll、epoll

2019年05月17日 ⁄ 综合 ⁄ 共 5503字 ⁄ 字号 评论关闭

I/O复用使锝程序能同时监听多个文件描述符,这对提高程序的性能至关重要。通常网络程序在下列情况下需要使用I/O复用技术:

(1)客户端程序要同时处理多个socket

(2)客户端程序要同时处理用户输入和网络连接

(3)TCP服务器要同时处理监听socket和连接socket(这时I/O复用使用最多的场合)

(4)服务器要同时处理TCP请求和UDP请求

(5)服务器要同时监听多个端口,或者处理多种服务。

 需要指出的是,I/O复用虽然能同时监听多个文件描述符,但是它本身是阻塞的。并且当多个文件描述符就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这使得服务器程序看起来像是串行工作。如果要实时并发,只能使用多进程和多线程等手段。

       接下在在介绍select、poll、epoll之前,我感觉还是用必要区分一下socket的同步(Sync)、异步(Async)、阻塞(Block)、非阻塞(Unblock)的概念。因为自己当时在理解这些概念的时候,很容易就将其混淆了。

1、同步、异步、阻塞、非阻塞

同步:所谓同步,就是指发出一个功能调用时,在没有得到结果之前,该调用就不返回;也就是说必须一件一件的,等前一件事昨晚再做下一件事。一个比较通俗的例子就是:你早上起床后睁开眼睛,然后下床,穿衣、洗脸...需要一件一件的来。

异步:异步就是和同步的概念是相对应的。当一个异步过程调用发生后,调用者不能立刻得到结果,该调用就返回。实际处理这个调用的部件在完成后,通过状态、通知和回调函数来通知调用者。一个通俗的例子就是:早上吃饭时突然口渴想喝水,发现水正在烧,这时你就可以边吃饭变等水烧开,当水烧开就会“咚”的一声,通知你这时水开了,你可以喝水了。

阻塞:阻塞调用是指调用过程返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停执行)。函数只有在得到结果之后才会返回。

非阻塞:非阻塞为是相对与阻塞的,指在不能立刻得到返回结果之前,该函数不会阻塞当前的线程,而会立即返回。

(1)同步,就是我掉用一个功能,该功能没有结束之前,我死等结果。

(2)异步,就是我调用一个功能,不需要知道该功能的结果,该功能有结果后通知我。

(3)阻塞,就是调用我,我没有接收完数据或者没有得到结果之前,我不会返回。

(4)非阻塞,就是调用我,我立即返回,通过select通知调用者。

注意:在这里可能会把同步和阻塞等同起来,实际上两者是不同的。对于同步调用开说,含多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。而阻塞线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停执行。

2、select函数

#include <sys/select.h>
Int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

返回值:select成功时返回就绪文件描述符的总数,超时没有人任何文件描述符准备就绪,返回0.;出错返回-1

函数参数介绍:

(1)nfds参数指定被监听的文件描述符的总数。它通常被设置为监听的所有文件描述符加一,因为文件描述符是从0开始计数的。

(2)Readfds、writefds、exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符,应用程序调用select函数时,通过这三个参数传入自己感兴趣的文件描述符。Select返回时,内核将修改他们来通知应用程序那些文件描述符已经就绪。这三个参数都是fd_set结构体指针类型。Fd_set结构体仅包含一个整形数组,该数组的每一个元素的每一位标记一个文件描述符,fd_set所能容纳的文件描述符有FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。可以通过下面的宏来访问fd_set结构体中的位:

#include <sys/select.h>
FD_ZERO(fd_set *fdset)              //清除fdset的所有位
FD_SET(int fd,  fd_set *fd_set)        //设置fdset的位fd
FD_CLR(int fd,  fd_set *fd_set)         //清除fdset的位fd
FD_ISSET(int fd,  fd_set *fd_set)     //测试fdset的位fd是否被设置

(3)timeout用来设置select函数的超时时间。

Struct timeval
{
   Long tv_sec;         //秒数
   Long tv_usev;        //微妙数 
};

这个参数有三种可能:当把参数设置为NULL,则select将一直阻塞,直到某个文件描述符准备就绪;设置为一个固定的时间,在有一个描述符准备好返回,但是不超过指定的时间;设置为0时,select则立即返回。

2、poll函数

Poll系统调用的select类似,也是在指定时间内轮询一定数量的文件描述符,以测试其是否就绪。

#include <poll.h>
Int poll(struct pollfd *fds, nfds_t nfds, int timrout);

(1)fds参数是一个pollds类型的结构体数组,它指定在我们感兴趣的文件描述符上发生可读、可写和异常事件。Pollfd结构体的定义:

Struct pollfd
{
     Int fd;                                      //文件描述符
      Short events;                        //注册的事件
      Short revents;                      //实际发生的事件,有内核完成
} ;

每个结构体的events域是监控该文件描述符的事件掩码,由用户来设置这个域,revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。合法的事件如下:
  

   POLLIN         有数据可读。
  POLLRDNORM        有普通数据可读。
  POLLRDBAND       有优先数据可读。
  POLLPRI         有紧迫数据可读。
  POLLOUT            写数据不会导致阻塞。
  POLLWRNORM        写普通数据不会导致阻塞。
  POLLWRBAND        写优先数据不会导致阻塞。
  POLLMSGSIGPOLL     消息可用。
  POLLER               指定的文件描述符发生错误。
  POLLHUP             指定的文件描述符挂起事件。
  POLLNVAL               指定的文件描述符非法

(2)nfds参数指定被监听事件集合fds的大小

(3)Timeout参数指定poll的超时值。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll将立即返回。

3、epoll函数

Epoll是Linux特有的I/O复用函数。它在实际和使用上和select、poll有很大的差异。首先,epoll使用一组函数来完成任务,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无需想select和poll那样每次调用都要重复传入文件描述符集。但是epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。

(1)这个文件描述符使用epoll_create函数来创建:

#include <sys/epoll.h>
Int epoll_create(int size)

Size参数现在并不起作用,只是给内核一个标识,告诉它事件表有多大。该函数返回的文件描述符将用作其它所有epoll系统调用的第一个参数,以指定要访问的内核事件表。

(2)下面的函数用来操作epoll的内核事件表:

#include <sys/epoll.h>
Int epoll_ctl(int epdf, int op, int fd, struct _event *event)

Fd参数是要操作的文件描述符,op参数指定操作类型。操作类型如下:

*EPOLL_CTL_ADD,往事件表中注册fd上的数据 
*EPOLL_CTL_MOD,修改fd上的注册事件
*EPOLL_CTL_DEL,删除fd上的注册事件

Event告诉内核需要监听什么事,struct epoll_event结构如下:

Strcut epoll_event
{
   _uint32_t events;
   Epoll_data_t data;
};

events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

(3)epoll系列函数调用的只要接口是epoll_wait函数。它在一段超时时间内等待一组文件描述符上的事件

#include <sys/epoll.h>
Int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
该函数返回成功就绪文件描述符的个数,失败是返回-1并设置errno。

类似于select调用。参数events用来从内核得到事件的集合,maxevents的值不能大于创建epoll_create时的size,参数timeout是超时时间

5、LT和ET模式

LT模式(水平触发):当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告次事件。

ET模式(边沿触发):当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。Epoll工作在ET模式的时候,必须使用非阻塞套接字,以避免由于一个文件句柄阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。

6、三组I/O复用函数的比较

(1)事件集:

Select:Select的参数类型fd_set没有将文件描述符和事件进行绑定,它仅仅是一个文件描述符的集合,因此select的参数需要提供3个这种类型的参数来分别传入和输出可读、可写及异常事件。这一方面使得select不能处理更多类型的事件,另一方面由于内核对fd_set集合的在线修改,应用程序下次调用select前不得不重置3个fd_set集合。

Poll:poll的参数pollfd,它把文件描述符和事件都定义在其中,任何事件都被统一处理,从而使编程接口简单的多,并且内核每次修改的是pollfd结构体的revents成员,而events 成员保持不变,因此下次调用poll时应用程序无需重置pollfd类型的事件集合。

Epoll:epoll采用完全不同与select和poll的方式来管理用户注册的事件。它在内核中维护一张事件表,并提供一个独立的系统调用epoll_ctl来控制往其中添加、删除、修改事件。这样,每次epoll_wait调用都直接从内核事件表中取得用户注册的事件,而无需反复从用户空间读入这些事件。

(2)最大支持文件描述符数:

Select:select打开的文件描述符fd是由一定限制的,有 fd_size设置,默认为2048,对于那些需要支持上万连接数目的服务器来说显然太少了。

Poll和epoll:poll和epoll分别用nfds和maxevents参数指定最多监听多少个文件描述符和事件,这两个值都能达到系统允许打开的最大文件描述符数目,即65535。

(3)工作模式:

Select和poll都只能工作在相对低效的LT模式,而epoll则可以工作在ET高效模式。

(4)实现原理:

Select和poll采用的都是轮询的方式,即每次调用都要扫描整个注册文件描述符集合,并将其中就绪的文件描述符返回给用户,因此他们检测就绪事件的算法时间复杂度是O(n)。

Epoll的epoll_wait则不同,它采用的是回调函数的方式。内核检测到就绪的文件描述符时,将触发回调函数,回调函数就将该文件描述符上对应的时间插入内核就绪事件队列。内核最后在适当的时候将就绪事件队列中的内容拷贝到用户空间。因此epoll_wait无需轮询整个文件描述符集合来检测那些事件已经准备就绪,其算法事件复杂度O(1)。但是,当活动链接比较多时,epoll_wait的效率未必比select和poll高,因为回调函数被触发得过于频繁。所以epoll_wait适用于连接数量多,但是活动连接少的情况。

(5)使用mmap加速内核与用户空间的消息传递。

这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。

   

抱歉!评论已关闭.