43 votes

Perte de temps avec execv() et fork()

Je suis actuellement en train d'apprendre fork() y execv() et j'avais une question concernant l'efficacité de la combinaison.

On m'a montré le code standard suivant :

pid = fork();
if(pid < 0){
    //handle fork error
}
else if (pid == 0){
    execv("son_prog", argv_son);
//do father code

Je sais que fork() clone l'ensemble du processus (en copiant l'intégralité du tas, etc.) et que execv() remplace l'espace d'adressage actuel par celui du nouveau programme. En gardant cela à l'esprit, l'utilisation de cette combinaison n'est-elle pas très inefficace ? Nous copions l'intégralité de l'espace d'adressage d'un processus pour ensuite l'écraser immédiatement.

Donc ma question :
Quel est l'avantage obtenu en utilisant cette combinaison (au lieu d'une autre solution) qui fait que les gens continuent à l'utiliser, même si nous avons des déchets ?

25 votes

Il y a très longtemps, les systèmes copiaient tout en même temps. Je vous suggère de vous renseigner sur radiomessagerie , mémoire virtuelle y copie sur écriture .

47voto

John Bollinger Points 16563

Quel est l'avantage obtenu en utilisant cette combinaison (au lieu d'une autre solution) qui fait que les gens continuent à l'utiliser même si nous avons des déchets ?

Vous devez créer un nouveau processus d'une manière ou d'une autre. Il existe très peu de moyens pour un programme en espace utilisateur d'accomplir cela. POSIX avait l'habitude d'avoir vfork() alognside fork() et certains systèmes peuvent avoir leurs propres mécanismes, tels que les mécanismes spécifiques à Linux. clone() mais depuis 2008, POSIX spécifie seulement fork() et le posix_spawn() famille. Le site fork + exec est plus traditionnelle, est bien comprise et présente peu d'inconvénients (voir ci-dessous). Le site posix_spawn est conçue comme un but spécial substitut pour une utilisation dans des contextes qui présentent des difficultés pour fork() ; vous trouverez des détails dans la section "Raison d'être" des sa spécification .

Cet extrait de la page de manuel de Linux pour vfork() peuvent être éclairantes :

Sous Linux, fork (2) est implémenté en utilisant des pages de copie sur écriture, donc la seule pénalité encourue par fork (2) le temps et la mémoire nécessaires pour reproduire les tables de pages du parent et créer une structure de tâches unique pour l'enfant. . Cependant, au bon vieux temps, un fork (2) nécessiterait de faire une copie complète de l'espace de données de l'appelant, souvent inutilement, puisqu'en général, immédiatement après l'exécution d'un exec (3) est fait. Ainsi, pour plus d'efficacité, BSD a introduit la fonction vfork (), qui ne copiait pas entièrement l'espace d'adressage du processus parent, mais empruntait la mémoire et le fil de contrôle du parent jusqu'à ce qu'un appel à la fonction execve (2) ou une sortie s'est produite. Le processus parent a été suspendu pendant que l'enfant utilisait ses ressources. L'utilisation de vfork () était délicat : par exemple, ne pas modifier les données dans le processus parent dépendait de la connaissance des variables contenues dans un registre.

(C'est nous qui soulignons)

Ainsi, votre préoccupation concernant le gaspillage n'est pas fondée pour les systèmes modernes (pas seulement pour Linux), mais c'était effectivement un problème historique, et il y avait effectivement des mécanismes conçus pour l'éviter. De nos jours, la plupart de ces mécanismes sont obsolètes.

4 votes

POSIX a posix_spawn aussi de nos jours.

2 votes

Merci, @hvd, j'avais évidemment négligé posix_spawn. J'ai mis à jour la réponse pour en tenir compte.

0 votes

Un clone avec CLONE_VM suivi d'un exec n'est-il pas plus efficace ?

24voto

user6928785 Points 231

Une autre réponse indique :

Cependant, dans le mauvais vieux temps, un fork(2) nécessitait de faire une copie complète de l'espace de données de l'appelant, souvent inutilement, puisque généralement un exec(3) est effectué immédiatement après.

Évidemment, les mauvais jours d'une personne sont beaucoup plus jeunes que ceux dont les autres se souviennent.

Les systèmes UNIX originaux ne disposaient pas de la mémoire nécessaire à l'exécution de plusieurs processus et ne possédaient pas de MMU pour maintenir plusieurs processus en mémoire physique prêts à être exécutés dans le même espace d'adressage logique : ils échangeaient sur le disque les processus qui n'étaient pas en cours d'exécution.

L'appel système fork était presque entièrement identique à l'échange du processus actuel sur le disque, à l'exception de la valeur de retour et de l'option pas en remplaçant la copie restante en mémoire par un autre processus. Puisqu'il fallait de toute façon échanger le processus parent pour exécuter le processus enfant, fork+exec n'entraînait aucune surcharge.

Il est vrai qu'il y a eu une période pendant laquelle fork+exec était gênant : quand il y avait des MMUs qui fournissaient une correspondance entre l'espace d'adressage logique et physique mais que les défauts de page ne conservaient pas assez d'informations pour que la copie sur écriture et un certain nombre d'autres schémas de mémoire virtuelle/de pagination à la demande soient réalisables.

Cette situation était suffisamment pénible, et pas seulement pour UNIX, pour que la gestion des fautes de page du matériel soit adaptée pour devenir "rejouable" assez rapidement.

6 votes

Les informations contenues dans cette réponse ne sont vraiment pas appréciées à leur juste valeur. Il est vrai qu'il serait plus approprié de la commenter ou de l'ajouter à une autre réponse, ou encore de l'éditer pour en faire une réponse complète reproduisant certaines des informations de l'autre réponse de référence - puisque dans sa forme actuelle, cette réponse est plus une clarification/addendum à l'une des autres réponses. Tout de même, +1 de ma part : Je suis vraiment content d'avoir appris ce petit bout d'information historique.

23voto

Tony Tannous Points 5953

Plus maintenant. Il y a quelque chose qui s'appelle COW (Copy On Write), ce n'est que lorsque l'un des deux processus (Parent/Child) essaie d'écrire sur une donnée partagée qu'elle est copiée.

Dans le passé :
Le site fork() L'appel système a copié l'espace d'adressage du processus appelant (le parent) pour créer un nouveau processus (l'enfant). La copie de l'espace d'adressage du parent dans l'enfant était la partie la plus coûteuse de l'appel système. fork() fonctionnement.

Maintenant :
Un appel à fork() est fréquemment suivi presque immédiatement par un appel à exec() dans le processus enfant, qui remplace la mémoire de l'enfant par un nouveau programme. C'est ce que fait généralement le shell, par exemple. Dans ce cas, le temps passé à copier l'espace d'adressage du parent est en grande partie perdu, car le processus enfant n'utilisera que très peu de sa mémoire avant de faire appel à exec() .

Pour cette raison, les versions ultérieures d'Unix ont tiré parti du matériel de mémoire virtuelle pour permettre au parent et à l'enfant de partager la mémoire mappée dans leurs espaces d'adressage respectifs jusqu'à ce que l'un des processus la modifie réellement. Cette technique est connue sous le nom de copie sur écriture . Pour ce faire, sur fork() le noyau copierait les mappages de l'espace d'adressage du parent vers l'enfant au lieu du contenu des pages mappées, et en même temps marquerait les pages maintenant partagées en lecture seule. Lorsque l'un des deux processus tente d'écrire sur l'une de ces pages partagées, le processus subit un défaut de page. À ce moment-là, le noyau Unix se rend compte que la page était en fait une copie "virtuelle" ou "copie sur l'écriture", et il crée donc une nouvelle copie privée, inscriptible, de la page pour le processus en défaut. De cette façon, le contenu des pages individuelles n'est pas réellement copié tant qu'elles ne sont pas écrites. Cette optimisation rend une fork() suivi d'un exec() dans l'enfant beaucoup moins cher : l'enfant n'aura probablement besoin de copier qu'une seule page (la page courante de sa pile) avant d'appeler exec() .

0 votes

Intéressant - Je suppose que l'appel pour écrire la valeur de retour de fork est ignoré (puisque cela se produirait toujours immédiatement après fork et irait à l'encontre du but recherché) ?

4 votes

@Dean : non, rien n'est ignoré - mais la valeur de retour de la fonction fork() ne touche probablement qu'une seule page (si elle en touche une ; selon l'ABI, elle peut ne jamais quitter les registres). C'est peu comparé à l'espace d'adressage d'un processus à longue durée de vie tel qu'un Emacs.

4 votes

Oui, il s'agira soit d'une page de pile (qui sera immédiatement dé-partagée dès que l'enfant commencera à travailler), soit d'une page d'album (qui sera immédiatement dé-partagée dès que l'enfant commencera à travailler). faire quoi que ce soit de toute façon) ou rien du tout parce que la valeur de retour est juste dans eax ou autre :)

2voto

Joshua Points 13231

Il s'avère que tous ces défauts de page COW ne sont pas du tout bon marché lorsque le processus dispose de quelques gigaoctets de RAM inscriptible. Ils vont tous tomber en panne une fois même si l'enfant a depuis longtemps appelé exec() . Parce que l'enfant de fork() n'est plus autorisé à allouer de la mémoire même dans le cas d'un thread unique (vous pouvez remercier Apple pour cela), en s'arrangeant pour appeler vfork()/exec() au lieu de cela n'est guère plus difficile maintenant.

Le véritable avantage de la vfork()/exec() est que vous pouvez configurer l'enfant avec un répertoire courant arbitraire, des variables d'environnement arbitraires, et des poignées fs arbitraires (pas seulement stdin/stdout/stderr ), un masque de signal arbitraire, et une mémoire partagée arbitraire (en utilisant les syscalls de la mémoire partagée) sans avoir un argument de 20 CreateProcess() API qui reçoit quelques arguments supplémentaires toutes les quelques années.

Il s'est avéré que la gaffe du "oops I leaked handles being opened by another thread" des premiers jours du threading était réparable en userspace sans verrouillage au niveau du processus grâce à /proc . Il n'en serait pas de même dans le géant CreateProcess() sans une nouvelle version du système d'exploitation, et en convainquant tout le monde d'appeler la nouvelle API.

Alors voilà. Un accident de conception s'est avéré bien meilleur que la solution directement conçue.

0 votes

Que voulez-vous dire par "Parce que le fils de fork() n'est plus autorisé à allouer de la mémoire même dans le cas d'un thread unique" ? Par curiosité, pourquoi est-ce le cas ? Quel est le lien avec Apple ?

0 votes

@JordanMelo : Apple a commencé à créer des threads d'arrière-plan dans la libc qui appellent malloc(). malloc() prend un verrou, donc si vous appelez fork() dans l'enfant, vous pouvez bloquer car il n'y a personne pour libérer le verrou dans l'enfant. Avant le changement d'Apple, il fallait être multithreadé pour avoir ce problème.

1voto

CSM Points 1070

Un processus créé par exec() et al, héritera de ses gestionnaires de fichiers du processus parent (y compris stdin, stdout, stderr). Si le parent les modifie après avoir appelé fork() mais avant d'appeler exec(), il peut contrôler les flux standards de l'enfant.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X