1. 同步、异步、阻塞与非阻塞

  • 同步:所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。
  • 异步:所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。
  • 阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起,一直处于等待消息通知,不能够执行其他业务。函数只有在得到结果之后才会返回。
  • 非阻塞:非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。虽然表面上看非阻塞的方式可以明显的提高CPU的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的CPU执行时间能不能补偿系统的切换成本需要好好评估。

同步和阻塞的区别:对于同步调用来说,很多时候当前线程可能还是激活的,只是从逻辑上当前函数没有返回而已。此时,这个线程可能也会处理其他的消息。

  1. 如果这个线程在等待当前函数返回时,仍在执行其他消息处理,那这种情况就叫做同步非阻塞;
  2. 如果这个线程在等待当前函数返回时,没有执行其他消息处理,而是处于挂起等待状态,那这种情况就叫做同步阻塞;

所以同步的实现方式会有两种:同步阻塞、同步非阻塞;同理,异步也会有两种实现:异步阻塞、异步非阻塞。对于阻塞调用来说,则当前线程就会被挂起等待当前函数返回。

2. 五种 IO 模型

2.1 同步阻塞 IO(Blocking IO)

在这个 IO 模型中,服务端线程使用 read() 函数执行一个系统调用,这会导致应用程序阻塞。这里 read() 函数阻塞在两个阶段:

  1. 数据从网卡拷贝到内核缓冲区。
  2. 数据从内核缓冲区拷贝到用户缓冲区。

输入输出(I/O)管理 - 图1

优点:1. 能够及时返回数据,无延迟。2. 对内核开发者来说这是省事了。 缺点:如果客户端一直不发数据,那么服务端线程将会一直阻塞在 read 函数上不返回,也无法接受其他客户端连接。

2.2 同步非阻塞 IO(Non-bloking IO)

在网络 IO 时候,非阻塞 IO 也会进行系统调用,检查数据是否准备好。与阻塞 IO 不一样,非阻塞 IO 系统调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好会返回 -1。进程在返回之后,可以干点别的事情,过一段时间再发起系统调用。重复上面的过程,循环往复的进行系统调用。

  • 非阻塞 IO 的 read() 指的是数据未到达网卡或者到达网卡还没有拷贝到内核缓冲区之前,这个阶段是阻塞的。
  • 当数据已经到达内核缓冲区,此时调用 read() 函数仍然是阻塞的,需要等待数据从内核缓冲区拷贝到用户缓冲区才能返回。

输入输出(I/O)管理 - 图2

优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是『后台』可以有多个任务在同时执行)。 缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次 read 操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。

2.3 IO 多路复用(IO multiplexing)

2.3.1 IO 多路复用是什么?

IO多路复用是指内核一旦发现进程指定的一个或者多个 IO 条件准备读取,它就通知该进程。IO 多路复用适用如下场合:

  1. 当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用 I/O 复用。
  2. 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
  3. 如果一个 TCP 服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到 I/O 复用。
  4. 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
  5. 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

与多进程和多线程技术相比,I/O 多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

目前支持I/O多路复用的系统调用有 select,pselect,poll,epoll,I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但 select,pselect,poll,epoll 本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

2.3.2 select

select 是操作系统提供的系统调用函数,通过它,我们可以把一个文件描述符的数组发给操作系统, 让操作系统去遍历,确定哪个文件描述符可以读写, 然后告诉我们去处理:
输入输出(I/O)管理 - 图3
可以看出 select 有三个缺点:

  1. select 调用需要传入 fd 数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制)
  2. select 在内核层仍然是通过遍历的方式检查文件描述符的就绪状态,是个同步过程,只不过无系统调用切换上下文的开销。(内核层可优化为异步事件通知)
  3. select 仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的文件描述符,无需用户做无效的遍历)

整个 select 的流程图如下:
输入输出(I/O)管理 - 图4
可以看到,这种方式,既做到了一个线程处理多个客户端连接(文件描述符),又减少了系统调用的开销(多个文件描述符只有一次 select 的系统调用 + n 次就绪状态的文件描述符的 read 系统调用)。

2.3.2 poll

poll 和 select 是非常相似的,poll 相对于 select 的优化仅仅在于解决了文件描述符不能超过 1024 个的限制,它没有最大连接数的限制,原因是它是基于链表来存储的。

select 和 poll 都需要在返回后,通过遍历文件描述符来获取已经就绪的 socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降,因此不适合高并发场景。

2.3.3 epoll

针对 select 面临的三个问题,epoll 进行了改进:

  1. 内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分即可。
  2. 内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。
  3. 内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。

epoll.gif

注:如果没有大量的 idle-connection 或者 dead-connection,epoll 的效率并不会比 select/poll 高很多,但是当遇到大量的 idle-connection,就会发现 epoll 的效率大大高于 select/poll。

2.3.4 select、poll、epoll 的区别

  1. 单个进程能打开的最大连接数
    输入输出(I/O)管理 - 图6
  2. FD 剧增后带来的 IO 效率问题
    输入输出(I/O)管理 - 图7
  3. 消息传递方式
    输入输出(I/O)管理 - 图8

综上,在选择 select,poll,epoll 时要根据具体的使用场合以及这三种方式的自身特点:

  1. 表面上看 epoll 的性能最好,但是在连接数少并且连接都十分活跃的情况下,select 和poll 的性能可能比 epoll 好,毕竟 epoll 的通知机制需要很多函数回调。
  2. select 低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。

    2.4 信号驱动式 IO(Signal-Driven IO)

    首先我们允许 Socket 进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用I/O操作函数处理数据。过程如下图所示:
    输入输出(I/O)管理 - 图9

    2.5 异步非阻塞 IO(Asynchronous IO)

    相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
    输入输出(I/O)管理 - 图10

    2.6 五种 IO 模型总结

    输入输出(I/O)管理 - 图11

    3. Java 的 IO 机制

    3.1 Java 中的 IO 流分类有哪几种?

  • 按功能来分:输入流输出流
  • 按类型来分:字节流字符流

字节流按 8 位传输,以字节为单位输入输出数据。字符流按 16 位传输(char 为 16 位),以字符为单位输入输出数据。
输入输出(I/O)管理 - 图12
输入输出(I/O)管理 - 图13

3.2 BIO、NIO、AIO 有什么区别?

3.2.1 BIO(Block IO)

Block IO,同步阻塞 IO,就是平常使用的传统 IO。它的特点是模式简单使用方便,并发处理能力低。同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机 1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。

3.2.2 NIO(Non-Blocking IO)

Non-Blocking IO,同步非阻塞 IO,是传统 IO 的升级,提供了 Channel、Selector、Buffer 等抽象,实现了多路复用。以 Socket 使用来说,多路复用器通过不断轮询各个连接的状态,只有在 Socket 有流可读或者可写时,应用程序才需要去处理它,在线程的使用上,就不需要一个连接就必须使用一个处理线程了,而是只是有效请求时(确实需要进行I/O处理时),才会使用一个线程去处理,这样就避免了BIO模型下大量线程处于阻塞等待状态的情景。

Channel:Channel 和 BIO 中的 Stream 差不多是一个等级的,只不过 Stream 是单向的,而 Channel 是双向的,既可以用来读也可以用来写。

Buffer:在进行读操作时,需要使用 Buffer 分配空间,然后将数据从 Channel 中读入 Buffer 中。对于 Channel 的写操作,也需要将现有数据写入 Buffer 中,然后将 Buffer 写入 Channel 中。

Selector:Selector 是 NIO 实现多路复用的基础。Selector 运行单线程处理多个 Channel,如果一个应用打开了多个 Channel,但每个连接的流量都很低,使用 Selector 就会很方便。
输入输出(I/O)管理 - 图14

3.2.3 AIO (Asynchronous IO)

AIO 实现了异步非阻塞 IO,它和 NIO 的区别在于NIO 需要使用者线程不停地轮询 IO 对象,来确定是否有数据准备好。而 AIO 则是在数据准备好之后,才会通知数据使用者,这样使用者就不需要不停地轮询了。
输入输出(I/O)管理 - 图15
输入输出(I/O)管理 - 图16
输入输出(I/O)管理 - 图17

3.3 序列化与反序列化

序列化是将对象转换成字节流的过程,而反序列化的是将字节流恢复成对象的过程。

序列化与序列化主要解决的是数据的一致性问题。简单来说,就是输入数据与输出数据是一样的。

3.4 什么是 Netty?

Netty是一款提供异步的、事件驱动的网络应用程序框架和工具,是基于NIO客户端、服务器端的编程框架。NettyNIO的基础上,封装了 Java 原生的NIO API,屏蔽了繁杂的编程细节,让开发者可以更加专注于业务逻辑的实现。

4. DMA 技术的发展

4.1 什么是 DMA?

什么是 DMA 技术?简单理解就是:在进行 I/O 传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。目前支持 DMA 的硬件包括:网卡声卡显卡磁盘控制器等。

4.2 仅 CPU 方式的拷贝

输入输出(I/O)管理 - 图18

  1. 当用户进程需要读取数据时,调用read()从用户态陷入内核态,请求读取数据到自己的内存缓冲区中,进程进入阻塞状态。
  2. CPU 向磁盘发起 I/O 请求,然后返回,CPU 可以执行其他任务,磁盘收到之后开始准备数据。
  3. 磁盘控制器搬运数据到磁盘缓冲区之后,向 CPU 发起 I/O 中断。
  4. CPU 收到中断信号后,停下手头的工作,将磁盘缓冲区的数据一次一个字节地读进自己的寄存器(内核缓冲区),然后再把寄存器中的数据写入到内存(用户缓冲区),在这个数据传输的期间 CPU 是执行其他任务的。
  5. 完成之后 read() 返回,从内核态切换为用户态。

整个数据的传输都需要 CPU 亲自参与搬运数据的过程,而且在这个过程中 CPU 是不能做其他事情的。 简单地搬运几个字符数据那没问题,但是如果我们用千兆网卡或者硬盘传输大量数据的时候,都用 CPU 来搬运的话,肯定忙不过来。这个事情是很严重的,因此 DMA 技术就诞生了。

4.3 CPU&DMA 方式

输入输出(I/O)管理 - 图19

  1. 当用户进程需要读取数据时,调用read()从用户态陷入内核态,请求读取数据到自己的内存缓冲区中,进程进入阻塞状态。
  2. CPU 收到请求后,将 I/O 请求发送 DMA,然后返回,CPU 可以执行其他任务。DMA 进一步将 I/O 请求发送给磁盘控制器磁盘控制器收到之后开始准备数据。
  3. 磁盘控制器搬运数据到磁盘缓冲区之后,向 DMA 发起 I/O 中断。
  4. DMA 收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,此时不占用 CPU,CPU 可以执行其他任务。
  5. 当 DMA 读取了足够多的数据,就会发送中断信号给 CPU。
  6. CPU 收到 DMA 的信号,知道数据已经准备好,于是将数据从内核拷贝到用户空间,完成之后read()返回,从内核态切换为用户态。

虽然整个数据传输全程由 DMA 完成,但是 CPU 在这个过程中也是必不可少的,因为传输什么数据,从哪里传输到哪里,都需要 CPU 来告诉 DMA 控制器。 无论从仅 CPU 方式和 DMA&CPU 方式,都存在多次冗余数据拷贝内核态&用户态的切换

5. 零拷贝技术的发展

5.1 传统的数据传输

代码通常如下,一般会需要两个系统调用:

  1. read(file, tmp_buf, len);
  2. write(socket, tmp_buf, len);

输入输出(I/O)管理 - 图20

  • 首先,期间共发生了 4 次上下文切换,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。
  • 其次,还发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的,下面说一下这个过程:
    1. 第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。
    2. 第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。
    3. 第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。
    4. 第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。

传统模式下,我们只是搬运一份数据,结果却搬运了 4 次,涉及多次空间切换和数据冗余拷贝,过多的数据拷贝无疑会消耗 CPU 资源,大大降低了系统性能。 我们可以看到,传统的数据传输需要将数据从内核缓冲区复制到用户缓冲区,再从用户缓冲区复制到内核缓冲区。两次数据拷贝都需要CPU的参与,并且涉及用户态与内核态的多次切换,加重了CPU负担。而在文件传输的应用场景中,在用户空间我们并不会对数据『再加工』,所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的

5.2 零拷贝技术

目前来看,零拷贝技术的几个实现手段包括:mmap()+write()sendfilesendfile+DMA收集、splice等。
输入输出(I/O)管理 - 图21

5.2.1 mmap()+write()

传统的数据传输是read() + write()方式,read()系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,我们可以用mmap()替换read()系统调用函数。mmap()会直接把内核缓冲区里的数据映射到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。

  1. buf = mmap(file, len);
  2. write(sockfd, buf, len);

输入输出(I/O)管理 - 图22

  1. 应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区。
  2. 应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据。
  3. 最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。

通过使用 mmap() 来代替 read(),可以减少一次数据拷贝的过程。但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。

5.2.2 sendfile()

在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile()
输入输出(I/O)管理 - 图23
它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量复制数据的长度,返回值是实际复制数据的长度

  • 首先,它可以替代前面的 read()write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。
  • 其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换、1 次 CPU 拷贝、2 次 DMA 拷贝。如下图:
    输入输出(I/O)管理 - 图24

但这还不是真正的零拷贝技术,如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),我们可以进一步优化通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程

你可以在你的 Linux 系统通过下面这个命令,查看网卡是否支持 scatter-gather 特性:

  1. $ ethtool -k eth0 | grep scatter-gather
  2. scatter-gather: on

5.2.3 sendfile() + DMA

Linux 2.4 内核对 sendfile 系统调用进行优化,对于支持网卡支持 SG-DMA 技术的情况下, sendfile() 系统调用的过程发生了点变化,具体过程如下:

  1. 通过 DMA 将磁盘上的数据拷贝到内核缓冲区里。
  2. sendfile()内核缓冲区中对应的数据描述信息(文件描述符、地址偏移量等信息) 传到 socket缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区中,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝;

所以,在这个过程中只进行了 2 次状态切换和 2 次数据拷贝,如下图:
输入输出(I/O)管理 - 图25

这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。 这种方式有 2 次状态切换、0 次CPU拷贝、2 次DMA拷贝,但是仍然无法对数据进行修改,并且需要硬件层面 SG-DMA 的支持,并且sendfile()只能将文件数据拷贝到 socket 描述符上,有一定的局限性。

5.2.4 splice()

splice()系统调用是 Linux 在 2.6 版本引入的,其不需要硬件支持,并且不再限定于 socket 上,实现两个普通文件之间的数据零拷贝。
输入输出(I/O)管理 - 图26
splice()系统调用可以在内核缓冲区和 socket 缓冲区之间建立管道来传输数据,避免了两者之间的 CPU 拷贝操作。
输入输出(I/O)管理 - 图27

splice()也有一些局限,它的两个文件描述符参数中有一个必须是管道设备。

零拷贝技术是基于 PageCache 的,PageCache 会缓存最近访问的数据,提升了访问缓存数据的性能,同时,为了解决机械硬盘寻址慢的问题,它还协助 I/O 调度算法实现了 IO 合并与预读,这也是顺序读比随机读性能好的原因。这些优势,进一步提升了零拷贝的性能。

需要注意的是,零拷贝技术是不允许进程对文件内容作进一步的加工的,比如压缩数据再发送。

5.3 PageCache 在零拷贝中的作用

在文件传输过程中,第一步都是需要先把磁盘文件数据拷贝到『内核缓冲区』里,这个『内核缓冲区』实际上是磁盘高速缓存(PageCache。PageCache 主要用于缓存最近被访问的数据,当空间不足时淘汰最久未被访问的缓存。

所以,读磁盘数据的时候,优先在 PageCache 找,如果数据存在则可以直接返回;如果没有,则从磁盘中读取,然后缓存 PageCache 中。

还有一点,读取磁盘数据的时候,需要找到数据所在的位置,但是对于机械磁盘来说,就是通过磁头旋转到数据所在的扇区,再开始「顺序」读取数据,但是旋转磁头这个物理动作是非常耗时的,为了降低它的影响,PageCache 使用了「预读功能」

比如,假设 read 方法每次只会读 32 KB 的字节,虽然 read 刚开始只会读 0 ~ 32 KB 的字节,但内核会把其后面的 32~64 KB 也读取到 PageCache,这样后面读取 32~64 KB 的成本就很低,如果在 32~64 KB 淘汰出 PageCache 前,进程读取到它了,收益就非常大。

所以,PageCache 的优点主要是两个:

  • 缓存最近被访问的数据;
  • 预读功能;

这两个做法,将大大提高读写磁盘的性能。

但是,在传输大文件(GB 级别的文件)的时候,PageCache 会不起作用,那就白白浪费 DMA 多做的一次数据拷贝,造成性能的降低,即使使用了 PageCache 的零拷贝也会损失性能

这是因为如果你有很多 GB 级别文件需要传输,每当用户访问这些大文件的时候,内核就会把它们载入 PageCache 中,于是 PageCache 空间很快被这些大文件占满。

另外,由于文件太大,可能某些部分的文件数据被再次访问的概率比较低,这样就会带来 2 个问题:

  • PageCache 由于长时间被大文件占据,其他「热点」的小文件可能就无法充分使用到 PageCache,于是这样磁盘读写的性能就会下降了;
  • PageCache 中的大文件数据,由于没有享受到缓存带来的好处,但却耗费 DMA 多拷贝到 PageCache 一次;

所以,针对大文件的传输,不应该使用 PageCache,也就是说不应该使用零拷贝技术,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache,这样在高并发的环境下,会带来严重的性能问题。

5.4 大文件传输最好别用零拷贝

来看最初的例子,当调用read()方法读取文件时,进程实际上会阻塞在read()方法调用,因为要等待磁盘数据的返回,如下图:
输入输出(I/O)管理 - 图28
对于阻塞的问题,可以用异步 I/O 来解决,如下图:
输入输出(I/O)管理 - 图29
它把读操作分为两部分:

  • 前半部分,内核向磁盘发起读请求,但是可以不等待数据就位就可以返回,于是进程此时可以处理其他任务;
  • 后半部分,当内核将磁盘中的数据拷贝到进程缓冲区后,进程将接收到内核的通知,再去处理数据;

而且,我们可以发现,异步 I/O 并没有涉及到 PageCache,所以使用异步 I/O 就意味着要绕开 PageCache。绕开 PageCache 的 I/O 叫直接 I/O,使用 PageCache 的 I/O 则叫缓存 I/O。通常,对于磁盘,异步 I/O 只支持直接 I/O。

前面也提到,大文件的传输不应该使用 PageCache,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache。于是,在高并发的场景下,针对大文件的传输的方式,应该使用「异步 I/O + 直接 I/O」来替代零拷贝技术

直接 I/O 应用场景常见的两种:

  • 应用程序已经实现了磁盘数据的缓存,那么可以不需要 PageCache 再次缓存,减少额外的性能损耗。在 MySQL 数据库中,可以通过参数设置开启直接 I/O,默认是不开启;
  • 传输大文件的时候,由于大文件难以命中 PageCache 缓存,而且会占满 PageCache 导致「热点」文件无法充分利用缓存,从而增大了性能开销,因此,这时应该使用直接 I/O。

所以,传输文件的时候,我们要根据文件的大小来使用不同的方式:

  • 传输大文件的时候,使用「异步 I/O + 直接 I/O」;
  • 传输小文件的时候,则使用「零拷贝技术」;

例如在 Nginx 里,可以通过配置,设定一个文件大小阈值,针对大文件使用异步 IO 和直接 IO,而对小文件使用零拷贝。


参考

  1. 原来 8 张图,就可以搞懂「零拷贝」了
  2. 这是三歪看过最好的零拷贝Zero-Copy文章了!
  3. 终于明白了,一起彻底理解 I/O 多路复用
  4. 你管这破玩意叫 IO 多路复用?