ページキャッシュは、ハードドライブから最近アクセスされたページをキャッシュするため、同じデータへの以降のアクセスのシーク時間が短縮されます。ページキャッシュは、ハード ドライブからページに初めてアクセスしたときのパフォーマンスを向上させません。したがって、アプリがファイルを一度だけ読み取る場合は、ページキャッシュをバイパスすることをお勧めします。これは、O_DIRECTフラグを使用することで可能になります。これは、カーネルがこの特定のデータをページキャッシュとは見なさないことを意味します。キャッシュの競合を減らすことは、他のページ(繰り返しアクセスされる)がページキャッシュに保持される可能性が高くなることを意味します。これにより、キャッシュヒット率が向上し、パフォーマンスが向上します。
void ioReadOnceFile()
{
/* direct_fd と direct_f を使用すると、カーネル ページ キャッシュがバイパスされます。
*- direct_fd は低レベルのファイルディスクリプター
* - direct_f は fopen() が返すものと同様のファイルストリームである
* 注: 最適なサイズのバッファを決定するには、getpagesize() を使用します
*/
int direct_fd = open("ファイル名", O_DIRECT |O_RDWR);
ファイル *direct_f = fdopen(direct_fd, "w +");
/* 直接ディスク I/O はここで実行されます*/
fclose(f);
close(fd);
}
膨大なページ数で構成された大きなファイル(つまり、データベース)での読み取りの場合を考えてみましょう。その後にアクセスされたすべてのページはページキャッシュに入れられ、より多くのページが読まれるにつれて後でドロップアウトされます。これにより、キャッシュヒット率が大幅に低下します。この場合、ページキャッシュはパフォーマンスを向上させません。したがって、大きなファイルにアクセスするときは、ページキャッシュをバイパスする方が良いでしょう。
void ioLargeFile()
{
/* direct_fd と direct_f を使用すると、カーネル ページ キャッシュがバイパスされます。
*- direct_fd は低レベルのファイルディスクリプター
* - direct_f は fopen() が返すものと同様のファイルストリームである
* 注: 最適なサイズのバッファを決定するには、getpagesize() を使用します
*/
int direct_fd = open("largefile.bin", O_DIRECT |O_RDWR |O_LARGEFILE);
ファイル *direct_f = fdopen(direct_fd, "w +");
/* 直接ディスク I/O はここで実行されます*/
fclose(f);
close(fd);
}
io-schedulerは、ハード ドライブのキューに登録されるI/O操作の順序を最適化します。シーク時間はハード ドライブ上で最も重いペナルティであるため、ほとんどの I/O スケジューラはシーク時間を最小化しようとします。これは、エレベーター アルゴリズムのバリアントとして実装されます。つまり、多数のプロセスからランダムに順序付けられたリクエストを、ハード ドライブ上にデータが存在する順序に並べ替えるには、かなりのCPU時間が必要です。
複雑な操作を伴う特定のタスクは、CPUが膨大な量のデータを処理できる速度によって制限される傾向があります。バックグラウンドで実行されている複雑なI/Oスケジューラは、貴重なCPUサイクルを消費するため、システム パフォーマンスが低下する可能性があります。この場合、no-op などのより単純なアルゴリズムに切り替えると、CPU 負荷が軽減され、システム パフォーマンスが向上します
echo noop > /sys/block//queue/scheduler
これは最終的に仕事を成し遂げますが、間違いなく最適な方法ではありません。カーネルの観点から見ると、I/O 要求の最適なサイズはファイルシステムのブロックサイズ (つまり、ページサイズ) です。ファイルシステム(およびカーネルページキャッシュ)のすべてのI/Oはページ単位であるため、アプリがページサイズの倍数で転送を行うことも理にかなっています。また、現在、マルチ セグメント キャッシュがハード ドライブに組み込まれるようになっているため、ブロック長の倍数でI/Oを実行することで大きなメリットが得られます。
次のコマンドを使用して、最適なブロック サイズを決定できます
--printf="bs=%s optimal-bs=%S\n" --file-system /dev/
アプリが SYNC I/O 読み取りを開始すると、カーネルはデータの読み取り操作をキューに入れ、要求されたデータのブロック全体が読み戻された後にのみ戻ります。この期間中、カーネルはアプリケーションのプロセスをI/Oに対してブロック済みとしてマークします。他のプロセスでCPUを利用できるため、システム全体のパフォーマンスが向上します。
アプリが SYNC I/O 書き込みを開始すると、カーネルはデータの書き込み操作をキューに入れ、アプリケーションのプロセスをブロックされた I/O に入れます。残念ながら、これが意味することは、現在のアプリケーションのプロセスがブロックされ、この書き込み操作が完了するまで他の処理(またはI/O)を実行できないということです。
アプリが ASYNC I/O 読み取りを開始すると、read() 関数は通常、大きなデータ ブロックのサブセットを読み取った後に戻ります。アプリは、必要なデータ全体が読み込まれるまで、読み取る残りのデータのサイズでread()を繰り返し呼び出す必要があります。read が呼び出されるたびに、ユーザー空間とカーネルの間でコンテキストの切り替えが発生するため、オーバーヘッドが発生します。read()を繰り返し呼び出すタイトなループを実装すると、他のプロセスが使用できるはずのCPUサイクルが浪費されます。したがって、通常、次の read() が 0 以外のバイトの読み込みを返すまで、select() を使用してブロッキングを実装します。つまり、ASYNCはSYNC読み取りと同じようにブロックされます。
アプリが非同期 I/O 書き込みを開始すると、カーネルはページキャッシュ内の対応するページを更新し、ダーティとしてマークします。その後、コントロールはすぐにアプリに戻り、実行を続行できます。データは、後日、より最適な時間(低いCPU負荷)で、より最適な方法(シーケンシャルに束ねられた書き込み)でハード ドライブにフラッシュされます。
したがって、 SYNC 読み取り と ASYNC 書き込み は、カーネルが基になる I/O 要求の順序とタイミングを最適化できるため、一般的には適切な方法です。