Netty のおかげで、私は非同期 IO についていくつかの知識を学びました。JAVA の NIO は、元の IO の補足であり、主に JAVA での IO の基本的な実装原理を記録し、Zerocopy テクノロジを紹介します。
IO は実際には、データが常にバッファの内外に移動される (バッファが使用される) ことを意味します。たとえば、ユーザー プログラムが読み取り操作を開始して「syscall read」システム コールが発生すると、データはバッファに移動されます。ユーザーが書き込み操作を開始して「syscall write」システム コールが発生した場合、データはバッファに移動されます。バッファ内のデータは移動されます (ネットワークに送信するか、ディスク ファイルに書き込みます)
上記のプロセスは単純に見えますが、基盤となるオペレーティング システムの実装方法と実装の詳細は非常に複雑です。実装方法が異なるからこそ、通常時のファイル転送(とりあえず通常のIOと呼ぶことにします)の実装方法もあれば、大容量ファイル転送や一括ビッグデータ転送の実装方法もあり、ゼロコピーテクノロジー。
IO プロセス全体の流れは次のとおりです。
1) プログラマは、バッファを作成するコードを作成します (このバッファはユーザー バッファです)。次に、while ループで read() メソッドを呼び出してデータを読み取ります (「syscall read」システム コールをトリガーします)
byte[] b = new byte[4096]; while((read = inputStream.read(b))>=0) { total = total + read; // other code…. }
2) read() メソッドが実行されると、多くの操作が実行されます。
①カーネルは、ディスク コントローラに次のコマンドを送信します。「ディスク上の特定のディスク ブロックのデータを読み取りたい」というものです。 –kernel は、ディスク コントローラー ハードウェアにコマンドを発行して、ディスクからデータをフェッチします。
②DMA の制御下で、ディスク上のデータをカーネル バッファーに読み取ります。 –ディスク コントローラは、DMA によってカーネル メモリ バッファにデータを直接書き込みます。
##③カーネルは、データをカーネル バッファからユーザー バッファにコピーします。 –kernel は、カーネル空間の一時バッファからデータをコピーします。ここでのユーザー バッファは、作成したコードの new の byte[] 配列である必要があります。 上記の手順から何が分析できるでしょうか? ⓐオペレーティング システムにとって、JVM はユーザー モード空間にある単なるユーザー プロセスです。ユーザー空間のプロセスは、基礎となるハードウェアを直接操作できません。 IO 操作には、ディスクなどの基盤となるハードウェアを操作する必要があります。したがって、IO 操作はカーネル (割り込み、トラップ) の助けを借りて完了する必要があります。つまり、ユーザー モードからカーネル モードへの切り替えが必要になります。 ⓑ新しい byte[] 配列のコードを記述するとき、通常は「任意のサイズ」の配列を「自由に」作成します。たとえば、新しいバイト[128]、新しいバイト[1024]、新しいバイト[4096]...ただし、ディスク ブロックを読み取る場合、ディスクにアクセスしてデータを読み取るたびに、読み取りが行われるわけではありません。データのサイズは任意ですが、一度に 1 つのディスク ブロックまたは複数のディスク ブロックを読み取ります (これは、ディスク操作にアクセスするコストが非常に高く、局所性の原則も信じているためです)。 「中間バッファ」 – つまり、カーネルバッファ。まずディスクからデータをカーネル バッファに読み取り、次にデータをカーネル バッファからユーザー バッファに移動します。 これが、最初の読み取り操作が遅いと常に感じられる理由ですが、その後の読み取り操作は非常に高速です。後続の読み取り操作では、読み取る必要があるデータがカーネル バッファーにある可能性が高いため、現時点では、カーネル バッファー内のデータをユーザー バッファーにコピーするだけで済み、基になるデータは関係ありません。ディスクの読み込み操作はもちろん高速です。The kernel tries to cache and/or prefetch data, so the data being requested by the process may already be available in kernel space. If so, the data requested by the process is copied out. If the data isn’t available, the process is suspended while the kernel goes about bringing the data into memory.
データが利用できない場合、プロセスは一時停止され、カーネルがディスクからカーネル バッファにデータをフェッチするまで待つ必要があります。
As the user process touches the mapped memory space, page faults will be generated automatically to bring in the file data from disk. If the user modifies the mapped memory space, the affected page is automatically marked as dirty and will be subsequently flushed to disk to update the file.
这就是是JAVA NIO中提到的内存映射缓冲区(Memory-Mapped-Buffer)它类似于JAVA NIO中的直接缓冲区(Directed Buffer)。MemoryMappedBuffer可以通过java.nio.channels.FileChannel.java(通道)的 map方法创建。
使用内存映射缓冲区来操作文件,它比普通的IO操作读文件要快得多。甚至比使用文件通道(FileChannel)操作文件 还要快。因为,使用内存映射缓冲区操作文件时,没有显示的系统调用(read,write),而且OS还会自动缓存一些文件页(memory page)
zerocopy技术介绍
看完了上面的IO操作的底层实现过程,再来了解zerocopy技术就很easy了。IBM有一篇名为《Efficient data transfer through zero copy》的论文对zerocopy做了完整的介绍。感觉非常好,下面就基于这篇文来记录下自己的一些理解。
zerocopy技术的目标就是提高IO密集型JAVA应用程序的性能。在本文的前面部分介绍了:IO操作需要数据频繁地在内核缓冲区和用户缓冲区之间拷贝,而zerocopy技术可以减少这种拷贝的次数,同时也降低了上下文切换(用户态与内核态之间的切换)的次数。
比如,大多数WEB应用程序执行的一项操作就是:接受用户请求—>从本地磁盘读数据—>数据进入内核缓冲区—>用户缓冲区—>内核缓冲区—>用户缓冲区—>socket发送
数据每次在内核缓冲区与用户缓冲区之间的拷贝会消耗CPU以及内存的带宽。而zerocopy有效减少了这种拷贝次数。
Each time data traverses the user-kernel boundary, it must be copied, which consumes CPU cycles and memory bandwidth.
Fortunately, you can eliminate these copies through a technique called—appropriately enough —zero copy
那它是怎么做到的呢?
我们知道,JVM(JAVA虚拟机)为JAVA语言提供了跨平台的一致性,屏蔽了底层操作系统的具体实现细节,因此,JAVA语言也很难直接使用底层操作系统提供的一些“奇技淫巧”。
而要实现zerocopy,首先得有操作系统的支持。其次,JDK类库也要提供相应的接口支持。幸运的是,自JDK1.4以来,JDK提供了对NIO的支持,通过java.nio.channels.FileChannel类的transferTo()方法可以直接将字节传送到可写的通道中(Writable Channel),并不需要将字节送入用户程序空间(用户缓冲区)
You can use the transferTo()method to transfer bytes directly from the channel on which it is invoked to
another writable byte channel, without requiring data to flow through the application
下面就来详细分析一下经典的web服务器(比如文件服务器)干的活:从磁盘中中读文件,并把文件通过网络(socket)发送给Client。
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
从代码上看,就是两步操作。第一步:将文件读入buf;第二步:将 buf 中的数据通过socket发送出去。但是,这两步操作需要四次上下文切换(用户态与内核态之间的切换) 和 四次拷贝操作才能完成。
①第一次上下文切换发生在 read()方法执行,表示服务器要去磁盘上读文件了,这会导致一个 sys_read()的系统调用。此时由用户态切换到内核态,完成的动作是:DMA把磁盘上的数据读入到内核缓冲区中(这也是第一次拷贝)。
②第二次上下文切换发生在read()方法的返回(这也说明read()是一个阻塞调用),表示数据已经成功从磁盘上读到内核缓冲区了。此时,由内核态返回到用户态,完成的动作是:将内核缓冲区中的数据拷贝到用户缓冲区(这是第二次拷贝)。
③第三次上下文切换发生在 send()方法执行,表示服务器准备把数据发送出去了。此时,由用户态切换到内核态,完成的动作是:将用户缓冲区中的数据拷贝到内核缓冲区(这是第三次拷贝)
④第四次上下文切换发生在 send()方法的返回【这里的send()方法可以异步返回,所谓异步返回就是:线程执行了send()之后立即从send()返回,剩下的数据拷贝及发送就交给底层操作系统实现了】。此时,由内核态返回到用户态,完成的动作是:将内核缓冲区中的数据送到 protocol engine.(这是第四次拷贝)
这里对 protocol engine不是太了解,但是从上面的示例图来看:它是NIC(NetWork Interface Card) buffer。网卡的buffer???
下面这段话,非常值得一读:这里再一次提到了为什么需要内核缓冲区。
コードのコピー
中間カーネル バッファの使用 (データを
ユーザー バッファに直接転送するのではなく) は非効率に見えるかもしれませんが、中間カーネル バッファはパフォーマンスを向上させるために
プロセスに導入されました。読み取り側で中間バッファを使用すると、アプリケーションがカーネル バッファが保持するほど多くのデータを要求していない場合に、カーネル バッファが「先読みキャッシュ」として機能できるようになります。要求されたデータ量がカーネル バッファ サイズより小さい場合のパフォーマンス。書き込み側の中間バッファにより、書き込みが非同期で完了できます。重要な点は、カーネル バッファによってパフォーマンスが向上することです。はぁ?奇妙ではありませんか?なぜなら、カーネルバッファ(中間バッファ)の導入により、データが往復コピーされ、効率が低下するということは前にも述べたとおりです。
まず、カーネル バッファーによってパフォーマンスが向上するといわれている理由を見てみましょう。
読み取り操作の場合、カーネル バッファは「先読みキャッシュ」に相当します。ユーザー プログラムが一度にデータの一部のみを読み取る必要がある場合、オペレーティング システムはまず大きなデータ ブロックを読み取ります。ユーザー プログラムは、バッファのほんの一部を取り出すだけです (128B バイトの配列を新しく作成できます! new byte[128])。ユーザー プログラムが次回データを読み取るときは、カーネル バッファから直接データをフェッチできるため、オペレーティング システムは再度ディスクにアクセスする必要がありません。それは、ユーザーが読みたいデータがすでにカーネル バッファーにあるからです。これは、前述した理由でもあります。つまり、後続の読み取り操作 (read() メソッド呼び出し) が最初の操作よりも明らかに高速になるのです。この観点から見ると、カーネル バッファにより読み取り操作のパフォーマンスが向上します。
書き込み操作を見てみましょう。書き込み操作は「非同期」で実行できます (非同期書き込み)。つまり、write(dest[]) の場合、ユーザー プログラムはオペレーティング システムに dest[] 配列の内容を XX ファイルに書き込むように指示し、write メソッドが戻ります。オペレーティング システムは、ユーザー バッファー (dest[]) の内容をバックグラウンドでカーネル バッファーにサイレントにコピーし、カーネル バッファーのデータをディスクに書き込みます。その後、カーネル バッファがいっぱいでない限り、ユーザーの書き込み操作はすぐに戻ることができます。これは、非同期ディスク ブラッシング戦略である必要があります。
(実際、これです。過去の複雑な問題は、同期 IO、非同期 IO、ブロッキング IO、および非ブロッキング IO の違いがもはやあまり意味をなさないということでした。これらの概念は、単に見るだけです。問題は観点が異なるだけです。ブロッキングと非ブロッキングはスレッド自体に関するものであり、同期と非同期はスレッドとそれに影響を与える外部イベントに関するものです。このシリーズの記事へ: システム間通信 (3) - IO 通信モデルと JAVA 実践パート 1]
カーネル バッファーは非常に強力で完璧だと言いましたが、なぜゼロコピーが必要なのでしょうか? ? ?
残念ながら、要求されたデータのサイズ
がカーネル バッファ サイズよりもかなり大きい場合、データがディスク、カーネル バッファ、## の間で複数回コピーされる場合、このアプローチ自体がパフォーマンスのボトルネックになる可能性があります。ゼロコピーは、これらの冗長なデータコピーを排除することでパフォーマンスを向上させます。#いよいよゼロコピーが登場します。転送する必要があるデータがカーネル バッファのサイズよりもはるかに大きい場合、カーネル バッファがボトルネックになります。これが、ゼロコピー テクノロジーが大規模ファイルの転送に適している理由です。カーネルバッファがボトルネックになったのはなぜですか? ――やはり、送信データ量が多すぎて「バッファ」として機能しなくなったことが大きな理由だと思います。
ゼロコピー テクノロジーがファイル転送をどのように処理するかを見てみましょう。
transferTo() メソッドが呼び出されると、ユーザー モードからカーネル モードに切り替わります。完了したアクションは次のとおりです。 DMA はディスクから読み取りバッファにデータを読み取ります (最初のデータ コピー)。次に、カーネル空間内で、データが読み取りバッファからソケット バッファにコピーされ (2 番目のデータ コピー)、最後にデータがソケット バッファから NIC バッファにコピーされます (3 番目のデータ コピー)。その後、カーネルモードからユーザーモードに戻ります。
上記のプロセス全体には、3 つのデータ コピーと 2 つのコンテキスト スイッチだけが含まれます。データのコピーが 1 つだけ保存されているように感じます。ただし、ここではユーザー空間バッファーは関係しません。
3 つのデータ コピーのうち、CPU 介入が必要なコピーは 1 つだけです。 (2 番目のコピー)、以前の従来のデータ コピーには 4 回のコピーが必要で、3 回のコピーには CPU 介入が必要です。
これは改善です。コンテキスト スイッチの数が 4 から 2 に減り、データ コピーの数が
4 から 3 に減りました (そのうち 1 つだけが CPU に関係します)ゼロコピー テクノロジーがこのステップのみを達成できるのであれば、それはまあまあです。
Linux カーネル 2.4 以降では、この要件に対応するためにソケット バッファ記述子が変更されました。複数のコンテキスト スイッチを減らすだけでなく、
CPU の関与を必要とする重複したデータ コピーを排除します。つまり、基盤となるネットワーク ハードウェアとオペレーティング システムがそれをサポートしている場合、データ コピーの数をさらに減らすことができます。 CPU介入時間。
コピーとコンテキスト スイッチは 2 つだけです。さらに、これら 2 つのコピーは DMA コピーであり、CPU の介入を必要としません (より厳密に言えば、完全に必要というわけではありません)。
Node.js_node.js のノンブロッキング IO とイベント ループの概要
以上がJAVA IO と NIO についての理解の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。