17 puntos por GN⁺ 2026-06-07 | 3 comentarios | Compartir por WhatsApp
  • plantillas de spawn es una propuesta de creación de procesos para el kernel de Linux con la que el kernel busca cachear la información de un ejecutable en aplicaciones que ejecutan repetidamente el mismo binario, para acelerar así los inicios posteriores de procesos
  • fork() debe copiar todo el estado del proceso, incluida la memoria, para el proceso hijo, y cuando justo después llega exec(), esa memoria muchas veces se descarta, lo que genera ineficiencias en el patrón tradicional
  • spawn_template_create() devuelve un descriptor de archivo de plantilla especificando el ejecutable mediante execfd o la ruta absoluta filename, y el kernel abre ese archivo y cachea la información necesaria para una ejecución rápida
  • spawn_template_spawn() funciona de una manera cercana a la ruta normal de fork()/exec(), mantiene las verificaciones que se aplican al ejecutar un archivo nuevo, y los benchmarks de la carta de presentación registran una mejora de alrededor de 2% {p:2}
  • la creación de procesos vacíos basada en pidfd y la configuración con pidfd_config() se considera un mejor enfoque, y el objetivo es dar soporte a una implementación de posix_spawn() en espacio de usuario

Límites del modelo de creación de procesos de Unix

  • Desde los inicios de Unix, fork() ha sido la llamada al sistema orientada a procesos que crea un proceso hijo como copia del padre, y exec() ejecuta un nuevo programa en lugar del proceso actual
  • En el kernel de Linux, la misma funcionalidad central es más conocida como clone() y execve()
  • Este modelo de creación de procesos tiene tanto elegancia como desventajas, y la propuesta de spawn templates de Li Chen, aunque no será aceptada en el kernel de Linux en su forma actual, podría conducir a nuevos primitivos de creación de procesos en el futuro
  • fork() es una llamada al sistema relativamente costosa porque debe copiar todo el estado del proceso, incluida la memoria, para crear el proceso hijo
  • A lo largo de los años ha habido varias optimizaciones, pero fork() sigue siendo una operación intrínsecamente costosa
  • Con frecuencia, a una llamada a fork() le sigue inmediatamente exec(), y exec() descarta toda la memoria copiada para el hijo
  • Ha habido intentos de optimización como vfork(), pero el patrón fork() seguido de exec() sigue siendo una estructura más costosa de lo que podría ser

Plantillas de spawn (Spawn templates)

  • El conjunto de parches de Li Chen se enfoca en aplicaciones que ejecutan repetidamente el mismo binario para optimizar el patrón fork() y exec()
  • Un ejemplo es un programa que necesita ejecutar Git repetidamente para obtener información sobre el contenido de un repositorio
  • En esos casos, el programa puede crear una plantilla para repartir el costo de preparación entre múltiples ejecuciones y acelerar las invocaciones con esa plantilla
  • La creación de la plantilla usa la llamada al sistema spawn_template_create()
    • con una firma de la forma int spawn_template_create(struct spawn_template_create_args *args, size_t args_size);
  • Esta llamada devuelve un descriptor de archivo que representa una plantilla de ejecutable
  • El ejecutable debe especificarse mediante el descriptor de archivo execfd o la ruta absoluta filename, y no se pueden usar ambos al mismo tiempo
  • El kernel abre el archivo especificado y cachea diversa información necesaria para ejecutarlo más rápido después
  • Cada ejecución puede tener distintos argumentos, entorno, cambios de descriptores de archivo y cambios en el manejo de señales
  • La información concreta de ejecución se coloca en la estructura spawn_template_spawn_args
    • argv es un puntero a la lista de argumentos que se pasarán al programa
    • envp es un puntero al entorno del programa
    • actions es un puntero a un arreglo de spawn_template_action que transmite cambios de descriptores de archivo y de manejo de señales
  • spawn_template_action está compuesto por los campos type, flags, fd, newfd y arg
    • Si en el hijo debe cerrarse el descriptor de archivo 4, type se establece en SPAWN_TEMPLATE_ACTION_CLOSE y fd en 4
    • Otras acciones admiten duplicación de descriptores de archivo, apertura de archivos, cambio de directorio de trabajo y cambios en el manejo de señales
  • Una vez completada la información de ejecución, se lanza un nuevo proceso con spawn_template_spawn()
    • con una firma de la forma int spawn_template_spawn(int template_fd, struct spawn_template_spawn_args *args, int args_size);
  • Su funcionamiento interno es cercano a la ruta normal de fork()/exec()
  • Todas las verificaciones normales que se aplican al ejecutar un archivo nuevo se mantienen intactas
  • La información cacheada en la plantilla acelera todo el flujo de creación
  • Los resultados de benchmark de la carta de presentación muestran una mejora de alrededor de 2%, una cifra que podría marcar diferencia para aplicaciones que encajen con el patrón previsto {p:2}

Hacia posix_spawn()

  • Mateusz Guzik evaluó que “todo el modismo fork + exec es terrible y debería desaparecer”
  • Un punto extraño del conjunto de parches es que deja intacta la parte de fork(), que es donde se considera que está la mayor parte del costo
  • La optimización debería eliminar la copia del proceso actual y crear un “proceso prístino”
  • Christian Brauner sostuvo que la idea de una builder API para exec “no es tan extraña”
  • Sin embargo, prefiere un enfoque en el que la nueva API se construya sobre la abstracción existente de pidfd
  • No hay detalles concretos todavía, pero el enfoque correcto sería agregar a pidfd_open() una opción para crear un proceso vacío
  • Luego se llamaría varias veces a una nueva llamada al sistema pidfd_config() para aplicar al nuevo proceso la configuración deseada, como el entorno y la imagen a ejecutar
  • pidfd_config() cumple un papel similar al de fsconfig()
  • Un objetivo importante de la nueva interfaz es dar soporte en espacio de usuario a la implementación de posix_spawn()
  • posix_spawn() encaja bien como alternativa al patrón fork()/exec()
  • La implementación actual oculta internamente fork() y exec(), mientras que una implementación nativa tendría una estructura distinta
  • Li Chen coincidió en que la API esbozada en términos amplios por Brauner parece mejor, y planea orientar el trabajo futuro en esa dirección
  • Las spawn templates no llegarán al kernel de Linux, pero si el trabajo futuro da frutos, Linux podría terminar teniendo una implementación adecuada de posix_spawn()

3 comentarios

 
GN⁺ 2026-06-08
Opiniones en Lobste.rs
  • No me queda muy claro por qué fork+exec existe en primer lugar
    Si diseñara llamadas al sistema de gestión de procesos sin saber mucho de desarrollo de kernel, probablemente pensaría que el caso común es “crear un nuevo proceso hijo limpio” y que el caso excepcional es “copiar el proceso que se está ejecutando actualmente”
    Lo segundo parecería algo que se usaría solo en lugares como servidores web que lanzan muchas copias del mismo proceso, así que me da curiosidad el contexto histórico de por qué fork fue diseñado de esa manera

    • La historia de fork()+exec() está explicada en detalle en un hilo en Orange Site
    • Parece depender de cómo definas “común”
      En muchos sistemas, la cantidad de procesos hijos y de hilos creados puede ser mayor que la cantidad de llamadas a execve()
      No conozco la historia antigua, pero la semántica en sí es mucho más vieja que Linux y, lógicamente, como de todos modos se necesitaba fork(), parece que construir encima de eso era más simple y componible que tener una API separada tipo exec_in_a_new_process() que duplicara parte de la lógica de creación de procesos
      Además, la estructura separada permite usar solo execve() sin fork() para reemplazar el proceso actual por otro programa, y en algunos casos eso es una optimización aceptable
      Es decir, si tu trabajo ya terminó y quieres pasarle la estafeta a otro programa, puedes hacerlo sin seguir ocupando una entrada en la tabla de procesos ni memoria
      En aplicaciones donde el rendimiento importa, incluso solo fork() o execve() ya pueden ser lentos, así que una API más concisa no resuelve todos los problemas
      Por eso aparecen optimizaciones como el proceso Zygote precargado de Android o los workers de servidor web preforkeados, y con ese enfoque basta con un cambio de contexto
    • No estoy seguro de si esa fue la razón original o una ventaja accidental, pero empezar desde una copia tiene la ventaja de que antes de la llamada a exec puedes cambiar libremente el entorno, grupo, usuario, descriptores de archivo, máscara de señales y cosas así
      Si miras posix_spawn, una API moderna para “crear un nuevo proceso hijo limpio”, tiene decenas de flags y opciones, y también es mucho más difícil de extender
    • Un ejemplo de usar solo fork es el subshell
      Cuando se crea un subshell desde el shell padre, tiene exactamente el mismo entorno, así que en la práctica es fork()
      Cuando portearon ash a Windows, tuvieron que serializar todos los datos hacia el nuevo proceso de subshell
  • Creo que estaría bien esperar una o dos semanas antes de publicar contenido de LWN
    Ellos merecen conseguir suscriptores, y sus suscriptores también merecen disfrutar de lo bueno

    • Cuando se enlaza un artículo de LWN en Lobsters, ¿es más probable que alguien que ya estaba suscrito, o que estaba pensando en suscribirse, decida no pagar más, o que aumente la cantidad de personas que encuentran interesante el contenido y consideren una suscripción a LWN?
  • Una ventaja de fork() es que las capacidades de los procesos pueden ampliarse sin que todos los programas tengan que cambiar su código de creación de procesos
    Por ejemplo, con chroot un proceso obtiene la capacidad de tener un directorio raíz, con el control de trabajos obtiene una terminal de control, o se le agregan namespaces de red
    En fork, el nuevo proceso hereda naturalmente las nuevas capacidades del padre, así que las funciones nuevas se comportan de forma consistente incluso en programas antiguos
    Solo los programas que manipulan esa nueva función necesitan conocerla
    En cambio, si un proceso nuevo fuera realmente vacío y hubiera que configurar todo con algo como pidfd_config(), entonces habría que actualizar todos los programas que crean otros procesos para que configuren correctamente cada función nueva
    En sistemas no Unix de este tipo, quizá habría que tener un objeto concreto separado del proceso para representar, en sentido amplio, el entorno del proceso, incluyendo directorio de trabajo, namespaces, propietario y grupos, entre otras cosas
    Pero entonces sería difícil trazar la línea entre qué parte es estado del entorno y desde dónde empieza el estado del proceso
    Incluso da para pensar si el entorno, en vez de ser la parte heredable del estado del proceso como en Unix, debería ser un contenedor compartido por varios procesos

  • Creo que todo esto podría resolverse mejor con clone/exec
    El núcleo del problema de fork está en cortar el uso compartido de memoria, y cuando entran en juego los hilos y muchas entradas de la tabla de páginas, ese modelo no encaja bien
    Hacer exec desde un hilo de memoria compartida resuelve buena parte de esto
    Parece que habría que experimentar un poco

 
shakespeares 2026-06-07

Parece una optimización a nivel de kernel que puede crear rápidamente procesos finales como child_process.

 
GN⁺ 2026-06-07
Comentarios en Hacker News
  • Hay un artículo relacionado, A fork() in the road: https://www.microsoft.com/en-us/research/wp-content/uploads/...
    En el resumen se sostiene que, al contrario de la idea común de que la combinación fork()+exec() es un diseño inspirado, fue un hack ingenioso para las máquinas y programas de los años 70, pero ahora es una mala abstracción para los programadores modernos y además limita la implementación de los sistemas operativos
    En vez de mantenerlo como una primitiva de primer nivel del sistema operativo, plantean que debería enseñarse como un artefacto histórico y no ser la primera forma de creación de procesos que aprenden los estudiantes

    • La razón de que fork()+exec() terminara así fue permitir ejecutar programas demasiado grandes para caber en memoria junto con el programa padre
      La implementación original, al llamar a fork(), intercambiaba a disco el programa que hacía el fork y, antes de devolverle el control, duplicaba y ajustaba la entrada de la tabla de procesos para que existieran un proceso en memoria y otro intercambiado a disco; el que quedaba en memoria recibía el control y podía llamar a exec()
      Gracias a este método se podían ejecutar programas grandes incluso en máquinas PDP-11 pequeñas, y era necesario en una época en la que la memoria era muy cara
      Curiosamente, en QNX la carga de programas no está dentro del sistema operativo sino en una biblioteca. Lee el encabezado del ejecutable, asigna memoria, carga el programa, lo prepara para ejecutarse y lo enlaza con un .so que lo inicia; el cargador de programas corre en espacio de usuario sin privilegios. Probablemente este enfoque se acerca más a la forma correcta
    • Es interesante que la creación de procesos en Windows, que es el sistema operativo “grande” más usado que no emplea fork(), sea muy lenta
      Estoy de acuerdo en que debería existir una primitiva distinta de fork(), pero no estoy seguro de que el rendimiento sea el mejor argumento
    • Este artículo también es bueno, y la referencia [29] fue especialmente buena porque trata los aspectos sutiles de las interfaces escalables, incluyendo fork(): The Scalable Commutativity Rule: Designing Scalable Software for Multicore Processors https://people.csail.mit.edu/nickolai/papers/clements-sc.pdf
    • La discusión de entonces está aquí: https://news.ycombinator.com/item?id=19621799 - A fork() in the road (2019-04-10, 178 comments)
    • fork() es excelente para el patrón zygote
      Cuesta imaginar una optimización tan eficiente y elegante como esa
  • Hace poco tuve un bug oscuro causado por tener que cerrar más descriptores de archivo en un proceso forkeado
    En mi experiencia, es mucho más común querer “un proceso completamente nuevo” que “una copia del proceso actual”, y se siente raro que no haya una forma de expresar directamente lo segundo y que solo pueda aproximarse clonando primero y arreglando después

    • Normalmente quieres comunicarte con ese proceso, así que necesitas configurar cosas como descriptores de archivo y pasar información del proceso padre
    • ¿Eso no se resuelve con O_CLOEXEC?
    • Si con “una forma de expresar directamente lo segundo” te refieres a eso, ¿no es precisamente para eso posix_spawn?
    • ¿Qué significa exactamente “un proceso completamente nuevo”?
  • Es raro decir que “fork() es una llamada al sistema relativamente costosa, y tiene que copiar todo el estado del proceso, incluida la memoria, para el proceso hijo. Ha habido muchas optimizaciones a lo largo de los años, pero en esencia sigue siendo una operación cara. Peor aún, en muchos casos a fork() le sigue inmediatamente exec(), lo que desecha toda la memoria copiada cuidadosamente para el hijo” y no mencionar copy-on-write
    Esa es la optimización que evita copiar realmente toda la memoria, y aquí falta

    • El artículo lo trató de forma implícita, pero aquí la copia del estado del proceso se refiere a las estructuras de administración de memoria. Principalmente tablas de páginas y VMA
      Aunque la memoria apuntada por las páginas reales se comparta, igual hay que asignar páginas nuevas para guardar copias de esas estructuras. Y recorrerlas todas para copiarlas sigue siendo costoso
    • Redis es un tipo de proceso en el que este costo importa mucho. fork() no copia la memoria en sí, pero aún debe copiar las tablas de páginas
      Si se trata de un proceso con decenas de GB de RAM, fork() puede tardar bastante, y eso ocurre cada vez que Redis hace un volcado del archivo .rdb o reescribe el AOF de logging binario
      Ya en 2012 había una publicación que mostraba el alto costo de esta operación: https://redis.io/blog/testing-fork-time-on-awsxen-infrastruc...
      En una m2.xlarge usando unos 25 GB de RAM, fork() tardó 5.67 segundos. Considerando que los clientes de Redis normalmente experimentan latencias de un solo dígito de milisegundos en la mayoría de las operaciones, es una pausa muy larga. Y eso es solo el tiempo de copia de las tablas de páginas
      Sorprende que no se mencione huge page, porque aquí parece una consideración clave. Catorce años después el hardware será más rápido, pero probablemente las instancias de Redis también usen más RAM, así que sería interesante repetir ese benchmark
    • Para el público al que va dirigido este tipo de artículo, copy-on-write probablemente se omite por darse por sabido
    • Incluso con copy-on-write, fork() tiene que pagar el costo de configurarlo. Si el proceso padre tiene muchos hilos ocupados, por ejemplo en Java, puede producirse mucho copy-on-write innecesario antes de que se ejecute exec()
    • El texto dice “estado”. Incluso con copy-on-write, aunque no se copien los contenidos, sigue habiendo un costo proporcional al número de entradas en la tabla de páginas
      Que forkar programas con tamaños grandes de memoria virtual es lento es un problema bien conocido
  • La elegancia del modelo fork()+exec() está en que, después de fork(), se puede usar la API normal tal cual para hacer todo tipo de configuraciones
    Hasta ahora, las alternativas de llamada combinada que he visto parecían fundamentalmente pobres, porque hay que agregar todas las opciones de configuración como parámetros de la llamada y además hacer que se puedan ampliar más adelante sin que se vuelva un desastre

    • No estoy del todo de acuerdo, pero le veo utilidad. Aunque fork()/exec() puede ser útil en algunos casos, parecería bastante bien que las APIs aceptaran un argumento pidfd. El 0 podría significar el proceso actual
      El problema serían más bien los binarios setuid/setgid; en ese caso quizá sería mejor manejarlo de forma especial en exec
      Por ejemplo, se podría crear un proceso detenido con pidfd_t ps = spawn(); y configurarlo con setuid(ps, 33);, capset(ps, ...);, socket(ps, ...);, mmap(ps, ...);, process_vm_writev(ps, ...);, exec(ps, ...);, signal(ps, SIGCONT);
      También es una crítica a que la API habitual de llamadas al sistema no considera lo suficiente la pregunta “¿y si quiero hacer esta operación sobre otro proceso al que sí tengo acceso?”. Así, fork() también podría ganar algo en seguridad de hilos
      Aun así, sí estoy de acuerdo en que un enfoque tipo CreateProcess, con una enorme cantidad de parámetros, no es excelente como API de espacio de usuario
    • Pienso exactamente lo contrario. El gran error del modelo tipo UNIX es que al crear un proceso se conserva demasiado estado
      Por ejemplo, existen APIs para hacer que cierto objeto quede como descriptor de archivo número 4, y luego puedes ejecutar un programa esperando que encuentre ese objeto en el descriptor 4. Eso es raro
      Windows, con todos sus defectos, no usa fork()+exec() y en cambio ofrece principalmente opciones sobre cómo crear el proceso. No era elegante, pero iba en la dirección correcta
    • Llamarlo elegante es dependencia de la trayectoria de la historia de fork()+exec()
      En otro mundo donde fork()+exec() nunca hubiera existido, muchas de esas “APIs generales” tendrían un argumento pid explícito para poder cambiar la configuración de otro proceso. Fuchsia funciona más o menos así
      Ese mundo tiene muchas ventajas. La más obvia es que no hace falta inventar mágicamente un mecanismo IPC aparte para reportar errores de configuración, y también sería bastante útil poder tener un proceso administrador que ajuste los atributos del hijo. A los depuradores en particular probablemente les encantaría
    • La forma correcta de eliminar fork() es que las APIs generales que cambian el estado del proceso acepten un handle de proceso explícito
      Entonces la misma API serviría para configurar un proceso vacío y también se podría combinar con otros métodos como IPC o depuración
    • El orden debería ser spawn, configure, exec
      Si el proceso empezara con ptrace conectado y sin hilos, durante la etapa de configuración se le podría obligar a ejecutar llamadas al sistema. Como Linux ni siquiera tiene el concepto de “proceso sin hilos”, probablemente haría falta un hilo ficticio
  • Es sorprendentemente común el malentendido de que fork() es barato, pero es O(N) respecto al tamaño del proceso, y siempre lo ha sido
    Sí, es copy-on-write. Pero hay una relación lineal entre el tamaño del proceso y la cantidad de entradas de tabla de páginas necesarias para representarlo

  • No sorprende que hayan rechazado el parche de Chen. Es un caso de uso demasiado específico y vale poco la pena soportarlo
    Desde la perspectiva de quien desarrolla shells, coincido con la conclusión de que “es probable que los desarrolladores reciban bien una implementación nativa que no esconda fork() y exec() internamente como hace la implementación actual”

    • Parece que hay interés no en esa implementación concreta, sino en el concepto mismo
  • Desde la primera vez que aprendí fork(), me pareció conceptualmente horrible. Si quieres hacer una sola tarea, es decir, iniciar un proceso, no deberías tener que pasar por el hechizo enigmático de forkar el proceso actual, que es otra tarea distinta y no relacionada
    Como en el ejemplo del artículo, me pregunto cuál sería la mejor forma de manejar una situación donde un proceso lanza muchos subprocesos de git. Reiniciar git una y otra vez desde cero mientras la tarea padre sigue ejecutándose no parece tener sentido; ¿cuál sería una abstracción de bajo costo que produzca el mismo resultado?

    • fork() es conceptualmente simple. Sin traer otras capas, empiezas un proceso con lo único cuya existencia sabes con certeza: tú mismo
      De otro modo, harían falta varios pasos: crear el proceso, llenarlo con algo que ejecutar y ponerlo a correr. O habría que fusionarlo permanentemente con otras capas como el sistema de archivos, el cargador de objetos y el enlazador, como en Win32
    • Como alguien que empezó en Windows, el modelo fork()+exec() nunca me hizo sentido. Ahora sé que es solo una rareza histórica, pero todavía hay gente que actúa como si fork()+exec() realmente fuera algo bueno
    • Está libgit2. Uno podría imaginar una comunicación por tuberías o sockets con algún gitd, pero no veo por qué sería una buena idea. Si no, hay que lanzar un proceso
  • La razón por la que es difícil reemplazar exec/fork es que normalmente hay que configurar el proceso nuevo. Por ejemplo, hay que ajustar handlers de señales, cerrar o abrir descriptores de archivo, cambiar de namespace, configurar seccomp y ajustar privilegios
    Pero las llamadas al sistema para eso solo se aplican al proceso actual, así que hace falta otro mecanismo. La propuesta del artículo era crear una API nueva para eso
    En mi opinión, una nueva llamada al sistema como spawn podría crear un proceso vacío, cargar dentro un loader liviano y luego pasarle datos arbitrarios de configuración. El loader configuraría el proceso y luego haría exec() del programa principal
    Así se podría evitar forkar la memoria y mantener la API existente, aunque igual habría que duplicar descriptores de archivo y otras cosas

    • Por suerte, parece que alguien viajó en el tiempo, vio este artículo y lo agregó a POSIX.1-2001 :)
      Perdón si no era broma, pero posix_spawn() ya existe y en glibc fork es simplemente un alias de clone()
      Aunque no sea exactamente igual a la propuesta original, fork()/exec() de verdad ya está cerca de ser legado
  • Si fork y exec pudieran mostrar un comportamiento persistente y algebraico más allá de su naturaleza copy-on-write, serían no solo más útiles sino también más interesantes de usar. Por ejemplo, podrían servir para evaluación diferida

  • Ha habido muchas discusiones sobre esta antigua API en Hacker News; por ejemplo, https://news.ycombinator.com/item?id=31739794