Question

Dans les systèmes UNIX, nous savons malloc() est une fonction non-rentrants (appel système). Pourquoi donc?

De même, printf() est également indiqué pour être non rentrante; pourquoi?

Je sais que la définition de réentrance, mais je voulais savoir pourquoi il applique à ces fonctions.   Ce qui empêche les être garantis rentrante?

Était-ce utile?

La solution

malloc et printf utilisent généralement des structures globales, et la synchronisation à base de verrouillage d'emploi en interne. Voilà pourquoi ils ne sont pas rentrants.

La fonction malloc pourrait être soit thread-safe ou thread dangereux. sont tous les deux pas rentrante:

  1. Malloc fonctionne sur un tas global, et il est possible que deux invocations différentes de malloc qui se produisent en même temps, retour le même bloc de mémoire. (Le 2ème appel malloc devrait se produire avant une adresse du morceau est extrait, mais le morceau est pas marqué comme indisponible). Cela porte atteinte à la postcondition de malloc, cette mise en œuvre ne serait pas rentrante.

  2. Pour éviter cet effet, une implémentation thread-safe de malloc utiliserait la synchronisation à base de verrouillage. Toutefois, si malloc est appelé un gestionnaire de signaux, la situation suivante peut se produire:

    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
    

    Cette situation ne se produira pas lorsque malloc est simplement appelé de fils différents. En effet, le concept de réentrée va au-delà fil et la sécurité exige également des fonctions pour fonctionner correctement même si l'un de son invocation ne se termine . C'est essentiellement le raisonnement pourquoi toute fonction de serrures ne serait pas rentrante.

La fonction printf opérait également sur les données globales. Tout flux de sortie utilise habituellement un tampon global attaché aux données relatives aux ressources sont envoyés (un tampon pour le terminal, ou pour un fichier). Le processus d'impression est généralement une séquence de copie de données vers le tampon et le vidage du tampon après. Ce tampon doit être protégée par des verrous de la même manière malloc fait. Par conséquent, printf est également non rentrante.

Autres conseils

Comprenons ce que nous entendons par rentrante . Une fonction rentrante peut être invoquée devant un précédent appel est terminé. Cela peut se produire si

  • une fonction est appelée dans un gestionnaire de signal (ou plus généralement de certains Unix de gestionnaire d'interruption) pour un signal qui a été soulevée pendant l'exécution de la fonction
  • une fonction est appelée récursive

malloc n'est pas rentrante, car il gère plusieurs structures de données globales qui assurent le suivi des blocs de mémoire libres.

printf n'est pas rentrante, car il modifie une variable globale à savoir le contenu du fichier * stout.

Il y a au moins trois concepts ici, dans un langage familier qui sont tous, amalgamé qui pourrait être la raison pour laquelle vous confondre.

  • thread-safe
  • section critique
  • rentrante

Pour prendre une plus facile d'abord: Les deux malloc et printf sont thread-safe . Ils ont été garantis pour être thread-safe dans la norme C depuis 2011, depuis 2001 Posix, et dans la pratique depuis bien avant. Ce que cela signifie est que le programme suivant est garanti de ne pas tomber en panne ou qui présentent un mauvais comportement:

#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 exemple d'une fonction qui est pas thread-safe est strtok. Si vous appelez strtok de deux threads différents simultanément, le résultat est un comportement non défini - parce que strtok utilise en interne un tampon statique pour garder la trace de son état. glibc ajoute strtok_r pour résoudre ce problème, et C11 a ajouté la même chose (mais en option et sous un autre nom, parce que Not Invented Here) comme strtok_s.

D'accord, mais fait les ressources mondiales d'utilisation non printf pour construire sa production, aussi? En fait, ce serait encore mean pour imprimer la sortie standard de deux fils en même temps? Cela nous amène au sujet suivant. Il est évident que printf va être un section critique dans tous les programme qui l'utilise. un seul thread d'exécution est autorisé à être à l'intérieur de la section critique à la fois.

Au moins dans les systèmes conformes POSIX, ceci est réalisé en ayant printf commencent par un appel à flockfile(stdout) et à la fin avec un appel à funlockfile(stdout), qui est essentiellement comme prendre un mutex global associé à stdout.

Cependant, chaque FILE distinct dans le programme est autorisé à avoir son propre mutex. Cela signifie qu'un fil peut appeler fprintf(f1,...) en même temps qu 'un deuxième fil se trouve au milieu d'un appel à fprintf(f2,...). Il n'y a pas état de la course ici. (Que votre libc fonctionne en fait ces deux appels en parallèle est un le numéro de QoI. Je ne sais pas vraiment ce que glibc.)

De même, il est peu probable malloc une section critique dans tout système moderne, parce que les systèmes modernes sont assez intelligent pour garder un pool de mémoire pour chaque fil dans le système, plutôt que d'avoir toutes les discussions sur une N combattent pool unique. (L'appel système sbrk sera probablement encore une section critique, mais malloc dépense très peu de son temps à sbrk. Ou mmap, ou quelles que soient les enfants de frais utilisent ces jours-ci.)

Bon, alors qu'est-ce que réentrance moyenne en fait en gros, cela signifie que la fonction peut sans risque être appelé récursive - l'invocation en cours est « mis en attente » tandis qu'un second parcours d'invocation, puis le premier appel est toujours en mesure de « ramasser où il a été interrompu « . (Techniquement, cela peut pas en raison d'un appel récursif: le premier appel est peut-être dans cette discussion A, qui est interrompu au milieu par fil B, ce qui en fait le deuxième appel Mais ce scénario est juste. cas particulier de fil de sécurité , afin que nous puissions l'oublier dans ce paragraphe.)

Ni printf ni malloc peuvent éventuellement être appelé récursive par un seul fil, car ils sont des fonctions de feuille (ils ne se disent pas, ni appellent à tout utilisateur-controllcode ed qui pourrait éventuellement faire un appel récursif). Et, comme nous l'avons vu plus haut, ils ont été contre thread-safe * multi * appels rentrants taraudés depuis 2001 (à l'aide de verrous).

Alors, celui qui vous a dit que printf et malloc étaient non-rentrants avait tort; ce qu'ils voulaient dire était sans doute que les deux d'entre eux ont le potentiel d'être sections critiques dans votre programme -. goulots d'étranglement où un seul thread peut passer à travers à la fois


Pédant Note: glibc ne fournit une extension qui printf peut être fait pour appeler un code utilisateur arbitraire, y compris lui-même re-appel. Ceci est parfaitement sûr dans toutes ses permutations - au moins en ce qui concerne la sécurité des threads. (De toute évidence, il ouvre la porte à absolument fou Failles format string.) Il existe deux variantes: register_printf_function (qui est consigné par écrit et raisonnablement sain d'esprit, mais officiellement « déconseillés ») et register_printf_specifier (qui est presque identiques, sauf pour un paramètre sans papier supplémentaire et une ). Je ne recommanderais pas l'un d'eux, et les mentionner ici que comme un côté intéressant.

#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);
}

Très probablement parce que vous ne pouvez pas commencer à écrire la sortie, tandis qu'un autre appel à printf est l'impression toujours soi-même. De même pour l'allocation de mémoire et désallocation.

Il est parce que les deux œuvres avec les ressources mondiales: structures de mémoire tas et console.

EDIT: le tas est rien d'autre qu'une structure de type liste chaînée. Chaque malloc ou free modifie, afin d'avoir plusieurs threads dans le même temps l'accès à l'écriture, il endommagera sa consistance.

EDIT2: un autre détail: ils pourraient être rentrante par défaut en utilisant mutex. Mais cette approche est coûteuse, et il n'y a pas garanty qu'ils seront toujours utilisés dans un environnement de MT.

Alors il y a deux solutions: pour faire 2 fonctions de bibliothèque, un rentrante et un pas ou laisser la partie mutex à l'utilisateur. Ils ont choisi le second.

En outre, il peut être parce que les versions originales de ces fonctions étaient non réentrant, donc the've été déclarés ainsi pour la compatibilité.

Si vous essayez d'appeler malloc de deux threads séparés (sauf si vous avez une version thread-safe, non garanti par la norme C), de mauvaises choses arrivent, parce qu'il n'y a qu'un seul tas de deux fils. Idem pour printf- le comportement est indéfini. C'est ce qui les rend en réalité non rentrante.

scroll top