Quelles sont les différences entre fork
et exec
?
L'utilisation de fork
et exec
illustre l'esprit d'UNIX en ce sens qu'elle fournit un moyen très simple de démarrer de nouveaux processus.
L’appel fork
crée essentiellement un duplicata du processus en cours, identique dans presque tous les sens (tout n’est pas copié, par exemple, les ressources sont limitées 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 utilise le PID de l'ancien processus (parent) comme PID parent (PPID). Étant donné que les deux processus exécutent maintenant exactement le même code, ils peuvent déterminer lequel correspond au code de retour 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 reçoit un code d’erreur.
L’appel exec
permet de remplacer 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.
Ainsi, fork
et exec
sont souvent utilisés en séquence pour lancer un nouveau programme en tant qu'enfant d'un processus en cours. Les shells font généralement cela lorsque vous essayez d'exécuter un programme comme find
- le Shell forks, puis l'enfant charge le programme find
en mémoire, en configurant tous les arguments de la ligne de commande, les E/S standard, etc.
Mais ils ne sont pas tenus d'être utilisés ensemble. Il est parfaitement acceptable pour un programme de fork
lui-même sans exec
ing si, par exemple, le programme contient à la fois un code parent et un code enfant (vous devez faire attention à ce que vous faites, chaque implémentation peut avoir des restrictions). Cela a été beaucoup utilisé (et le reste) pour les démons qui écoutent simplement sur un port TCP et fork
une copie d'eux-mêmes pour traiter une requête spécifique pendant que le parent retourne à l'écoute.
De même, les programmes qui savent qu'ils sont finis et veulent simplement exécuter un autre programme n'ont pas besoin de fork
, exec
et ensuite wait
pour l'enfant. Ils peuvent simplement charger l'enfant directement dans leur espace de processus.
Certaines implémentations UNIX ont une fork
optimisée qui utilise ce qu'elles appellent une copie sur écriture. C'est une astuce pour retarder la copie de l'espace de processus dans fork
jusqu'à ce que le programme tente de modifier quelque chose dans cet espace. Ceci est utile pour les programmes utilisant uniquement fork
et non exec
, dans la mesure où ils ne doivent pas copier tout un espace de processus.
Si la exec
est est appelée après fork
(et c'est ce qui se produit le plus souvent), cela entraîne une écriture dans l'espace de processus, qui est ensuite copié pour le processus enfant.
Notez qu'il existe toute une famille d'appels exec
(execl
, execle
, execve
et ainsi de suite), mais exec
dans le contexte signifie ici aucun des deux.
Le diagramme suivant illustre l'opération fork/exec
typique dans laquelle le shell bash
est utilisé pour répertorier un répertoire avec la commande ls
:
+--------+
| pid=7 |
| ppid=4 |
| bash |
+--------+
|
| calls fork
V
+--------+ +--------+
| pid=7 | forks | pid=22 |
| ppid=4 | ----------> | ppid=7 |
| bash | | bash |
+--------+ +--------+
| |
| waits for pid 22 | calls exec to run ls
| V
| +--------+
| | pid=22 |
| | ppid=7 |
| | ls |
V +--------+
+--------+ |
| pid=7 | | exits
| ppid=4 | <---------------+
| bash |
+--------+
|
| continues
V
fork()
divise le processus en cours en deux processus. Ou, en d’autres termes, votre programme facile à penser Nice linéaire devient soudain deux programmes séparés exécutant un code:
int pid = fork();
if (pid == 0)
{
printf("I'm the child");
}
else
{
printf("I'm the parent, my child is %i", pid);
// here we can kill the child, but that's not very parently of us
}
Cela peut en quelque sorte souffler votre esprit. Vous avez maintenant un morceau de code avec un état à peu près identique exécuté par deux processus. Le processus enfant hérite de tout le code et de la mémoire du processus qui l'a créé, y compris à partir de l'endroit où l'appel fork()
vient de s'interrompre. La seule différence est le code de retour fork()
qui vous indique si vous êtes le parent ou l’enfant. Si vous êtes le parent, la valeur de retour est l'id de l'enfant.
exec
est un peu plus facile à comprendre, vous dites simplement à exec
d'exécuter un processus en utilisant l'exécutable cible et vous n'avez pas deux processus exécutant le même code ou héritant du même état. Comme @Steve Hawkins le dit, exec
peut être utilisé après fork
pour exécuter dans le processus en cours l'exécutable cible.
Je pense que certains concepts de "Advanced Unix Programming" de Marc Rochkind ont été utiles pour comprendre les différents rôles de fork()
exec()
, en particulier pour les habitués du modèle Windows CreateProcess()
:
Un _/programme est une collection d'instructions et de données conservées dans un fichier normal sur disque. (de 1.1.2 Programmes, processus et threads)
.
Pour exécuter un programme, le noyau est d’abord invité à créer un nouveau process, qui est un environnement dans lequel un programme s’exécute. (également de 1.1.2 Programmes, processus et threads)
.
Il est impossible de comprendre les appels système exec ou fork sans une bonne compréhension de la distinction entre un processus et un programme. Si ces termes sont nouveaux pour vous, vous voudrez peut-être revenir en arrière et consulter la section 1.1.2. Si vous êtes prêt à procéder maintenant, nous résumerons la distinction en une phrase: Un processus est un environnement d'exécution qui comprend des segments d'instruction, de données utilisateur et de système, ainsi que de nombreuses autres ressources acquises au moment de l'exécution. , alors qu’un programme est un fichier contenant des instructions et des données utilisées pour initialiser les segments d’instruction et de données utilisateur d’un processus. (à partir de 5,3
exec
appels système)
Une fois que vous avez compris la distinction entre un programme et un processus, le comportement des fonctions fork()
et exec()
peut être résumé comme suit:
fork()
crée un duplicata du processus en coursexec()
remplace le programme dans le processus en cours par un autre programme(Ceci est essentiellement une version simplifiée 'pour les nuls' de la réponse beaucoup plus détaillée de de paxdiablo )
Fork crée une copie d'un processus d'appel. suit généralement la structure
int cpid = fork( );
if (cpid = = 0)
{
//child code
exit(0);
}
//parent code
wait(cpid);
// end
(pour le texte du processus enfant (code), les données, la pile est la même chose que le processus appelant) le processus enfant exécute le code si bloqué.
EXEC remplace le processus en cours par le nouveau code du processus, les données, la pile . Suit généralement la structure
int cpid = fork( );
if (cpid = = 0)
{
//child code
exec(foo);
exit(0);
}
//parent code
wait(cpid);
// end
(après l'appel exec, le noyau unix efface le texte du processus enfant, les données, la pile et se remplit avec le texte/les données liés au processus foo) ainsi, le processus enfant utilise un code différent (le code de foo {différent de parent})
Ils sont utilisés ensemble pour créer un nouveau processus enfant. Tout d'abord, l'appel de fork
crée une copie du processus en cours (le processus enfant). Ensuite, exec
est appelé depuis le processus enfant pour "remplacer" la copie du processus parent par le nouveau processus.
Le processus ressemble à ceci:
child = fork(); //Fork returns a PID for the parent process, or 0 for the child, or -1 for Fail
if (child < 0) {
std::cout << "Failed to fork GUI process...Exiting" << std::endl;
exit (-1);
} else if (child == 0) { // This is the Child Process
// Call one of the "exec" functions to create the child process
execvp (argv[0], const_cast<char**>(argv));
} else { // This is the Parent Process
//Continue executing parent process
}
fork () crée une copie du processus en cours, avec exécution dans le nouvel enfant à partir de juste après l'appel de fork (). Après le fork (), ils sont identiques, à l'exception de la valeur de retour de la fonction fork (). (RTFM pour plus de détails.) Les deux processus peuvent alors diverger davantage, l'un ne pouvant pas interférer avec l'autre, sauf éventuellement via les descripteurs de fichiers partagés.
exec () remplace le processus actuel par un nouveau. Cela n'a rien à voir avec fork (), sauf qu'un exec () suit souvent fork () quand on veut, c'est lancer un processus enfant différent, plutôt que remplacer le processus actuel.
La principale différence entre fork()
et exec()
est que,
L'appel système fork()
crée un clone du programme en cours d'exécution. Le programme d'origine continue l'exécution avec la ligne de code suivante après l'appel de la fonction fork (). Le clone commence également l'exécution à la ligne de code suivante. Regardez le code suivant que j'ai obtenu de http://timmurphy.org/2014/04/26/using-fork-in-cc-a-minimum-working-example/
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv)
{
printf("--beginning of program\n");
int counter = 0;
pid_t pid = fork();
if (pid == 0)
{
// child process
int i = 0;
for (; i < 5; ++i)
{
printf("child process: counter=%d\n", ++counter);
}
}
else if (pid > 0)
{
// parent process
int j = 0;
for (; j < 5; ++j)
{
printf("parent process: counter=%d\n", ++counter);
}
}
else
{
// fork failed
printf("fork() failed!\n");
return 1;
}
printf("--end of program--\n");
return 0;
}
Ce programme déclare une variable de compteur, définie sur zéro, avant fork()
ing. Après l'appel à la fourchette, deux processus s'exécutant en parallèle, chacun incrémentant sa propre version du compteur. Chaque processus s'achève et se termine. Comme les processus se déroulent en parallèle, nous n’avons aucun moyen de savoir lequel finira le premier. L'exécution de ce programme imprimera quelque chose de similaire à ce qui est présenté ci-dessous, bien que les résultats puissent varier d'une exécution à l'autre.
--beginning of program
parent process: counter=1
parent process: counter=2
parent process: counter=3
child process: counter=1
parent process: counter=4
child process: counter=2
parent process: counter=5
child process: counter=3
--end of program--
child process: counter=4
child process: counter=5
--end of program--
La famille d'appels système exec()
remplace le code d'un processus en cours d'exécution par un autre élément de code. Le processus conserve son PID mais il devient un nouveau programme. Par exemple, considérons le code suivant:
#include <stdio.h>
#include <unistd.h>
main() {
char program[80],*args[3];
int i;
printf("Ready to exec()...\n");
strcpy(program,"date");
args[0]="date";
args[1]="-u";
args[2]=NULL;
i=execvp(program,args);
printf("i=%d ... did it work?\n",i);
}
Ce programme appelle la fonction execvp()
pour remplacer son code par le programme de date. Si le code est stocké dans un fichier nommé exec1.c, son exécution produit la sortie suivante:
Ready to exec()...
Tue Jul 15 20:17:53 UTC 2008
Le programme affiche la ligne ―Ready to exec (). . . ‖ Et après avoir appelé la fonction execvp (), remplace son code par le programme de date. Notez que la ligne -. . . Cela a-t-il fonctionné? ne s'affiche pas, car le code a alors été remplacé. Au lieu de cela, nous voyons le résultat de l'exécution de ―date -u.‖