La différence entre fork (), vfork (), exec () et clone ()

197

Je cherchais à trouver la différence entre ces quatre sur Google et je m'attendais à ce qu'il y ait une énorme quantité d'informations à ce sujet, mais il n'y avait vraiment pas de comparaison solide entre les quatre appels.

Je me suis mis à essayer de compiler une sorte de coup d'œil de base sur les différences entre ces appels système et voici ce que j'ai obtenu. Toutes ces informations sont-elles correctes / manque-t-il quelque chose d'important?

Fork : L'appel de fork crée essentiellement un doublon du processus actuel, identique à presque tous les niveaux (tout n'est pas copié, par exemple, les limites de ressources dans certaines implémentations mais l'idée est de créer une copie aussi proche que possible).

Le nouveau processus (enfant) obtient un ID de processus différent (PID) et a le PID de l'ancien processus (parent) comme PID parent (PPID). Étant donné que les deux processus exécutent désormais exactement le même code, ils peuvent déterminer lequel par le code de retour de fork - l'enfant obtient 0, le parent obtient le PID de l'enfant. C'est tout, bien sûr, en supposant que l'appel fork fonctionne - sinon, aucun enfant n'est créé et le parent obtient un code d'erreur.

Vfork: La différence fondamentale entre vfork et fork est que lorsqu'un nouveau processus est créé avec vfork (), le processus parent est temporairement suspendu et le processus enfant peut emprunter l'espace d'adressage du parent. Cet état de choses étrange se poursuit jusqu'à ce que le processus enfant se termine ou appelle execve (), auquel cas le processus parent continue.

Cela signifie que le processus enfant d'un vfork () doit faire attention à ne pas modifier de manière inattendue les variables du processus parent. En particulier, le processus enfant ne doit pas retourner de la fonction contenant l'appel vfork (), et il ne doit pas appeler exit () (s'il doit quitter, il doit utiliser _exit (); en fait, cela est également vrai pour l'enfant d'une fourche normale ()).

Exec :L'appel exec est un moyen de remplacer fondamentalement l'ensemble du processus en cours par un nouveau programme. Il charge le programme dans l'espace de processus actuel et l'exécute à partir du point d'entrée. exec () remplace le processus actuel par un exécutable pointé par la fonction. Le contrôle ne revient jamais au programme d'origine, sauf en cas d'erreur exec ().

Clone :Clone, en tant que fork, crée un nouveau processus. Contrairement à fork, ces appels permettent au processus enfant de partager des parties de son contexte d'exécution avec le processus appelant, comme l'espace mémoire, la table des descripteurs de fichiers et la table des gestionnaires de signaux.

Lorsque le processus enfant est créé avec clone, il exécute l'application de fonction fn (arg). (Cela diffère de fork, où l'exécution continue dans l'enfant à partir du point de l'appel de fork d'origine.) L'argument fn est un pointeur sur une fonction qui est appelée par le processus enfant au début de son exécution. L'argument arg est passé à la fonction fn.

Lorsque l'application de fonction fn (arg) revient, le processus enfant se termine. L'entier renvoyé par fn est le code de sortie du processus enfant. Le processus enfant peut également se terminer explicitement en appelant exit (2) ou après avoir reçu un signal fatal.

Formulaire d'information obtenu:

Merci d'avoir pris le temps de lire ceci ! :)

user476033
la source
2
Pourquoi vfork ne doit-il pas appeler exit ()? Ou ne pas revenir? Exit () n'utilise-t-il pas simplement _exit ()? J'essaie aussi de comprendre :)
LazerSharks
2
@Gnuey: car il est potentiellement (s'il est implémenté différemment de fork(), ce qu'il est sous Linux, et probablement tous les BSD) empruntant l'espace d'adressage de son parent. Tout ce qu'il fait, en plus d'appeler execve()ou _exit(), a un grand potentiel pour gâcher le parent. En particulier, exit()appelle les atexit()gestionnaires et autres "finaliseurs", par exemple: il vide les flux stdio. Le retour d'un vfork()enfant pourrait potentiellement (même mise en garde que précédemment) gâcher la pile des parents.
ninjalj
Je me demandais ce qui arrive aux threads du processus parent; Sont-ils tous clonés ou uniquement le thread qui appelle le forksyscall?
Mohammad Jafar Mashhadi
@LazerSharks vfork produit un processus de type thread où la mémoire est partagée sans protection contre la copie en écriture, donc faire des trucs de pile pourrait mettre le processus parent à la poubelle.
Jasen

Réponses:

159
  • vfork()est une optimisation obsolète. Avant une bonne gestion fork()de la mémoire, il fallait faire une copie complète de la mémoire du parent, donc c'était assez cher. étant donné que dans de nombreux cas, a a fork()été suivi par exec(), qui supprime la carte mémoire actuelle et en crée une nouvelle, c'était une dépense inutile. De nos jours, fork()ne copie pas la mémoire; il est simplement défini comme "copier sur écriture", donc fork()+ exec()est tout aussi efficace que vfork()+ exec().

  • clone()est l'appel système utilisé par fork(). avec certains paramètres, il crée un nouveau processus, avec d'autres, il crée un thread. la différence entre eux est juste quelles structures de données (espace mémoire, état du processeur, pile, PID, fichiers ouverts, etc.) sont partagées ou non.

Javier
la source
22
vforkévite le besoin de commettre temporairement beaucoup plus de mémoire juste pour que l'on puisse l'exécuter exec, et c'est encore plus efficace que fork, même si ce n'est pas presque aussi élevé. Ainsi, on peut éviter d'avoir à surcharger la mémoire juste pour qu'un gros programme puisse générer un processus enfant. Donc, pas seulement une amélioration des performances, mais cela pourrait le rendre possible.
Deduplicator
5
En fait, j'ai pu constater de visu comment fork () est loin d'être bon marché lorsque votre RSS est volumineux. Je suppose que c'est parce que le noyau doit encore copier toutes les tables de pages.
Martina Ferrari
4
Il doit copier toutes les tables de pages, définir toutes les copies en écriture de la mémoire inscriptible dans les deux processus , vider le TLB, puis il doit annuler toutes les modifications apportées au parent (et vider à nouveau le TLB) exec.
zwol
3
vfork est toujours utile dans cygwin (un noyau émulant une DLL, qui s'exécute sur Windows de Microsoft). cygwin ne peut pas implémenter un fork efficace, car le système d'exploitation sous-jacent n'en a pas.
ctrl-alt-delor
80
  • execve() remplace l'image exécutable actuelle par une autre chargée à partir d'un fichier exécutable.
  • fork() crée un processus enfant.
  • vfork()est une version optimisée historique de fork(), destinée à être utilisée quand execve()est appelée directement après fork(). Cela s'est avéré bien fonctionner dans des systèmes non MMU (où fork()ils ne peuvent pas fonctionner de manière efficace) et lors de fork()processus avec une énorme mémoire pour exécuter un petit programme (pensez à Java Runtime.exec()). POSIX a standardisé le posix_spawn()pour remplacer ces deux dernières utilisations plus modernes de vfork().
  • posix_spawn()fait l'équivalent d'un fork()/execve(), et permet également de jongler avec fd entre les deux. Il est censé remplacer fork()/execve(), principalement pour les plates-formes non MMU.
  • pthread_create() crée un nouveau fil.
  • clone()est un appel spécifique à Linux, qui peut être utilisé pour implémenter n'importe quoi de fork()à pthread_create(). Cela donne beaucoup de contrôle. Inspiré rfork().
  • rfork()est un appel spécifique au Plan 9. C'est censé être un appel générique, permettant plusieurs degrés de partage, entre les processus complets et les threads.
ninjalj
la source
2
Merci d'avoir ajouté plus d'informations que ce qui était réellement demandé, cela m'a aidé à gagner du temps
Neeraj
5
Le plan 9 est une telle allumeuse.
JJ
1
Pour ceux qui ne se souviennent pas de ce que signifie MMU: "Unité de gestion de la mémoire" - lecture supplémentaire sur Wikipedia
mgarey
43
  1. fork()- crée un nouveau processus enfant, qui est une copie complète du processus parent. Les processus enfant et parent utilisent différents espaces d'adressage virtuels, qui sont initialement remplis par les mêmes pages mémoire. Ensuite, à mesure que les deux processus sont exécutés, les espaces d'adressage virtuels commencent à différer de plus en plus, car le système d'exploitation effectue une copie paresseuse des pages de mémoire qui sont écrites par l'un de ces deux processus et attribue une copie indépendante des pages modifiées de mémoire pour chaque processus. Cette technique est appelée Copy-On-Write (COW).
  2. vfork()- crée un nouveau processus enfant, qui est une copie "rapide" du processus parent. Contrairement à l'appel système fork(), les processus enfant et parent partagent le même espace d'adressage virtuel. REMARQUE! En utilisant le même espace d'adressage virtuel, le parent et l'enfant utilisent la même pile, le pointeur de pile et le pointeur d'instruction, comme dans le cas du classique fork()! Pour éviter les interférences indésirables entre le parent et l'enfant, qui utilisent la même pile, l'exécution du processus parent est gelée jusqu'à ce que l'enfant appelle exec()(créer un nouvel espace d'adressage virtuel et une transition vers une autre pile) ou _exit()(terminer l'exécution du processus ). vfork()est l'optimisation du fork()modèle "fork-and-exec". Il peut être effectué 4 à 5 fois plus rapidement que le fork(), car contrairement aufork()(même avec COW gardé à l'esprit), la mise en œuvre de l' vfork()appel système n'inclut pas la création d'un nouvel espace d'adressage (l'allocation et la mise en place de nouveaux répertoires de pages).
  3. clone()- crée un nouveau processus enfant. Divers paramètres de cet appel système spécifient quelles parties du processus parent doivent être copiées dans le processus enfant et quelles parties seront partagées entre eux. Par conséquent, cet appel système peut être utilisé pour créer toutes sortes d'entités d'exécution, à partir de threads et se terminant par des processus complètement indépendants. En fait, l' clone()appel système est la base utilisée pour l'implémentation pthread_create()et toute la famille des fork()appels système.
  4. exec()- réinitialise toute la mémoire du processus, charge et analyse le binaire exécutable spécifié, met en place une nouvelle pile et passe le contrôle au point d'entrée de l'exécutable chargé. Cet appel système ne renvoie jamais le contrôle à l'appelant et sert au chargement d'un nouveau programme dans le processus déjà existant. Cet appel système et l'appel fork()système forment ensemble un modèle de gestion de processus UNIX classique appelé "fork-and-exec".
ZarathustrA
la source
2
Notez que les exigences BSD et POSIX vforksont si faibles qu'il serait légal de faire vforkun synonyme de fork(et POSIX.1-2008 supprime vforkcomplètement la spécification). S'il vous arrive de tester votre code sur un système qui les synonyme (par exemple, la plupart des BSD post-4.4 en dehors de NetBSD, les noyaux Linux antérieurs à 2.2.0-pré6, etc.), cela peut fonctionner même si vous violez le vforkcontrat, puis explosez si vous l'exécutez ailleurs. Certains de ceux qui le simulent fork(par exemple OpenBSD) garantissent toujours que le parent ne reprendra pas l'exécution jusqu'à ce que l'enfant execou les enfants _exit. C'est ridiculement non portable.
ShadowRanger
2
concernant la dernière phrase de votre 3ème point: j'ai remarqué sur Linux en utilisant strace que bien que le wrapper glibc pour fork () appelle le clone syscall, le wrapper pour vfork () appelle le vfork syscall
ilstam
7

Les fork (), vfork () et clone () appellent tous do_fork () pour faire le vrai travail, mais avec des paramètres différents.

asmlinkage int sys_fork(struct pt_regs regs)
{
    return do_fork(SIGCHLD, regs.esp, &regs, 0);
}

asmlinkage int sys_clone(struct pt_regs regs)
{
    unsigned long clone_flags;
    unsigned long newsp;

    clone_flags = regs.ebx;
    newsp = regs.ecx;
    if (!newsp)
        newsp = regs.esp;
    return do_fork(clone_flags, newsp, &regs, 0);
}
asmlinkage int sys_vfork(struct pt_regs regs)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, &regs, 0);
}
#define CLONE_VFORK 0x00004000  /* set if the parent wants the child to wake it up on mm_release */
#define CLONE_VM    0x00000100  /* set if VM shared between processes */

SIGCHLD means the child should send this signal to its father when exit.

Pour fork, l'enfant et le père ont la table de pages VM indépendante, mais comme l'efficacité, fork ne copiera pas vraiment les pages, il définit simplement toutes les pages inscriptibles en lecture seule pour le processus enfant. Ainsi, lorsque le processus enfant souhaite écrire quelque chose sur cette page, une exception de page se produit et le noyau alloue une nouvelle page clonée à partir de l'ancienne page avec une autorisation d'écriture. Cela s'appelle "copie sur écriture".

Pour vfork, la mémoire virtuelle est exactement celle de l'enfant et du père --- juste à cause de cela, le père et l'enfant ne peuvent pas être éveillés simultanément car ils s'influenceront mutuellement. Ainsi, le père dormira à la fin de "do_fork ()" et se réveillera lorsque l'enfant appellera exit () ou execve (), car il possédera alors une nouvelle table de pages. Voici le code (dans do_fork ()) que le père dort.

if ((clone_flags & CLONE_VFORK) && (retval > 0))
down(&sem);
return retval;

Voici le code (en mm_release () appelé par exit () et execve ()) qui réveille le père.

up(tsk->p_opptr->vfork_sem);

Pour sys_clone (), il est plus flexible puisque vous pouvez y saisir n'importe quel clone_flags. Donc pthread_create () appelle cet appel système avec de nombreux clone_flags:

int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM);

Résumé: les fork (), vfork () et clone () créeront des processus enfants avec différents montages de ressources de partage avec le processus père. Nous pouvons également dire que vfork () et clone () peuvent créer des threads (en fait, ce sont des processus car ils ont un task_struct indépendant) car ils partagent la table de pages VM avec le processus père.

user991800
la source
-4

dans fork (), le processus enfant ou parent s'exécutera en fonction de la sélection du processeur. Mais dans vfork (), l'enfant s'exécutera sûrement en premier. après la fin de l'enfant, le parent s'exécutera.

Raj Kannan B.
la source
3
Faux. vfork()peut simplement être implémenté en tant que fork().
ninjalj
après AnyFork (), il n'est pas défini qui exécute le premier parent / enfant.
AjayKumarBasuthkar
5
@Raj: Vous avez des malentendus conceptuels si vous pensez qu'après avoir bifurqué, il existe une notion implicite d'ordre en série. Le fork crée un nouveau processus et rend ensuite le contrôle aux deux processus (chacun renvoyant un processus différent pid) - le système d'exploitation peut planifier le nouveau processus pour qu'il s'exécute en parallèle si une telle chose a du sens (par exemple plusieurs processeurs). Si, pour une raison quelconque, vous avez besoin que ces processus s'exécutent dans un ordre série particulier, vous avez besoin d'une synchronisation supplémentaire que le forking ne fournit pas; franchement, vous ne voudriez probablement même pas une fourchette en premier lieu.
Andon M. Coleman
En fait, @AjayKumarBasuthkar et @ninjalj, vous vous trompez tous les deux. avec vfork(), l'enfant court en premier. C'est dans les pages de manuel; l'exécution des parents est suspendue jusqu'à ce que l'enfant décède ou que l' execart. Et ninjalj recherche le code source du noyau. Il n'y a aucun moyen d'implémenter vfork()as fork()car ils transmettent différents arguments au do_fork()sein du noyau. Vous pouvez cependant implémenter vforkavec le clonesyscall
Zac Wimer
@ZacWimer: voir le commentaire de ShadowRanger à une autre réponse stackoverflow.com/questions/4856255/… Old Linux les a synchronisés, comme le font apparemment les BSD autres que NetBSD (qui a tendance à être porté sur de nombreux systèmes non MMU). À partir de la page de manuel Linux: dans 4.4BSD, il était également synonyme de fork (2) mais NetBSD l'a à nouveau introduit; voir ⟨netbsd.org/Documentation/kernel/vfork.html⟩ . Sous Linux, il a été équivalent à fork (2) jusqu'à 2.2.0-pre6 environ.
ninjalj