mmap
を介して取得されたメモリ セグメントがありますMAP_ANONYMOUS
。
Linux (現在動作中の Linux 2.6.36) で、最初のメモリ セグメントを参照する同じサイズの 2 番目のメモリ セグメントを割り当て、両方をコピーオン ライトにするにはどうすればよいでしょうか。
新しいプロセスを作成せずに、とまったく同じ効果を実現したいと考えていますfork
。新しいマッピングを同じプロセス内に維持したいと考えています。
プロセス全体は、元のページとコピー ページの両方で繰り返すことができる必要があります (親と子が継続するかのようにfork
)。
セグメント全体の直接コピーを割り当てたくない理由は、セグメントが数ギガバイトの大きさであり、コピーオンライト共有される可能性のあるメモリを使用したくないからです。
私が試したこと:
mmap
セグメントは共有され、匿名です。複製時にmprotect
読み取り専用に設定し、remap_file_pages
読み取り専用の 2 番目のマッピングを作成します。
次に、libsigsegv
書き込み試行を傍受し、ページのコピーを手動で作成して、mprotect
読み取りと書き込みの両方を実行します。
うまくいきますが、非常に汚いです。基本的には独自の VM を実装しています。
残念ながら、mmap
ing は/proc/self/mem
現在の Linux ではサポートされていませんが、そうでなければ、MAP_PRIVATE
そこでのマッピングがうまくいく可能性があります。
コピーオンライト機構は Linux VM の一部であるため、新しいプロセスを作成せずにこれを利用する方法が必要です。
注記:Mach VM で適切なメカニズムを見つけました。
次のコードは OS X 10.7.5 でコンパイルされ、期待どおりの動作をします。Darwin 11.4.2 Darwin Kernel Version 11.4.2: Thu Aug 23 16:25:48 PDT 2012; root:xnu-1699.32.7~1/RELEASE_X86_64 x86_64 i386
gcc version 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
#include <sys/mman.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#ifdef __MACH__
#include <mach/mach.h>
#endif
int main() {
mach_port_t this_task = mach_task_self();
struct {
size_t rss;
size_t vms;
void * a1;
void * a2;
char p1;
char p2;
} results[3];
size_t length = sysconf(_SC_PAGE_SIZE);
vm_address_t first_address;
kern_return_t result = vm_allocate(this_task, &first_address, length, VM_FLAGS_ANYWHERE);
if ( result != ERR_SUCCESS ) {
fprintf(stderr, "Error allocating initial 0x%zu memory.\n", length);
return -1;
}
char * first_address_p = first_address;
char * mirror_address_p;
*first_address_p = 'a';
struct task_basic_info t_info;
mach_msg_type_number_t t_info_count = TASK_BASIC_INFO_COUNT;
task_info(this_task, TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count);
task_info(this_task, TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count);
results[0].rss = t_info.resident_size;
results[0].vms = t_info.virtual_size;
results[0].a1 = first_address_p;
results[0].p1 = *first_address_p;
vm_address_t mirrorAddress;
vm_prot_t cur_prot, max_prot;
result = vm_remap(this_task,
&mirrorAddress, // mirror target
length, // size of mirror
0, // auto alignment
1, // remap anywhere
this_task, // same task
first_address, // mirror source
1, // Copy
&cur_prot, // unused protection struct
&max_prot, // unused protection struct
VM_INHERIT_COPY);
if ( result != ERR_SUCCESS ) {
perror("vm_remap");
fprintf(stderr, "Error remapping pages.\n");
return -1;
}
mirror_address_p = mirrorAddress;
task_info(this_task, TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count);
results[1].rss = t_info.resident_size;
results[1].vms = t_info.virtual_size;
results[1].a1 = first_address_p;
results[1].p1 = *first_address_p;
results[1].a2 = mirror_address_p;
results[1].p2 = *mirror_address_p;
*mirror_address_p = 'b';
task_info(this_task, TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count);
results[2].rss = t_info.resident_size;
results[2].vms = t_info.virtual_size;
results[2].a1 = first_address_p;
results[2].p1 = *first_address_p;
results[2].a2 = mirror_address_p;
results[2].p2 = *mirror_address_p;
printf("Allocated one page of memory and wrote to it.\n");
printf("*%p = '%c'\nRSS: %zu\tVMS: %zu\n",results[0].a1, results[0].p1, results[0].rss, results[0].vms);
printf("Cloned that page copy-on-write.\n");
printf("*%p = '%c'\n*%p = '%c'\nRSS: %zu\tVMS: %zu\n",results[1].a1, results[1].p1,results[1].a2, results[1].p2, results[1].rss, results[1].vms);
printf("Wrote to the new cloned page.\n");
printf("*%p = '%c'\n*%p = '%c'\nRSS: %zu\tVMS: %zu\n",results[2].a1, results[2].p1,results[2].a2, results[2].p2, results[2].rss, results[2].vms);
return 0;
}
Linux でも同じ効果を実現したいです。
ベストアンサー1
私も同じことをやろうとしました (実際、ライブ領域のスナップショットを撮るだけで、コピーのコピーを撮る必要がないので、少し簡単です)。これに対する良い解決策は見つかりませんでした。
直接カーネルサポート(またはその欠如): モジュールを変更/追加することで、これを実現できるはずです。ただし、既存の COW 領域から新しい COW 領域を設定する簡単な方法はありません。fork ( copy_page_rank
)で使用されるコードvm_area_struct
は、1 つのプロセス/仮想アドレス空間から別の (新しい) プロセス/仮想アドレス空間にコピーしますが、新しいマッピングのアドレスは古いマッピングのアドレスと同じであると想定しています。「再マップ」機能を実装する場合は、関数を変更/複製して、アドレス変換を使用してコピーする必要がありますvm_area_struct
。
BTRFS: このために、btrfs で COW を使用することを考えました。reflink された 2 つのファイルをマッピングする簡単なプログラムを作成し、それらをマッピングしようとしました。ただし、ページ情報を見ると、/proc/self/pagemap
ファイルの 2 つのインスタンスが同じキャッシュ ページを共有していないことがわかります (少なくとも私のテストが間違っていない限り)。したがって、これを行うことで得られるものはあまりありません。同じデータの物理ページは、異なるインスタンス間で共有されません。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <inttypes.h>
#include <stdio.h>
void* map_file(const char* file) {
struct stat file_stat;
int fd = open(file, O_RDWR);
assert(fd>=0);
int temp = fstat(fd, &file_stat);
assert(temp==0);
void* res = mmap(NULL, file_stat.st_size, PROT_READ, MAP_SHARED, fd, 0);
assert(res!=MAP_FAILED);
close(fd);
return res;
}
static int pagemap_fd = -1;
uint64_t pagemap_info(void* p) {
if(pagemap_fd<0) {
pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
if(pagemap_fd<0) {
perror("open pagemap");
exit(1);
}
}
size_t page = ((uintptr_t) p) / getpagesize();
int temp = lseek(pagemap_fd, page*sizeof(uint64_t), SEEK_SET);
if(temp==(off_t) -1) {
perror("lseek");
exit(1);
}
uint64_t value;
temp = read(pagemap_fd, (char*)&value, sizeof(uint64_t));
if(temp<0) {
perror("lseek");
exit(1);
}
if(temp!=sizeof(uint64_t)) {
exit(1);
}
return value;
}
int main(int argc, char** argv) {
char* a = (char*) map_file(argv[1]);
char* b = (char*) map_file(argv[2]);
int fd = open("/proc/self/pagemap", O_RDONLY);
assert(fd>=0);
int x = a[0];
uint64_t info1 = pagemap_info(a);
int y = b[0];
uint64_t info2 = pagemap_info(b);
fprintf(stderr, "%" PRIx64 " %" PRIx64 "\n", info1, info2);
assert(info1==info2);
return 0;
}
mprotect
+mmap
匿名ページ: あなたのケースでは機能しませんが、解決策としては、メイン メモリ領域に MAP_SHARED ファイルを使用することです。スナップショットでは、ファイルは別の場所にマップされ、両方のインスタンスが保護されません。書き込み時に、匿名ページがスナップショットにマップされ、データはこの新しいページにコピーされ、元のページは保護されません。ただし、この解決策は、スナップショットでプロセスを繰り返すことができないため、あなたのケースでは機能しません (これは、単純な MAP_SHARED 領域ではなく、いくつかの MAP_ANONYMOUS ページを含む MAP_SHARED であるためです)。さらに、コピーの数に応じて拡張されません。COW コピーが多数ある場合、コピーごとに同じプロセスを繰り返す必要があり、このページはコピーに対して複製されません。また、コピー内の匿名ページをマップできないため、元の領域に匿名ページをマップすることはできません。この解決策は、とにかく機能しません。
mprotect
+remap_file_pages
: これは Linux カーネルに触れずにこれを行う唯一の方法のようです。欠点は、一般に、コピーを行うときに各ページに対して remap_file_page システムコールを実行する必要があることです。多くのシステムコールを実行するのは効率的ではない可能性があります。共有ページを重複排除する場合、少なくとも次の操作を行う必要があります。新しい書き込み先ページに対して新しい/空きページを remap_file_page し、新しいページの保護を解除します。各ページの参照カウントが必要です。
ベースとなるアプローチは、あまりうまく拡張できないと思いますmprotect()
(このように大量のメモリを処理する場合)。Linux では、mprotect()
メモリ ページの粒度ではなく、粒度 (/prod//maps にあるエントリ) で動作しますvm_area_struct
。メモリ ページの粒度で実行すると、mprotect()
カーネルは vm_area_struct を常に分割および結合することになります。
非常に になってしまいます
mm_struct
。vm_area_struct (仮想メモリ関連の操作のログに使用される) の検索はオンになっています
O(log #vm_area_struct)
が、それでもパフォーマンスに悪影響を与える可能性があります。これらの構造のメモリ消費量。
このような理由から、ファイルの非線形メモリ マッピングを行うために remap_file_pages() システム コールが作成されました [http://lwn.net/Articles/24468/]。これを mmap で行うには、 のログが必要ですvm_area_struct
。これがページ粒度マッピング用に設計されたとは思えません。remap_file_pages() は、ページごとにシステム コールが必要になるため、このユース ケースにはあまり最適化されていません。
唯一の実行可能な解決策は、カーネルに実行させることだと思います。 remap_file_pages を使用してユーザー空間で実行することは可能ですが、スナップショットを生成するにはページ数に比例した数のシステムコールが必要になるため、おそらく非常に非効率的です。 remap_file_pages のバリアントが役に立つかもしれません。
ただし、このアプローチではカーネルのページ ロジックが重複します。私はカーネルにこれを実行させるべきだと考えています。全体的に、カーネルに実装する方がよいソリューションのようです。カーネルのこの部分を知っている人にとっては、非常に簡単に実行できるはずです。
ケイエスエム(カーネル同一ページマージ): カーネルで実行できることがあります。ページの重複排除を試みることができます。データのコピーは必要ですが、カーネルはそれらをマージできるはずです。コピー用に新しい匿名領域を mmap し、memcpy とmadvide(start, end, MADV_MERGEABLE)
領域を使用して手動でコピーする必要があります。KSM を有効にする必要があります (ルートで):
echo 1 > /sys/kernel/mm/ksm/run
echo 10000 > /sys/kernel/mm/ksm/pages_to_scan
これはうまくいきます。私のワークロードではうまくいきませんでしたが、それはおそらくページが最終的にあまり共有されないからでしょう。欠点は、コピーを実行する必要があることです (効率的な COW を持つことはできません)。その後、カーネルがページをアンマージします。コピーを実行するとページ フォールトとキャッシュ フォールトが生成され、KSM デーモン スレッドは CPU を大量に消費し (シミュレーション全体で CPU を A00% で実行しています)、おそらくログ キャッシュを消費します。したがって、コピーを実行するときに時間は節約できませんが、メモリはいくらか節約できる可能性があります。主な動機が長期的にメモリ使用量を減らすことであり、コピーを回避することにそれほど関心がない場合は、このソリューションが適している可能性があります。