tipos - Asignar copia en memoria de escritura dentro de un proceso
terminologia de memorias (2)
Hmm ... puedes crear un archivo en /dev/shm
con MAP_SHARED
, escribir en él y luego volver a MAP_PRIVATE
dos veces con MAP_PRIVATE
.
Tengo un segmento de memoria que se obtuvo a través de mmap
con MAP_ANONYMOUS
.
¿Cómo puedo asignar un segundo segmento de memoria del mismo tamaño que hace referencia al primero y hacer que ambos se copien en Linux (trabajando en Linux 2.6.36 en este momento)?
Quiero tener exactamente el mismo efecto que el fork
, solo sin crear un nuevo proceso. Quiero que el nuevo mapeo se mantenga en el mismo proceso.
Todo el proceso debe ser repetible tanto en el origen como en las páginas de copia (como si padre e hijo continuaran fork
).
La razón por la que no quiero asignar una copia directa de todo el segmento es porque son de varios gigabytes de gran tamaño y no quiero usar la memoria que podría compartirse con copia en escritura.
Lo que he intentado:
mmap
el segmento compartido, anónimo. En la duplicación, mprotect
para solo lectura y crear una segunda asignación con remap_file_pages
también de solo lectura.
Luego use libsigsegv
para interceptar intentos de escritura, haga una copia de la página manualmente y luego mprotect
ambos para leer-escribir.
Tiene el truco, pero está muy sucio. Básicamente estoy implementando mi propia máquina virtual.
Lamentablemente, el mmap
/proc/self/mem
no se admite en Linux actual, de lo contrario, una asignación MAP_PRIVATE
podría hacer el truco.
Los mecanismos de copia en escritura son parte de la máquina virtual de Linux, tiene que haber una manera de hacer uso de ellos sin crear un nuevo proceso.
Como nota: he encontrado la mecánica adecuada en la máquina virtual de Mach.
El siguiente código se compila en mi OS X 10.7.5 y tiene el comportamiento esperado: 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
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;
}
Quiero el mismo efecto en Linux.
Intenté lograr lo mismo (de hecho, es bastante más simple, ya que solo necesito tomar instantáneas de una región en vivo, no necesito copias de las copias). No encontré una buena solución para esto.
Soporte directo del kernel (o la falta del mismo): Al modificar / agregar un módulo debería ser posible lograrlo. Sin embargo, no existe una forma sencilla de configurar una nueva región de COW a partir de una existente. El código utilizado por fork ( copy_page_rank
) copia un vm_area_struct
de un proceso / espacio de direcciones virtuales a otro (nuevo) pero asume que la dirección de la nueva asignación es la misma que la dirección de la anterior. Si se desea implementar una función de "reasignación", la función debe modificarse / duplicarse para copiar una vm_area_struct
con la traducción de direcciones.
BTRFS : Pensé en usar COW en btrfs para esto. Escribí un programa simple que mapea dos archivos de reflexión y traté de mapearlos. Sin embargo, mirando la información de la página con /proc/self/pagemap
muestra que las dos instancias del archivo no comparten las mismas páginas de caché. (Al menos a menos que mi prueba sea incorrecta). Así que no ganarás mucho haciendo esto. Las páginas físicas de los mismos datos no se compartirán entre diferentes instancias.
#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;
}
Páginas anónimas mprotect
+ mmap
: no funciona en su caso, pero una solución es utilizar un archivo MAP_SHARED para mi región de memoria principal. En una instantánea, el archivo se asigna en otro lugar y ambas instancias están protegidas. En una escritura, una página anónima asignada en la instantánea, los datos se copian en esta nueva página y la página original está desprotegida. Sin embargo, esta solución no funciona en su caso, ya que no podrá repetir el proceso en la instantánea (porque no es un área simple de MAP_SHARED sino de MAP_SHARED con algunas páginas de MAP_ANONYMOUS. Además, no se escala con el número de copias: si tengo muchas copias de COW, tendré que repetir el mismo proceso para cada copia y esta página no se duplicará para las copias. Y no puedo asignar la página anónima en el área original ya que no será posible asignar Las páginas anónimas en las copias. Esta solución no funciona de ninguna manera.
mprotect
+ remap_file_pages
: Esto parece ser la única forma de hacerlo sin tocar el kernel de Linux. La desventaja es que, en general, es probable que tenga que hacer una remisión de remap_file_page para cada página al hacer una copia: puede que no sea tan eficiente hacer muchas llamadas al sistema. Al desduplicar una página compartida, necesita al menos: remap_file_page una página nueva / gratuita para la nueva página escrita, en la que no se debe proteger la página. Es necesario hacer referencia al recuento de cada página.
No creo que los enfoques basados en mprotect()
se escalarían muy bien (si maneja mucha memoria como esta). En Linux, mprotect()
no funciona en la granularidad de la página de memoria sino en la granularidad vm_area_struct
(las entradas que se encuentran en / prod // maps). Si se hace un mprotect()
en la granularidad de la página de memoria, el núcleo se dividirá y fusionará constantemente vm_area_struct:
terminarás con un muy
mm_struct
;buscar un vm_area_struct (que se usa para un registro de operaciones relacionadas con la memoria virtual) está en
O(log #vm_area_struct)
pero aún puede tener un impacto negativo en el rendimiento;Consumo de memoria para esas estructuras.
Por este tipo de razón, se creó el remap_file_pages () syscall [ http://lwn.net/Articles/24468/] para realizar un mapeo de memoria no lineal de un archivo. Hacer esto con mmap, requiere un registro de vm_area_struct
. Evidentemente, no creo que estén diseñados para el mapeo de granularidad de la página: remap_file_pages () no está muy optimizado para este caso de uso, ya que necesitaría un syscall por página.
Creo que la única solución viable es dejar que el núcleo lo haga. Es posible hacerlo en el espacio de usuario con remap_file_pages pero probablemente será bastante ineficiente, ya que una instantánea generará una cantidad de llamadas proporcionales en el número de páginas. Una variante de remap_file_pages podría hacer el truco.
Sin embargo, este enfoque duplica la lógica de la página del kernel. Tiendo a pensar que deberíamos dejar que el kernel haga esto. Con todo, una implementación en el kernel parece ser la mejor solución. Para alguien que conoce esta parte del núcleo, debería ser bastante fácil de hacer.
KSM (Kernel Samepage Merging): Hay una cosa que el kernel puede hacer. Puede tratar de deduplicar las páginas. Todavía tendrá que copiar los datos, pero el kernel debería poder fusionarlos. Debe madvide(start, end, MADV_MERGEABLE)
un área anónima nueva para su copia, copiarla manualmente con las áreas memcpy y madvide(start, end, MADV_MERGEABLE)
. Necesitas habilitar KSM (en root):
echo 1 > /sys/kernel/mm/ksm/run
echo 10000 > /sys/kernel/mm/ksm/pages_to_scan
Funciona, no funciona tan bien con mi carga de trabajo, pero es probable que al final las páginas no se compartan mucho. La desventaja es que todavía tiene que hacer la copia (no puede tener un COW eficiente) y luego el kernel no fusionará la página. Generará fallas en la página y en la memoria caché cuando haga las copias, el subproceso del demonio KSM consumirá una gran cantidad de CPU (tengo una CPU que se ejecuta en A00% para toda la simulación) y probablemente consumirá un registro en la memoria caché. Por lo tanto, no ganará tiempo al hacer la copia, pero podría ganar algo de memoria. Si su motivación principal es usar menos memoria a largo plazo y no le importa mucho evitar las copias, esta solución podría funcionar para usted.