IO及其五种模型

391 阅读9分钟
                    2020 年第16篇文章

IO

哪究竟什么是IO呢? 所谓的IO(输入/输出)是在主存和外部设备(例如磁盘驱动器/终端/网络)之间复制数据的过程。其中输入操作是从IO设备复制数据到主存,而输出操作是从主存复制数据到IO设备

一般来说,像我们这种Java程序员,日常工作接触IO的机会就是熟悉使用Java对操作系统的各种IO模型的封装。程序员在使用这些API的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码,只需要使用Java的API就可以了。

四种IO模型

IO模型包括四种

  1. 同步阻塞IO
  2. 同步非阻塞IO
  3. IO多路复用
  4. 异步IO
  5. 信号式驱动IO

下面我会简单讲一下这几种IO

阻塞IO

所谓的阻塞IO,指的是当应用进程进行系统调用的时候,若数据没有准备好则线程会被挂起阻塞,也就是线程像“假死”了一样,直到内核将数据准备好后则会唤醒线程。这时候线程就会可以继续执行下去。下面就是一个简单的流程图:

我们根据图可以看出,当内核在读取数据的时候,线程就要在旁边等着。这样子实现起来简单(因为过程是非常直观的),但是同时也代表着效率非常低下(因为线程是宝贵的,若系统读取数据太慢的话,线程就只能在旁边等着,做不到线程复用。想想如果非常多线程同时一起读取数据,那就意味着有多少线程在等待)。

所以我们可以总结出了它的应用场景:如果你的业务场景非常简单,而且并发率低,那么你可以考虑一下使用阻塞IO

非阻塞IO

上面阻塞IO的场景对于高并发来说,的确有点不给力,那么我们有什么解决方案呢?非阻塞IO模型就闪亮登场了。

根据阻塞IO的描述,其实我们很容易理解非阻塞IO其实就是它的反面 - 当系统读取文件的时候,线程不再是“假死”状态而是去处于活跃状态。在这个过程中,线程还是会不断地调用系统方法询问内核是否将数据准备好了。等到系统将数据复制好到了内存后,线程就可以得到数据满意地走了。下图就是非阻塞IO的交互图。

我们可以清楚的看到右边有两部分:等待数据和将数据从内核复制到用户空间。只有在等待数据的过程中,线程会不断地调用 recvfofrom 去询问内核。

可能你会说,这样不断地调用不是很浪费性能吗?其实并不是,在读取文件的间隙,其实线程还可以接一下外快,例如去偷偷懒或者做做别的任务。这样是不是不用在这里瞎等,时间得到了高效的利用?

IO多路复用

IO的多路复用,可能是大家面试的时候被面试官问到最多的一种IO。因为什么呢?因为很有名的框架 Netty 就是使用了这个模型,形成了它的三大特点:

  1. 并发高
  2. 传输快
  3. 封装好

既然框架都选择它了,那么说明它肯定有过人之处!让我们来了解一下它。

所谓的IO多路复用,指的是通过一个线程去不断地轮训多个Socket状态。只有当Socket真正有读写时间时候,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个Socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有Socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。在NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。多路复用IO模式,通过一个线程就可以管理多个Socket,只有当Socket真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用IO比较适合连 接数比较多的情况。下图是IO多路复用的流程图:

我们从流程图中可以看到左边有两个流程:受阻于select和数据复制到缓冲区。通过select单条线程的方法,会不断地轮询询问底层是否有Socket已经数据准备好了;同时当数据准备好后,线程就可以对应用缓冲区的数据进行处理了。

从流程的右边我们同样可以看到类似非阻塞IO的两个流程:等待数据和将数据从内核复制到用户空间。但是对于这部分而言,非阻塞IO和IO多路复用有什么区别呢?

多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问Socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个Socket状态是内核在进行的,这个效率要比用户线程要高的多

看来IO多路复用还是好处多多。但是有一点需要注意,因为IO多路复用的本质就是单线程监控多个Socket,所以还是相当于一个人干多份话。毕竟人也没有三头六臂,所以这个线程只能一个一个处理。当某一个Socket返回的数据特别大的时候,可能这个线程一时候处理不过来,导致了后续的时间迟迟得不到处理,并且会影响新的事件轮询。这个需要注意!

信号式驱动IO

当用户线程请求IO,会给对应的Socket注册一个信号函数,然后用户线程继续执行。

当内核将数据准备好后,就通过函数来通知线程。用户线程得到信号后开始调用IO读写操作进行实际的IO请求。

上面四种IO的总结

临时插了一个总结性的文章。过去我们讲了四种IO,其实四种IO都是同步的IO模型。原因是,无论上面的那一种模型,真正的数据拷贝过程都是同步进行的。

怎么理解上面这句有下划线的话?无论是阻塞IO,非阻塞IO还是IO多路复用,还是信号驱动IO,都有那么一个步骤:在内核准备好数据后将数据从内核复制到用户空间。在这一个步骤的过程中,线程都是处于阻塞状态的。也就是说,以上的四种IO模型都无可避免会出现一段时间去做同一个动作:复制数据!

可能你会说,这也没什么事啊,我们都已经优化了这么多了,去拿一下数据怎么了?其实不然,如果你对性能追求到极致的话,你会很讲究这一点事情的。就好像,你去一家餐厅,你点了菜之后(发起系统调用),你就可以玩手机(线程不阻塞)。当菜烧好了(内核的数据准备好了),你是希望服务员端上来呢(异步),还是你自己去端(同步)?相信你已经明白了该怎么选择了!

说了这么多,那世界上有没有真正的异步IO?其实有的。请看下面的异步IO。

异步IO

异步IO估计是所有程序员的梦中情人。为什么这么说?请听我详细讲。在异步IO模型中,当用户线程发起read操作之后,立刻就 可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后, 它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程 发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。

现在估计你可以看出了同步和异步的区别了。异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用IO函数进行实际的读写操作。

结局

其实五种IO模型是最基本的的知识,对于未来我们去了解具体的实际应用有着基本的理解以及解决问题和书写代码有着极大的作用。

完!