常见IO模型-IO多路复用

常见IO模型:

  • 同步阻塞IO(Blocking IO):使用recv一直等数据直到拷贝到用户空间,并同步返回状态,这段时间内进程始终阻塞

  • 同步非阻塞IO(Non-blocking IO): socket设置为non-block,recv不管有没有获取到数据都返回,如果没有数据则等一段时间后在调用recv,如此循环. 只在检查无数据时是非阻塞的,有数据时仍然同步等待数据从内核到用户空间。

  • IO多路复用: 用Reactor模式较多,具体参考第2部分介绍

  • select


//创建fds数组
sockfd = socket(...);
bind(sockfd, addr);
listen(sockfd,port);
for...{
    //通过listen_socket,创建connect_socket. listen_socket负责继续监听,connect_socket负责处理数据读写
    fds[i] = accept(sockfd);
}
//创建bitmap/rset,并关联fds,最多1024bit
while(1) {

    FD_ZERO(&bitmap); //bitmap由用户创建,并拷贝到内核更改,无法复用
    for ...{
        FD_SET(fds[i], &bitmap)
    }

    //此处多路复用器阻塞,等待有事件唤醒
    select(max+1,&rset);
    //traversal fds,时间复杂度O(n)

}

复制代码

限制:

  1. 一个进程最多能标记1024个fd
  2. 用户态和内核态之间切换拷贝bitmap
  3. bitmap无法重用
  4. 需要完整遍历bitmap每一位
  • poll: 相比select无1024数量限制,并能复用pollfd_arr
    stuct pollfd{
        int fd;
        //POLLIN 有数据可读, POLLOUT写事件允许, POLLERR 错误,POLLRDHUP流socket对端关闭连接;...
        short events;
        short revents;
    }

    ...

    for...{
        pollfd_arr[i].fd = accept(listen_socket_fd);
        pollfd_arr[i].events = POLLIN;
    }
    
    while(1){
        //此处多路复用器阻塞,
        poll(pollfd_arr, count,...);
        for i in count{
            if (pollfd_arr[i].revents & POLLIN){
                read(pollfd_arr[i].fd, buffer);
                pollfds[i].revents = 0; //重置状态 复用
            }
        }
    }

复制代码
  • epoll: bsd上是kqueue. 不仅告诉调用方有数据,还能提供具体哪个sock有数据,避免了遍历查找
    struct epoll_event events[1024]
    int epfd = epoll_create

    ...

    for...{
        static struct epoll_event ev;
        ev.data.fd = accept(listen_socket_fd,....);
        ev.events = EPOLLIN;
        epoll_ctl(epfd, ev.data.fd,&ev ...);

        whil(1){
            //阻塞, 返回产生事件的个数,并且 events是内核已排序好的
            int count = epoll_wait(epfd, events, ....);
            for(i in count){
                read(events[i].data.fd, buffer..);
            }
        }
    }

复制代码

epoll在poll基础上进一步优化, events由用户态空间和内核空间共享避免了切换, 并且返回产生事件的句柄数量,以及内核排序好的句柄数组,调用方只需遍历有事件发生的fd数组.

  • 异步非阻塞IO: 由操作系统内核负责读取socket数据,并写入用户指定的缓冲区,用户线程不会被阻塞,目前操作系统支持不太完善。

扩展

1. IO多路复用中常用的Reactor反应堆模式

主要角色:

  • handle : linux中称为文件描述符,windows中称为句柄,是对资源在os上的抽象,如socet\打开的文件\timer等,

  • synchronous event demultiplexer 同步多路事件分离器,本质是系统调用,如select poll epoll等,可以等待多个handle,在等待过程中所在线程牌挂起状态不消耗CPU时间。当某个句柄有事件产生时才会返回

  • Event Handler/Concrete Event Handler 定义一些钩子函数或称为回调方法,handle有事件产生就会调用钩子方法,在socket中一般有decode-process-encode这些过程,Concrete处理具体的业务逻辑

  • Initiation Dispatcher 初始事件分发器,用于管理event handler, 并调用多路分离器等待其返回时,将事件分发到event handle事件处理器进行处理

按线程划分:

  • 单线程Reactor模型: 与NIO流程相似,一个线程处理所有的IO和process()过程,要求IO和CPU速度匹配,无处理较慢的请求,否则引起后续的请求积压

  • 多线程Reactor模型: 一个或多个IO线程,多个工作处理线程,由于reactor既处理IO请求也响应连接请求容易出现瓶颈。

  • 主从Reactor模型: 一个main reactor负责监听selector连接池,多个sub reactor 处理accept连接请求,性能较好。

线程池缺点是大并发下耗满线程,服务阻塞。对于大文件传输占用长时间数据读写操作的,可以考虑使用每个连接建立新的线程或异步非阻塞IO模式。并发数少用哪个都没有区别。异步非阻塞IO由于操作系统支持限制用的较少,大多高性能并发服务采用IO多路复用+多线程(线程池)的架构。

2. TCP粘包分包问题

每个UDP消息头部都有来源和端口号的消息头,一次write就是一条消息,接收端也以消息为单位提取,不完整的包会被丢弃,因此不会出现粘包。而TCP是面向流的协议接收端无法得知接收的数据是否已传完,因此要处理粘包、半包问题,解决方案有:

  1. 基于固定长度消息
  2. 基于消息边界符
  3. 消息头指定消息长度。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享