Asignación de copia en memoria de escritura dentro de un proceso

13 minutos de lectura

avatar de usuario
sergey l

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 haga referencia al primero y hacer que ambos se copien y escriban en Linux (Linux en funcionamiento 2.6.36 en este momento)?

Quiero tener exactamente el mismo efecto que fork, simplemente sin crear un nuevo proceso. Quiero que la nueva asignación permanezca en el mismo proceso.

Todo el proceso tiene que ser repetible tanto en la página de origen como en la 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 tienen varios gigabytes de tamaño y no quiero usar memoria que podría compartirse con copia en escritura.

Lo que he probado:

mmap el segmento compartido, anónimo. Sobre la duplicación mprotect para que sea de solo lectura y cree una segunda asignación con remap_file_pages también de solo lectura.

Entonces usa libsigsegv para interceptar intentos de escritura, haga manualmente una copia de la página y luego mprotect tanto para leer como para escribir.

Hace el truco, pero está muy sucio. Esencialmente estoy implementando mi propia máquina virtual.

Desafortunadamente mmapEn g /proc/self/mem no es compatible con Linux actual, de lo contrario un MAP_PRIVATE el mapeo allí podría hacer el truco.

La mecánica de copia en escritura es parte de la máquina virtual de Linux, tiene que haber una forma de utilizarla sin crear un nuevo proceso.

Como nota:
He encontrado la mecánica apropiada en Mach VM.

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

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.

  • Podría usar btrfs y usar su duplicación de archivos con la función de copia en escritura… sin embargo, entonces tendría copias innecesarias de sus datos en el FS. Debería funcionar, pero no exactamente de alto rendimiento.

    – el jh

    6 de junio de 2013 a las 15:21

  • ¿Parchar el kernel está fuera de discusión?

    – el jh

    6 de junio de 2013 a las 15:31

  • @thejh Desafortunadamente lo es :(. El código está destinado a ser implementable en máquinas en las que no tengo root. Implementar otro sistema de archivos tampoco es una opción por la misma razón y rendimiento./dev/shm (tmpfs) es lo más lejos que estoy dispuesto a llegar con la memoria respaldada por archivos.

    – sergey l.

    6 jun 2013 a las 15:50


  • @ChrisStratton El nuevo mapeo de copias se puede colocar en cualquier lugar de mi espacio de direcciones virtuales y devolver un puntero. El mapeo de origen debe permanecer donde está. Por favor, checa el vm_remap llamar en el código mach. Esta es exactamente la semántica que quiero, solo en Linux.

    – sergey l.

    17 de junio de 2013 a las 10:23


  • También posiblemente relacionado: obtenga el comportamiento de copia en escritura de fork() ing, sin fork().

    – Damon

    14 de abril de 2014 a las 9:26

avatar de usuario
ysdx

Traté de lograr lo mismo (de hecho, es un poco más simple ya que solo necesito tomar instantáneas de una región en vivo, no necesito tomar 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 lograr esto. Sin embargo, no existe una forma sencilla de configurar una nueva región COW a partir de una existente. El código usado por fork (copy_page_rank) copiar un vm_area_struct de un proceso/espacio de direcciones virtuales a otro (nuevo) pero asume que la dirección del nuevo mapeo es la misma que la dirección del antiguo. Si se desea implementar una función de “reasignación”, la función debe modificarse/duplicarse para poder copiar una vm_area_struct con traducción de direcciones.

BTRFS: Pensé en usar COW en btrfs para esto. Escribí un programa simple que mapeaba dos archivos reflink-ed 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;
}

mprotect+mmap páginas anónimas: No funciona en su caso, pero una solución es usar un archivo MAP_SHARED para mi región de memoria principal. En una instantánea, el archivo se asigna a otro lugar y ambas instancias están protegidas. En una escritura, se asigna una página anónima en la instantánea, los datos se copian en esta nueva página y la página original queda 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 MAP_SHARED simple sino un MAP_SHARED con algunas páginas MAP_ANONYMOUS. Además, no escala con el número de copias: si tengo muchas copias COW, tendré que repetir el mismo proceso para cada copia y esta página no se duplicará para las copias y no puedo mapear la página anónima en el área original ya que no será posible mapear las páginas anónimas en las copias Esta solución no funciona de todos modos.

mprotect+remap_file_pages: Esta parece ser la única forma de hacerlo sin tocar el kernel de Linux. La desventaja es que, en general, probablemente tendrá que hacer una llamada al sistema remap_file_page para cada página al hacer una copia: puede que no sea tan eficiente hacer muchas llamadas al sistema. Al deduplicar una página compartida, necesita al menos: reasignar_archivo_página una página nueva/gratuita para la nueva página escrita, m-desproteger la nueva página. Es necesario referenciar el recuento de cada página.

no creo que el mprotect() los enfoques basados ​​​​en 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 vm_area_struct granularidad (las entradas que encuentras en /prod//maps). haciendo un mprotect() en la granularidad de la página de memoria, el kernel se dividirá y fusionará constantemente vm_area_struct:

  • terminarás con una muy mm_struct;

  • buscar un vm_area_struct (que se usa para un registro de operaciones relacionadas con la memoria virtual) está activado O(log #vm_area_struct) pero aún podría tener un impacto negativo en el rendimiento;

  • consumo de memoria para esas estructuras.

Por este tipo de razón, se creó la llamada al sistema remap_file_pages() [http://lwn.net/Articles/24468/] para hacer un mapeo de memoria no lineal de un archivo. Hacer esto con mmap requiere un registro de vm_area_struct. No creo que esto haya sido diseñado 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 una llamada al sistema por página.

Creo que la única solución viable es dejar que el kernel 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 al sistema proporcional a la cantidad de páginas. Una variante de remap_file_pages podría ser la solución.

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 algo que el kernel puede hacer. Puede intentar deduplicar las páginas. Aún tendrá que copiar los datos, pero el kernel debería poder fusionarlos. Necesita mmmap una nueva área anónima para su copia, cópiela manualmente con memcpy y madvide(start, end, MADV_MERGEABLE) las areas. Debe habilitar KSM (en la raíz):

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 probablemente sea porque las páginas no se comparten mucho al final. La desventaja es que todavía tiene que hacer la copia (no puede tener un COW eficiente) y luego el núcleo desintegrará la página. Generará fallas de página y caché al hacer las copias, el subproceso del demonio KSM consumirá una gran cantidad de CPU (tengo una CPU funcionando al 00% durante toda la simulación) y probablemente consumirá un registro de caché. Por lo tanto, no ganará tiempo al hacer la copia, pero puede ganar algo de memoria. Si su principal motivación es usar menos memoria a largo plazo y no le importa mucho evitar las copias, esta solución podría funcionar para usted.

  • Tienes muchas buenas ideas, lamentablemente ninguna cumple con los requisitos suficientes para mi propósito. ya he discutido mprotect+mmap páginas anónimas y mprotect+remap_file_pages en mi pregunta No he investigado BRTFS, por lo que puede comprobarlo. Lamentablemente, KSM no es una opción porque eso depende de que yo cree copias en primer lugar y quiero evitar hacerlas. Incluso he buscado parchear el kernel de Linux yo mismo, pero nunca encontré el tiempo para hacerlo. +1 para algunas buenas ideas.

    – sergey l.

    14 de abril de 2014 a las 11:06

  • Para referencia, remap_file_pages es ahora obsoleto y probablemente será eliminado/reemplazado por una emulación lenta.

    – ysdx

    24/03/2015 a las 13:50

  • ¿Cuál es la cantidad de café recomendada para alguien que está considerando seriamente jugar con el Kernel? pido un amigo…

    – Blam Kiwi

    4 de junio de 2017 a las 5:08

avatar de usuario
eljh

Hmm… podrías crear un archivo en /dev/shm con MAP_SHAREDescríbale y vuelva a abrirlo dos veces con MAP_PRIVATE.

  • Quieres decir reabrirlo con MAP_PRIVATE. Sí, esto funciona. Una vez. Necesito poder repetir el proceso, duplicando y reduplicando páginas.

    – sergey l.

    6 de junio de 2013 a las 15:14

  • ¿Cuál sería el código/mensaje de error en ese caso? Por mi experiencia puedes mmap un archivo tantas veces como quieras con MAP_PRIVATE.

    – David Foerster

    11 de junio de 2013 a las 12:18

  • @DavidFoerster: quiere decir que, por ejemplo, después de crear una copia de copia en escritura de A llamada B y hacer algunos cambios en B, quiere crear una copia de copia en escritura de B. Eso no es posible con este método.

    – el jh

    11 de junio de 2013 a las 13:01

  • @DavidFoerster no puedes escribir una sucia MAP_PRIVATE página de nuevo a un archivo y vuelva a abrirla porque no tiene adjunto ningún descriptor de archivo.

    – sergey l.

    11 de junio de 2013 a las 18:27

¿Ha sido útil esta solución?