いつもいろんなところでゼロコピーを見かけますが、ゼロコピーとは一体何なのでしょうか。
次に、整理してみましょう。
コピーとは、コンピュータの I/O 操作、つまりデータの読み取りおよび書き込み操作を指します。コンピュータはソフトウェアとハードウェアを含む複雑なものであり、ソフトウェアとは主にオペレーティング システム、ドライバー、アプリケーションを指します。CPU、メモリ、ハードディスクなどのハードウェアはたくさんあります。
このような複雑なデバイスは読み取りおよび書き込み操作を実行する必要がありますが、これは面倒で複雑です。
従来の I/O 読み取りおよび書き込みプロセス
ゼロ コピーを理解したい場合は、コンピューターが一般的にどのようにデータを読み書きするかを知る必要があります。私はこの状況を従来の I/O と呼んでいます。
データの読み書きを開始するのは、一般的に使用されているブラウザ、オフィス ソフトウェア、オーディオおよびビデオ ソフトウェアなどのコンピュータ内のアプリケーション プログラムです。
データのソースは通常、ハードディスク、外部記憶装置、またはネットワーク ソケットです (つまり、ネットワーク上のデータはネットワーク ポート + ネットワーク カードを通じて処理されます)。
このプロセスは本質的に非常に複雑であるため、大学の授業では、「オペレーティング システム」と「コンピュータ構成原理」を使用して、コンピュータのソフトウェアとハードウェアについて具体的に説明します。
読み取り操作プロセスの簡易版
そのような詳細について話す方法はないので、読み取りと書き込みのプロセスを単純化し、詳細の大部分を無視して、プロセスについてのみ話しましょう。
上図は、アプリケーションプログラムが読み取り操作を実行するプロセスです。
-
アプリケーションは最初に読み取り操作を開始し、データを読み取る準備が整います。
-
カーネルは、ハードディスクまたは外部ストレージからカーネル バッファにデータを読み取ります。
-
カーネルは、カーネル バッファからユーザー バッファにデータをコピーします。
-
アプリケーション プログラムは、処理のためにユーザー バッファ内のデータを読み取ります。
詳細な読み取りおよび書き込み操作プロセス
以下は、より詳細な I/O 読み取りおよび書き込みプロセスです。この図は非常に便利です。この図を使用して、I/O 操作の基本的かつ非常に重要な概念を説明します。
まずこの図を見てください、上の赤とピンクの部分が読み取り動作、下の青い部分が書き込み動作です。
すぐに少し混乱しているように見えても、問題はありません。次の概念を見れば明らかになるでしょう。
応用
オペレーティング システムにインストールされているさまざまなアプリケーションです。
システムカーネル
システムカーネルとは、CPUやバスなどのハードウェアデバイスだけでなく、プロセス管理、ファイル管理、メモリ管理、デバイスドライバ、システムコールなどの一連の機能を含む、一連のコンピュータの中核となるリソースの集合体です。
外部記憶装置
外部ストレージとは、ハードディスクや U ディスクなどの外部記憶媒体を指します。
カーネル状態
-
カーネル状態は、オペレーティング システム カーネルが実行されるモードであり、オペレーティング システム カーネルが特権命令を実行するとき、カーネル状態になります。
-
カーネル モードでは、オペレーティング システム カーネルが最高の権限を持ち、コンピュータのすべてのハードウェア リソースと機密データにアクセスし、特権命令を実行し、システムの全体的な動作を制御できます。
-
カーネル モードは、コンピューター ハードウェアを管理および制御するオペレーティング システムの機能を提供し、システム コール、割り込み、ハードウェア例外などのコア タスクの処理を担当します。
ユーザー状態
ここでいうユーザーとは、アプリケーションプログラムのことです。このユーザーとは、コンピュータのカーネルを指します。カーネルに対して、システム上のさまざまなアプリケーションが、カーネルのリソースを呼び出す命令を発行します。このとき、アプリケーションプログラムは、カーネルのユーザー。
-
ユーザーモードとは、アプリケーションプログラムが動作するモードであり、アプリケーションプログラムが通常の命令を実行するときは、ユーザーモードとなる。
-
ユーザー モードでは、アプリケーションは独自のメモリ空間と限られたハードウェア リソースにのみアクセスでき、オペレーティング システムの機密データに直接アクセスしたり、コンピューターのハードウェア デバイスを制御したりすることはできません。
-
ユーザー モードは、アプリケーションが相互に隔離され、悪意のあるプログラムがシステムに影響を与えるのを防ぐための安全な動作環境を提供します。
モードスイッチ
セキュリティのため、コンピュータはカーネルモードとユーザーモードを区別しています アプリケーションプログラムからカーネルリソースを直接呼び出すことはできません カーネルモードに切り替えた後は、カーネルに呼び出させます カーネルがリソースを呼び出すと、カーネルは元の状態に戻りますこのとき、システムはユーザーモードに切り替えた後、アプリケーションはユーザーモードでのみデータを処理できます。
上記のプロセスでは、1 回の読み取りと 1 回の書き込みに対して 2 回のモード切り替えが発生しました。
カーネルバッファ
カーネル バッファは、カーネルによる直接使用のために特別に使用されるメモリ内のメモリ空間を指します。これは、アプリケーションと外部ストレージ間のデータ対話のための中間メディアとして理解できます。
アプリケーションが外部データを読み取りたい場合は、ここから読み取る必要があります。外部ストレージに書き込みたいアプリケーションは、カーネル バッファを経由します。
ユーザーバッファ
ユーザーバッファは、アプリケーションプログラムが直接読み書きできるメモリ空間として理解できます。アプリケーション プログラムはカーネルに対してデータを直接読み書きできないため、データを処理する場合は、まずユーザー バッファを通過する必要があります。
ディスクバッファ
ディスク バッファは、ディスクに読み書きされるデータを書き込む前にステージングするために使用される、コンピュータ メモリ内の一時的な記憶領域です。ディスクI/O動作を最適化する仕組みで、メモリの高速アクセスを利用することで、低速ディスクへの頻繁なアクセスを減らし、データの読み書きのパフォーマンスと効率を向上させます。
ページキャッシュ
-
PageCache は、Linux カーネルがファイル システムをキャッシュするメカニズムです。空きメモリを使用してファイル システムから読み取られたデータ ブロックをキャッシュし、ファイルの読み取りおよび書き込み操作を高速化します。
-
アプリケーションまたはプロセスがファイルを読み取るとき、データはまずファイル システムから PageCache に読み込まれます。同じデータを後で再度読み取る場合は、PageCache から直接取得できるため、ファイル システムを再度訪問する必要がありません。
-
同様に、アプリケーションまたはプロセスがデータをファイルに書き込む場合、データは一時的に PageCache に保存され、その後 Linux カーネルがそのデータを非同期でディスクに書き込むため、書き込み操作の効率が向上します。
データの読み取りと書き込みの操作プロセスについて話しましょう
上記の概念を理解した後、フローチャートをもう一度見て、より明確になっているかどうかを確認してください。
読み取り操作
-
まず、アプリケーション プログラムがカーネルに対して読み取り要求を開始し、このときにユーザー モードからカーネル モードに切り替わるモード切り替えが実行されます。
-
カーネルは、外部ストレージまたはネットワーク ソケットへの読み取り操作を開始します。
-
データをディスクバッファに書き込みます。
-
システム カーネルは、データをディスク バッファからカーネル バッファにコピーし、その 1 つ (または一部) を PageCache にコピーします。
-
カーネルは、アプリケーションによる処理のためにデータをユーザー バッファにコピーします。このとき、別のモード切り替えが実行され、カーネル モードからユーザー モードに戻ります。
書き込み操作
-
アプリケーションはカーネルへの書き込み要求を開始し、この時点でモード切り替えが実行され、ユーザー モードからカーネル モードに切り替わります。
-
カーネルは、書き込まれるデータをユーザー バッファから PageCache にコピーし、同時にカーネル バッファにもデータをコピーします。
-
次に、カーネルはデータをディスク バッファー、つまりディスクに書き込むか、ネットワーク ソケットに直接書き込みます。
どこがボトルネックなのか
しかし、従来の I/O にはボトルネックがあり、それがゼロコピー テクノロジの出現の理由です。ボトルネックは何ですか? もちろん、それは遅すぎるパフォーマンスの問題です。特に同時実行性の高いシナリオでは、I/O パフォーマンスがスタックすることがよくあります。
では、どこで時間が無駄になっているのでしょうか?
データコピー
従来の I/O では、通常、データの転送には複数のデータ コピーが含まれます。データは、アプリケーションのユーザー バッファからカーネル バッファにコピーし、次にカーネル バッファからデバイスまたはネットワーク バッファにコピーする必要があります。これらのデータ コピー プロセスにより、複数のメモリ アクセスとデータの重複が発生し、大量の CPU 時間とメモリ帯域幅が消費されます。
ユーザーモードとカーネルモードの切り替え
データはカーネル バッファを通過するため、データはユーザー モードとカーネル モードの間で切り替えられ、切り替えプロセス中にコンテキストの切り替えが発生し、データ処理の複雑さと時間のオーバーヘッドが大幅に増加します。
各操作に費やされる時間は非常にわずかですが、同時実行の量が多い場合、合計すると膨大な量になり、これは決して小さなオーバーヘッドではありません。したがって、パフォーマンスを向上させ、オーバーヘッドを削減するには、上記の 2 つの問題から始める必要があります。
このとき、問題を解決するためにゼロコピー技術が登場しました。
ゼロコピーとは何ですか
問題はデータのコピーとモードの切り替えです。
ただし、I/O 操作であるため、データのコピーがないことは不可能であるため、コピーの数を減らす唯一の方法は、データをできるだけアプリケーション (ユーザー バッファー) の近くに保存することです。
ユーザー モードとカーネル モードを区別するさらに重要な理由は他にもありますが、純粋に I/O 効率のためにこの設計を変更することは不可能です。これにより、切り替えの数を最小限に抑えることができます。
ゼロコピーの理想的な状態はコピーを行わずにデータを操作することですが、これは必ずしもディスプレイケース内でコピー操作が存在しないことを意味するわけではなく、コピー操作の回数を最小限に抑えることを意味します。
ゼロコピーを達成するには、次の 3 つの側面から始める必要があります。
-
ディスク バッファからカーネル バッファなど、さまざまなストレージ領域でのデータ コピー操作を最小限に抑えます。
-
ユーザー モードとカーネル モード間の切り替えおよびコンテキスト切り替えの数を最小限に抑えます。
-
最初に操作する必要があるデータをキャッシュするなど、いくつかの最適化方法を使用します。カーネルの PageCache はこの目的に使用されます。
ゼロコピー ソリューションを実装する
ダイレクト メモリ アクセス (DMA)
DMA は、周辺機器 (ネットワーク アダプタ、ディスク コントローラなど) が CPU の介入なしにシステム メモリに直接アクセスできるようにするハードウェア機能です。データ送信中、DMA はメモリからペリフェラルへ、またはペリフェラルからメモリへデータを直接転送することができ、ユーザー モードとカーネル モードの間でデータが複数コピーされることを回避します。
DMA1
上の図に示すように、カーネルはデータ読み取り操作のほとんどを DMA コントローラーに引き渡し、空いたリソースは他のタスクの処理に使用できます。
ファイルを送信
一部のオペレーティング システム (Linux など) は、ネットワーク経由でファイルを転送するときにゼロコピーを実現するために、sendfile などの特別なシステム コールを提供します。sendfile を使用すると、アプリケーションはユーザー バッファーやカーネル バッファーを経由せずに、ファイル システムからネットワーク ソケットまたはターゲット ファイルにファイル データを直接転送できます。
sendfile を使用しない場合、A ファイルが B ファイルに書き込まれる場合。
-
ファイル A のデータは、最初にカーネル バッファにコピーし、次にカーネル バッファからユーザー バッファにコピーする必要があります。
-
次に、カーネルはユーザー バッファー内のデータをカーネル バッファーにコピーし、B ファイルに書き込むことができます。
sendfile を使用すると、ユーザー バッファーとカーネル バッファーのコピーが使用されないため、多くのオーバーヘッドが節約されます。
共有メモリ
共有メモリ テクノロジを使用すると、アプリケーションとカーネルが同じメモリ領域を共有できるため、ユーザー モードとカーネル モードの間でのデータのコピーが回避されます。アプリケーションは共有メモリにデータを直接書き込むことができ、カーネルは転送のために共有メモリからデータを直接読み取ることができ、またその逆も可能です。
データ共有はメモリ領域を共有することで実現します。プログラム内の参照オブジェクトと同様に、実際にはポインタとアドレスです。
メモリマップされたファイル
メモリマップトファイルは、ディスクファイルをアプリケーションプログラムのアドレス空間に直接マッピングすることで、アプリケーションプログラムがメモリ上のファイルデータを直接読み書きできるようにすることで、マッピング内容の変更が直接反映されます。実際のファイル。
ファイル データを転送する必要がある場合、カーネルは転送のためにメモリ マップ領域からデータを直接読み取ることができるため、ユーザー状態とカーネル状態の間で追加のデータのコピーが回避されます。
見た目は共有メモリと変わりませんが、両者の実装方法は全く異なり、1つは共有アドレス、もう1つはマップされたファイルの内容です。
Java がゼロコピーを実装する方法
Java 標準 IO ライブラリにはゼロコピー実装がなく、標準 IO は上記の従来のモードと同等です。ByteBuffer
Java によって導入された NIO にのみ、 やなど Channel
、ある程度のゼロコピーを実現できる新しい I/O クラスのセットが含まれています 。
ByteBuffer
: バイトデータを直接操作できるため、ユーザーモードとカーネルモードの間でのデータの重複を回避できます。
Channel
: ファイル チャネルまたはネットワーク チャネルから別のチャネルへのデータの直接転送をサポートし、ファイルとネットワークのゼロコピー転送を実現します。
これら 2 つのオブジェクトを NIO の API と組み合わせることで、Java でゼロコピーを実現できます。
まず、新しい NIO と比較するために、従来の IO を使用するメソッドを作成します このプログラムの目的は非常に単純で、約 100M の PDF ファイルをあるディレクトリから別のディレクトリにコピーすることです。
public static void ioCopy() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileInputStream fis = new FileInputStream(sourceFile);
FileOutputStream fos = new FileOutputStream(targetFile)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
System.out.println("传输 " + formatFileSize(sourceFile.length()) + " 字节到目标文件");
} catch (IOException e) {
e.printStackTrace();
}
}
以下は、このコピー プログラムの実行結果です (109.92M、所要時間は 1.29 秒)。
「109.92 MB を宛先ファイルに転送するのにかかる時間: 1.290 秒
」
FileChannel.transferTo() と transferFrom()
FileChannel は、ファイルの読み取り、書き込み、マッピング、および操作のためのチャネルであり、同時実行環境ではスレッドセーフです。FileInputStream、FileOutputStream、または RandomAccessFile に基づく getChannel() メソッドは、ファイル チャネルを作成して開くことができます。FileChannel は、チャネル間の接続を確立することによってデータ転送を実装する、transferFrom() と transferTo() という 2 つの抽象メソッドを定義します。
これら 2 つの方法では、sendfile を使用することをお勧めしますが、現在のオペレーティング システムがサポートしている限り、Linux や MacOS などの sendfile を使用してください。Windows などのシステムがサポートしていない場合は、メモリ マップ ファイルの形式で実装されます。
transferTo()
以下は transferTo の例です。まだ約 100M の PDF をコピーしています。私のシステムは MacOS です。
public static void nioTransferTo() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long transferredBytes = sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}
所要時間はわずか 0.536 秒で、2 倍の速さになりました。
「109.92 MB を宛先ファイルに転送するのにかかる時間: 0.536 秒
」
からの転送()
以下は transferFrom の例です。まだ約 100M の PDF をコピーしています。私のシステムは MacOS です。
public static void nioTransferFrom() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long transferredBytes = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}
実行時間:
「109.92 MB を宛先ファイルに転送するのにかかる時間: 0.603 秒
」
メモリマップされたファイル
Java の NIO は、実装を通じてメモリ マップ ファイル (メモリ マップ ファイル) もサポートします FileChannel.map()
。
以下は例で FileChannel.map()
、まだ約 100M の PDF をコピーしています。私のシステムは MacOS です。
public static void nioMap(){
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long fileSize = sourceChannel.size();
MappedByteBuffer buffer = sourceChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
targetChannel.write(buffer);
System.out.println("传输 " + formatFileSize(fileSize) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}
実行時間:
「109.92 MB を宛先ファイルに転送するのにかかる時間: 0.663 秒
」