Pourquoi fork() fonctionne de la manière qu'il le fait

Donc, j'ai utilisé fork() et je sais ce qu'il fait. En tant que débutant, j'ai été assez peur (et je ne comprends toujours pas entièrement). La description générale de fork() que vous pouvez trouver en ligne est, qu'il copie le processus actuel et assigne différent PID, parent PID et le processus aura l'espace d'adresse différent. Tout est bon, cependant, étant donné cette description de fonctionnalité un débutant se demanderait "pourquoi cette fonction est si importante... pourquoi voudrais-je copier mon processus?". Je me suis donc posé la question et j'ai finalement découvert que c'est comme ça qu'on peut appeler d'autres processus à l'intérieur de son processus actuel au moyen de la famille execve() .

ce que je ne comprends toujours pas c'est pourquoi tu dois faire ça de cette façon? La chose la plus logique serait d'avoir une fonction que vous pouvez appeler comme

create_process("executable_path+name",params..., more params); 

qui produirait un nouveau processus et commencerait à l'exécuter au début de main() et retournerait le nouveau PID.

ce qui me dérange, c'est le sentiment que la solution fork/execve fait potentiellement un travail inutile. Et si mon processus utilise des tonnes de mémoire? Est-ce que le noyau copie mes tables de page et autres. Je suis sûr qu'il n'affecte pas vraiment la mémoire réelle à moins que je l'ai touché. Et que se passe-t-il si j'ai des fils? Il me semble juste que c'est trop salissant.

presque toutes les descriptions de ce que fork fait, disons qu'il copie juste le processus et le nouveau processus commence à courir après le fork() appel. C'est en effet ce qui se passe, mais pourquoi cela se passe-t-il de cette façon et pourquoi fork/execve est-il la seule façon de générer de nouveaux processus et quelle est la façon la plus générale d'unix de créer un nouveau processus à partir de votre processus actuel? Est-il un autre moyen plus efficace pour pondre?** Qui n'a pas besoin de copier plus de mémoire.

ce " fil parle de la même question, mais je ne l'ai pas trouvé tout à fait satisfaisant:

Merci vous.

32
demandé sur Community 2011-11-28 10:27:44

14 réponses

Cela est dû à des raisons historiques. Comme expliqué à https://www.bell-labs.com/usr/dmr/www/hist.html , très tôt Unix n'avait ni fork() ni exec*() , et la façon dont le shell exécutait les commandes était:

  • faire l'initialisation nécessaire (ouverture stdin/stdout ).
  • Lire une ligne de commande.
  • ouvrez la commande, chargez du bootstrap code et sauter.
  • le code bootstrap lit la commande opened, (écrasant la mémoire du shell), et saute dessus.
  • une fois la commande terminée, elle appellerait exit() , qui fonctionnerait alors en rechargeant le shell (écrasant la mémoire de la commande), et en sautant à lui, en remontant à l'étape 1.

de là, fork() était un ajout facile (27 lignes de montage), en réutilisant le reste du code.

à ce stade du développement D'Unix, l'exécution d'une commande est devenue:

  • lire une ligne de commande.
  • fork() un enfant traite et attend (en lui envoyant un message).
  • le processus de l'enfant chargea la commande (écrasant la mémoire de l'enfant), et sauta à elle.
  • une fois la commande terminée, on l'appellerait exit() , ce qui était maintenant plus simple. Il vient de nettoyer son procédé. d'entrée, et a renoncé à un contrôle.

à l'Origine, fork() ne fait pas de copie sur écriture. Comme cela rendait fork() cher, et fork() a souvent été utilisé pour engendrer de nouveaux processus (si souvent a été immédiatement suivi par exec*() ), une version optimisée de fork() est apparu: vfork() qui a partagé la mémoire entre le parent et l'enfant. Dans ces mises en œuvre de vfork() le parent serait suspendu jusqu'à ce que l'enfant exec*() 'ed ou _exit() ' ed, abandonnant ainsi la mémoire du parent. Plus tard, fork() a été optimisé pour faire des copies sur écriture, faisant des copies des pages de mémoire seulement quand ils ont commencé à différer entre le parent et l'enfant. vfork() plus tard vu un regain d'intérêt pour les ports à !Systèmes MMU (E. g: Si vous avez un routeur ADSL, il tourne probablement Linux sur a !MMU MIPS CPU), qui ne pouvait pas faire l'optimisation de la vache,et de plus ne pouvait pas supporter efficacement les processus fork() .

autres source d'inefficacité dans fork() est qu'il duplique initialement l'espace d'adresse (et les tables de page) du parent, ce qui peut rendre l'exécution de programmes courts à partir de programmes énormes relativement lent, ou peut faire le système d'exploitation nier un fork() pensant qu'il n'y a peut-être pas assez de mémoire pour lui (pour contourner celui-ci, vous pourriez augmenter votre espace de pagination, ou changer les paramètres de surcommit mémoire de votre système d'exploitation). Comme anecdote, Java 7 utilise vfork()/posix_spawn() pour éviter ces problèmes.

sur d'autre part, fork() rend très efficace la création de plusieurs instances d'un même processus: E. g: un serveur web peut avoir plusieurs processus identiques desservant des clients différents. D'autres plateformes favorisent les fils, parce que le coût de la fraie d'un processus différent est beaucoup plus élevé que le coût de la duplication du processus actuel, qui peut être juste un peu plus grand que celui de la fraie d'un nouveau fil. Ce qui est regrettable, car les threads De shared-everything attirent les erreurs.

17
répondu ninjalj 2016-01-24 23:55:05

rappelez-vous que fork a été inventé très tôt dans Unix (et peut-être avant) sur des machines qui semblent aujourd'hui ridiculement petites (par exemple 64K octets de mémoire).

et elle est plus en phase avec la philosophie globale (originale) de fournir des mécanismes de base, et non des politiques, à travers les actions les plus élémentaires possibles.

fork crée simplement un nouveau processus, et la façon la plus simple de penser qui est de cloner le processus actuel. De sorte que le fork la sémantique est très naturelle, et c'est le machanisme le plus simple possible.

les autres appels système ( execve ) sont chargés de charger un nouvel exécutable, etc..

les séparer (et fournir aussi des pipe et dup2 syscalls) donne beaucoup de flexibilité.

et sur les systèmes actuels, fork est mis en œuvre de manière très efficace (grâce à la copie paresseuse sur les techniques de pagination en écriture). Il est connu que le mécanisme fork rend la création de processus Unix assez rapide (par exemple plus rapide que sur Windows ou sur VAX/VMS, qui ont des appels système créant des processus plus similaires à ce que vous proposez).

il y a aussi le vfork syscall, que je ne prends pas la peine d'utiliser.

Et le posix_spawn API est beaucoup plus complexe que fork ou execve seul, si illustre que fork est plus simple...

10
répondu Basile Starynkevitch 2011-11-28 06:46:47

"fork ()" était une brillante innovation qui a résolu toute une série de problèmes avec une seule API. Il a été inventé à une époque où le multiprocesseur N'était pas commun (et a précédé le type de multiprocesseur que vous et moi utilisons aujourd'hui d'environ vingt ans).

5
répondu paulsm4 2011-11-28 06:35:06

regardez spawn et ses amis.

2
répondu Matt Joiner 2011-11-28 06:31:41

Quand fork crée un nouveau processus en copiant le processus actuel, il effectue une copie sur écriture. Cela signifie que la mémoire de ce nouveau procédé est partagée avec la mère jusqu'à ce qu'il soit changé. Lorsque la mémoire est modifié, la mémoire est copié pour s'assurer que chaque processus a son propre copie valide de la mémoire. En faisant un execve juste après fork ing, il n'y a pas de copie de la mémoire, puisque le nouveau processus charge juste un nouvel exécutable, et donc une nouvelle mémoire espace.

quant à la question de savoir pourquoi cela est fait, je ne sais pas avec certitude, mais cela semble faire partie de L'Unix-way - do one thing well. Au lieu de créer une fonction qui crée un nouveau processus et charge un nouvel exécutable, l'opération est divisée en deux fonctions. Cela donne au développeur un maximum de flexibilité. Bien que je n'ai pas utilisé soit en fonction de sa propre encore...

2
répondu Eli Iser 2011-11-28 06:33:57

comme les autres ont dit, fork est mis en œuvre pour être très rapide de sorte que ce n'est pas un problème. Mais pourquoi pas une fonction comme create_process() ? La réponse est: simplicité et souplesse. tous les appels système dans unix sont programmés pour faire une seule chose. Une fonction comme create_process ferait deux choses: créer un processus et charger un binaire dans cela.

chaque fois que vous essayez de mettre en parallèle des choses, vous pouvez utiliser des threads - ou des processus ouverts avec fork() . Dans la plupart des cas, vous ouvrez les processus n via fork() et utilisez ensuite un mécanisme IPC pour communiquer et synchroniser entre ces processus. Certaines IPC insistent pour avoir des variables dans l'espace global.

exemple avec pipes:

  • Création du tube
  • fourche un enfant qui hérite du manche du tuyau
  • l'enfant ferme le côté d'entrée
  • le parent ferme le côté de sortie

Impossible sans fork() ...

un autre fait important est que L'ensemble de L'API Unix n'a que quelques fonctions. Chaque programmeur pourrait se rappeler facilement sur les fonctions utilisées. Mais voyez L'API Windows: sur des milliers de fonctions dont personne ne se souvient jamais.

donc pour résumer et le dire à nouveau: simplicité pour la flexibilité

2
répondu Christoph 2014-03-07 11:26:39

il est possible pour fork() d'être implémenté avec très peu d'allocation de mémoire, en supposant que l'implémentation sous-jacente utilise un système d'adressage copie-en-écriture. Il est impossible qu'une fonction create_process soit implémentée avec cette optimisation.

1
répondu speedplane 2011-11-28 06:34:31

ainsi, votre principale préoccupation est: fork() conduit à la copie mémoire inutile.

la réponse est: non, il n'y a pas de perte de mémoire. En bref, fork () est né alors que la mémoire était une ressource très limitée, donc personne ne songerait même à la gaspiller comme ça.

bien que chaque processus ait son propre espace d'adresse, il n'y a pas de correspondance entre la page mémoire physique et la page mémoire virtuelle du processus. Au lieu de cela, une page de mémoire physique peut être mappé sur plusieurs pages virtuelles (rechercher CPU TLB pour plus de détails).

Ainsi, lorsque vous créez un nouveau processus avec fork (), leurs espaces d'adresses virtuelles sont mappés sur les mêmes pages de mémoire physique. Aucune copie mémoire n'est requise. Cela signifie également qu'il n'y a pas de duplicata des bibliothèques utilisées parce que leurs sections de code sont marquées en lecture seule.

la copie mémoire réelle ne se produit que lorsque le processus parent ou enfant modifie une page mémoire. Dans ce cas, de nouvelles la page mémoire physique est attribuée et mappée à l'espace d'adresse virtuelle du processus qui a modifié la page.

1
répondu Kirill Gamazkov 2014-03-07 14:14:05

C'est une grande question. J'ai dû creuser un peu dans la source pour voir exactement ce qui se passait.

fork () crée un nouveau processus en dupliquant le processus d'appel.

sous Linux, fork () est implémenté en utilisant des pages copy-on-write, de sorte que la seule pénalité qu'il encourt 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.

le nouveau processus, appelé l'enfant, est une copie exacte du processus appelant (le parent). Sauf pour:

  • l'enfant a son propre numéro d'identification de processus unique, et ce numéro D'identification de processus ne correspond pas l'IDENTIFIANT de groupe de processus.
  • L'Identificateur de processus du parent de l'enfant est le même que l'Identificateur de processus du parent.
  • l'enfant n'hérite pas des serrures de mémoire de ses parents.
  • Processus de ressources les utilisations et les compteurs de temps CPU sont réinitialisés à zéro dans l'enfant.
  • la série de signaux en attente de l'enfant est initialement vide.
  • l'enfant n'hérite pas des ajustements sémaphores de son parent.
  • l'enfant n'hérite pas des Cadenas de ses parents.
  • l'enfant n'hérite pas de timers de son parent.
  • l'enfant n'hérite pas de l'entrée/sortie asynchrone en cours opérations de son parent, il n'hérite pas non plus de contextes d'E/S asynchrones de son parent.

Conclusion:

l'objectif principal de fork est de diviser les tâches du processus des parents en sous-tâches plus petites sans affecter la structure unique des tâches du parent. C'est pourquoi fork clone le processus existant.

Sources:

http://www.quora.com/Linux-Kernel/After-a-fork-where-exactly-does-the-childs-execution-start http://learnlinuxconcepts.blogspot.in/2014/03/process-management.html

1
répondu Anudeep Samaiya 2014-03-08 12:22:23

en termes de pagination/mémoire virtuelle, il existe des techniques dans lesquelles fork() ne copie pas toujours la totalité de l'espace d'adresse d'un processus. Il y a une copie sur write où un processus en forme de fourche obtient le même espace d'adresse que son parent et copie alors seulement une partie de l'espace qui est modifié (par l'un ou l'autre processus).

0
répondu urbanspr1nter 2011-11-28 06:30:25

la principale raison d'utiliser fork est la vitesse d'exécution.

si comme vous l'avez suggéré vous avez commencé une nouvelle copie du processus avec un ensemble de paramètres le nouveau processus devrait analyser ces paramètres et répéter la plupart du traitement que le processus parent a fait. Avec" fork ()", une copie complète de la pile de processus parent est immédiatement disponible pour l'enfant avec tout ce qui est analysé et formaté comme il se doit.

également dans la plupart des cas le programme sera un".donc "ou".dll" ainsi les instructions exécutables ne seront pas copiées seulement la pile et le stockage tas seront copiés.

0
répondu James Anderson 2011-11-28 06:35:16

vous pouvez penser à cela un peu comme le frai d'un thread dans Windows, sauf que les processus ne partagent pas les ressources sauf les poignées de fichier, la mémoire partagée, et d'autres choses qui sont explicitement héréditaires. Donc si vous avez une nouvelle tâche à faire, vous pouvez bifurquer et un processus continue sur son travail original pendant que le clone s'occupe de la nouvelle tâche.

si vous voulez faire du calcul parallèle, vos processus peuvent se diviser en plusieurs clones juste au-dessus de la boucle. Chacun des clones fait un sous-ensemble du calcul alors que le père attend pour eux de complet. Les systèmes d'exploitation s'assurent qu'ils peuvent fonctionner en parallèle. Dans Windows, vous aurez par exemple besoin D'utiliser OpenMP pour obtenir la même expressibilité.

si vous avez besoin de lire ou d'écrire à partir d'un fichier mais ne pouvez pas attendre, vous pouvez simplement bifurquer et votre clone fait l'e/s pendant que vous continuez sur votre tâche originale. Sur Windows, vous pourriez envisager de frayer des fils ou d'utiliser des e/s superposées dans de nombreuses situations où une fourchette simple fera l'affaire avec Unix. En particulier, les processus n'ont pas les mêmes problèmes de scability que les threads. Ceci est particulièrement vrai sur les systèmes 32 bits. Juste bifurcation est beaucoup plus convaincant que d'avoir à faire face aux complexités des entrées-sorties superposées. Alors que les processus ont leur propre espace mémoire, les threads vivent dans le même, et donc il y a une limite au nombre de threads que vous devriez considérer pour mettre dans un processus 32 bits. Faire une application serveur 32 bits avec fork est très simple, tout en faisant un 32 bits serveur d'application avec des threads peut être un cauchemar. Et donc, si vous programmez sur des fenêtres 32 bits, vous devrez recourir à d'autres solutions comme overlapped I/o, qui est une PITA pour travailler avec.

parce que les processus ne partagent pas de ressources globales comme les threads to (par exemple une serrure globale dans malloc), c'est beaucoup plus évolutif. Alors que les threads se bloquent souvent mutuellement, les processus fonctionnent indépendamment.

sur Unix parce que fork fait un clone copy-on-write de votre processus il n'est pas plus lourd que de frayer un nouveau fil dans les fenêtres.

si vous avez affaire à des langages interprétés, où il y a typiquement un lock d'interpréteur global (Python, Ruby, PHP...), un OS qui vous donne la capacité de bifurquer est indispensable. Autrement, votre capacité à exploiter plusieurs processeurs est beaucoup plus limitée.

une autre chose est qu'il y a un problème de sécurité ici. Les processus ne partagent pas d'espace mémoire et ne peuvent pas tout gâcher autres détails internes. Cela conduit à une plus grande stabilité. Si vous avez un serveur qui utilise des threads, un crash dans un thread détruira toute l'application serveur. Avec forking un crash ne fera tomber que le clone fourchu. Cela simplifie également le traitement des erreurs. Il suffit souvent de laisser votre clone bifurqué avorter car cela ne fait aucune différence pour l'application originale.

il y a aussi un problème de sécurité. Si un processus Fourché est injecté avec du code malveillant il ne peut pas affecte encore plus le parent. Les navigateurs Web modernes font usage de cela par exemple pour protéger un onglet d'un autre. Tout cela est beaucoup plus pratique pour le programme si vous avez un appel système fourche.

0
répondu Sturla Molden 2015-01-14 00:30:16

les autres réponses ont fait un bon travail en expliquant pourquoi fork est plus rapide qu'il n'y paraît, et comment il est venu à l'origine pour exister. Mais il y a aussi de bonnes raisons de garder la combinaison fork + exec , et c'est la flexibilité qu'elle offre.

Souvent, quand la ponte d'un processus enfant, il y a des étapes préparatoires à prendre avant l'exécution de l'enfant. Par exemple: vous pouvez créer une paire de pipes en utilisant pipe (un lecteur et un écrivain), puis rediriger le processus enfant stdout ou stderr à l'écrivain, ou utiliser le lecteur comme le processus stdin - ou tout autre descripteur de fichier, d'ailleurs. Ou, vous pouvez définir des variables d'environnement (mais seulement chez l'enfant). Ou fixez des limites de ressources avec setrlimit pour limiter la quantité de ressources que l'enfant pourrait utiliser (sans limiter le parent). Ou changer les utilisateurs avec setuid / seteuid (sans changer le parent). Etc etc.

bien sûr, vous pouvez faire tout cela avec une hypothétique fonction create_process . Mais c'est beaucoup de choses à couvrir! Pourquoi ne pas offrir la flexibilité d'exécuter fork , en faisant tout ce que vous voulez pour configurer l'enfant, puis en exécutant exec ?

Aussi, parfois, vous n'avez pas réellement besoin d'un processus enfant. Si votre programme (ou script) actuel existe uniquement pour faire certaines de ces étapes de configuration, et la dernière chose qu'il va jamais faire est d'exécuter le nouveau processus, alors pourquoi avoir deux processus à tous? Vous pouvez utiliser exec pour simplement remplacer le processus actuel, en libérant votre propre mémoire et PID.

Forking permet également certains comportements utiles en ce qui concerne les ensembles de données en lecture seule. Par exemple, vous pourriez avoir un processus parent qui recueille et indexe une énorme quantité de données, puis bifurque les enfants travailleurs pour effectuer des travers et des calculs basés sur ces données. Le parent n'a pas besoin d'enregistrer n'importe où, les enfants n'avez pas besoin de le lire, et vous n'avez pas besoin de faire un travail complexe avec de la mémoire partagée. (À titre d'exemple: certaines bases de données utilisent ce moyen pour qu'un processus enfant décharge la base de données en mémoire sur le disque, sans bloquer le processus parent.)

ce qui précède inclut également tout programme qui lit une configuration, une base de données, et/ou un ensemble de fichiers de code, puis procède à bifurquer les processus enfants pour traiter les requêtes et faire une meilleure utilisation des CPU multicouches. Cela inclut les serveurs web, mais aussi les applications web (ou autres) elles-mêmes, en particulier si ces applications passent une quantité importante de temps de démarrage à simplement lire et/ou compiler du code de niveau supérieur.

fourche peut également être un moyen utile de gérer la mémoire et d'éviter la fragmentation, en particulier pour les langages de niveau supérieur qui utilisent la gestion automatique de la mémoire (collecte des ordures) et n'ont pas de contrôle direct sur leur disposition de la mémoire. Si votre processus brièvement besoin d'une grande quantité de mémoire pour un opération particulière, vous pouvez bifurquer et effectuer cette opération, puis sortir, libérant toute la mémoire que vous venez d'attribuer. En revanche, si vous avez effectué l'opération dans le parent, vous pourriez avoir une fragmentation de mémoire significative qui pourrait persister pendant la durée du processus - pas grand pour un processus à long terme.

et enfin: une fois que vous acceptez que fork et exec ont tous deux leurs propres usages, indépendants l'un de l'autre, la question devient - pourquoi se donner la peine de créer un fonction distincte qui combine les deux? Il a été dit que la philosophie d'Unix était d'avoir ses outils de "faire une chose et le faire bien". En vous donnant fork et exec comme éléments de construction distincts - et en rendant chacun aussi rapide et efficace que possible - ils permettent beaucoup plus de flexibilité qu'une seule fonction create_process .

0
répondu Wisq 2016-09-29 20:32:37

historiquement, Unix fonctionnait sur des systèmes assez petits ne permettant pas à plus d'un processus de fonctionner en RAM (ils fonctionnaient tous dans le même espace d'adresse, aucun MMU n'était présent). fork échangeait simplement le processus courant sur un disque (ou un autre stockage secondaire) sans prendre la peine de le faire dans un autre processus. Vous pouvez soit continuer à exécuter la copie en mémoire, soit utiliser exec pour charger et continuer avec un exécutable différent.

les gens se sont habitués à être capable de configurer un nouvel environnement de travail (ouvrir les descripteurs de fichiers, pipes et autres) avant d'appeler exec , donc fork est resté dans les alentours.

0
répondu 2018-07-08 13:17:31