I/O多路复用-附plantuml流程图

简介

I/O多路复用,以read为例。
注意:

  1. 本笔记的描述的例子是建立连接-读一次数据-关闭连接;
  2. 线程划分可以做适当的合并与分解;
  3. 可以进行适当的扩展;
  4. 流程图均是使用plantuml语法绘制,已提供代码。

参考:
图解 | 原来这就是 IO 多路复用
linux下用select写socket demo

阻塞I/O-最原始版本

@startuml 阻塞IO
start
fork
    :网卡接收数据;
    :将数据从网卡复制到内核缓冲区;
    :将文件描述符confd置为就绪;
    ':从内核缓冲区复制到用户缓冲区buf;
    detach
fork again
    :listenfd=socket();
    :bind(listenfd);
    :listen(listenfd);
    while (Process ?) is (Run)
        :confd = accept(listenfd);
        :int n = read(confd, buf);
        :doSomething(buf);
        :close(confd);
    endwhile (exit)

    stop
end fork
end

@enduml

在这里插入图片描述

最原始的阻塞型的IO,可以理解为两个线程:

  1. 服务侧使用accept建立连接并通过read函数接收数据;
  2. 客户端将数据发到服务侧,服务侧的数据先经过网卡,再经过内核缓冲区,然后将fd的状态设置为就绪,只有fd的状态为就绪,read才能读到数据

那么问题来了,上面的流程当中,到底卡住在哪里?下面再看一张将上面的伪代码分解图。
在这里插入图片描述

@startuml 阻塞IO-分解版
start
fork
    :网卡接收数据;
    :将数据从网卡复制到内核缓冲区;
    :将文件描述符confd置为就绪;
    ':从内核缓冲区复制到用户缓冲区buf;
fork again
    :listenfd=socket();
    :bind(listenfd);
    :listen(listenfd);
    while (进程在运行 ?) is (Run)
        :confd = accept(listenfd);
        partition read {
            :等待confd为就绪;
            :将内核缓冲区数据复制到用户缓冲区buf;
        }

        :doSomething(buf);
        :close(confd);
    endwhile (exit)

    stop
end fork
end

@enduml

这幅图与前面的一幅图的区别在于,细化了read函数,所以

  1. accept函数阻塞,即新连接建立的时候;
  2. read卡住。
    通过流程图来看,服务侧在通过accept获取到confd之后,如果客户端一直没有发送数据,那么服务侧的confd就一直为未就绪状态,那么read函数就会一直等待confd状态为就绪,此时就会阻塞。

阻塞IO第一次修改-不阻塞但不靠谱版

有人会说,既然每个confd可能阻塞,那么为所有的confd各自建立一个线程不久OK了?于是乎,就来了第一次修改,流程图如下:

@startuml 阻塞IO-不靠谱-Code-1
start
fork
    :网卡接收数据;
    :将数据从网卡复制到内核缓冲区;
    :将文件描述符confd置为就绪;
    ':从内核缓冲区复制到用户缓冲区buf;
fork again
    :listenfd=socket();
    :bind(listenfd);
    :listen(listenfd);
    while (进程在运行 ?) is (Run)
        :confd = accept(listenfd);
        :pthread_create(confd, doWork);
        note right
            void doWork()
            {
                int n = read(confd, buf);
                doSomething(buf)
                close(confd)
            }
        end note
    endwhile (exit)

    stop
end fork
end
@enduml

在这里插入图片描述

此时修改成:

  1. 每个confd都各自创建了线程(pthread_create),虽然主线程不阻塞了,但是各自处理的线程阻塞啊,没有彻底解决问题
  2. socket当中,往往连接是非常多的,而一个进程里面也不可能允许创建那么多线程,所以在高并发场景,这是不靠谱的;
  3. 但是为什么还要提到这个呢,那是因为在实际当中,遇到阻塞的业务,都要建立线程,防止干扰其他线程。

异步IO-不阻塞且不能用

前面使用多线程的方式,只是用来规避了,现在提供一种新的方式。



@startuml 异步IO-Code-1
fork
    :网卡接收数据;
    :将数据从网卡复制到内核缓冲区;
    :将文件描述符confd置为就绪;
    ':从内核缓冲区复制到用户缓冲区buf;
fork again
    :listenfd=socket();
    :bind(listenfd);
    :listen(listenfd);
    while (进程在运行 ?) is (Run)
        :confd = accept(listenfd);
        partition 非阻塞read {
            :fcntl(confd, F_SETFL, O_NONBLOCK);
            repeat
                :void;
                note right
                    立即返回,不阻塞
                end note
            repeat while (read返回值 ?) is (等于-1) not (正数)
            :将内核缓冲区数据复制到用户缓冲区buf;
        }

        :doSomething(buf);
        :close(confd);
    endwhile (exit)

    stop
end fork
end
@enduml

在这里插入图片描述
此处,只是多了一个修改,使用fcntl函数,作用是:调用了read函数,如果发现confd是未就绪状态,直接返回,而不是等待了。

所以,当前的操作流程是,只要状态是未就绪,read函数立即返回,且继续循环,直到,confd是就绪状态为止

那么问题来了:

  1. 虽然不阻塞了,但是没啥用啊,因为不知道什么时候才能获取到数据,只能循环操作,判断错误码;
  2. 当前只是用的是一个confd,如果是多个confd,难道还得建立多个线程,然后一直循环操作下去,前面提到也是不靠谱的?

IO多路复用-最原始的模型



@startuml 伪IO多路复用-1
fork
    :网卡接收数据;
    :将数据从网卡复制到内核缓冲区;
    :将文件描述符confd置为就绪;
    ':从内核缓冲区复制到用户缓冲区buf;
fork again
    repeat 
        repeat 
            :从fdlist获取fd;
            if (OK==read(fd, bug)) then(read成功)
                :doSomething(buf);
                :close(confd);
                :fdlist.remove(fd);
            else (read失败)
                :ignore;
            endif
        repeat while (遍历fdlist?) is (fdlist不为空)
    repeat while (循环?) is (循环)
    detach

fork again
    :listenfd=socket();
    :bind(listenfd);
    :listen(listenfd);
    while (进程在运行 ?) is (Run)
        :confd = accept(listenfd);
        :fcntl(confd, F_SETFL, O_NONBLOCK);
        :fdlist.add(confd);
        note right
            将confd放到容器fdlist中
        end note


    endwhile (exit)

    stop
end fork
end
@enduml

在这里插入图片描述
此处的修改是:

  1. 单独建立一个线程,去遍历所有的confd,如果数据可以读取,则读取且处理;
  2. 需要增加一个新的全局变量fdlist,容器,专门存放confd
  3. 不妨碍主线程继续添加新的连接confd

IO多路复用-Select

前面的,IO多路复用原始版,一般人也可以写个单独线程,但是写的代码毕竟是在用户态,而不是内核态,在高并发上还是存在不少问题的,所以出了一个select,功能差不多,只不过是在内核态执行的。


@startuml Select
fork
    :网卡接收数据;
    :将数据从网卡复制到内核缓冲区;
    :将文件描述符confd置为就绪;
    ':从内核缓冲区复制到用户缓冲区buf;
fork again
    repeat 
        :ret=select(fdlist, timeout);
        if (-1 == ret) then (select 失败)
            :ignore;
        else if (0 == ret) then (select 超时)
            :ignore;
        else (正数,事件个数)
            :初始化cnt为0,计数;
            repeat 
                :获取遍历中的fd;
                if (FD_ISSET(fd, &fdlist)) (是)
                    :doSomething(buf);
                    :close(confd);
                    :fdlist.remove(fd);
                    :cnt += 1;
                    if (cnt == ret) then (遍历结束,停止遍历)
                        break;
                    else (未遍历结束)
                        :继续遍历fdlist;
                    endif 
                else (否)
                    :ignore;
                endif
            repeat while (遍历fdlist) is (遍历fdlist)
        endif

    repeat while (循环?) is (循环)
    detach

fork again
    :listenfd=socket();
    :bind(listenfd);
    :listen(listenfd);
    while (进程在运行 ?) is (Run)
        :confd = accept(listenfd);
        :fcntl(confd, F_SETFL, O_NONBLOCK);
        :fdlist.add(confd);
        note right
            将confd放到容器fdlist中
        end note


    endwhile (exit)

    stop
end fork
end
@enduml

在这里插入图片描述

坑点:

  1. select需要将fdlist数据传入到内核,多做内存拷贝;
  2. select仍然是通过遍历的方式检查,是同步过程,只不过无系统调用切换切换上下文的开销。(内核层可以优化为异步事件通知)
  3. select返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历.

注意:
4. 可以将select 线程与其他线程合一;
5. 可以采用多个线程,多个线程调用select,只不过在fdlist.add(confd)的时候要指明放到哪个线程;
6. 如果doSomething(buf)函数阻塞,可以开启线程池,并以添加任务队列的方式放入线程池中,避免阻塞select
7. 可以将主线程中的listenfd放到select函数所在线程中,进行监控。

IO多路复用-poll

poll其实与select差不多,只不过是监控的连接数目更多了点,挺鸡肋的,可以忽略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值