zoukankan      html  css  js  c++  java
  • IO多路复用

    一、基本概念

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

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

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

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

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

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

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

    二、常用的IO多路复用

    1.select

    (1)基本用法

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


    #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
    函数参数介绍如下:

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

    (2)中间的三个参数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);   // 检查集合中指定的文件描述符是否可以读写 

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

             struct timeval{

                       long tv_sec;   //seconds

                       long tv_usec;  //microseconds

           };

    这个参数有三种可能:

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

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

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

    (2)实现原理

    (1)使用copy_from_user从用户空间拷贝fd_set到内核空间

    (2)注册回调函数__pollwait

    (3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)

    (4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

    (5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

    (6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

    (7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。

    (8)把fd_set从内核空间拷贝到用户空间。

    总结:

    select的几大缺点:

    (1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

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

    (3)select支持的文件描述符数量太小了,默认是1024

    2.poll

    (1)基本用法

    poll()函数:这个函数是某些Unix系统提供的用于执行与select()函数同等功能的函数


    #include <poll.h>
    int poll(struct pollfd fds[], nfds_t nfds, int timeout);
    参数说明:
    fds:是一个struct pollfd结构类型的数组,用于存放需要检测其状态的Socket描述符;每当调用这个函数之后,系统不会清空这个数组,操作起来比较方便;特别是对于socket连接比较多的情况下,在一定程度上可以提高处理的效率;这一点与select()函数不同,调用select()函数之后,select()函数会清空它所检测的socket描述符集合,导致每次调用select()之前都必须把socket描述符重新加入到待检测的集合中;因此,select()函数适合于只检测一个socket描述符的情况,而poll()函数适合于大量socket描述符的情况;
    nfds:nfds_t类型的参数,用于标记数组fds中的结构体元素的总数量;
    timeout:是poll函数调用阻塞的时间,单位:毫秒;
    返回值:
    >0:数组fds中准备好读、写或出错状态的那些socket描述符的总数量;
    ==0:数组fds中没有任何socket描述符准备好读、写,或出错;此时poll超时,超时时间是timeout毫秒;换句话说,如果所检测的socket描述符上没有任何事件发生的话,那么poll()函数会阻塞timeout所指定的毫秒时间长度之后返回,如果timeout==0,那么poll() 函数立即返回而不阻塞,如果timeout==INFTIM,那么poll() 函数会一直阻塞下去,直到所检测的socket描述符上的感兴趣的事件发生是才返回,如果感兴趣的事件永远不发生,那么poll()就会永远阻塞下去;
    -1:  poll函数调用失败,同时会自动设置全局变量errno;

    如果待检测的socket描述符为负值,则对这个描述符的检测就会被忽略,也就是不会对成员变量events进行检测,在events上注册的事件也会被忽略,poll()函数返回的时候,会把成员变量revents设置为0,表示没有事件发生;

    另外,poll() 函数不会受到socket描述符上的O_NDELAY标记和O_NONBLOCK标记的影响和制约,也就是说,不管socket是阻塞的还是非阻塞的,poll()函数都不会收到影响;而select()函数则不同,select()函数会受到O_NDELAY标记和O_NONBLOCK标记的影响,如果socket是阻塞的socket,则调用select()跟不调用select()时的效果是一样的,socket仍然是阻塞式TCP通讯,相反,如果socket是非阻塞的socket,那么调用select()时就可以实现非阻塞式TCP通讯;

    typedef struct pollfd {
    int fd;
    short events;
    short revents;
    } pollfd_t;
    typedef unsigned long nfds_t;

    经常检测的事件标记: POLLIN/POLLRDNORM(可读)、POLLOUT/POLLWRNORM(可写)、POLLERR(出错)

    如果是对一个描述符上的多个事件感兴趣的话,可以把这些常量标记之间进行按位或运算就可以了;

    比如:对socket描述符fd上的读、写、异常事件感兴趣,就可以这样做:

    struct pollfd fds;
    fds[nIndex].events=POLLIN | POLLOUT | POLLERR;

    当 poll()函数返回时,要判断所检测的socket描述符上发生的事件,可以这样做:

    struct pollfd fds;
    检测可读TCP连接请求:


    if((fds[nIndex].revents & POLLIN) == POLLIN){//接收数据/调用accept()接收连接请求}
    检测可写:

    if((fds[nIndex].revents & POLLOUT) == POLLOUT){//发送数据}
    检测异常:

    if((fds[nIndex].revents & POLLERR) == POLLERR){//异常处理}
    (2)实现原理
    未完待续。
    3.epoll

    (1)基本用法

    epoll是Linux内核为处理大批量句柄而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。


    epoll有2种工作方式:LT和ET。   

    LT(level triggered,水平触发)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。   

    ET (edge-triggered,边缘触发)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。

    epoll相关的系统调用有3个:

    1.


    int epoll_create(int size);
    参数size:用来告诉内核要监听的数目一共有多少个。

    返回值:成功时,返回一个非负整数的文件描述符,作为创建好的epoll句柄。调用失败时,返回-1,错误信息可以通过errno获得。
        说明:创建一个epoll句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

    2.

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    参数epfd:epoll_create()函数返回的epoll句柄。

    参数op:操作选项。

    参数fd:要进行操作的目标文件描述符。

    参数event:struct epoll_event结构指针,将fd和要进行的操作关联起来。

    返回值:成功时,返回0,作为创建好的epoll句柄。调用失败时,返回-1,错误信息可以通过errno获得。

    说明:epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

    参数op的可选值有以下3个:

    EPOLL_CTL_ADD:注册新的fd到epfd中;

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

    EPOLL_CTL_DEL:从epfd中删除一个fd;


    struct epoll_event {
    __u32 events;
    __u64 data;
    } EPOLL_PACKED;
    events可以是以下几个宏的集合:
    EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    EPOLLOUT:表示对应的文件描述符可以写;
    EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    EPOLLERR:表示对应的文件描述符发生错误;
    EPOLLHUP:表示对应的文件描述符被挂断;
    EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
    EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
       3.

    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
        参数epfd:epoll_create()函数返回的epoll句柄。

    参数events:struct epoll_event结构指针,用来从内核得到事件的集合。

    参数 maxevents:告诉内核这个events有多大

    参数 timeout: 等待时的超时时间,以毫秒为单位。

    返回值:成功时,返回需要处理的事件数目。调用失败时,返回0,表示等待超时。

    说明:等待事件的产生。

    (2)实现原理

    未完待续。

    三、三种IO多路复用的区别


    select           
         select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。
         这样所带来的缺点是:
    1 单个进程可监视的fd数量被限制
    2 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
    3 对socket进行扫描时是线性扫描
    poll
    poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,
    如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
    它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
    poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
    epoll  epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。
    在前面说到的复制问题上,epoll使用mmap减少复制开销。
    还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
    对比以下几个方面:
    1 支持一个进程所能打开的最大连接数
         

    select                         单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上 FD_SETSIZE为32*64),当然我们可以对它进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
    poll poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的
    epoll 虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。


    2 FD剧增后带来的IO效率问题

    select                         因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降
    poll 同上
    epoll 因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。
        3 消息传递方式

    select                         内核需要将消息传递到用户空间,都需要内核拷贝动作
    poll 同上
    epoll epoll通过内核和用户空间共享一块内存来实现的

    ------------

  • 相关阅读:
    SD卡测试
    测试人员可能会遇到的问题
    HDU 1024 Max Sum Plus Plus
    HDU 1176 免费馅饼
    HDU 1257 最少拦截系统
    HDU 1087 Super Jumping! Jumping! Jumping!
    poj 1328 Radar Installation
    poj 1753 Flip Game
    HDU 1003 Max Sum
    HDU 5592 ZYB's Premutation(BestCoder Round #65 C)
  • 原文地址:https://www.cnblogs.com/liurenli/p/10128153.html
Copyright © 2011-2022 走看看