いつもいろんなところでゼロコピーを見かけますが、そもそもゼロコピーとは何なのでしょうか?
次に、整理してみましょう。
コピーとは、コンピュータにおける I/O 操作、つまりデータの読み取りおよび書き込み操作を指します。コンピュータはソフトウェアとハードウェアを含む複雑なもので、ソフトウェアとは主にオペレーティング システム、ドライバー、アプリケーションを指します。CPU、メモリ、ハードディスクなどのハードウェアはたくさんあります。
このような複雑なデバイスは読み取りおよび書き込み操作を実行する必要がありますが、これは面倒で複雑です。
従来の I/O の読み取りおよび書き込みプロセス
ゼロ コピーを理解したい場合は、コンピューターが一般にデータを読み書きする方法を知る必要があります。私はこの状況を従来の I/O と呼んでいます。
データの読み書きを開始するのは、一般的に使用されているブラウザ、オフィス ソフトウェア、オーディオおよびビデオ ソフトウェアなどのコンピュータ内のアプリケーション プログラムです。
データのソースは通常、ハードディスク、外部記憶装置、またはネットワーク ソケットです (つまり、ネットワーク上のデータはネットワーク ポート + ネットワーク カードを通じて処理されます)。
このプロセスは本質的に複雑であるため、大学のコースでは、「オペレーティング システム」と「コンピュータ構成の原理」を通じてコンピュータ ソフトウェアとハードウェアを特に取り上げる必要があります。
読み取り操作プロセスの簡易版
これをこれほど詳しく説明する方法はないので、この読み取りと書き込みのプロセスを簡略化し、詳細の大部分を無視して、そのプロセスについてのみ説明します。
上の図は、アプリケーションによる読み取り操作のプロセスを示しています。
-
アプリケーションは最初に読み取り操作を開始し、データを読み取る準備が整います。
-
カーネルは、ハードディスクまたは外部ストレージからカーネル バッファにデータを読み取ります。
-
カーネルは、カーネル バッファからユーザー バッファにデータをコピーします。
-
アプリケーションは、処理のためにユーザー バッファ内のデータを読み取ります。
詳細な読み取りおよび書き込み操作プロセス
以下は、より詳細な I/O 読み取りおよび書き込みプロセスです。この図は非常に便利です。この図を使用して、I/O 操作の基本的かつ非常に重要な概念を説明します。
まずこの図を見てください、上の赤とピンクの部分が読み取り動作、下の青い部分が書き込み動作です。
すぐに少し混乱しているように見えても、問題はありません。次の概念を見れば明らかになるでしょう。
応用
オペレーティング システムにインストールされているさまざまなアプリケーションです。
システムカーネル
システムカーネルとは、CPUやバスなどのハードウェアデバイスだけでなく、プロセス管理、ファイル管理、メモリ管理、デバイスドライバ、システムコールなどの一連の機能を含む、一連のコンピュータの中核となるリソースの集合体です。
外部記憶装置
外部ストレージとは、ハードディスクや U ディスクなどの外部記憶媒体を指します。
カーネル状態
-
カーネル状態は、オペレーティング システム カーネルが実行されるモードであり、オペレーティング システム カーネルが特権命令を実行するとき、カーネル状態になります。
-
カーネル状態では、オペレーティング システム カーネルが最高の権限を持ち、コンピュータのすべてのハードウェア リソースと機密データにアクセスし、特権命令を実行し、システム全体の動作を制御できます。
-
カーネル状態は、オペレーティング システムにコンピュータ ハードウェアを管理および制御する機能を提供し、システム コール、割り込み、ハードウェア例外などのコア タスクの処理を担当します。
ユーザーモード
ここでのユーザーはアプリケーションとして理解できます。このユーザーはコンピュータのカーネルです。カーネルに対して、システム上のさまざまなアプリケーションがカーネルのリソースを呼び出す指示を出します。このとき、アプリケーションはカーネルのユーザーです。カーネル。
-
ユーザー モードはアプリケーションが実行されるモードであり、アプリケーションが通常の命令を実行するときはユーザー モードになります。
-
ユーザー モードでは、アプリケーションは独自のメモリ空間と限られたハードウェア リソースにのみアクセスでき、オペレーティング システムの機密データに直接アクセスしたり、コンピューターのハードウェア デバイスを制御したりすることはできません。
-
ユーザー モードは、アプリケーションが相互に隔離され、悪意のあるプログラムがシステムに影響を与えるのを防ぐ安全な動作環境を提供します。
モードスイッチ
セキュリティ上の理由から、コンピュータはカーネル状態とユーザー状態を区別します。アプリケーションはカーネル リソースを直接呼び出すことはできません。アプリケーションはカーネル状態に切り替えて、カーネルに呼び出しを行わせる必要があります。カーネルはリソースを呼び出した後、アプリケーションに戻ります。このとき、システムはユーザーモードに切り替えた後、アプリケーションはユーザーモードでのみデータを処理できます。
上記のプロセスでは、実際には 1 回の読み取りと 1 回の書き込みに対して 2 つのモード切り替えが必要になります。
カーネルバッファ
カーネル バッファとは、カーネルが直接使用するために特に使用されるメモリ内のメモリ空間を指します。これは、アプリケーションと外部ストレージ間のデータ対話のための中間メディアとして理解できます。
アプリケーションが外部データを読みたい場合は、ここからそれを読み取る必要があります。外部ストレージに書き込みたいアプリケーションは、カーネル バッファを経由する必要があります。
ユーザーバッファ
ユーザー バッファは、アプリケーションが直接読み書きできるメモリ空間として理解できます。アプリケーションはカーネルに対してデータを直接読み書きできないため、データを処理したい場合は、最初にユーザー バッファーを渡す必要があります。
ディスクバッファ
ページキャッシュ
-
PageCache は、Linux カーネルがファイル システムをキャッシュするメカニズムです。空きメモリを使用してファイル システムから読み取られたデータ ブロックをキャッシュし、ファイルの読み取りおよび書き込み操作を高速化します。
-
アプリケーションまたはプロセスがファイルを読み取るとき、データはまずファイル システムから PageCache に読み込まれます。同じデータを後で再度読み取る場合は、PageCache から直接取得できるため、ファイル システムに再度アクセスする必要がなくなります。
-
同様に、アプリケーションまたはプロセスがデータをファイルに書き込む場合、データはまず PageCache に一時的に保存され、次に Linux カーネルがそのデータを非同期でディスクに書き込むため、書き込み操作の効率が向上します。
データの読み取りと書き込みの操作プロセスについて説明します。
上記の概念を理解した後、フローチャートを振り返ると、より明確になります。
読み取り動作
-
まず、アプリケーションはカーネルに対して読み取り要求を開始し、このとき、ユーザー モードからカーネル モードに切り替わるモード切り替えが実行されます。
-
カーネルは、外部ストレージまたはネットワーク ソケットへの読み取り操作を開始します。
-
データをディスクバッファに書き込みます。
-
システム カーネルは、データをディスク バッファからカーネル バッファにコピーし、コピー (または一部) を 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 を使用すると、ユーザー バッファーとカーネル バッファーのコピーが使用されないため、多くのオーバーヘッドが節約されます。
共有メモリ
共有メモリ テクノロジを使用すると、アプリケーションとカーネルが同じメモリ領域を共有できるため、ユーザー モードとカーネル モードの間でのデータのコピーが回避されます。アプリケーションは共有メモリにデータを直接書き込むことができ、カーネルは転送のために共有メモリからデータを直接読み取ることができ、またその逆も可能です。
メモリ領域を共有することで、データの共有が実現されます。プログラム内の参照オブジェクトと同様に、実際にはポインタとアドレスです。
メモリマップされたファイル
メモリ マップされたファイルは、ディスク ファイルをアプリケーションのアドレス空間に直接マップし、アプリケーションがメモリ内でファイル データを直接読み書きできるようにします。このようにして、マップされたコンテンツへの変更は実際のファイルに直接反映されます。
ファイル データを転送する必要がある場合、カーネルは転送のためにメモリ マップ領域からデータを直接読み取ることができるため、ユーザー モードとカーネル モード間でのデータの追加コピーが回避されます。
共有メモリと何ら変わらないように見えますが、両者の実装方法は全く異なり、一方は共有アドレス、もう一方はマップされたファイルの内容です。
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 M バイトをターゲット ファイルに転送するのにかかる時間: 1.290 秒
」
FileChannel.transferTo() と transferFrom()
FileChannel は、ファイルの読み取り、書き込み、マッピングおよび操作のためのチャネルです。同時実行環境ではスレッドセーフです。ファイル チャネルは、FileInputStream、FileOutputStream、または RandomAccessFile の getChannel() メソッドに基づいて作成および開くことができます。FileChannel は、チャネル間の接続を確立することによってデータ転送を実装する、transferFrom() と transferTo() という 2 つの抽象メソッドを定義します。
これら 2 つの方法では、sendfile 方法が優先されますが、現在のオペレーティング システムがサポートしている限り、Linux や MacOS などの sendfile を使用してください。Windows などのシステムがサポートしていない場合は、メモリ マップド ファイルを使用して実装されます。
transferTo()
以下は、約 100M の PDF をコピーする transferTo の例です。私のシステムは 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 M バイトをターゲット ファイルに転送するのにかかる時間: 0.536 秒
」
からの転送()
以下は、約 100M の PDF をコピー中の transferFrom の例です。私のシステムは 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 M バイトをターゲット ファイルに転送するのにかかる時間: 0.603 秒
」
メモリマップされたファイル
Java の NIO は、実装を通じてメモリ マップ ファイル (Memory-mapped Files) もサポートします 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 M バイトをターゲット ファイルに転送するのにかかる時間: 0.663 秒
」
ディスク バッファは、ディスクから読み取られたデータ、またはデータがディスクに書き込まれる前にデータを保持するために使用されるコンピュータ メモリ内の一時記憶域です。ディスクI/O動作を最適化する仕組みで、メモリのアクセス速度が速いことを利用して、遅いディスクへの頻繁なアクセスを減らし、データの読み書きのパフォーマンスと効率を向上させます。