IO多路复用机制(转)

1.简介

希望通过这篇文章,可以回答以下几个问题?

  1. 为什么需要IO多路复用?
  2. 什么是IO多路复用机制?
  3. IO多路复用的机制该怎么使用?
  4. epoll比select/poll相比,优势在哪里?

在了解I/O多路复用之前,先来了解流的概念。

1.1流的概念

一个流可以文件、socket、pipe等可以进行IO操作的内核对象。不管是文件,还是套接字,还是管道,我们都可以把他们看作流。

从流中读取数据或者写入数据到流中,可能存在这样的情况:读取数据时,流中还没有数据;写入数据时,流中数据已经满了,没有空间写入了。典型的例子为客户端要从socket流中读入数据,但是服务器还没有把数据准备好。此时有两种处理办法:

  • 阻塞,等待数据准备好了,再读取出来返回;
  • 非阻塞,通过轮询的方式,查询是否有数据可以读取,直到把数据读取返回。

接下来再来了解以下I/O同步、异步、阻塞、非阻塞的概念。

1.2 I/O同步、异步、阻塞、非阻塞

在IO操作过程中,可能会涉及到同步(synchronous)、异步(asynchronous)、阻塞(blocking)、非阻塞(non-blocking)、IO多路复用(IO multiplexing)等概念。他们之间的区别是什么呢?

以网络IO为例,在IO操作过程会涉及到两个对象:

  • 一个是调用这个IO的process (or thread);
  • 另外一个是一个就是系统内核(kernel)。

在一个IO操作过程中,以read为例,会涉及到两个过程:

  1. 等待数据准备好(Waiting for the data to be ready);
  2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

这两个阶段是否发生阻塞,将产生不同的效果。

阻塞VS非阻塞

阻塞IO:

  • 在1、2阶段都发生阻塞;
  • 调用阻塞IO会一直block住进程,直到操作完成;

非阻塞IO:

  • 在第1阶段没有阻塞,在第2阶段发生阻塞;
  • 调用非阻塞IO会在kernel准备数据的情况下立即返回;
  • 非阻塞IO需要不断轮询,查看数据是否已经准备好了;

阻塞与非阻塞可以简单理解为调用一个IO操作能不能立即得到返回应答,如果不能立即获得返回,需要等待,那就阻塞了;否则就可以理解为非阻塞。

同步VS异步

同步IO:

同步IO操作将导致请求的进程一直被blocked,直到IO操作完成。从这个层次来,阻塞IO、非阻塞IO操作、IO多路复用都是同步IO。

异步IO:

异步IO操作不会导致请求的进程被blocked。当发出IO操作请求,直接返回,等待IO操作完成后,再通知调用进程。

多路复用IO

多路复用IO也是阻塞IO,只是阻塞的方法是select/poll/epoll。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理是select/epoll这个函数会不断轮询所负责的IO操作,当某个IO操作有数据到达时,就通知用户进程。然后由用户进程去操作IO。

1.3 多路复用概念

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

阻塞I/O有一个比较明显的缺点是在I/O阻塞模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,需要多个进程或者多个线程,但是这种方式效率不高。

非阻塞的I/O需要轮询查看流是否已经准备好了,比较典型的方式是忙轮询。

忙轮询 
忙轮询方式是通过不停的把所有的流从头到尾轮询一遍,查询是否有流已经准备就绪,然后又从头开始。如果所有流都没有准备就绪,那么只会白白浪费CPU时间。轮询过程可以参照如下:

while true {
    for i in stream[]; {
        if i has data
              read until unavailable
    }
}

无差别的轮询方式 
为了避免白白浪费CPU时间,我们采用另外一种轮询方式,无差别的轮询方式。即通过引进一个代理,这个代理为select/poll,这个代理可以同时观察多个流的I/O事件。当所有的流都没有准备就绪时,会把当前线程阻塞掉;当有一个或多个流的I/O事件就绪时,就从阻塞状态中醒来,然后轮询一遍所有的流,处理已经准备好的I/O事件。轮询的过程可以参照如下:

while true {
    select(streams[])
    for i in streams[] {
        if i has data
              read until unavailable
    }
}

如果I/O事件准备就绪,那么我们的程序就会阻塞在select处。我们通过select那里只是知道了有I/O事件准备好了,但不知道具体是哪几个流(可能有一个,也可能有多个),所以需要无差别的轮询所有的流,找出已经准备就绪的流。可以看到,使用select时,我们需要O(n)的时间复杂度来处理流,处理的流越多,消耗的时间也就越多。

最小轮询方式

无差别的轮询方式有一个缺点就是,随着监控的流越来越多,需要轮询的时间也会随之增加,效率也会随之降低。所以还有另外一种轮询方式,最小轮询方式,即通过epoll方式来观察多个流,epoll只会把发生了I/O事件的流通知我们,我们对这些流的操作都是有意义的,时间复杂度降低到O(k),其中k为产生I/O事件的流个数。轮询的过程如下:

while true {
    active_stream[] = epoll_wait(epollfd)
    for i in active_stream[] {
        read or write till unavailable
    }
}

select/poll/epoll都是采用I/O多路复用机制的,其中select/poll是采用无差别轮询方式,而epoll是采用最小的轮询方式。

1.4 I/O复用的优势

I/O多路复用的优势并不是对于单个连接能处理的更快,而是在于可以在单个线程/进程中处理更多的连接。

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

2. I/O复用函数介绍

下面简单的介绍一下I/O复用函数。

2.1 select函数

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

函数原型

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfd,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

参数说明:

  • maxfd:需要监视的最大的文件描述符值+1;
  • readset:需要检测的可读文件描述符的集合;
  • writeset:需要检测的可写文件描述符的集合
  • exceptset:需要检测的异常文件描述符的集合
  • timeout:超时时间;超时时间有三种情况: 
    • NULL:永远等待下去,仅在有一个描述字准备好I/O时才返回;
    • 0:立即返回,仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生;
    • 特定的时间值: 如果在指定的时间段里没有事件发生,select将超时返回;

函数返回值有三种情况:

  • 返回0表示超时了;
  • 返回-1,表示出错了;
  • 返回一个大于0的数,表示文件描述符状态改变的个数;

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.2 Poll函数

Poll的处理机制与Select类似,只是Poll选择了pollfd结构体来处理文件描述符的相关操作:

struct pollfd {
    int fd;         /* 文件描述符 */
    short events;   /* 等待的事件 */
    short revents;  /* 实际发生了的事件 */
} ;

每一个pollfd结构体都指定了一个文件描述符fd,events代表了需要监听该文件描述的事件掩码,可选的有:

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

revents代表文件描述符的操作结果掩码,内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回,除此之外,revents域还可以包含以下事件:

  • POLLER:指定的文件描述符发生错误。
  • POLLHUP:指定的文件描述符挂起事件。
  • POLLNVAL:指定的文件描述符非法。

poll的函数原型:

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

参数说明:

  • fds:需要被监视的文件描述符集合;
  • nfds:被监视的文件描述符数量;
  • timeout:超时时间,有三种取值: 
    • 负数:无限超时,一直等到一个指定事件发生;
    • 0:立即返回,并列出准备好的文件描述符;
    • 正数:等待指定的时间,单位为毫秒;

poll函数与select函数的最大不同之处在于:select函数有最大文件描述符的限制,一般1024个,而poll函数对文件描述符的数量没有限制。但select和poll函数都是通过轮询的方式来查询某个文件描述符状态是否发生了变化,并且需要将整个文件描述符集合在用户空间和内核空间之间来回拷贝,这样随着文件描述符的数量增加,相应的开销也随之增加。

2.3 epoll函数

epoll是在Linux内核2.6引进的,是select和poll函数的增强版。与select相比,epoll没有文件描述符数量的限制。epoll使用一个文件描述符管理多个文件描述符,将用户关心的文件描述符事件存放到内核的一个事件列表中,这样在用户空间和内核空间只需拷贝一次。

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用来告诉内核这个监听的数目一共有多大,占用一个fd值;

epoll_ctl函数:

  • epoll的事件注册函数;
  • 参数:
    • epfd:epoll_create()的返回值;
    • op:动作,有三种取值:
      • EPOLL_CTL_ADD:注册新的fd到epfd中;
      • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
      • EPOLL_CTL_DEL:从epfd中删除一个fd;
    • fd:需要监听的fd;
    • event: 告诉内核需要监听什么事件,取值有: 
      • EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
      • EPOLLOUT:表示对应的文件描述符可以写;
      • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
      • EPOLLERR:表示对应的文件描述符发生错误;
      • EPOLLHUP:表示对应的文件描述符被挂断;
      • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
      • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列;

epoll_wait函数:

  • 等待事件的产生;
  • 参数: 
    • events:从内核得到事件的集合;
    • maxevents:事件集合的大小;
    • timeout:超时时间,0会立即返回,-1表示永久阻塞,正数表示一个指定的值;

工作模式

epoll对文件描述符的操作由两种模式:水平触发LT(level trigger)和边沿触发ET(edge trigger)。默认的情况下为LT模式。LT模式与ET模式的区别在于:

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

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

2.4 epoll相比于select/poll的优势?

从上面对select/poll/epoll函数的介绍,可以知道epoll与select/poll相比,具有如下优势:

  1. 监视的描述符数量不受限制,所支持的FD上限是最大可以打开文件的数目;
  2. I/O效率不会随着监视fd的数量增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的,只有就绪的fd才会执行回调函数。

3.总结

IO复用机制可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立即通知相应程序进行读或写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的

原文地址:https://www.cnblogs.com/Roc-Atlantis/p/9457020.html

时间: 2024-11-08 05:31:14

IO多路复用机制(转)的相关文章

自动化运维Python系列之IO多路复用、SocketServer源码分析

IO多路复用 IO多路复用是指:通过一种机制,可以监视多个描述符,一旦某个系统描述符就绪(一般是读就绪或者写就绪)能够通知程序进行相应的读写操作 实例化例子就是在SocketServer模块中,客户端和服务端建立好连接,此时服务端通过监听conn这条链路,一旦客户端发送了数据,conn链路状态就发生变化,服务端就知道有数据要接收... Linux系统中同时存在select.pull.epoll三种IO多路复用机制 windows中只有select机制 1)select select本质上是通过设

Event Loop、函数式编程、IO多路复用、事件驱动、响应式、

IO多路复用.事件驱动.响应式概念类似或者一样 就是很多网络连接(多路),共(复)用少数几个(甚至是一个)线程. 连接很多的时候,不能每个连接一个线程,会耗尽系统内存的.线程也不能阻塞在任何一个连接上,等新的数据来,这样就不能及时响应其他连接发来的数据了:也不能用非阻塞方式,轮询所有的连接,这会浪费掉大量CPU时间:只能告诉系统,我对哪些连接感兴趣,有消息来的时候,通知我处理. IO多路复用: 一种在后端网络编程中的一种技术 IO多路复用机制详解    服务器,并发,"事件驱动"的本质

PHP实现系统编程(一) --- 网络Socket及IO多路复用【网摘】

一直以来,PHP很少用于socket编程,毕竟是一门脚本语言,效率会成为很大的瓶颈,但是不能说PHP就无法用于socket编程,也不能说PHP的socket编程性能就有多么的低,例如知名的一款PHP socket框架 workerman 就是用纯PHP开发,并且号称拥有优秀的性能,所以在某些环境下,PHP socket编程或许也可一展身手. PHP提供了一系列类似C语言socket库中的方法供我们调用: [php] view plain copy socket_accept - Accepts 

IO多路复用和local概念

一.local 在多个线程之间使用threading.local对象,可以实现多个线程之间的数据隔离 import time import random from threading import Thread,local loc = local() def func1(): global loc print(loc.name,loc.age) def func2(name,age): global loc loc.name = name loc.age = age time.sleep(ran

聊聊IO多路复用之select、poll、epoll详解

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程.IO多路复用适用如下场合: 当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用. 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现. 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用. 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用. 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用. 与多进程和多线程技术相

IO多路复用的几种实现机制的分析

http://blog.csdn.net/zhang_shuai_2011/article/details/7675797 select,poll,epoll都是IO多路复用的机制.所谓I/O多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作.但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步

linux设备驱动程序中的阻塞、IO多路复用与异步通知机制

一.阻塞与非阻塞 阻塞与非阻塞是设备访问的两种方式.在写阻塞与非阻塞的驱动程序时,经常用到等待队列. 阻塞调用是指调用结果返回之前,当前线程会被挂起,函数只有在得到结果之后才会返回. 非阻塞指不能立刻得到结果之前,该函数不会阻塞当前进程,而会立刻返回. 函数是否处于阻塞模式和驱动对应函数中的实现机制是直接相关的,但并不是一一对应的,例如我们在应用层设置为阻塞模式,如果驱动中没有实现阻塞,函数仍然没有阻塞功能. 二.等待队列 在linux设备驱动程序中,阻塞进程可以使用等待队列来实现. 在内核中,

JAVA IO编程 IO多路复用底层机制

前面IO模型中有提到IO多路复用,这里介绍下linux下的三种机制(基于多路复用模型的) select,poll,epoll 反应器模式Reactor和Proactor模式 两者的主要区别是就是真正的读取和写入操作是由谁来完成的 Reactor中需要应用程序自己读取或者写入数据 Proactor模式,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO的设备 Reactor(反应器模式) 说明你可以进行读写操作了,关注的是IO操作的就

Unix C语言编写基于IO多路复用的小型并发服务器

背景介绍 如果服务器要同时处理网络上的套接字连接请求和本地的标准输入命令请求,那么如果我们使用accept来接受连接请求,则无法处理标准输入请求;类似地,如果在read中等待一个输入请求,则无法处理网络连接的请求. 所谓I/O多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作.但 select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而还