Perte de temps de execv () et fork()
je suis actuellement en apprentissage sur fork()
et execv()
et j'ai eu une question concernant l'efficacité de la combinaison.
on m'a montré la norme suivante de code:
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 tout le processus (copier tout le tas, etc) et cela execv()
remplace l'espace d'adresse actuel par celui du nouveau programme. Avec cela à l'esprit, n'est-ce pas très inefficace d'utiliser cette combinaison? Nous copions l'ensemble de l'espace d'adresse d'un processus et ensuite immédiatement le remplacer.
alors ma question:
Quel est l'avantage qu'on obtient en utilisant ce combo (au lieu d'une autre solution) qui fait que les gens utilisent encore cela, même si nous avons des déchets?
5 réponses
Quel est l'avantage que l'on obtient en utilisant ce combo (au lieu d'une autre solution) qui fait que les gens utilisent encore ce même si nous avons du gaspillage?
Vous devez créer un nouveau processus en quelque sorte. Il y a très peu de façons pour un programme userspace d'accomplir cela. POSIX utilisé pour avoir vfork()
aognside fork()
, et certains systèmes peuvent avoir leurs propres mécanismes, commeclone()
, mais depuis 2008, POSIX spécifie seulement fork()
et la posix_spawn()
la famille. fork
+ exec
le parcours est plus traditionnel, est bien compris et présente peu d'inconvénients (voir ci-dessous). posix_spawn
la famille est conçue comme un usage particulier substituer à l'utilisation dans les contextes qui présentent des difficultés pour fork()
; Vous pouvez trouver des détails dans la section" raison D'être " de spécifications.
cet extrait de la page de manuel de Linux pour vfork()
peut être éclairante:
Sous Linux,
fork
(2) est implémentée en utilisant des pages copy-on-write, donc la seule pénalité encourue parfork
(2) est le temps et la mémoire nécessaires pour dupliquer les tables de page du parent, et de créer une structure de tâche unique pour l'enfant. Cependant, dans les mauvais joursfork
(2) nécessiterait de faire une copie complète de l'espace de données de l'appelant, souvent inutilement, puisque habituellement immédiatement après unexec
(3) est effectuée. Ainsi, pour plus d'efficacité, BSD a introduit levfork
() système de call, qui n'a pas entièrement copié l'espace d'adresse du processus parent, mais a emprunté la mémoire du parent et le fil de contrôle jusqu'à un appel àexecve
(2) ou une sortie s'est produite. Le processus parental a été suspendu pendant que l'enfant utilisait ses ressources. L'utilisation devfork
() était délicat: par exemple, ne pas modifier les données dans le processus parent dépendait de savoir quelles variables sont conservées dans un registre.
(non souligné dans l'original)
ainsi, votre préoccupation au sujet des déchets n'est pas bien fondé pour les systèmes modernes (pas limité à Linux), mais il était en effet un problème historique, et il y avait en effet des mécanismes conçus pour l'éviter. Ces jours, la plupart de ces mécanismes sont obsolètes.
une Autre réponse états:
Cependant, dans le mauvais vieux temps, un fork(2) exigerait de faire une copie complète de l'espace de données de l'appelant, souvent inutilement, puisque habituellement immédiatement après un exec(3) est fait.
de toute évidence, les mauvais vieux jours d'une personne sont beaucoup plus jeunes que d'autres s'en souviennent.
les systèmes UNIX originaux n'avaient pas la mémoire pour exécuter plusieurs processus et ils n'avaient pas de MMU pour garder plusieurs processus dans la mémoire physique prêts à exécuter dans le même espace d'adresse logique: ils ont échangé les processus sur un disque qui n'était pas en cours d'exécution.
l'appel de système fork était presque entièrement le même que l'échange du processus courant vers le disque, sauf pour la valeur de retour et pour remplacer la copie en mémoire restante par un échange dans un autre processus. Puisque vous avez dû de toute façon changer le processus parent pour faire tourner l'enfant, fork+exec n'a pas eu de problème généraux.
il est vrai qu'il y a eu une période de temps où fork+exec était maladroit: quand il y avait des MMU qui fournissaient un mappage entre l'espace d'adresse logique et physique, mais les défauts de page ne conservaient pas assez d'information que la copie-sur-écriture et un certain nombre d'autres schémas de demande/mémoire virtuelle étaient réalisables.
cette situation a été assez douloureuse, pas seulement pour UNIX, que la gestion des erreurs de page du matériel a été adaptée pour devenir "rejouable" assez rapidement.
plus maintenant. Il y a quelque chose qui s'appelle COW
(Copie Sur Écriture), que lorsque l'un des deux processus (Parent/Enfant) tente d'écrire de données partagée, il est copié.
Dans le passé:
fork()
appel système copié l'espace d'adressage du processus appelant (le parent) pour créer un nouveau processus (l'enfant).
La copie de l'espace d'adresse du parent dans l'enfant était la partie la plus chère de la fork()
opération.
Maintenant:
Un appel à fork()
est souvent suivi presque immédiatement d'un appel à exec()
dans le processus de l'enfant, qui remplace l'enfant de la mémoire avec un nouveau programme. C'est ce que la coquille est généralement, par exemple. Dans ce cas, le temps passé à copier l'espace d'adresse du parent est largement gaspillé, car le processus de l'enfant utilisera très peu de sa mémoire avant d'appeler exec()
.
Pour cette raison, plus tard les versions D'Unix ont profité du matériel de mémoire virtuelle pour permettre au parent et à l'enfant de partager la mémoire cartographiée dans leurs espaces d'adresse respectifs jusqu'à ce que l'un des processus la modifie réellement. Cette technique est connue sous le nom decopy-on-write. Pour ce faire, sur fork()
le noyau copierait les correspondances d'adresses 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 essaie d'écrire à l'une de ces pages partagées, le processus prend un défaut de page. À ce stade, le noyau Unix se rend compte que la page était en fait une copie "virtuelle" ou "copy-on-write", et il fait donc une nouvelle copie privée, accessible en écriture, de la page pour le processus défectueux. De cette façon, les contenus des pages individuelles ne sont pas réellement copiés jusqu'à ce qu'ils soient réellement écrits. Cette optimisation fait un fork()
suivi de exec()
dans l'enfant beaucoup moins cher: l'enfant aura probablement besoin de copier une page (la page courante de sa pile) avant d'appeler exec()
.
il s'avère que tous ces défauts de page de vache ne sont pas du tout bon marché lorsque le processus a quelques gigaoctets de mémoire vive. Ils vont tous le blâmer une fois, même si l'enfant a appelé depuis longtemps exec(). Parce que l'enfant de fork() n'est plus autorisé à allouer de mémoire, même pour le seul cas fileté (vous pouvez remercier apple pour celui-ci), organiser pour appeler vfork()/exec() à la place n'est guère plus difficile maintenant.
le véritable avantage du modèle vfork()/exec() est que vous pouvez configurer l'enfant en haut 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 certaine mémoire partagée arbitraire (en utilisant les syscalls de mémoire partagée) sans avoir une API CreateProcess() de vingt arguments qui obtient quelques arguments de plus tous les quelques années.
il s'est avéré que la gaffe "oops I leaked handles being opened by another thread" des premiers jours du filage était réparable dans l'espace utilisateur sans qu'il y ait de processus verrouillage grâce à /proc. La même chose ne serait pas dans le modèle Giant CreateProcess() sans une nouvelle version D'OS, et convaincre tout le monde d'appeler la nouvelle API.
Alors voilà. Un accident de conception a fini bien mieux que la solution directement conçue.
un processus créé par exec() et al, héritera de ses poignées de fichier du processus parent (y compris stdin, stdout, stderr). Si le parent change ces paramètres après avoir appelé fork() mais avant d'appeler exec (), alors il peut contrôler les flux standards de l'enfant.