美文网首页
I/O多路复用

I/O多路复用

作者: Sarahhhh | 来源:发表于2019-05-04 15:57 被阅读0次

    参考资料:
    select、poll、epoll之间的区别总结[整理]

    epoll机制:epoll_create、epoll_ctl、epoll_wait、close

    I/O多路复用就是通过一种机制,可以监视多个描述符,一旦内核发现某个描述符就绪(一般是读就绪或者写就绪),即进程指定的一个或者多个IO条件就绪,能够通知进程进行相应的读写操作

    优势

    与多进程和多线程技术相比,使用I/O多路复用技术,系统不必创建或维护进程/线程,从而大大减小了系统的开销

    select

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

    支持的最大文件描述符数量默认是1024

    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)
    /*
    maxfdp1:
    待测试的描述字个数,它的值是待测试的最大描述字加1(因此把该参数命名为maxfdp1),描述字0、1、2...maxfdp1-1均将被测试
    
    readset、writeset、exceptset:
    指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,可以把它设为空指针
    struct fd_set可以理解为一个集合,存放文件描述符
    
    timeout:
    告知内核等待所指定描述字中的任何一个就绪可花多少时间
    (1)设为null:永远等待
    (2)设为0:轮询
    (3)设为正数:等待一段时间
    struct timeval{
        long tv_sec;   //seconds
        long tv_usec;  //microseconds
    };
    
    return(int):
    就绪描述符的数目,超时返回0,出错返回-1
    */
    

    调用过程

    (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从内核空间拷贝到用户空间。

    poll

    与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,只是描述fd集合的方式不同。但poll没有最大文件描述符数量的限制,也不需要显式地请求异常情况报告。

    poll函数

    # include <poll.h>
    
    int poll(struct pollfd * fds, unsigned int nfds, int timeout);
    /*
    fds:
    struct pollfd {
    int fd;        // 文件描述符
    short events;  // 等待的事件(用户设置)
    short revents; // 实际发生了的事件(内核调用返回时设置)
    } ;
    
    nfds:
    监视的文件描述符个数
    
    timeout:
    指定等待的毫秒数
    (1)设为负数:使poll()一直挂起直到一个指定事件发生
    (2)设为0:立即返回
    (3)设为正数:等待一段时间
    
    return:
    (1)success:返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,返回0
    (2)error:返回-1
    */
    

    epoll

    epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次

    epoll接口

    #include <sys/epoll.h>
    
    // 创建一个epoll的句柄
    int epoll_create(int size);
    /*
    size:
    告诉内核这个监听的数目一共有多大
    
    return:
    一个epoll专用的文件描述符,其实是申请了内核空间
    
    注意:
    创建好epoll句柄后,它就是会占用一个fd值,所以使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽
    */
    
    // 操作监听事件
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    /*
    epfd:
    epoll_create()的返回值
    
    op:
    要进行的操作
    EPOLL_CTL_ADD 注册
    EPOLL_CTL_MOD 修改
    EPOLL_CTL_DEL 删除
    
    fd:
    需要监听的fd
    
    event:
    告诉内核需要监听什么事件
    struct epoll_event {
      __uint32_t events;  // Epoll events 
      epoll_data_t data;  // User data variable
    };
    typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
    } epoll_data_t;
    
    return:
    调用成功返回0,不成功返回-1
    */
    
    // 等待事件的产生
    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    /*
    events:
    用来从内核得到事件的集合
    
    maxevents:
    告之内核这个events有多大,每次能处理的事件数
    
    timeout:
    指定等待的毫秒数,同poll()的timeout
    
    return:
    需要处理的事件数目,如返回0表示已超时
    */
    

    对于epoll_event的举例:

    struct epoll_event ev;
    ev.data.fd=listenfd;
    ev.events=EPOLLIN|EPOLLET;
    // 注册事件
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    

    epoll_wait原理
    等侍注册在epfd上的socket fd的事件的发生。如果发生,则将发生的socket fd和事件类型放入到events数组中。并且将注册在epfd上的socket fd的事件类型给清空,所以如果下一个循环你还要关注这个socket fd的话,则需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)来重新设置socket fd的事件类型。这时不用EPOLL_CTL_ADD,因为socket fd并未清空,只是事件类型清空。这一步非常重要。

    总结

    • select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间

    • select的几大缺点:

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

      2. 每次调用select都需要在内核遍历传递进来的fd集合(把current进程轮流加入fd对应的设备等待队列中),这个开销在fd很多时也很大

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

    • epoll的解决方案:

      1. 调用epoll_ctl()函数注册新的事件时,把所有的fd拷贝进内核,在epoll_wait时不再拷贝,保证每个fd在整个过程中只会拷贝一次
      2. 只在epoll_ctl()时把current进程挂一遍(注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列),并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表。epoll_wait()的工作只需要在这个就绪链表中查看有没有就绪的fd,不用遍历fd集合
      3. epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目

    相关文章

      网友评论

          本文标题:I/O多路复用

          本文链接:https://www.haomeiwen.com/subject/sdoxoqtx.html