7

仮想メモリ システムを使用して、取得した数値データに対して透過的なデータ変換 (int から float など) を実行できるようにするというアイデアをいじっています。基本的な考え方は、私が書いているライブラリが必要なデータ ファイルを mmap すると同時に、変換されたデータを保持する適切なサイズの匿名領域を mmap し、このポインタがユーザーに返されるというものです。

匿名領域は読み取り/書き込み保護されているため、ユーザーがポインターを介してデータにアクセスしようとすると、すべての新しいページでセグメンテーション違反が発生します。これをキャッチし、mmaped ファイルから透過的にデータを変換し、アクセス許可を修正します。アクセスして続行します。全体のこの部分は、これまでのところうまく機能しています。

ただし、非常に大きなファイル (数百ギガバイト) を mmap する場合があり、匿名メモリがプロキシ アクセスすると、匿名ページがディスクにドロップされるため、すぐにスワップ スペースを消費し始めます。私の考えでは、変換されたデータを匿名ページに書き込んだ後に匿名ページのダーティ ビットを明示的に false に設定できれば、OS はそれらを削除し、後で再アクセスされた場合にオンデマンドでゼロを埋めるだろうと考えていました。

ただし、これが機能するためには、ダーティ ビットを falseに設定し、ページがスワップ アウトされたときにページが読み取り保護されるように OS を説得して、その後のセグメンテーション違反を再度キャッチし、データを再変換する必要があると思いますオンデマンド。いくつかの調査を行った後、これはカーネルのハッキングなしでは不可能だと思いますが、仮想メモリシステムについて詳しく知っている人に、これを達成する方法を知っているかどうか尋ねてみようと思いました.

4

2 に答える 2

2

以前の関連する質問で言及した提案を拡張すると、次の (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_tmremap()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()必要な仮想メモリ構造のカーネル内オーバーヘッドのため。

于 2012-07-31T01:02:00.273 に答える
2

ここにアイデアがあります (完全にテストされていません): 変換されたデータと、mmap必要munmapに応じて個々のページ用です。ページは匿名メモリに支えられているため、マップが解除されると破棄する必要があります。Linux は隣接するマッピングを 1 つの VMA に結合するため、許容できるオーバーヘッドが生じる可能性があります。

もちろん、マッピング解除をトリガーするメカニズムが必要です。LRU 構造を維持し、新しいページを取り込む必要があるときに古いページを削除して、マップされた領域のサイズを一定に保つことができます。

于 2012-07-29T17:48:13.767 に答える