Más allá de `fork()` + `exec()`

Costo y comportamiento de fork()+exec()

  • Varios comentarios critican la formulación del artículo de que fork “copia toda la memoria”; destacan copy-on-write, pero señalan que el costo real es copiar las tablas de páginas y las VMAs y preparar COW, lo cual es O(tamaño del proceso).
  • Para procesos muy grandes (decenas de GB), las pausas de fork pueden llegar a segundos; ejemplos incluyen Redis y grandes JVMs. A menudo se usan helpers tipo zygote para evitar hacer fork desde procesos enormes.
  • vfork() reduce algunos costos al evitar COW, pero tiene restricciones de uso estrictas e interactúa mal con hilos en algunos sistemas.
  • Se considera que el overcommit y COW son casi obligatorios para que fork sea rápido, pero eso a su vez restringe el diseño del SO.

Modelos de creación de procesos y alternativas

  • Muchos defienden primitivas estilo spawn: posix_spawn (nativo en algunos SO), CreateProcess en Windows, NtCreateProcess, el posix_spawn sin fork de XNU, el modelo de Mach de “task/address space/threads”.
  • Ideas propuestas: un proceso “vacío” creado por una syscall y luego configurado mediante otras syscalls (posiblemente tomando un pidfd); o un proceso cargador diminuto que ejecuta configuración y luego exec.
  • Algunos quieren que las APIs existentes (setuid, operaciones de fd, etc.) acepten descriptores del proceso objetivo, de modo que la configuración pueda hacerse externamente en lugar de dentro de un hijo forkeado.
  • Otros sugieren usar mecanismos tipo ptrace o hooks de eBPF para conducir la configuración sin nuevas banderas de spawn por cada característica.

Herencia de estado y configuración

  • Una queja recurrente: la mayoría de los casos reales quieren “lanzar un nuevo programa con un conjunto pequeño y explícito de cosas heredadas”, pero el comportamiento por defecto de fork es “heredar todo y luego excluir laboriosamente”.
  • Los descriptores de archivo son el principal punto de dolor; O_CLOEXEC y la iteración de fds se consideran propensos a errores.
  • Un spawn verdaderamente de “no compartir nada” es difícil de definir: contenedores, cgroups, namespaces, UID/gid, señales, entorno y más interactúan de formas no obvias.
  • Los padres multihilo hacen que fork sea frágil: solo un hilo sobrevive en el hijo, por lo que los locks en memoria y el estado de malloc pueden quedar inconsistentes; de ahí la restricción POSIX de usar solo funciones async-signal-safe entre fork y exec.

Casos de uso y sensibilidad al rendimiento

  • Algunos sostienen que “spawn no debería estar en la ruta caliente”; otros señalan que sistemas de build, fuzzers, aislamiento intensivo de procesos, navegadores y plataformas móviles contradicen esto en la práctica.
  • Los patrones zygote (ayudantes pre-fork) se usan mucho, pero se consideran difíciles de adaptar y frágiles.

Elegancia vs “truco histórico”

  • Un bando elogia fork+exec por simple, componible y clave para el poder del shell Unix; las APIs alternativas de spawn con enormes structs de parámetros se ven feas y menos extensibles.
  • El bando opuesto llama a fork un truco obsoleto de los 70 que fuerza abstracciones incómodas (por ejemplo, codificar APIs mediante fds numerados), complica el threading y fosiliza las internals del kernel.
  • Varios señalan que, si Unix hubiera empezado con spawn+configure, muchas syscalls se habrían diseñado con argumentos explícitos de proceso, simplificando el debugging y la supervisión.

Librerías compartidas y memoria

  • Corrección de una idea equivocada: librerías compartidas como libgcc se mapean una sola vez y se comparten entre procesos mediante mmap; solo los datos de relocación/GOT son por proceso.
  • Este compartir se cita como una de las grandes ventajas de la vinculación dinámica.