• IO多路转接


    IO多路转接

    从系统角度看待数据接收

    网卡接收数据

    网卡接收数据分为如下几步:

    1. 网卡收到网线传过来的数据

    2. 网卡会把接收到的数据写入内存

    3. 网卡通知操作系统读取网络数据

    网卡如何通知操作系统读取网络数据

    网卡通知系统读取网络数据是通过中断的方式完成的,当网卡收到数据将其写入内存之后,网卡会给CPU发送一个中断信号,由硬件产生的中断信号优先级较高,CPU应该优先进行处理,CPU会执行中断程序,将网络数据拷贝到接收缓冲区

    recv阻塞读取,底层发生了什么?

    下面是一段经典的TCP服务端代码:

    int listenSock = socket(AF_INET, SOCK_STREAM, 0);
    bind(listenSock,...);
    listen(listenSock,...);
    int fd=accept(listenSock,...);
    recv(fd,...);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当程序运行到socket时,Linux文件系统在底层会创建一个文件对象struct file,通过文件描述符可以找到该文件对象,通过bind和listen可以让文件对象与网卡建立关联,程序运行到accept会从TCP的全连接队列获取连接,通过recv读取对端发送过来的数据。

    1. 每一个连接在底层都是一个socket对象,本质是一个网络文件,用户通过文件描述符可以访问到socket对象
    2. 每一个socket对象都有发送缓冲区和接收缓冲区,除此之外,每一个socket对象还有一个等待列表,等待列表是所有需要等待该socket事件就绪的进程集合,一般而言,一个socket对象的等待列表中只有一个进程,但是一个进程可以同时处于多个socket对象的等待列表,也就是IO多路复用
    3. 网卡将接收到的数据写入内存之后,会向CPU发送中断信号,CPU接收到中断信号之后,执行中断处理程序,中断处理程序主要完成2件工作:将网络数据拷贝到socket对象的接收缓冲区、将阻塞的进程从socket对象的等待列表中移除,并添加到CPU运行队列,此时进程执行recv就可以直接从socket对象的接收缓冲区读取数据
    4. 进程阻塞就是进程在socket对象的等待列表中等待数据就绪,进程阻塞不占用CPU资源

    在这里插入图片描述

    操作系统如何知道网络数据对应哪一个socket对象?

    一个服务端可能有多个客户端与之连接,底层就会存在多个socket对象,网卡接收到数据之后,操作系统是如何得知将数据拷贝到哪一个socket对象的接收缓冲区呢?

    实际上,在accpet获取新连接的时候,操作系统知道发起连接的客户端对应的IP和端口

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    
    • 1

    操作系统在底层维护了socket对象与客户端的映射(即文件描述符fd与客户端的映射),网卡接收到的数据中,存在源IP地址和源端口号,根据客户端IP和端口就可以索引到fd,找到socket对象,从而将数据写入指定socket对象的缓冲区

    如何同时监视多个文件描述符?

    从原理上来讲,同时监视多个文件描述符就是将一个进程添加到多个socket对象的等待队列中,只要这多个socket对象中有1个或者1个以上有数据就绪,就可以将进程从所有socket对象的等待队列中移除,将其添加到CPU的运行队列。这就是select和poll的基本原理

    select

    int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
    
    • 1

    select原理

    调用select函数的进程,会被添加到多个socket对象的等待队列中,只要网卡有数据到来,CPU就会执行中断处理程序,select的中断回调与recv的中断回调不同,select的中断回调完成的功能:

    1. 将网络数据写入对应socket对象的接收缓冲区
    2. 将进程从所有socket对象的等待队列中移除

    select特点

    • select可以监视的文件描述符有上限,一般为1024,即sizeof(fd_set)*8
    • select每一次在调用时都要将fd_set结构从用户拷贝到内核,即用户告诉内核,你需要帮我检测哪些文件描述符上的哪些事件,select返回时需要将数据由内核拷贝到用户,即内核告知用户,哪些文件描述符上的哪些事件已经就绪。这2次拷贝存在较大的开销,出于效率的考量,才规定了select最大监视的文件描述符数量
    • fd_set结构不可重用,每一次内核返回时都会对fd_set进行修改,下一次调用select需要重新设置fd_set
    • select返回时,用户只知道有几个文件描述符就绪,但是不知道有哪些文件描述符就绪,因此用户只能一个一个的进行遍历,效率较低。需要注意的是,select返回时,内核是知道哪些文件描述符就绪的,但是由于select函数在设计上没有告知用户有哪些文件描述符就绪,只是告诉了用户有几个文件描述符就绪,因此用户需要进行遍历操作
    • select可以同时监视多个文件描述符,相比于recv等接口,在一定程度上提高了效率

    poll

    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    struct pollfd {
        int   fd;         /* file descriptor */
        short events;     /* requested events */
        short revents;    /* returned events */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    poll原理

    poll的原理和select类似,也是将一个进程添加到多个socket对象的等待队列,在网卡接收到数据后,CPU执行中断程序将数据写入到接收缓冲区,然后将进程从所有等待队列中移除

    poll的特点

    • 监视的文件描述符理论上没有上限,由用户提供的fds数组决定
    • poll把监听事件与就绪事件进行分离,在接口上使用起来更加方便
    • fds是可重用的,在每一次重用fds之前,只需要对revents进行清空即可
    • poll在调用时也需要将fds从用户拷贝到内核,在返回时从内核拷贝到用户,存在一定开销
    • poll可以同时监视多个文件描述符,一定程度上提高了效率

    epoll

    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);
    typedef union epoll_data {
        void        *ptr;
        int          fd;
        uint32_t     u32;
        uint64_t     u64;
    } epoll_data_t;
    struct epoll_event {
        uint32_t     events;      /* Epoll events */
        epoll_data_t data;        /* User data variable */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    epoll_create

    epoll_create接口用于创建一个eventpoll结构体,eventpoll结构体的主要成员:

    struct eventpoll{
        //...
        struct mutex mtx;
        spinlock_t lock;//mtx和lock保证访问eventpoll是线程安全的
        wait_queue_head_t wq;//wait queue,等待队列
        struct list_head rdllist;//ready list,就绪列表,双向链表
        struct rd_root_cached rbr;//red balck tree root,红黑树根节点
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    epoll_ctl

    epoll_ctl可以向eventpoll中的红黑树添加或删除节点,如果调用epoll_ctl添加文件描述符,那么内核会封装一个epitem结构体,将epitem中的rbn添加到eventpoll的红黑树中,同时将eventpoll添加到socket对象的等待队列

    struct epitem{
        struct rb_node rbn;//red black tree node,将该结点添加到eventpoll中的红黑树中
        struct epoll_filefd ffd;//文件描述符
        struct list_head rdlink;//当文件描述符上的事件就绪时,会将rdlink插入到eventpoll的就绪列表中
        struct eventpoll* ep;//指向eventpoll
        struct epoll_event event;//监视的事件
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在epoll中,并不是进程直接在socket对象的等待队列中等待数据就绪,而是让eventpoll在多个socket对象的等待队列中进行等待,当socket对象上有数据时,触发回调,让eventpoll的rdllist引用已经就绪的socket对象

    在这里插入图片描述

    epoll_wait

    调用epoll_wait时,用户请求内核检测eventpoll的rdllist是否为空,若为空,则进程阻塞在eventpoll的wq上,若不为空,则内核将rdllist中的文件描述符拷贝到用户空间,同时epoll_wait返回已经就绪的文件描述符个数。

    int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
    
    • 1

    内核会将已经就绪的文件描述符信息写入events数组中,用户就不需要对events数组进行完全遍历,只需要依据epoll_wait的返回值取出events数组中头部指定个数的元素即可,例如epoll_wait返回3,用户在遍历时只需要:

    for(int i=0;i<3;i++){
        if(events[i].events&EPOLLIN){
            //...
        }
        if(events[i].events&EPOLLOUT){
            //...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    epoll的特点

    • epoll将"用户告知内核"与"内核通知用户"在接口上进行了分离,使其使用更加方便
    • 使用epoll_ctl添加文件描述符,就是将epitem中的rbn添加到红黑树,时间复杂度是O(logN)
    • epoll让eventpoll结构等待多个socket对象是否有数据就绪,让eventpoll的rdllist引用已经就绪的socket对象。eventpoll作为进程与socket对象的中间层,socket的数据接收并不直接影响进程,进程只需要在eventpoll的wq中等待即可,当rdllist成功引用socket对象时,epoll会通过回调的方式唤醒进程,通过rdllist+回调机制,进程索引文件描述符的时间复杂度为O(1)
    • epoll可以同时监视多个文件描述符,提高了效率

    epoll的工作方式

    epoll的工作方式有水平出发(LT,level triggered)和边缘触发(ET,edge triggered),LT指的是只要底层有数据就绪,epoll_wait就会一直通知上层读取,ET指的是只有数据从无到有或者从有到多,epoll_wait才会通知上层一次,如果用户没有把数据读取干净,那么以后也不会通知了,epoll如果采用ET模式,必须反复读取,并且将文件描述符设置为非阻塞,原因是在进行recv读取时,如果恰好底层没有数据,并且对端没有关闭连接,此时recv就会被阻塞,导致服务无法正常运行。

    一般而言,ET模式要比LT模式效率高,因为内核只需要通知一次即可,但是如果采用LT模式每一次底层通知事件就绪时上层都把数据读完,那么效率也是很高的。

    如何设置文件描述符为非阻塞?

    通过fcntl可以设置一个文件描述符为非阻塞

    bool SetNoBlock(int sock) {
        int fl = fcntl(sock, F_GETFL);
        if (fl < 0) {
            return false;
        }
        fl |= O_NONBLOCK;
        return fcntl(sock, F_SETFL, fl) == 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如何设置ET模式?

    在调用epoll_ctl添加文件描述符时,设置EPOLLET

    struct epoll_event event;
    memset(&event, 0, sizeof(event));
    event.events = EPOLLIN|EPOLLET;
    event.data.fd = accept(...);
    SetNoBlock(event.data.fd);
    epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &event)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    思考

    为什么eventpoll的rdllist使用双向链表,单链表不行吗?

    在调用epoll_ctl时,除了向红黑树中添加节点,还有可能删除节点,如果删除的那个文件描述符上有事件就绪,需要将它从rdllist中移除,如果rdllist是单链表,那么删除的时间复杂度就是O(N),如果是双向链表,删除的时间复杂度是O(1)

    epoll保存监视的文件描述符为什么使用红黑树?使用哈希表可以吗?

    epoll_ctl可以支持的功能有插入、删除和修改,eventpoll就需要使用一种便于插入、删除和寻找的数据结构,红黑树正是满足条件的结构,插入、删除和修改时间复杂度都是O(logN),使用哈希表从理论上而言可行,但是哈希表在最糟糕的情况下可能所有的键都哈希到了同一个桶,那么这时查找起来时间复杂度也退化为O(N),不如红黑树稳定,除此之外,哈希表不支持范围查找

    eventpoll中的mtx和lock作用是什么?在什么情况下会有多个执行流访问eventpoll结构?

    mtx和lock的作用是保证eventpoll在访问时是线程安全的。存在这样一种场景:用户调用epoll_wait访问eventpoll中的rdllist,恰好此时eventpoll等待的socket对象上有数据就绪,需要将对应epitem中的rdlink添加到eventpoll的rdllist,就出现了2个执行流同时访问rdllist的情况,因此需要通过eventpoll的mtx和lock成员保证rdllist线程安全

    为什么select和poll索引就绪文件描述符的时间复杂度是O(N),epoll索引就绪文件描述符的时间复杂度是O(1)?

    主要是底层使用的结构和机制不同,对于select和poll,都是由进程直接在socket对象的等待队列中阻塞,返回时用户只知道就绪的文件描述符个数,不知道具体是哪些文件描述符,因此需要进行遍历操作,时间复杂度是O(N);对于epoll,由于其使用了先进的结构eventpoll、rddlist,并且通过回调让rddlist引用已经就绪的socket对象,进程只需要在eventpoll的wq中等待,只需要关系rdllist,无需关心socket对象,返回时根据rdllist就可以直接索引到就绪的文件描述符,无需遍历,因此时间复杂度是O(1)

    总结

    系统调用selectpollepoll
    事件集合通过fd_set设置关心事件,返回时也是通过fd_set获取就绪事件,fd_set不可重用通过events和revents分别表示关心事件和就绪事件,pollfd结构可重用通过epoll_ctl设置关心事件,通过epoll_wait检测eventpoll的rdllist是否为空,从而获取就绪事件
    索引就绪文件描述符的时间复杂度O(N)O(N)O(1)
    最大支持的文件描述符个数1024使用ulimit -a查看使用ulimit -a查看
    工作模式LTLTLT和ET
    内核实现和工作效率底层一个进程在多个socket对象的等待队列上等待,只要其中一个有事件就绪就返回,需要进行遍历操作,时间复杂度为O(N)底层一个进程在多个socket对象的等待队列上等待,只要其中一个有事件就绪就返回,需要进行遍历操作,时间复杂度为O(N)通过eventpoll、rdllist、wq、rbr等先进结构,让eventpoll在socket对象的等待队列等待,使用rdllist引用就绪的文件描述符,进程只需要在wq中等待并获取rdllist中就绪的文件描述符,时间复杂度为O(1)
  • 相关阅读:
    pxtorem
    使用Requests发送HTTP请求
    (部署服务器系列三)eclipse远程调试springboot项目代码
    python中argparse模块关于 parse_args() 函数详解(全)
    【Unity3D日常开发】Unity3D的Resources不同目录的加载分析
    redis的数据类型
    IDA pro逆向工具寻找socket server的IP和port
    [oeasy]python0014_二进制_binary_bin
    【每日一题Day350】LC2652倍数求和 | 数学+容斥原理
    米尔MYD-JX8MPQ yocto
  • 原文地址:https://blog.csdn.net/Slowstep_/article/details/133585342