以前の関連する質問で言及した提案を拡張すると、次の (Linux 固有であり、移植性がない) スキームは非常に確実に機能するはずです。
を使用してデータグラム ソケット ペアを設定し、 のsocketpair(AF_UNIX, SOCK_DGRAM, 0, &sv)
シグナル ハンドラを設定しSIGSEGV
ます。SIGBUS
(他のプロセスがデータ ファイルを切り捨てる可能性がある場合でも、心配する必要はありません。)
シグナル ハンドラーは、ソケットの最後にwrite()
を書き込むために使用します。size_t addr = siginfo->si_addr;
次に、シグナルハンドラはread()
、書き込み先のソケットから 1 バイトを取得し (ブロッキング -- これは基本的に信頼できるものですsleep()
-- 処理することを忘れないでEINTR
ください)、戻ります。
複数のスレッドが同時にまたはほぼ同時にフォルトした場合でも、競合状態は発生しないことに注意してください。信号は、マッピングが修正されるまで再生成されます。
ソケット通信になんらかの問題がある場合は、 with を使用sigaction()
し.sa_handler = SIG_DFL
てデフォルトのSIGSEGV
シグナル ハンドラを復元し、同じシグナルが再生成されたときにプロセス全体が通常どおり終了するようにすることができます。
別のスレッドがソケット ペアのもう一方の端からエラーが発生したアドレスを読み取り、SIGSEGV
必要なすべてのマッピングとファイル I/O を実行し、最後にソケット ペアの同じ端に 0 バイトを書き込み、実際のシグナル ハンドラにマッピングを知らせます。今すぐ修正する必要があります。
これは基本的に、実際のシグナル ハンドラの欠点のない「実際の」シグナル ハンドラです。マッピングが修正されるまで、同じスレッドが同じシグナルを再生成し続けるため、別のスレッドとSIGSEGV
シグナルの間の競合状態は無関係であることを忘れないでください。
元のデータ ファイルのサイズに一致する1 つPROT_NONE
の ,マッピングを用意します。MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE
実際の RAM のコストを削減するMAP_NORESERVE
には (マッピングには RAM も SWAP も使用しませんが、ギガバイトのデータの場合、ページ テーブル エントリ自体にかなりの RAM が必要です)、使用することもできMAP_HUGETLB
ます。巨大なページを使用するため、エントリが大幅に少なくなりますが、通常のページサイズの穴が最終的にマッピングにパンチされた場合に問題があるかどうかはわかりません。おそらくずっと huge ページを使わなければならないでしょう。
これは、「ユーザー空間」がデータにアクセスするために使用する「完全な」マッピングです。
元の状態またはダーティな (それぞれ) 変換されたデータの1 つPROT_READ
またはPROT_READ | PROT_WRITE
のマッピングがあります。MAP_PRIVATE | MAP_ANONYMOUS
「ユーザー空間」がほぼ常にデータを変更する場合、変換されたデータを常に「ダーティ」として扱うことができますが、それ以外の場合は、最初に変換されたデータPROT_READ
のみをマッピングすることで、変更されていないデータの不要な書き込みを回避できます。エラーが発生した場合は、ダーティ マークをmprotect()
付けPROT_READ | PROT_WRITE
ます (そのため、変換してファイルに保存し直す必要があります)。これら 2 つの段階をそれぞれ「クリーン」マッピングと「ダーティ」マッピングと呼びます。
専用スレッドが「クリーン」ページの「フル」マッピングに穴を開けると、最初mmap(NULL, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, ...)
に適切なサイズの新しいメモリ領域read()
、目的のデータファイルからのデータがそこにデータを変換し、mprotect(..., PROT_READ)
分離する場合はデータを変換します「クリーン」および「ダーティ」マッピング、そして最後mremap(newly_mapped, size, size, MREMAP_MAYMOVE | MREMAP_FIXED, new_ptr)
に「フル」マッピングのセクションです。
事故を避けるために、これらの s および他の場所での呼び出しの期間中に取得されるglobal を使用して、カーネルが誤って間違ったスレッドに穴を開けないようにする必要があることに注意してください。ミューテックスは、他のスレッドが間に入るのを防ぎます。(そうしないと、カーネルが別のスレッドから要求された小さなマップを一時的な穴に配置する可能性があります。)pthread_mutex_t
mremap()
mmap()
「クリーン」ページを破棄するときmmap(NULL, length, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0)
は、適切な長さの新しいマップを取得するために呼び出してから、上記のグローバル ミューテックスを取得mremap()
し、「クリーン」ページに対するその新しいマップを取得します。カーネルは暗黙的にmunmap()
. ミューテックスのロックを解除します。
「汚れた」ページを破棄するときは、適切な長さの2 つのmmap(NULL, length, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0)
新しいマップを取得するために *2 回呼び出します*。次に、上記のグローバル ミューテックスと、新しいマッピングの最初のダーティ データを取得します。(基本的に、ダーティ データを移動する適切なアドレスを見つけるためにのみ使用されました。) 次に、ダーティ データが存在していた場所への新しいマッピングの 2 番目。ミューテックスのロックを解除します。mremap()
mremap()
別のスレッドを使用して障害条件を処理すると、非同期シグナルセーフ関数の問題がすべて回避されます。read()
、write()
、およびsigaction()
はすべて非同期シグナルセーフです。
pthread_mutex_t
カーネルが最近移動されたホール (mremap()
メモリー領域から ped) を別のスレッドに渡すケースを回避するために必要なグローバルは 1 つだけです。内部データ構造 (複数の同時ファイル マッピングをサポートする場合はポインター チェーン) を保護するためにも使用できます。
競合状態があってはなりません (他のスレッドが上記のミューテックスによって処理されるmmap()
orを使用する場合を除く)。mremap()
「ダーティ」ページまたはページ グループが移動されると、変換および保存される前に、他のスレッドからアクセスできなくなります。別のスレッドによる完全な同時アクセスであっても、完全に処理する必要があります。ページは単純にファイルから再読み取りされ、再変換されます。(これが頻繁に発生する場合は、最近保存したページ グループをキャッシュすることをお勧めします。)
オーバーヘッドを減らすために、単一のページではなく、たとえば 2M 以上の大きなページ グループを使用することをお勧めします。最適なサイズはアプリケーションのアクセス パターンによって異なりますが、ヒュージ ページ サイズ (アーキテクチャでサポートされている場合) は非常に良い出発点です。
データ構造がページまたはページ グループに対応していない場合は、完全に変換された最初と最後のレコード (ページまたはページ グループ内に部分的にしか存在しない) をキャッシュする必要があります。通常、これにより、ストレージ形式への変換がはるかに簡単になります。
ファイル内の典型的なアクセス パターンを知っている、または検出できる場合は、おそらく を使用posix_fadvise()
してカーネルに通知する必要があります。POSIX_FADV_WILLNEED
そしてPOSIX_FADV_DONTNEED
最も便利です。これは、カーネルが実際のデータ ファイルの不要なページをページ キャッシュに保持しないようにするのに役立ちます。
最後に、ダーティ レコードを非同期的に変換してディスクに書き戻すための 2 つ目の特別なスレッドを追加することを検討してください。最初のスレッドが 2 番目のスレッドによってまだディスクに書き込まれているレコードを再読み取りしようとしているときに、2 つのスレッドが混乱しないように注意すれば、他にも問題はないはずですが、非同期書き込みいずれにせよ I/O バウンドでない限り、または RAM が本当に不足している場合 (比較的言えば) でない限り、ほとんどのアクセス パターンでスループットが向上する可能性があります。
別のメモリ マップの代わりにread()
andを使用する理由 write()
必要な仮想メモリ構造のカーネル内オーバーヘッドのため。