IO复用细节

99 阅读5分钟

IO复用的三种模型

select模型:只能管理1024个客户端连接
poll模型:可以管理更多的客户端连接,但是连接越多,性能线性下降
epoll模型:只要内存足够,管理的连接数没有上限,性能不会下降

阻塞/非阻塞IO

阻塞:程序在调用结果返回之前,会等待,得到结果之后才会返回
非阻塞:不管调用是否得到结果,程序都不会等待,立即返回
缺省阻塞的函数:connect(), send(), revc(), accept()

select模型:

事件:select()等待事件的发生(读事件,写事件)
    1)新客户端的连接请求accept;
    2)客户端有报文到达recv,可以读;
    3)客户端连接已断开;
    4)可以向客户端发送报文send,可以写。

写事件:
    TCP有缓存区,如果缓冲区己填填满,send函数会阻塞
    如果发送端关闭了socket,缓冲区中的数据会继续发送给接收端
    可以用发送端发快一点,接收端收慢一点,会出现发送端停一会,发一会儿。
    如果tcp缓冲区没有满,那么socket连接是可写的(一般情况是填不满的,所以如果关心可写事件,select会立即返回)
    tcp发送缓存区2.5M, 接收缓存区1M
    getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &bufsize, &optlen); //获取发送缓存区的大小
    getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &bufsize, &optlen); //获取接收缓存区的大小
    在高并发和流媒休传输场景中,缓冲区有填满的可能
    百度查询TCP的缓存区

超时机制
    第5个参数,超时时间。
        >0表示超过多久没有事件发生,则返回=0
        NULL,表示不设置超时,直到有事件发生才返回

水平触发
    如果事件和数据已经在缓存冲区里,程序调用select()时会报告事件,数据也不会丢失。
    服务端select()之前先sleep 20秒,客户端发送完数据后退出,
    服务端sleep完后,调用select(),都可以接收到客户端连接请求,发送数据,以及断开链接的事件,一个都没有丢失
    
    如果select()己经报告了事件,但是程序没有处理它,下次调用select()的时个会重接报告。   

性能测试
    1000000/s个报文

存在的问题
    支持的连接数太小,才1024, 调整的意义不大
    每次调用 select(), 要把fdset从用户态拷贝到内核, 调用select()之后,把fdset从内核态拷贝到用户态
    select()返回后,需要遍历bitmap, 效率比较低

poll模型

poll和select本质上没有区别,弃用了bitmap,采用数组表示法
每次调用poll()要把数组从用户态拷贝到内核,调用poll()之后把数组从内核态拷贝到用户态
poll()返回后,需要遍历数组,效率比较低
man 3 poll 
poll(struct pollfd fds[], nfds_t nfds, int timeout);
pollfd这个结构体在man poll里
ppoll只是多了一个信号屏蔽参数
poll(fd[], maxfd, -1) //-1代表不设置超时间,有事件发生,立即返回,否则一直等待

epoll模型

epoll只在有linux下才有,windows中没有,包含sys/epoll.h头文件

水平触发&边缘触发
    select和poll采用水平触发
    epoll有水平触发和边缘触发两种机制
    水平触发:
        如果接收缓冲区不为空,表示有数据可读,如果数据没有被读取完,再次调用epoll_wait的时候,读事件一直触发
        如果发送缓冲区没有满,表示可以写入数据,只要缓冲区没有写满,再次调用epoll_wait的时候,写事件一直触发
        读:    
            有客户端连接上来,会触发读事件,如果一直不处理客户端连接(注释此语句块代码),就会一直触发
            如果客户端有报文过来,会触发读事件,不处理(注释此语句块代码), 一直触发 
        写:
            如果关注客户端的写事件,可以向客户端缓冲发送报文的话,一直触发写事件,直到缓冲区写满
            每个连接的客户端都有发送缓冲区,各自填满为止

    边缘触发:
        socket加入epoll后,如果接收缓冲区不为空,触发可读事件,如果有新的数据到达,再次触发可读事件
        边缘触发(如果有客户端连接请求未处理,只触发一次)
        epoll触发可读事件后,不管程序有没有处理可读的事件,epoll都不会再触发,只有当新的数据到达时,才再次触发可读事件
        
        可写事件:
            socket加入epoll后,如果发送缓冲区不为空,触发可写事件,如果发送缓冲区由满变成有空时,再次触发可写事件
            网上有文章说边缘触发模式下,只要发送缓冲区有变化,就会触发写事件,是不对的。

IO复用的场景要使用非阻塞IO

1.当数据达到socket缓冲区的时候,可能会因为某些原因被内核丢弃,比如校验和错误,这时候,如果采用了阻塞的IO
唤醒的程序读到不到数据,acceptrecv函数就会阻塞
2.达到缓冲区的数据有可能被别人取走,比如多个进程accept同一个socket时引发的惊群现象,只有一个连接到来,
但是所有的监听进程都会被唤醒,最终只有一个进程可以accept到这个请求,其他进程accept会被阻塞
3.ET边缘触发模式必须要使用非阻塞IO,因为程序中需要循环读和写,直到EAGAN出现,如果使用阻塞IO容易被阻塞住
    读的方法:
        如果接收缓冲区中有事件没有处理或有数据没有读完
        水平触发:epoll_wait会重复报告,不必担心遗漏事件
        边缘触发:epoll_wait不会重复报告,程序要用一个循环处理全部的事件或读取全部的数据
    写的方法:
        想写就直接写,出现EAGAIN就别写了
        水平触发:epoll_wait会重复报告,如果不想写了,可以注销事件,否则一直触发
        边缘触发:epoll_wait不会重复报告,不想写了就不写,不必注销事件
百度为什么IO多路复用要搭配非阻塞IO