Linux IO 多路复用(select、poll、epoll)

  • 复用的意思是不用每个进程/线程只能操空一个IO,只需要一个进程/线程来操作多个IO,复用的是进程/线程。

  • 内核空间不能直接解引用用户态的指针。

  • IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:

    • 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用

    • 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。

    • 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。

    • 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。

  • 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

  • 与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

  • select

  • 该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下:
    在这里插入图片描述

  • 函数参数介绍如下:

  • 第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此把该参数命名为maxfdp1),描述字0、1、2…maxfdp1-1均将被测试。因为文件描述符是从0开始的。

  • 中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct
    fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:

    • void FD_ZERO(fd_set *fdset); //清空集合

    • void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中

    • void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除

    • int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写

  • timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。

  • 这个参数有三种可能:

    • 永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。

    • 等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。

    • 根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。

  • 原理图

在这里插入图片描述

  • select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

  • 套接字描述符就绪条件

  • select测试返回,某个套接字准备好可读,会发生什么

    • 第一种情况是套接字接收缓冲区有数据可以读,如果我们使用read函数去执行读操作,肯定不会被阻塞,而 是会直接读到这部分数据。

    • 第二种情况是对方发送了FIN,使用read函数执行读操作,不会被阻塞,直接返回0。

    • 第三种情况是针对一个监听套接字而言的,有已经完成的连接建立,此时使用accept函数去执行不会阻塞, 直接返回已经完成的连接。

    • 第四种情况是套接字有错误待处理,使用read函数去执行读操作,不阻塞,且返回-1。

  • 内核通知我们套接字有数据可以读了,使用read函数不会阻塞。

  • select检测套接字可写,完全是基于套接字本身的特性来说的,具体来说有 以下几种情况。

    • 第一种是套接字发送缓冲区足够大,如果我们使用非阻塞套接字进行write操作,将不会被阻塞,直接返回。

    • 第二种是连接的写半边已经关闭,如果继续进行写操作将会产生SIGPIPE信号。

    • 第三种是套接字上有错误待处理,使用write函数去执行读操作,不阻塞,且返回-1。

  • 内核通知我们套接字可以往里写了,使用write函数就不会阻塞。

  • select函数提供了最基本的I/O多路复用方法,在使用select时,我们需要建立两个重要的认识:

    • 描述符基数是当前最大描述符+1;
  • 每次select调用完成之后,记得要重置待测试集合。

  • poll

  • poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

  • poll是linux的事件轮询机制函数,每个进程可以管理一个pollfd队列,由poll函数进行事件注册和查询。

  • poll函数

  • # include <poll.h> int poll ( struct pollfd * fds, unsigned int nfds, int timeout);

    • fd是文件描述符,用来指示linux给当前pollfd分配的文件。编程时需要给events注册我们想要的事件,之后使用poll函数对pollfd队列进行轮询,轮询结束后,revents由内核设置为实际发生的事件。如果fd是负数,那么会忽略events,而且events会置为0
  • pollfd结构体定义如下:
    在这里插入图片描述

  • 每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。

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

    • POLLIN         有数据可读。

    • POLLRDNORM      有普通数据可读。

    • POLLRDBAND      有优先数据可读。

    • POLLPRI         有紧迫数据可读。

    • POLLOUT       写数据不会导致阻塞。

    • POLLWRNORM      写普通数据不会导致阻塞。

    • POLLWRBAND      写优先数据不会导致阻塞。

    • POLLMSGSIGPOLL     消息可用。

  • 此外,revents域中还可能返回下列事件:

    • POLLER   指定的文件描述符发生错误。

    • POLLHUP   指定的文件描述符挂起事件。

    • POLLNVAL  指定的文件描述符非法。

  • 这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。

  • 使用poll()和select()不一样,你不需要显式地请求异常情况报告。

  • POLLIN | POLLPRI等价于select()的读事件,POLLOUT |POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM |POLLRDBAND,而POLLOUT则等价于POLLWRNORM。例如,要同时监视一个文件描述符是否可读和可写,我们可以设置events为POLLIN |POLLOUT。

  • 在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。

  • timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;

  • timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。

  • nfds:用来指定第一个参数数组元素个数

  • 成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:

    • EBADF   一个或多个结构体中指定的文件描述符无效。

    • EFAULTfds   指针指向的地址超出进程的地址空间。

    • EINTR     请求的事件之前产生一个信号,调用可以重新发起。

    • EINVALnfds  参数超出PLIMIT_NOFILE值。

  • ENOMEM   可用内存不足,无法完成请求。

  • epoll

  • epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

  • epoll接口

    在这里插入图片描述

  • int epoll_create(int size);

  • 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

  • epoll的事件注册函数,在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:

    • EPOLL_CTL_ADD:注册新的fd到epfd中;

    • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

    • EPOLL_CTL_DEL:从epfd中删除一个fd;

  • 第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
    在这里插入图片描述

  • 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对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

    • LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

    • ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

  • LT模式

  • LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。

  • ET模式

  • ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。

  • 它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)

  • 如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)

  • ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

  • 总结

  • 假如有这样一个例子:

    • 1、我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符

    • 2、这个时候从管道的另一端被写入了2KB的数据

    • 3、调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作

    • 4、然后我们读取了1KB的数据

    • 5、调用epoll_wait(2)…

.

  • LT模式:

    • 如果是LT模式,那么在第5步调用epoll_wait(2)之后,仍然能受到通知。
  • ET模式:

    • 我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。

    • 只有在监视的文件句柄上发生了某个事件的时候 ET工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。

    • 当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取。

  • epoll总结

  • 在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。)

  • 优点:

  • 监视的描述符数量不受限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048

  • 在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大

  • select的最大缺点就是进程打开的fd是有数量限制的。这对 于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案(Apache就是这样实现的),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。

  • IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。

  • 如果没有大量的idle-connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle- connection,就会发现epoll的效率大大高于select/poll。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值