Dans Unix chaque fois que nous voulons créer un nouveau processus, nous bifurquons le processus actuel, créant un nouveau processus enfant qui est exactement le même que le processus parent; puis nous faisons un appel système exec pour remplacer toutes les données du processus parent par celles du nouveau processus.
Pourquoi créons-nous une copie du processus parent en premier lieu et ne créons-nous pas directement un nouveau processus?
La réponse courte est: fork
est sous Unix parce qu'il était facile de s'intégrer dans le système existant à l'époque, et parce qu'un système prédécesseur à Berkeley avait utilisé le concept de fourches.
De L'évolution du système de partage de temps Unix (le texte pertinent a été mis en évidence ):
Le contrôle des processus sous sa forme moderne a été conçu et mis en œuvre en quelques jours. Il est étonnant de voir comment il s’intègre facilement au système existant; en même temps, il est facile de voir comment certaines des caractéristiques légèrement inhabituelles de la conception sont présentes précisément parce qu'elles représentaient de petites modifications facilement codées de ce qui existait . Un bon exemple est la séparation des fonctions fork et exec. Le modèle le plus courant pour la création de nouveaux processus consiste à spécifier un programme à exécuter par le processus; sous Unix, un processus bifurqué continue d'exécuter le même programme que son parent jusqu'à ce qu'il effectue un exec explicite. La séparation des fonctions n'est certainement pas propre à Unix, et en fait, elle était présente dans le système de temps partagé de Berkeley, bien connu de Thompson . Pourtant, il semble raisonnable de supposer que il existe sous Unix principalement en raison de la facilité avec laquelle fork peut être implémenté sans changer grand chose . Le système gérait déjà plusieurs (c'est-à-dire deux) processus; il y avait une table de processus, et les processus ont été échangés entre la mémoire principale et le disque. La mise en œuvre initiale de fork requise uniquement
1) Extension de la table de processus
2) Ajout d'un appel fork qui a copié le processus en cours dans la zone de swap de disque, en utilisant les primitives swap déjà existantes IO, et fait quelques ajustements à la table de processus.
En fait, l'appel fork du PDP-7 nécessitait précisément 27 lignes de code d'assemblage. Bien sûr, d'autres changements dans le système d'exploitation et les programmes utilisateur étaient nécessaires, et certains d'entre eux étaient plutôt intéressants et inattendus. Mais un fork-exec combiné aurait été considérablement plus compliqué , ne serait-ce que parce que l'exec en tant que tel n'existait pas; sa fonction était déjà exécutée, à l'aide d'E/S explicites, par le Shell.
Depuis ce document, Unix a évolué. fork
suivi de exec
n'est plus le seul moyen d'exécuter un programme.
vfork a été créé pour être un fork plus efficace dans le cas où le nouveau processus a l'intention de faire un exec juste après le fork. Après avoir effectué une vfork, les processus parent et enfant partagent le même espace de données et le processus parent est suspendu jusqu'à ce que le processus enfant exécute un programme ou se ferme.
posix_spawn crée un nouveau processus et exécute un fichier en un seul appel système. Il prend un tas de paramètres qui vous permettent de partager sélectivement les fichiers ouverts de l'appelant et de copier sa disposition de signal et d'autres attributs dans le nouveau processus.
[Je vais répéter une partie de ma réponse de ici .]
Pourquoi ne pas simplement avoir une commande qui crée un nouveau processus à partir de zéro? N'est-il pas absurde et inefficace d'en copier un qui ne va être remplacé qu'à droite un moyen?
En fait, cela ne serait probablement pas aussi efficace pour plusieurs raisons:
La "copie" produite par fork()
est un peu une abstraction, car le noyau utilise un système copie sur écriture; il suffit de créer une carte mémoire virtuelle. Si la copie appelle alors immédiatement exec()
, la plupart des données qui auraient été copiées si elles avaient été modifiées par l'activité du processus n'ont jamais réellement à être copiées/créées car le processus ne fait rien nécessitant son utilisation.
Il n'est pas nécessaire de dupliquer ou de définir individuellement divers aspects importants du processus enfant (par exemple, son environnement) sur la base d'une analyse complexe du contexte, etc. Ils sont simplement supposés être les mêmes que ceux du processus appelant, et c'est le système assez intuitif que nous connaissons.
Pour expliquer # 1 un peu plus loin, la mémoire qui est "copiée" mais jamais accessible par la suite n'est jamais vraiment copiée, du moins dans la plupart des cas. Une exception dans ce contexte peut-être si vous avez bifurqué un processus, puis que le processus parent se termine avant que l'enfant ne se remplace par exec()
. Je dis pourrait parce qu'une grande partie du parent pourrait être mise en cache s'il y a suffisamment de mémoire libre, et je ne sais pas dans quelle mesure cela serait exploité (ce qui dépendrait de la mise en œuvre du système d'exploitation).
Bien sûr, cela ne rend pas l'utilisation d'une copie plus efficace qu'en utilisant une ardoise vierge - sauf que "l'ardoise vierge" n'est pas littéralement rien, et doit impliquer une allocation. Le système peut avoir un modèle de processus générique vierge/nouveau qu'il copie de la même manière,1 mais cela ne sauverait alors vraiment rien par rapport à la fourche de copie sur écriture. Donc # 1 démontre juste que l'utilisation d'un "nouveau" processus vide ne serait pas plus efficace.
Le point # 2 explique pourquoi l'utilisation de la fourche est probablement plus efficace. L'environnement d'un enfant est hérité de son parent, même s'il s'agit d'un exécutable complètement différent. Par exemple, si le processus parent est un shell et l'enfant un navigateur Web, $HOME
Est toujours le même pour les deux, mais comme l'un ou l'autre pourrait le modifier par la suite, il doit s'agir de deux copies distinctes. Celui de l'enfant est produit par l'original fork()
.
1. Une stratégie qui peut ne pas avoir beaucoup de sens littéral, mais mon point est que la création d'un processus implique plus que la copie de son image en mémoire à partir du disque.
Je pense que la raison pour laquelle Unix n'avait que la fonction fork
pour créer de nouveaux processus est le résultat de la philosophie Unix
Ils construisent une fonction qui fait bien une chose. Il crée un processus enfant.
Ce que l'on fait avec le nouveau processus revient alors au programmeur. Il peut utiliser l'un des exec*
fonctionne et démarre un programme différent, ou il ne peut pas utiliser exec et utiliser les deux instances du même programme, ce qui peut être utile.
Vous bénéficiez donc d'un plus grand degré de liberté puisque vous pouvez utiliser
et en plus il suffit de mémoriser le fork
et le exec*
appels de fonction, ce que vous deviez faire dans les années 1970.
Il existe deux philosophies de création de processus: fork avec héritage, et create avec arguments. Unix utilise évidemment fork. (OSE, par exemple, et VMS utilisent la méthode create.) Unix a BEAUCOUP de caractéristiques héritables, et d'autres sont ajoutés périodiquement. Par héritage, ces nouvelles caractéristiques peuvent être ajoutées SANS MODIFIER LES PROGRAMMES EXISTANTS! En utilisant un modèle de création avec arguments, l'ajout de nouvelles caractéristiques signifierait l'ajout de nouveaux arguments à l'appel de création. Le modèle Unix est plus simple.
Il offre également le modèle fork-without-exec très utile, où un processus peut se diviser en plusieurs parties. Cela était vital lorsqu'il n'y avait aucune forme d'E/S asynchrones, et est utile pour tirer parti de plusieurs processeurs dans un système. (Pré-threads.) J'ai fait beaucoup de choses au fil des ans, même récemment. En substance, il permet de conteneuriser plusieurs "programmes" en un seul programme, donc il n'y a absolument aucune place pour la corruption ou les incompatibilités de version, etc.
Le modèle fork/exec permet également à un enfant spécifique d'hériter d'un environnement radicalement étrange, installé entre le fork et l'exec. Des choses comme les descripteurs de fichiers hérités, en particulier. (Une extension de stdio fd.) Le modèle create n'offre pas la possibilité d'hériter de tout ce qui n'était pas envisagé par les créateurs de l'appel create.
Certains systèmes peuvent également prendre en charge la compilation dynamique de code natif, où le processus écrit en fait son propre programme de code natif. En d'autres termes, il veut un nouveau programme qu'il écrit lui-même à la volée, SANS avoir à passer par le cycle code source/compilateur/éditeur de liens, et occupant de l'espace disque. (Je crois qu'il existe un système de langage Verilog qui fait cela.) Le modèle fork prend en charge cela, le modèle create ne le ferait normalement pas.
La fonction fork () ne sert pas seulement à copier le processus père, elle renvoie une valeur qui fait référence au fait que le processus est le processus père ou fils, l'image ci-dessous explique comment pouvez-vous utiliser fork () comme père et fils:
comme indiqué lorsque le processus est le père fork () renvoie l'ID du processus fils PID
sinon il renvoie 0
par exemple, vous pouvez l'utiliser si vous avez un processus (serveur Web) qui reçoit les demandes et à chaque demande, il crée un son process
pour traiter cette demande, ici le père et ses fils ont des emplois différents.
Donc, pas exécuter une copie d'un processus n'est pas exactement la même chose que fork ().
La redirection d'E/S est plus facilement implémentée après fork et avant exec. L'enfant, sachant qu'il est l'enfant, peut fermer les descripteurs de fichiers, en ouvrir de nouveaux, les copier (dup () ou dup2 () pour les obtenir sur le bon numéro fd, etc., sans affecter le parent. Après cela, et peut-être que toute modification de variable d'environnement souhaitée (n'affectant pas non plus le parent), il peut exécuter le nouveau programme dans l'environnement personnalisé.