Pregunta

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

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

Yo sé la definición de re-entrada, pero yo quería saber por qué se aplica a estas funciones.   Lo que les impide ser garantizados por reentrada?

¿Fue útil?

Solución

malloc y printf suelen utilizar estructuras globales, y la sincronización basada en la cerradura emplean internamente. Es por eso que no estamos de reentrada.

La función malloc o bien podría ser thread-safe o hilo-inseguro. Ambos no son reentrantes:

  1. Malloc opera en un montón global, y es posible que dos invocaciones diferentes de malloc que suceden al mismo tiempo, devuelven el mismo bloque de memoria. (La segunda llamada malloc debería ocurrir antes de una dirección de la porción es exagerado, pero el trozo no está marcado como no disponible). Esto viola la condición posterior de malloc, por lo que esta aplicación no sería re-entrante.

  2. Para evitar este efecto, una implementación thread-safe de malloc usaría de sincronización basado en la cerradura. Sin embargo, si malloc se llama desde el manejador de la señal, la siguiente situación puede ocurrir:

    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 va a pasar cuando malloc se llama simplemente a partir de diferentes hilos. De hecho, el concepto va más allá de reentrada hilo de seguridad y también requiere funciones para que funcione correctamente incluso si uno de su invocación no termina . Eso es básicamente el razonamiento por la que cualquier función con cerraduras sería no reentrante.

La función printf también operó en datos global. Cualquier flujo de salida por lo general emplea una memoria intermedia mundial unido a los datos de recursos son enviados a (un buffer para el terminal, o para un archivo). El proceso de impresión es por lo general una secuencia de copia de datos para amortiguar y vaciar el búfer después. Este tampón debe estar protegida por las cerraduras de la misma manera malloc hace. Por lo tanto, printf es también no reentrante.

Otros consejos

Vamos a entender lo que entendemos por reentrante . Una función reentrante puede ser invocado ante una invocación anterior ha terminado. Esto puede suceder si

  • una función se llama en un controlador de señal (o más generalmente de Unix algunos manejador de interrupciones) para una señal que se planteó durante la ejecución de la función
  • se llama una función recursiva

malloc no es reentrante, ya que es la gestión de varias estructuras de datos globales que hacen un seguimiento bloques de memoria libre.

printf no es reentrante porque modifica una variable global es decir, el contenido del archivo * robusto.

Existen al menos tres conceptos aquí, todos los cuales se confunden en un lenguaje coloquial, que puede ser por eso que estaban confundidos.

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

Para tomar la más fácil primero: Tanto malloc y printf son seguro para subprocesos . Han sido garantiza que sea seguro para subprocesos en C estándar desde 2011, en POSIX desde 2001, y en la práctica desde mucho antes. Lo que esto significa es que el siguiente programa se garantiza que no se bloquee o presentan 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 seguro para subprocesos es strtok. Si llama strtok a partir de dos hilos diferentes al mismo tiempo, el resultado es un comportamiento indefinido - porque strtok internamente utiliza un buffer estático para realizar un seguimiento de su estado. glibc añade strtok_r para solucionar este problema, y ??C11 añadió lo mismo (pero opcionalmente y con un nombre diferente, porque no inventado aquí) como strtok_s.

De acuerdo, pero no hace uso printf recursos globales para construir su salida, también? De hecho, ¿cuál sería incluso media para imprimir en la salida estándar de dos hilos al mismo tiempo? Esto nos lleva al siguiente tema. Obviamente printf va a ser un sección crítica en cualquier programa que lo utiliza. Sólo un hilo de ejecución se le permite estar dentro de la sección crítica a la vez.

Por lo menos en los sistemas compatibles con POSIX, esto se consigue teniendo printf comienzan con una llamada a flockfile(stdout) y al final con una llamada a funlockfile(stdout), que básicamente es como tomar un mutex global asociado con la salida estándar.

Sin embargo, cada FILE distinto en el programa se le permite tener su propia exclusión mutua. Esto significa que un hilo puede llamar fprintf(f1,...) al mismo tiempo que un segundo hilo de rosca está en el medio de una llamada a fprintf(f2,...). No hay ninguna condición de carrera aquí. (Ya sea que su libc realmente ejecuta esas dos llamadas en paralelo es un tema QoI . yo 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 inteligente como para mantener una reserva de memoria para cada hilo en el sistema , en lugar de tener todas las discusiones N pelean por un solo grupo. (La llamada al sistema sbrk también debería ser una sección crítica, pero malloc pasa muy poco de su tiempo en sbrk. O mmap, o lo que los niños frescos lo están usando en estos días.)

Bueno, por lo re-entrada realidad media Básicamente, esto significa que la función de seguridad se puede llamar de forma recursiva - la invocación actual está "en espera" mientras que un segundo corre de invocación, y luego la primera invocación es todavía capaz de "pick up donde lo dejó ". (Técnicamente esto fuerza no puede ser debido a una llamada recursiva: la primera invocación podría estar en Tema A, que se interrumpe en el medio por el hilo B, lo que hace que la segunda invocación Pero ese escenario es sólo una. caso especial de hilo de seguridad , por lo que puede olvidarse de él en este párrafo.)

Ni printf ni malloc pueden posiblemente pueden denominan de forma recursiva por un solo hilo, ya que son funciones de la hoja (no llaman a sí mismos ni llaman a cualquier usuario-controlled código que podría hacer una llamada recursiva). Y, como hemos visto más arriba, han sido compatibles con el proceso contra múltiples * * reentrantes llamadas roscados desde 2001 (por el uso de bloqueos).

Por lo tanto, quien te dijo eso printf y malloc eran no reentrante estaba mal; lo que querían decir era probable que ambos tienen el potencial de ser secciones críticas en su programa -. cuellos de botella en el que sólo un hilo puede conseguir a través a la vez


pedante nota: glibc proporciona una extensión por la cual printf se puede hacer para llamar a código de usuario arbitraria, incluyendo re-llamar a sí mismo. Esto es perfectamente seguro en todas sus permutaciones - al menos en lo que se refiere a hilos de seguridad. (Obviamente se abre la puerta a absolutamente loco vulnerabilidades de formato de cadena.) Hay dos variantes: register_printf_function (que se documenta y razonablemente correcta, pero oficialmente "en desuso") y register_printf_specifier (que es casi idénticos excepto por un parámetro adicional y un indocumentado total falta de documentación destinada a los usuarios ). No recomendaría cualquiera de ellos, y mencionarlas aquí simplemente como un dato 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);
}

Lo más probable es porque no se puede empezar a escribir de salida, mientras que otra llamada a printf está imprimiendo es uno mismo. Lo mismo vale para la asignación de memoria y cancelación de asignación.

Es porque ambas obras con recursos globales: las estructuras de memoria heap y la consola.

EDIT: el montón no es otra cosa que una estructura de lista tipo que vinculados. Cada malloc o free modifica, por lo que tener varios hilos en el mismo tiempo con la escritura de acceso a la misma puede dañar su consistencia.

Edit2: otro detalle: podían ser hechas por reentrada por defecto mediante el uso de exclusiones mutuas. Pero este enfoque es costoso, y no hay garantia de que serán siempre se utilizan en el entorno de MT.

Así que hay dos soluciones: hacer de 2 funciones de la biblioteca, uno de reentrada y uno no, o dejar la parte mutex para el usuario. Han choosed el segundo.

También, puede ser debido a que las versiones originales de estas funciones eran no reentrante, por lo the've sido declarados por lo que para la compatibilidad.

Si intenta llamar a malloc a partir de dos hilos separados (a menos que tenga una versión flujos seguros, no está garantizado por la norma C), suceden cosas malas, porque sólo hay un montón de dos hilos. Lo mismo para printf- el comportamiento no está definido. Eso es lo que les hace, en realidad, no reentrante.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top