序文
前回の記事シリーズでは、NIO のバッファー、チャネル、セレクターについて体系的に整理しており、その内容によって Android の学生は Kan Dashan の敷居を越えることができるはずです。
NIO-1.0 には、拡張する価値のあるコンテンツがまだ 2 つあります。
- 散らばる・集まる
- ゼロコピー ゼロコピー
NIO-2.0 の内容は、最下層を深く掘り下げると確かに多くなりますが、Android の学生が山について話すために使用できる知識は比較的少ないため、1 つの記事に統合しました。
JDKの分散&収集
著者の注: この章で説明する内容は、オペレーティング システム レベルの内容ではなく、JDK の Scatter&Gather 機能を反映する内容であることに読者は注意する必要があります。
Scatter
_分散
に翻訳Gather
聚集
Scatter
NIO-1.0 のアプリケーションは、Scattering Reads
1 つのチャネルから複数のチャネルにデータを読み取ることを指しますBuffer
。
一般的なアプリケーションの方向性はデータ プロトコルを実装することですが、アプリケーション作成の観点から見ると、コーディングの考え方はより単純です。
たとえば、データ送信のためのデータパケットプロトコルについて合意します。各パケットには「10バイトのヘッダー」と「50バイトのボディ(パディングには不十分)」が含まれます。
//ignore imports
public class ScatterExample {
public static void main(String[] args) {
try (SocketChannel channel = SocketChannel.open()) {
channel.connect(new InetSocketAddress("localhost", 8080));
ByteBuffer headerBuffer = ByteBuffer.allocate(10);
ByteBuffer bodyBuffer = ByteBuffer.allocate(50);
ByteBuffer[] buffers = {headerBuffer, bodyBuffer};
long bytesRead = channel.read(buffers);
headerBuffer.flip();
bodyBuffer.flip();
// Process the data in buffers, hexString代指16进制两位补齐的字符串
System.out.println("Header: " + hexString(headerBuffer.array()));
System.out.println("Body: " + hexString(bodyBuffer.array()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
ヘッダー情報の識別、コンテンツのパッケージ化などを比較的簡単に実現できることは想像に難くありません。
もちろん、堅牢なプロトコルはそれほど単純ではありません。これは単なる例です。
分散読み取りは「固定長」の読み取り状況に適していることに注意してください。
同様に、Gather
NIO-1.0 のアプリケーションは、Gathering Writes
複数のバッファーから同じチャネルへのデータの連続書き込みを指します。
以下は簡単なデモです
//ignore imports
public class GatherExample {
public static void main(String[] args) {
try {
// 创建SocketChannel并连接到服务器
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));
// 准备多个缓冲区
ByteBuffer buffer1 = ByteBuffer.wrap("Hello,".getBytes());
ByteBuffer buffer2 = ByteBuffer.wrap(" World!".getBytes());
// 将多个缓冲区的数据写入到通道中
ByteBuffer[] buffers = {buffer1, buffer2};
socketChannel.write(buffers);
// 关闭SocketChannel
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
とは異なりScatter
、Gather
次の点で優れています动态长度
OS内のゼロコピー
作者按:诸君请注意,本文中讨论零拷贝、Zero-Copy时,均指操作系统中的相关内容,如与Java间存在关联,会单独说明
首先需要记住,零拷贝中并非没有拷贝,而是指新增各种机制,以减少主内存中不必要的拷贝,例如免去从内核态到用户态的拷贝。
发展历程中涉及到的技术:
- mmap
- sendfile
- splice 等
我们以“将文件系统中的文件通过网卡发出“为例,简单讨论。
传统IO
在JAVA中使用传统IO实现该需求时,即前文中所述经典IO,需要将文件系统中的文件内容,拷贝到应用内部,继而通过 Socket
从网卡发送.
包含两个关键操作:
read()
write()
流程图和数据拷贝过程如下图:
DMA: Direct Memory Access, 直接内存访问, 计算机总线架构提供的功能,它能使数据从附加设备(如磁盘驱动器)直接发送到计算机主板的内存上。
整个过程中,发生两次系统调用,共发生了 4次用户态与内核态的 上下文切换
,和4次数据拷贝:
第一次拷贝
,把磁盘上的数据拷贝到操作系统内核的缓冲区里,通过 DMA 搬运。第二次拷贝
,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,由 CPU 完成。第三次拷贝
,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,由 CPU 完成。第四次拷贝
,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,通过 DMA 搬运。
很显然,这一过程中,文件数据进入用户缓存区再离开,并没有附加必不可少的操作,上下文切换也比较多,存在改进的空间。
mmap取代read
使用 mmap
取代 read
后,整个过程包含两个关键操作:
mmap()
write()
先补充一张 虚拟内存
的原理示意图,如下:
使用虚拟地址取代物理地址后,多个虚拟内存可以指向同一个物理地址,虚拟内存表示的空间可以大于实际物理内存空间。
将 用户空间缓存区
中的部分虚拟内存 和 内核空间缓存区
中的部分虚拟内存,映射到同一物理内存区域时,可以减少不必要的拷贝。
在Linux中,mmap
将一个文件或一块设备内存(如设备寄存器)映射到进程的地址空间,实现 文件磁盘地址
或 设备io地址
与进程虚拟地址空间中一段虚拟地址建立映射,ioremap
实现向内核空间映射 。
使用该技术后,可减少一次CPU拷贝,但上下文切换次数不变,流程图和数据流示意图如下:
3次 数据拷贝
,系统调用次数不变,4次 上下文切换
。
java中使用Demo,从上层编码也能体现一二:
class Demo {
public static void main(String[] args) {
try {
// 获取文件
FileChannel readChannel = FileChannel.open(Paths.get("/..../test1.txt"), StandardOpenOption.READ);
MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size());
FileChannel writeChannel = FileChannel.open(Paths.get("/..../test2.txt"),
StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//数据传输
writeChannel.write(data);
readChannel.close();
writeChannel.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
sendfile 取代 mmap+write
上文提到,将文件数据读入用户空间内存后并没有附加必不可少的操作,那么就存在减少系统调用的优化空间。
Linux 2.1 版本开始,Linux 引入了 sendfile
替换 mmap+write
方式,简化流程。
流程图和数据流示意图如下:
共发生 3次 数据拷贝
,1次 系统调用
, 即2次 上下文切换
scatter/gather 优化的 sendfile方式
在 sendfile
中,还有CPU拷贝的过程,能不能进一步优化呢?
Linux 2.4 内核进行了优化,提供了带有 scatter/gather
的 sendfile
操作,可以减少拷贝的内容,注意,仍然有描述信息需要拷贝。
原理为:
- 目标:内核空间 Read Buffer 和 Socket Buffer 之间不做数据复制
- 将 Read Buffer 的内存地址、偏移量信息等拷贝到 Socket Buffer 中。参考虚拟内存的解决思路实现目标。
Read Buffer 的内存地址、偏移量信息等,即所谓描述信息
流程图和数据流示意图如下:
从内核缓冲区到网卡的DMA拷贝,为 Gather Copy
sendfile只适用于将数据从文件拷贝到套接字上,限定了它的使用范围。
Linux在2.6.17版本引入splice,用于在两个文件描述符中移动数据:
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h>
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
splice
在两个文件描述符之间移动数据,从 fd_in
拷贝长度为 len
的数据到 fd_out
,有一方必须是管道设备。
以java中的transferTo为例
在Java中,transferTo
底层使用零拷贝技术,但从上层编码并不能体现出来:
class Demo {
public static void main(String[] args) {
try {
FileChannel readChannel = FileChannel.open(Paths.get("/..../test1.txt"), StandardOpenOption.READ);
long len = readChannel.size();
long position = readChannel.position();
FileChannel writeChannel = FileChannel.open(Paths.get("/..../test2.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
readChannel.transferTo(position, len, writeChannel);
readChannel.close();
writeChannel.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
在 zulu版本的实现中:
class FileChannelImpl {
public long transferTo(long position, long count,
WritableByteChannel target)
throws IOException {
ensureOpen();
//ignore
long n;
// Attempt a direct transfer, if the kernel supports it
if ((n = transferToDirectly(position, icount, target)) >= 0)
return n;
// Attempt a mapped transfer, but only to trusted channel types
if ((n = transferToTrustedChannel(position, icount, target)) >= 0)
return n;
// Slow path for untrusted targets
return transferToArbitraryChannel(position, icount, target);
}
}
通过注释与方法名可以看出端倪,感兴趣的读者可继续追溯源码,本文不再展开。
NIO-2.0
操作系统中的AIO
还请读者诸君回忆一下 总纲 中提到的AIO,
很显然,这是操作系统中的AIO,例如,Windows 中提供了 IOCP(I/O CompletionPort,I/O完成端口)
Java中的NIO-2.0
回想一下Java中经典IO(BIO),和NIO-1.0,并没有在JDK层面提供开箱即用的异步IO编程框架。当然,这和Java的多线程编程、异步编程发展有关。
而在JDK1.7中,配套提供了异步IO的编程框架,同样置于nio包下,惯称为NIO-2.0,也有人称之为AIO。
注意,阅读其他文章时,对于 异步
、 阻塞
的讨论,要界定清楚讨论的对象和范围
在应用程序部分,发起IO调用和执行IO操作是异步的,但在JVM中,是否使用了操作系统异步IO则需要看操作系统平台,像Linux是通过 epoll
,模拟了AIO。
在 Java.nio.channels
包下增加了四个异步通道:
- AsynchronousSocketChannel
- AsynchronousServerSocketChannel
- AsynchronousFileChannel
- 非同期データグラムチャネル
Future
非同期プログラミングの場合は、次のように組み合わせます。
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.ByteBuffer;
import java.util.concurrent.Future;
class Demo {
public static void main(String[] args) {
Path file = Paths.get("/path/to/file.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(file, StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
Future<Integer> operation = channel.read(buffer, 0);
while (!operation.isDone()) {
// can do other work here while reading is in progress asynchronously
}
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println(new String(data));
channel.close();
}
}
もちろん、を使用することもできますCallback
これらの非同期チャネルは、Future
+ Callback
+ 线程池
+を介してファイル非同期ノンブロッキング IONative API
を実装します。
- ネイティブ API 部分は、オペレーティング システムの AIO に相当します。
Future
、、、非同期プログラミングのフレームワーク サポートを提供しますCallback
。线程池
エピローグ
Java IO シリーズはこれで終わりですが、Android プログラマとしては、Okio
結局のところ、OKHttp
Android プログラマの食い物に近い について、また記事を書きます。
少し前に仕事内容が変わったため、まだ馴染めずにいますが、この記事の下書きは一ヶ月以上保存してあり、その間何度も考えました。基本的なシリーズは確かに非常に退屈です。後で調整するには楽しいシリーズと考えるシリーズに頼るかもしれません。