Pourquoi vfork () est-il destiné à être utilisé lorsque le processus enfant appelle exec () ou exit () immédiatement après sa création?

11

Concepts du système d'exploitation et APUE disent

Avec vfork (), le processus parent est suspendu et le processus enfant utilise l'espace d'adressage du parent. Étant donné que vfork () n'utilise pas la copie sur écriture, si le processus enfant modifie des pages de l'espace d'adressage du parent, les pages modifiées seront visibles par le parent une fois qu'il reprendra. Par conséquent, vfork () doit être utilisé avec prudence pour garantir que le processus enfant ne modifie pas l'espace d'adressage du parent.

vfork () est destiné à être utilisé lorsque le processus enfant appelle exec () ou exit () immédiatement après sa création.

Comment dois-je comprendre la dernière phrase?

Lorsqu'un processus enfant créé par des vfork()appels exec(), ne exec()modifie pas l'espace d'adressage du processus parent, en chargeant le nouveau programme?

Lorsqu'un processus enfant créé par des vfork()appels exit(), ne exit()modifie pas l'espace d'adressage du processus parent lors de l'arrêt de l'enfant?

J'ai une préférence pour Linux.

Merci.

Tim
la source

Réponses:

15

Lorsqu'un processus enfant créé par des vfork()appels exec(), ne exec()modifie pas l'espace d'adressage du processus parent, en chargeant le nouveau programme?

Non, exec()fournit un nouvel espace d'adressage pour le nouveau programme; il ne modifie pas l'espace d'adressage parent. Voir par exemple la discussion des execfonctions dans POSIX et la execve()page de manuel Linux .

Lorsqu'un processus enfant créé par vfork () appelle exit (), exit () ne modifie-t-il pas l'espace d'adressage du processus parent lors de l'arrêt de l'enfant?

Tout simplement exit()- il exécute les hooks de sortie installés par le programme en cours d'exécution (y compris ses bibliothèques). vfork()est plus restrictif; ainsi, sous Linux, il rend obligatoire l'utilisation de _exit()ce qui n'appelle pas les fonctions de nettoyage de la bibliothèque C.

vfork()s'est avéré assez difficile à obtenir correctement; il a été supprimé dans les versions actuelles de la norme POSIX et posix_spawn()devrait être utilisé à la place.

Cependant, à moins que vous ne sachiez vraiment ce que vous faites, vous ne devez pas utiliser ni vfork()ou posix_spawn(); s'en tenir au bon vieux fork()et exec().

La page de manuel Linux liée ci-dessus fournit plus de contexte:

Cependant, dans le mauvais vieux temps, fork(2) il faudrait faire une copie complète de l'espace de données de l'appelant, souvent inutilement, car généralement immédiatement après, une opération exec(3)est effectuée. Ainsi, pour plus d'efficacité, BSD a introduit l' vfork() appel système, qui ne copiait pas entièrement l'espace d'adressage du processus parent, mais empruntait la mémoire et le thread de contrôle du parent jusqu'à ce qu'un appel execve(2)ou une sortie se produise. Le processus parental a été suspendu pendant que l'enfant utilisait ses ressources. L'utilisation de vfork()était délicate: par exemple, la non-modification des données dans le processus parent dépendait de la connaissance des variables contenues dans un registre.

Stephen Kitt
la source
Merci. "exec () fournit un nouvel espace d'adressage pour le nouveau programme;" Le comportement normal de exec () est-il de charger un programme dans l'espace d'adressage du processus? Je n'ai pas trouvé dans les deux liens où cela crée un nouvel espace d'adressage normalement ou particulièrement pour vfork ().
Tim
1
Ce qui est drôle, c'est que vfork () gagne contre à peu près tout le reste maintenant. C'est ridiculement plus rapide que fork () lorsque vous avez un gigaoctet de mémoire inscriptible.
Joshua
2
Veuillez ne pas dire aux gens d'utiliser posix_spawn. Il est beaucoup plus difficile d'écrire du code correct en utilisant posix_spawnqu'avec du vieux simple fork, et si vous essayez, vous risquez de tomber sur le mur de briques en l'absence d'une action de fichier ou d'un attribut qui fait ce que vous devez faire entre forket exec. Et il n'est pas garanti d'avoir une efficacité de type vfork, donc cela ne résout même pas le problème que les gens veulent qu'il résout.
zwol
1
@zwol: C'est vraiment un mauvais conseil. Bien qu'il posix_spawnpuisse manquer les fonctionnalités que vous souhaitez (vous pouvez résoudre ce problème via un programme d'assistance intermédiaire, écrit en C ou un script shell inline-on-cmdline), toute tentative pour atteindre ce que vous voulez avec vforkinvoque un comportement non défini dangereux. La spécification de vforkne permet pas d'appeler des fonctions aléatoires pour configurer l'état pour que l'enfant hérite avant execve, et les tentatives de le faire peuvent corrompre l'état du parent.
R .. GitHub STOP HELPING ICE
1
@Joshua: Une implémentation moderne de posix_spawnfonctionne à peu près comme vforkdans la plupart des conditions. Ceux où il y a une différence ont tendance à être exactement les cas où vforkc'est très dangereux: où il y a des gestionnaires de signaux installés qui posix_spawndoivent empêcher l'exécution chez l'enfant avant l'exécution.
R .. GitHub STOP HELPING ICE
4

Lorsque vous appelez vfork(), un nouveau processus est créé et ce nouveau processus emprunte l'image de processus du processus parent à l'exception de la pile. Le processus enfant reçoit une nouvelle étoile de pile, mais ne permet pas returnde la fonction qui a appelé vfork().

Pendant que l'enfant s'exécute, le processus parent est bloqué, car l'enfant a emprunté l'espace d'adressage du parent.

Peu importe ce que vous faites, tout ce qui accède à la pile ne modifie que la pile privée de l'enfant. Cependant, si vous modifiez des données globales, cela modifie les données communes et affecte donc également le parent.

Les choses qui modifient les données globales sont par exemple:

  • appeler malloc () ou gratuit ()

  • en utilisant stdio

  • modification des paramètres du signal

  • modification des variables qui ne sont pas locales à la fonction qui a appelé vfork().

  • ...

Une fois que vous appelez _exit()(important, ne jamais appeler exit()), l'enfant est terminé et le contrôle est rendu au parent.

Si vous appelez une fonction de la exec*()famille, un nouvel espace d'adressage est créé avec un nouveau code de programme, de nouvelles données et une partie de la pile du parent (voir ci-dessous). Une fois que cela est prêt, l'enfant n'emprunte plus l'espace d'adressage à l'enfant, mais utilise un propre espace d'adressage.

Le contrôle est rendu au parent, car son espace d'adressage n'est plus utilisé par un autre processus.

Important: sous Linux, il n'y a pas de véritable vfork()implémentation. Linux implémente plutôt vfork()basé sur le fork()concept Copy on Write introduit par SunOS-4.0 en 1988. Afin de faire croire aux utilisateurs qu'ils utilisent vfork(), Linux configure simplement les données partagées et suspend le parent pendant que l'enfant n'appelle pas _exit()ou l'une des exec*()fonctions.

Linux ne bénéficie donc pas du fait qu'un réel vfork()n'a pas besoin de mettre en place une description de l'espace d'adressage pour l'enfant dans le noyau. Il en résulte un vfork()qui n'est pas plus rapide que fork(). Sur les systèmes qui implémentent un réel vfork(), il est généralement 3 fois plus rapide fork()et affecte les performances des shells qui utilisent vfork()- ksh93, le récent Bourne Shellet le csh.

La raison pour laquelle vous ne devriez jamais appeler exit()de l' vfork()enfant ed est qu'il exit()vide stdio au cas où il y aurait des données non vidées de la période précédant l'appel vfork(). Cela pourrait provoquer des résultats étranges.

BTW: posix_spawn()est implémenté par-dessus vfork(), vfork()ne va donc pas être supprimé du système d'exploitation. Il a été mentionné que Linux n'utilise pas vfork()pour posix_spawn().

Pour la pile, il y a peu de documentation, voici ce que dit la page de manuel Solaris:

 The vfork() and vforkx() functions can normally be used  the
 same  way  as  fork() and forkx(), respectively. The calling
 procedure, however, should not return while running  in  the
 child's  context,  since the eventual return from vfork() or
 vforkx() in the parent would be to a  stack  frame  that  no
 longer  exists. 

L'implémentation peut donc faire ce qu'elle veut. L'implémentation Solaris utilise la mémoire partagée pour le cadre de pile de l'appel de fonction vfork(). Aucune implémentation n'accorde l'accès aux parties plus anciennes de la pile à partir du parent.

schily
la source
4
Ni la bibliothèque GNU C ni la bibliothèque musl C ne sont implémentées posix_spawn()sur Linux vfork(). Ils l'implémentent tous les deux par dessus __clone().
JdeBP
1
@JdeBP: Vous savez, vfork()juste des appels, clone()non? C'est littéralement une doublure dans le noyau.
Joshua
1
"Important: sous Linux, il n'y a pas de véritable implémentation de vfork ()." <- Ce n'est pas vrai et ce n'est pas le cas depuis au moins une décennie. Si votre benchmark shell n'observe aucune différence de performances entre vforket forksur Linux, il fait quelque chose de mal.
zwol
1
La deuxième moitié de cette réponse commençant par "Important: sous Linux, il n'y a pas de véritable implémentation de vfork ()" est la plupart du temps erronée.
R .. GitHub STOP HELPING ICE
1
Veuillez ne pas faire de réclamation sans avoir vérifié. Le Bourne Shell actuel peut être compilé avec et sans le support de vfork, donc même si vous pensez que les fonctionnalités de débogage de Linux ne peuvent pas donner de résultats fiables, vous pouvez comparer les temps d'exécution pour un appel de configuration avec et avec vfork dans le shell. J'utilise un script de configuration avec 800 tests. Sous Solaris, le Bourne Shell utilisant vfork a besoin de 30% de temps de processeur système en moins par rapport au boîtier fork. Sous Linux, le même test se traduit par moins de 10% de temps de processeur système en moins. Sous Solaris, ce n'est pas 3x car de nombreux appels de compilateur sont inclus.
schily