对于 Java 的 IO 模型,对于大多数小伙伴都不陌生。但是想要准确的讲述这些 IO 模型之间的不同特点以及应用的范围,可能有一部分小伙伴很难讲清楚。在这篇文章中,我将尽可能清楚的讲述这些问题,希望对于坐在屏幕面前的你有一定的帮助。
在这篇文章中,我会先叙述 Linux 的 IO 模型,因为对于一些基本概念同步、异步、阻塞、非阻塞 Linux 和 Java 都是一致的,而且 Linux 的模型和 Java 的 IO 模型有相似的地方,或者说 Java 的 IO 模型有参考 Linux 的 IO 模型的地方。
Linux IO 模型
概念
内核态:核心态代码拥有完全的底层资源控制权限,可以执行任何 CPU 指令,访问任何内存地址,其占有的处理机是不允许被抢占的。
用户态:是用户程序能够使用的指令,不能直接访问底层硬件和内存地址。用户态运行的程序必须委托系统调用来访问硬件和内存。
内核态会涉及到一些比较底层或者比较危险的指令。如果对于用户开放的话,会造成比较危险的状况,所以用户不能直接调用这些指令。用户需要借助系统调用才能执行一些相关的操作。
同步/异步关注的是消息通信机制 。
同步:就是在发出一个调用时,在没有得到结果之前, 该调用就不返回。 异步:调用在发出之后,这个调用就直接返回了,所以没有返回结果。
阻塞/非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
阻塞:指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。 非阻塞:指在不能立刻得到结果之前,该调用不会阻塞当前线程。
举个简单的例子。
淘宝双 11 活动,小图同学买了一本《图解 Java》的书,此时快递小哥正在派送快递的情景。
同步、异步
小图(线程/进程 A)给快递小哥(线程/进程 B)打电话(发起调用),询问送到家的时间(结果)。如果此时快递小哥说,5 分钟或者马上送到(结果)。小图(线程/进程 A)挂了电话(调用结束)。此时就是小图(线程/进程 A)和快递小哥(线程/进程 B)就是异步调用。
小图(线程/进程 A)给快递小哥(线程/进程 B)打电话(发起调用),询问送到家的时间(结果)。如果此时快递小哥说,我也不知道还要多久,我得先把手中的这几件快递先送完(不是结果)。小图(线程/进程 A)没挂电话(仍然调用)。过了一会,快递小哥送完那几件快递告诉小图,5 分钟后送到(结果)。此时就是小图(线程/进程 A)和快递小哥(线程/进程 B)就是同步调用。
同步、异步一般发生在不同的线程/进程之间。如果调用者发起调用,能马上获得调用结果,这就是异步调用。如果不能马上获得结果,需要等待被调用的线程/进程一段时间,才能获得结果,这就是同步调用。
阻塞、非阻塞
小图(线程/进程 A)给快递小哥(线程/进程 B)打电话(发起调用),但是快递小哥的电话在通话中。小图就一直等待快递小哥,也没挂电话。此时小图(线程/进程 A)处于阻塞状态。
小图(线程/进程 A)给快递小哥(线程/进程 B)打电话(发起调用),但是快递小哥的电话在通话中。小图听到快递小哥的电话在通话中,就挂断电话。之后看起了《图解 Java》公众号的文章(别的任务)。一会后,又给快递小哥打电话 …… 。此时小图(线程/进程 A)处于非阻塞状态。
阻塞、非阻塞一般发生在单个进程/线程中。当进程/线程想要进行 A 操作,但是不能立刻完成(需要其他的条件 C)。如果线程/进程一直等待,不执行其他的操作,即此时处于阻塞状态。如果该线程/进程又开始执行另外的操作 B,此时线程处于非阻塞状态。
Linux IO 模型
根据前面描述的内核态和用户态的相关概念。当应用程序发起文件调用的时候,用户态运行的程序必须委托系统调用来访问硬件和内存。这个过程主要分为两个阶段,等待数据
和将数据从内核拷贝到用户空间
。根据这两个阶段中,调用进程和内核进程的表现,分为五种模型。
阻塞 IO 模型:
调用进程会一直阻塞,直到数据拷贝完成 。即应用程序调用一个 IO 函数,导致应用程序阻塞,等待数据准备好。数据准备好后,从内核拷贝到用户空间,IO 函数返回成功指示。 在整个调用过程中都是阻塞的。
非阻塞 IO 模型:
调用进程在等待数据的过程中,会不断轮询数据有没有准备好。数据准备好后,从内核拷贝到用户空间,IO 函数返回成功指示。在等待数据的过程中是非阻塞的;将数据从内核拷贝到用户空间是阻塞的。
IO 复用模型:
在非阻塞 IO 模型中,需要调用线程不断去轮询,检查数据有没有准备好,在这个过程中消耗了 CPU 大量的时间。如果有其他的线程帮助去检查多个线程数据的完成状态,这样会提高效率。
Linux 提供了select
、poll
和epoll
帮助我们。一个线程可以对多个 IO 端口进行监听,当 socket 有读写事件时分发到具体的线程进行处理。
select、poll、epoll 区别总结:
支持一个进程打开连接数 | IO 效率 | 消息传递方式 | |
---|---|---|---|
select | 32 位机器 1024 个,64 位 2048 个 | IO 效率低 | 内核需要将消息传递到用户空间,都需要内核拷贝动作 |
poll | 无限制,原因基于链表存储 | IO 效率低 | 内核需要将消息传递到用户空间,都需要内核拷贝动作 |
epoll | 有上限,但很大,2G 内存 20W 左右 | 只有活跃的 socket 才调用 callback,IO 效率高 | 通过内核与用户空间共享一块内存来实现 |
信号驱动式 I/O:
首先我们允许 Socket 进行信号驱动 IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO
信号,可以在信号处理函数中调用 I/O 操作函数处理数据。
异步 IO 模型:
相对于同步 IO,异步 IO 不是顺序执行。用户进程进行aio_read
系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到 socket 数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO 两个阶段,进程都是非阻塞的。
本文使用 mdnice 排版