c unix operating-system reentrancy

¿Por qué se dice malloc() y printf() como no reentrante?



unix operating-system (6)

Aquí hay al menos tres conceptos, todos los cuales se combinan en un lenguaje coloquial, que podría ser la razón por la que estaba confundido.

  • a salvo de amenazas
  • sección crítica
  • reentrante

Para tomar el más fácil primero: tanto malloc como printf son thread-safe . Se han garantizado sus hilos seguros en Standard C desde 2011, en POSIX desde 2001, y en la práctica desde mucho antes. Lo que esto significa es que se garantiza que el siguiente programa no se bloqueará o exhibirá un mal comportamiento:

#include <pthread.h> #include <stdio.h> void *printme(void *msg) { while (1) printf("%s/r", (char*)msg); } int main() { pthread_t thr; pthread_create(&thr, NULL, printme, "hello"); pthread_create(&thr, NULL, printme, "goodbye"); pthread_join(thr, NULL); }

Un ejemplo de una función que no es segura para subprocesos es strtok . Si llama strtok desde dos subprocesos diferentes al mismo tiempo, el resultado es un comportamiento indefinido, porque strtok usa internamente un búfer estático para realizar un seguimiento de su estado. glibc agrega strtok_r para solucionar este problema, y ​​C11 agregó lo mismo (pero opcionalmente y con un nombre diferente, porque no se inventó aquí) como strtok_s .

De acuerdo, ¿pero printf no usa recursos globales para construir su producción también? De hecho, ¿qué significa imprimir en stdout desde dos hilos simultáneamente? Eso nos lleva al siguiente tema. Obviamente printf va a ser una sección crítica en cualquier programa que lo use. Solo se permite que un hilo de ejecución esté dentro de la sección crítica a la vez.

Al menos en sistemas que cumplen con POSIX, esto se logra haciendo que printf comience con una llamada a flockfile(stdout) y finalice con una llamada a funlockfile(stdout) , que es básicamente como tomar un mutex global asociado con stdout.

Sin embargo, cada FILE distintivo en el programa tiene permitido tener su propio mutex. Esto significa que un hilo puede llamar a fprintf(f1,...) al mismo tiempo que un segundo hilo está en el medio de una llamada a fprintf(f2,...) . No hay condiciones de carrera aquí. (Si tu libc realmente ejecuta esas dos llamadas en paralelo es un problema de QoI . Realmente no sé qué hace glibc).

Del mismo modo, malloc es poco probable que sea una sección crítica en cualquier sistema moderno, porque los sistemas modernos son lo suficientemente inteligentes como para mantener un grupo de memoria para cada subproceso en el sistema , en lugar de tener todos los subprocesos N luchando en un solo grupo. (La sbrk sistema sbrk probablemente aún sea una sección crítica, pero malloc pasa muy poco tiempo en sbrk . O mmap , o lo que sea que los niños estén usando en estos días).

De acuerdo, entonces, ¿qué significa realmente la re-entrancy ? Básicamente, significa que la función puede llamarse recursivamente de manera segura: la invocación actual se "pone en espera" mientras se ejecuta una segunda invocación, y luego la primera invocación aún puede "retomar donde quedó". (Técnicamente, esto podría no deberse a una llamada recursiva: la primera invocación podría estar en el subproceso A, que se interrumpe en el medio por el subproceso B, que hace la segunda invocación. Pero ese escenario es solo un caso especial de seguridad de subprocesos , para que podamos olvidarnos de esto en este párrafo.)

Ni printf ni malloc pueden ser llamados recursivamente por un solo hilo, porque son funciones de hoja (no se llaman a sí mismos ni llaman a ningún código controlado por el usuario que pueda hacer una llamada recursiva). Y, como vimos anteriormente, han estado protegidos contra subprocesos frente a las * reentradas multi-* enrutadas desde 2001 (mediante el uso de bloqueos).

Entonces, quienquiera que le haya dicho que printf y malloc no fueron reentrantes estaba equivocado; lo que querían decir fue probablemente que ambos tienen el potencial de ser secciones críticas en su programa, cuellos de botella donde solo un hilo puede pasar a la vez.

Nota pedante: glibc sí proporciona una extensión mediante la cual se puede hacer printf para llamar a un código de usuario arbitrario, incluido el volver a llamar a sí mismo. Esto es perfectamente seguro en todas sus permutaciones, al menos en lo que respecta a seguridad de hilos. (Obviamente, abre la puerta a vulnerabilidades de cadenas de formato absolutamente insanas .) Hay dos variantes: register_printf_function (que está documentado y razonablemente cuerdo, pero oficialmente "obsoleto") y register_printf_specifier (que es casi idéntico excepto por un parámetro no documentado adicional y falta total de documentación para el usuario ). No recomendaría ninguno de ellos, y los mencionaré aquí simplemente como un lado interesante.

#include <stdio.h> #include <printf.h> // glibc extension int widget(FILE *fp, const struct printf_info *info, const void *const *args) { static int count = 5; int w = *((const int *) args[0]); printf("boo!"); // direct recursive call return fprintf(fp, --count ? "<%W>" : "<%d>", w); // indirect recursive call } int widget_arginfo(const struct printf_info *info, size_t n, int *argtypes) { argtypes[0] = PA_INT; return 1; } int main() { register_printf_function(''W'', widget, widget_arginfo); printf("|%W|/n", 42); }

En los sistemas UNIX sabemos que malloc() es una función no reentrante (llamada al sistema). ¿Porqué es eso?

Del mismo modo, printf() también se dice que no es reentrante; ¿por qué?

Conozco la definición de reentrada, pero quería saber por qué se aplica a estas funciones. ¿Qué impide que se les garantice reentrada?


Comprendamos lo que queremos decir con re-entrant . Se puede invocar una función de reentrada antes de que haya finalizado una invocación anterior. Esto podría suceder si

  • se llama a una función en un manejador de señal (o más generalmente que Unix a algún manejador de interrupción) para una señal que se generó durante la ejecución de la función
  • una función se llama recursivamente

Malloc no es reentrante porque está administrando varias estructuras de datos globales que rastrean bloques de memoria libres.

printf no es reentrante porque modifica una variable global, es decir, el contenido de FILE * stout.


Es porque ambos funcionan con recursos globales: estructuras de memoria de montón y consola.

EDITAR: el montón no es más que una estructura de lista vinculada. Cada malloc o free modifica, por lo que tener varios hilos en el mismo tiempo con acceso de escritura dañará su consistencia.

EDIT2: otro detalle: se pueden hacer reentrantes por defecto mediante el uso de mutexes. Pero este enfoque es costoso, y no existe garantía de que siempre se utilizarán en el entorno MT.

Entonces hay dos soluciones: hacer 2 funciones de biblioteca, una reentrada y otra no, o dejar la parte mutex al usuario. Han elegido el segundo.

Además, puede deberse a que las versiones originales de estas funciones no fueron reentrantes, por lo que se ha declarado así por compatibilidad.


Lo más probable es que no pueda comenzar a escribir la salida mientras que otra llamada a printf todavía está imprimiéndose. Lo mismo aplica para asignación de memoria y desasignación.


Si intentas llamar a malloc desde dos subprocesos separados (a menos que tengas una versión segura para subprocesos, no garantizada por el estándar C), suceden cosas malas, porque solo hay un montón para dos subprocesos. Lo mismo para printf: el comportamiento no está definido. Eso es lo que los hace en realidad no reentrantes.


malloc y printf generalmente usan estructuras globales y emplean sincronización basada en bloqueo internamente. Es por eso que no son reentrantes.

La función malloc podría ser segura para subprocesos o insegura de subprocesos. Ambos no son reentrantes:

  1. Malloc opera en un montón global, y es posible que dos invocaciones diferentes de malloc que ocurren al mismo tiempo, devuelvan el mismo bloque de memoria. (La segunda llamada malloc debe ocurrir antes de que se busque una dirección del fragmento, pero el fragmento no está marcado como no disponible). Esto viola la condición posterior de malloc , por lo que esta implementación no sería reentrante.

  2. Para evitar este efecto, una implementación segura de subprocesos de malloc usaría sincronización basada en bloqueo. Sin embargo, si se llama a malloc desde el manejador de señal, puede ocurrir la siguiente situación:

    malloc(); //initial call lock(memory_lock); //acquire lock inside malloc implementation signal_handler(); //interrupt and process signal malloc(); //call malloc() inside signal handler lock(memory_lock); //try to acquire lock in malloc implementation // DEADLOCK! We wait for release of memory_lock, but // it won''t be released because the original malloc call is interrupted

    Esta situación no ocurrirá cuando malloc simplemente se llame desde diferentes subprocesos. De hecho, el concepto de reentrada va más allá de la seguridad de subprocesos y también requiere que las funciones funcionen correctamente incluso si una de sus invocación nunca finaliza . Ese es básicamente el razonamiento de por qué cualquier función con bloqueos no sería reentrante.

La función printf también operaba con datos globales. Cualquier flujo de salida suele emplear un búfer global adjunto a los datos de recursos a los que se envía (un búfer para el terminal o para un archivo). El proceso de impresión suele ser una secuencia de copia de datos en el búfer y luego en el búfer. Este buffer debe estar protegido por bloqueos de la misma manera que lo hace malloc . Por lo tanto, printf tampoco es reentrante.