¿Por qué se dice que malloc() y printf () no son reentrantes?


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

De manera similar, printf() también se dice que no es reentrante; ¿por qué?

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

Author: Jonathan Leffler, 2010-10-15

6 answers

malloc y printf generalmente usan estructuras globales, y emplean sincronización basada en bloqueos internamente. Por eso no vuelven a entrar.

La función malloc podría ser thread-safe o thread-unsafe. 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 obtenga una dirección del fragmento, pero el trozo no está marcado como no disponible). Esto viola la postcondición de malloc, por lo que esta implementación no sería reentrada.

  2. Para evitar este efecto, una implementación segura de subprocesos de malloc usaría sincronización basada en bloqueos. Sin embargo, si se llama a malloc desde el manejador de señales, 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 sucederá cuando malloc simplemente se llame desde diferentes hilos. De hecho, el concepto de reentrada va más allá thread-safety y también requiere que las funciones funcionen correctamente incluso si una de sus invocaciones nunca termina. Ese es básicamente el razonamiento por el que cualquier función con bloqueos no sería reentrante.

La función printf también operaba en datos globales. Cualquier flujo de salida generalmente emplea un búfer global adjunto al recurso al que se envían los datos (un búfer para terminal o para un archivo). El proceso de impresión suele ser una secuencia de copia de datos para almacenar en búfer buffer después. Este búfer debe estar protegido por bloqueos de la misma manera que malloc. Por lo tanto, printf tampoco es reentrante.

 52
Author: P Shved,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-18 23:02:41

Entendamos lo que queremos decir con reentrante. Una función reentrante puede ser invocada antes de que una invocación anterior haya terminado. Esto podría suceder si

  • una función es llamada en un manejador de señales (o más generalmente que Unix algún manejador de interrupciones) para una señal que fue levantada durante la ejecución de la función
  • una función se llama recursivamente

Malloc no es reentrante porque administra varias estructuras de datos globales que rastrean la memoria libre bloque.

Printf no es reentrante porque modifica una variable global, es decir, el contenido del ARCHIVO* stout.

 10
Author: JeremyP,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-10-15 10:46:35

Hay al menos tres conceptos aquí, todos los cuales están mezclados en lenguaje coloquial, que podría ser la razón por la que estabas confundido.

  • thread-safe)
  • sección crítica
  • reentrante

Para tomar primero la más fácil: Ambos malloc y printf son seguro para roscas. Se ha garantizado que son seguros para roscas en la norma C desde 2011, en POSIX desde 2001 y en la práctica desde hace mucho tiempo antes de eso. 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 es no thread-safe es strtok. Si llamas a strtok desde dos subprocesos diferentes simultáneamente, el resultado es un comportamiento indefinido, porque strtok utiliza 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 bajo un nombre diferente, porque no Inventado Aquí) como strtok_s.

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

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

Sin embargo, cada FILE distinto en el programa tiene permitido tener su propio mutex. Esto significa que un hilo puede llamar fprintf(f1,...) al mismo tiempo que un segundo hilo está en medio de una llamada a fprintf(f2,...). No hay condiciones de carrera aquí. (Si su libc realmente ejecuta esas dos llamadas en paralelo es un QoI cuestión. En realidad no sé lo que 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 luchan por un solo grupo. (La llamada al sistema sbrk seguirá siendo probablemente una sección crítica, pero malloc pasa muy poco de su tiempo en sbrk. O mmap, o lo que sea que los chicos guays estén usando estos jornadas.)

Bien, entonces reentrancy ¿realmente malo? Básicamente, significa que la función se puede llamar recursivamente de forma segura: la invocación actual se "pone en espera" mientras se ejecuta una segunda invocación, y luego la primera invocación todavía puede "continuar donde lo dejó"."(Técnicamente esto podría no ser debido a una llamada recursiva: la primera invocación podría estar en el Hilo A, que se interrumpe en el medio por el Hilo B, que hace la segunda invocación. Pero ese escenario es solo un caso especial de thread-safety, por lo que podemos olvidarlo en este párrafo.)

Ni printf ni malloc pueden posiblemente ser llamados recursivamente por un solo hilo, porque son funciones hoja (no se llaman a sí mismas ni llaman a ningún código controlado por el usuario que posiblemente podría hacer una llamada recursiva). Y, como vimos anteriormente, han sido seguros contra llamadas de reentrante *multi-*threaded desde 2001 (al usar bloqueo).

Entonces, quienquiera que te dijera que printf y malloc no eran reentrantes estaba equivocado; lo que querían decir era probablemente que ambos tienen el potencial de ser secciones críticas en tu programa-cuellos de botella donde solo un hilo puede pasar a la vez.


Nota pedante: glibc proporciona una extensión mediante la cual printf puede hacer que llame a código de usuario arbitrario, incluyendo volver a llamarse a sí mismo. Esto es perfectamente seguro en todas sus permutaciones, al menos en la medida en cuanto a la seguridad del hilo se refiere. (Obviamente abre la puerta a vulnerabilidades de cadena de formato absolutamente dementes.) 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 extra indocumentado y una falta total de documentación de cara al usuario). No recomendaría ninguno de ellos, y los mencionaría aquí simplemente como un interesante aparte.

#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);
}
 3
Author: Quuxplusone,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-11-11 20:10:18

Lo más probable es que no pueda comenzar a escribir la salida mientras otra llamada a printf todavía está imprimiendo su auto. Lo mismo ocurre con la asignación y desasignación de memoria.

 1
Author: stdan28,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-10-15 10:16:05

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

EDITAR: el montón no es otra cosa que una estructura de lista enlazada tipo. Cada malloc o free lo modifica, por lo que tener varios subprocesos al mismo tiempo con acceso de escritura dañará su consistencia.

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

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

También, puede ser porque las versiones originales de estas funciones no eran reentrantes, por lo que se han declarado así por compatibilidad.

 -2
Author: ruslik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-10-15 10:41:56

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. Mismo para printf - el comportamiento es indefinido. Eso es lo que los hace en realidad no reentrantes.

 -4
Author: Puppy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-10-15 10:20:11