I/O多路复用:select、poll、epoll

一. I/O多路复用

1. 流

  • 不管是文件,还是套接字,还是管道,我们都可以把他们看作流。
  • 从流中读取数据或者写入数据到流中,可能存在这样的情况:读取数据时,流中还没有数据;写入数据时,流中数据已经满了,没有空间写入了。典型的例子为客户端要从socket流中读入数据,但是服务器还没有把数据准备好。此时有两种处理办法:
    ①阻塞,等待数据准备好了,再读取出来返回
    ②非阻塞,通过轮询的方式,查询是否有数据可以读取,直到把数据读取返回

2. I/O同步、异步、阻塞、非阻塞

  • 以网络IO为例,在IO操作过程会涉及到两个对象:①调用这个IO的进程或线程 ②系统内核(kernel)。
  • 在一个IO操作过程中,以read为例,会涉及到两个过程:
    ①等待数据准备好(Waiting for the data to be ready)
    ②将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
  • 阻塞VS非阻塞
    ①阻塞IO:
    在上述1、2阶段都可能会发生阻塞。
    调用阻塞IO会一直block住进程,直到操作完成。
    ②非阻塞IO:
    在第1阶段没有阻塞,在第2阶段可能会发生阻塞。
    调用非阻塞IO会在kernel准备数据的情况下立即返回。
    非阻塞IO需要不断轮询,查看数据是否已经准备好了。
  • 阻塞IO: 资源不可用时,IO请求一直阻塞,直到反馈结果(有数据或超时)。
    非阻塞IO:资源不可用时,IO请求离开返回,返回数据标识资源不可用
  • 同步IO:同步IO操作将导致请求的进程一直被blocked,直到IO操作完成。从这个层次来看,阻塞IO、非阻塞IO操作、IO多路复用都是同步IO。
    异步IO:异步IO操作不会导致请求的进程被blocked。当发出IO操作请求,直接返回,等待IO操作完成后,再通知调用进程。
  • 多路复用IO:多路复用IO也是阻塞IO,只是阻塞的方法是select/poll/epoll。
  • I/O多路复用就是通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
  • 一个知乎通俗解释:IO 多路复用是什么意思? - levin的回答 - 知乎
  • IO多路复用适用如下场合:
    当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用。
    当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
    如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
    如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
    如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
  • 通俗例子:多路网络连接复用一个IO线程。一个进程可以同时对多个客户请求进行服务。
  • select、poll、epoll都是Linux下/IO多路复用的机制。但select、poll、epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

二. select / poll

  • 系统提供Select函数来实现多路复用输入/输出模型,Select系统调用是用来让我们的程序监视多个文件句柄的状态变化。程序会阻塞在select函数上,直到被监视的文件句柄中有一个或多个发生了状态变化。

1.简介

  • 原型:
    #include <sys/select.h>
    #include >sys/time.h>
    
    int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);
  • 返回值:若有就绪描述符返回其数目,若超时则为0,若出错则为-1。
  • 参数说明:
    maxfdpl:需要监视的最大的文件描述符值+1;
    readset:需要检测的可读文件描述符的集合;
    writeset:需要检测的可写文件描述符的集合
    exceptset:需要检测的异常文件描述符的集合
    timeout:超时时间;超时时间有三种情况: ①NULL:永远等待下去,仅在有一个描述字准备好I/O时才返回;②0:立即返回,仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生;③特定的时间值: 如果在指定的时间段里没有事件发生,select将超时返回
  • fd_set类型:
    fd_set是一个文件描述符集合,可以通过以下宏来操作:
    FD_CLR(inr fd,fd_set* set):用来清除文件描述符集合set中相关fd的位
    FD_ISSET(int fd,fd_set *set):用来测试文件描述符集合set中相关fd的位是否为真
    FD_SET(int fd,fd_set*set):用来设置文件描述符集合set中相关fd的位
    FD_ZERO(fd_set *set):用来清除文件描述符集合set的全部位

2. 优缺点

  • select优点:
    ①select()的可移植性更好,在某些Unix系统上不支持poll()
    ②select() 对于超时值提供了更好的精度:微秒,而poll是毫秒。
  • select缺点:
    ①单个进程可监视的fd数量被限制。select支持的文件描述符数量太小了,默认是1024。
    ②需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
    ③对fd进行扫描时是线性扫描。fd剧增后,IO效率较低,因为每次调用都对fd进行线性扫描遍历,所以随着fd的增加会造成遍历速度慢的性能问题
    ④select() 函数的超时参数在返回时也是未定义的,考虑到可移植性,每次在超时之后在下一次进入到select之前都需要重新设置超时参数。

3. poll

  • poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。 
  • poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。 
  • 原型:
    #include <poll.h>
    int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
  • 结构体pollfd:
    每一个pollfd结构体都指定了一个文件描述符fd。
    revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。 
    events代表了需要监听该文件描述的事件掩码,可选的有:

    POLLIN:有数据可读。
    POLLRDNORM:有普通数据可读。
    POLLRDBAND:有优先数据可读。
    POLLPRI:有紧迫数据可读。
    POLLOUT:写数据不会导致阻塞。
    POLLWRNORM:写普通数据不会导致阻塞。
    POLLWRBAND:写优先数据不会导致阻塞。
    POLLMSGSIGPOLL:消息可用。

    revents域中还可能返回下列事件: 
    POLLER     指定的文件描述符发生错误。
    POLLHUP   指定的文件描述符挂起事件。
    POLLNVAL  指定的文件描述符非法。

struct pollfd {
    int fd;         /* 文件描述符 */
    short events;   /* 等待的事件 */
    short revents;  /* 实际发生了的事件 */
} ; 
  • poll优点:
    ①poll() 不要求开发者计算最大文件描述符加一的大小。
    ②poll() 在应付大数目的文件描述符的时候相比于select速度更快
    ③它没有最大连接数的限制,原因是它是基于链表来存储的。
  • poll缺点:
    ①大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。 
    ②与select一样,poll返回后,需要轮询pollfd来获取就绪的描述符

三. epoll

  • Linux 2.5.44版本后,poll被epoll取代。(2.5是开发版本,2.6时候正式)
  • epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
  • 相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。相对于select和poll来说,epoll更加灵活,没有描述符限制。 

1. 使用简介

  • epoll操作过程需要三个接口,分别如下:
#include <sys/epoll.h>

int epoll_create(int size);

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);

  • 三个接口简介:
    ①首先要调用epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的最大句柄数,多于这个最大数时内核可不保证效果。
    epoll_ctl可以操作上面建立的epoll,例如,将刚建立的socket加入到epoll中让其监控,或者把 epoll正在监控的某个socket句柄移出epoll,不再监控它等等。
    epoll_wait在调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。
  • 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);
    ①epfd:
    是epoll_create()的返回值
    ②op:表示动作,用三个宏来表示:
    EPOLL_CTL_ADD:注册新的fd到epfd中;
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL:从epfd中删除一个fd;
    ③fd:是需要监听的fd。
    ④event:告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};


//其中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);
    该函数等待事件的产生,类似于select()调用。 

    ①events:用来从内核得到事件的集合
    ②maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size。 
    ②timeout:是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。 
     
  • epoll_wait范围之后应该是一个循环,遍历所有的事件。 
    我们调用epoll_ wait时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的句柄列表。 
    所以,实际上在你调用epoll_ create后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的socket句柄。 
    在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。

2. 其他

  • 这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。 
  • epoll的高效就在于,当我们调用epoll_ ctl往里塞入百万个句柄时,epoll_ wait仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。 
  • epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
    ①LT模式:
    当epoll_ wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
    ②ET模式:
    当epoll_ wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
  • epoll优点:
    ①支持一个进程打开大数目的socket描述符(FD)
    ②IO效率不随FD数目增加而线性下降
    ③使用mmap加速内核与用户空间的消息传递。
     

参考文章:

IO多路复用

select,poll,epoll优缺点及比较

猜你喜欢

转载自blog.csdn.net/weixin_39731083/article/details/82053017