Veuillez expliquer la fonction exec () et sa famille


98

Quelle est la exec()fonction et sa famille? Pourquoi cette fonction est-elle utilisée et comment fonctionne-t-elle?

S'il vous plaît, n'importe qui explique ces fonctions.


4
Essayez de relire Stevens et clarifiez ce que vous ne comprenez pas.
vlabrecque

Réponses:


244

Simplement, sous UNIX, vous avez le concept de processus et de programmes. Un processus est un environnement dans lequel un programme s'exécute.

L'idée simple derrière le «modèle d'exécution» UNIX est qu'il y a deux opérations que vous pouvez effectuer.

Le premier est to fork(), qui crée un tout nouveau processus contenant un duplicata (principalement) du programme actuel, y compris son état. Il existe quelques différences entre les deux processus qui leur permettent de déterminer qui est le parent et qui est l'enfant.

Le second est to exec(), qui remplace le programme dans le processus actuel par un tout nouveau programme.

À partir de ces deux opérations simples, le modèle d'exécution UNIX entier peut être construit.


Pour ajouter plus de détails à ce qui précède:

L'utilisation de fork()et exec()illustre l'esprit d'UNIX en ce sens qu'il offre un moyen très simple de démarrer de nouveaux processus.

L' fork()appel fait un quasi-double du processus actuel, identique dans presque tous les sens ( 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). Un seul processus appelle fork() mais deux processus reviennent de cet appel - cela semble bizarre mais c'est vraiment assez élégant

Le nouveau processus (appelé enfant) obtient un ID de processus différent (PID) et a le PID de l'ancien processus (le parent) comme son PID parent (PPID).

Parce que les deux processus sont en cours d' exécution maintenant exactement le même code, ils doivent être en mesure de dire qui est qui - le code de retour fork()fournit ces informations - l'enfant obtient 0, le parent obtient le PID de l'enfant (si l' fork()échec, pas l'enfant est créé et le parent reçoit un code d'erreur).

De cette façon, le parent connaît le PID de l'enfant et peut communiquer avec lui, le tuer, l'attendre et ainsi de suite (l'enfant peut toujours trouver son processus parent avec un appel à getppid()).

L' exec()appel remplace tout le contenu actuel du processus par un nouveau programme. Il charge le programme dans l'espace de processus actuel et l'exécute à partir du point d'entrée.

Donc, fork()et exec()sont souvent utilisés en séquence pour faire exécuter un nouveau programme en tant qu'enfant d'un processus en cours. Les shells font généralement cela chaque fois que vous essayez d'exécuter un programme comme find- le shell forks, puis l'enfant charge le findprogramme en mémoire, définissant tous les arguments de ligne de commande, les E / S standard, etc.

Mais ils ne doivent pas être utilisés ensemble. Il est parfaitement acceptable pour un programme d'appeler fork()sans suite exec()si, par exemple, le programme contient à la fois du code parent et du code enfant (vous devez faire attention à ce que vous faites, chaque implémentation peut avoir des restrictions).

Cela a été beaucoup utilisé (et l'est toujours) pour les démons qui écoutent simplement sur un port TCP et se chargent d'une copie d'eux-mêmes pour traiter une requête spécifique pendant que le parent revient à l'écoute. Dans ce cas, le programme contient à la fois le code parent et le code enfant.

De même, les programmes qui savent qu'ils sont terminés et qui veulent simplement exécuter un autre programme n'ont pas besoin de le faire fork(), exec()puis wait()/waitpid()pour l'enfant. Ils peuvent simplement charger l'enfant directement dans leur espace de processus actuel avec exec().

Certaines implémentations UNIX ont une version optimisée fork()qui utilise ce qu'elles appellent la copie sur écriture. C'est une astuce pour retarder la copie de l'espace de processus fork()jusqu'à ce que le programme tente de changer quelque chose dans cet espace. Ceci est utile pour les programmes utilisant uniquement fork()et non exec()dans la mesure où ils n'ont pas à copier un espace de processus entier. Sous Linux, fork()ne fait qu'une copie des tableaux de pages et une nouvelle structure de tâches, exec()fera le gros travail de "séparation" de la mémoire des deux processus.

Si le exec est appelé suivant fork(et c'est ce qui se passe le plus souvent), cela provoque une écriture dans l'espace de processus et il est ensuite copié pour le processus enfant, avant que les modifications ne soient autorisées.

Linux a également un vfork(), encore plus optimisé, qui partage à peu près tout entre les deux processus. Pour cette raison, il existe certaines restrictions dans ce que l'enfant peut faire, et le parent s'arrête jusqu'à ce que l'enfant appelle exec()ou _exit().

Le parent doit être arrêté (et l'enfant n'est pas autorisé à revenir de la fonction courante) puisque les deux processus partagent même la même pile. Ceci est légèrement plus efficace pour le cas d'utilisation classique de fork()suivi immédiatement de exec().

Notez qu'il ya une famille de execappels ( execl, execle, execveetc.) , mais execdans le contexte signifie ici l' un d'eux.

Le diagramme suivant illustre l' fork/execopération typique où le bashshell est utilisé pour lister un répertoire avec la lscommande:

+--------+
| 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

12
Merci pour cette explication élaborée :)
Faizan

2
Merci pour la référence du shell avec le programme find. Exactement ce que j'avais besoin de comprendre.
Utilisateur

Pourquoi execl'utilitaire est-il utilisé pour rediriger les E / S du processus actuel? Comment le cas "nul", exécutant exec sans argument, a-t-il été utilisé pour cette convention?
Ray

@Ray, j'y ai toujours pensé comme une extension naturelle. Si vous pensez execau moyen de remplacer le programme actuel (le shell) dans ce processus par un autre, alors ne pas spécifier cet autre programme par lequel le remplacer peut simplement signifier que vous ne voulez pas le remplacer.
paxdiablo

Je vois ce que vous entendez si par "extension naturelle" vous entendez quelque chose du genre "croissance organique". Il semble que la redirection aurait été ajoutée pour prendre en charge la fonction de remplacement de programme, et je peux voir ce comportement rester dans le cas dégénéré d' execêtre appelé sans programme. Mais c'est un peu étrange dans ce scénario car l'utilité initiale de la redirection pour un nouveau programme - un programme qui serait effectivement executilisé - disparaît et vous avez un artefact utile, redirigeant le programme actuel - qui n'est pas executilisé ou démarré en aucune façon - à la place.
Ray

36

Les fonctions de la famille exec () ont des comportements différents:

  • l: les arguments sont passés sous forme de liste de chaînes au main ()
  • v: les arguments sont passés sous forme de tableau de chaînes au main ()
  • p: chemin / s pour rechercher le nouveau programme en cours d'exécution
  • e: l'environnement peut être spécifié par l'appelant

Vous pouvez les mélanger, donc vous avez:

  • int execl (const char * chemin, const char * arg, ...);
  • int execlp (const char * fichier, const char * arg, ...);
  • int execle (const char * chemin, const char * arg, ..., char * const envp []);
  • int execv (const char * chemin, char * const argv []);
  • int execvp (const char * fichier, char * const argv []);
  • int execvpe (const char * fichier, char * const argv [], char * const envp []);

Pour tous, l'argument initial est le nom d'un fichier à exécuter.

Pour plus d'informations, lisez la page de manuel exec (3) :

man 3 exec  # if you are running a UNIX system

1
Fait intéressant, vous avez manqué execve()votre liste, qui est définie par POSIX, et vous avez ajouté execvpe(), qui n'est pas définie par POSIX (principalement pour des raisons de précédent historique; elle complète l'ensemble des fonctions). Sinon, une explication utile de la convention de dénomination pour la famille - un complément utile à paxdiablo 'une réponse qui explique plus sur le fonctionnement des fonctions.
Jonathan Leffler

Et, pour votre défense, je vois que la page de manuel Linux pour execvpe()(et al) ne répertorie pas execve(); il a sa propre page de manuel séparée (au moins sur Ubuntu 16.04 LTS) - la différence étant que les autres exec()fonctions de la famille sont répertoriées dans la section 3 (fonctions) alors qu'elles execve()sont répertoriées dans la section 2 (appels système). Fondamentalement, toutes les autres fonctions de la famille sont implémentées en termes d'appel à execve().
Jonathan Leffler

18

La execfamille de fonctions permet à votre processus d'exécuter un programme différent, remplaçant l'ancien programme qu'il exécutait. Ie, si vous appelez

execl("/bin/ls", "ls", NULL);

puis le lsprogramme est exécuté avec l'identifiant du processus, le répertoire de travail actuel et l'utilisateur / groupe (droits d'accès) du processus qui a appelé execl. Ensuite, le programme d'origine ne fonctionne plus.

Pour démarrer un nouveau processus, l' forkappel système est utilisé. Pour exécuter un programme sans remplacer l'original, vous devez forkalors exec.


Merci qui a été vraiment utile. Je fais actuellement un projet nous obligeant à utiliser exec () et votre description a solidifié ma compréhension.
TwilightSparkleTheGeek

7

quelle est la fonction exec et sa famille.

La execfamille de fonctions est toutes les fonctions utilisées pour exécuter un fichier, comme execl, execlp, execle, execvet execvp.Ils sont tous frontend pour execveet fournir des méthodes différentes de l' appeler.

pourquoi cette fonction est-elle utilisée

Les fonctions Exec sont utilisées lorsque vous souhaitez exécuter (lancer) un fichier (programme).

et comment ça marche.

Ils fonctionnent en écrasant la mémoire image actuelle par celle que vous avez lancée. Ils remplacent (en mettant fin) le processus en cours d'exécution (celui qui a appelé la commande exec) par le nouveau processus qui a été lancé.

Pour plus de détails: voir ce lien .


7

execest souvent utilisé en conjonction avec fork, dont j'ai vu que vous avez également posé des questions, je vais donc en discuter avec cela à l'esprit.

exectransforme le processus en cours en un autre programme. Si vous avez déjà regardé Doctor Who, alors c'est comme quand il se régénère - son ancien corps est remplacé par un nouveau corps.

La façon dont cela se produit avec votre programme et execest que beaucoup de ressources que le noyau du système d'exploitation vérifie pour voir si le fichier que vous passez en exectant qu'argument du programme (premier argument) est exécutable par l'utilisateur actuel (identifiant utilisateur du processus faire l' execappel) et si tel est le cas, il remplace le mappage de mémoire virtuelle du processus en cours par une mémoire virtuelle, le nouveau processus et copie les données argvet envpqui ont été transmises lors de l' execappel dans une zone de cette nouvelle carte de mémoire virtuelle. Plusieurs autres choses peuvent également se produire ici, mais les fichiers qui étaient ouverts pour le programme qui a appelé le execseront toujours pour le nouveau programme et ils partageront le même ID de processus, mais le programme qui a appelé execcessera (sauf si l'exécution échoue).

La raison pour laquelle cela est fait de cette manière est qu'en séparant l' exécution d' un nouveau programme en deux étapes comme celle-ci, vous pouvez faire certaines choses entre les deux étapes. La chose la plus courante à faire est de s'assurer que le nouveau programme a certains fichiers ouverts en tant que certains descripteurs de fichiers. (rappelez-vous ici que les descripteurs de fichiers ne sont pas les mêmes que FILE *, mais sont des intvaleurs que le noyau connaît). En faisant cela, vous pouvez:

int X = open("./output_file.txt", O_WRONLY);

pid_t fk = fork();
if (!fk) { /* in child */
    dup2(X, 1); /* fd 1 is standard output,
                   so this makes standard out refer to the same file as X  */
    close(X);

    /* I'm using execl here rather than exec because
       it's easier to type the arguments. */
    execl("/bin/echo", "/bin/echo", "hello world");
    _exit(127); /* should not get here */
} else if (fk == -1) {
    /* An error happened and you should do something about it. */
    perror("fork"); /* print an error message */
}
close(X); /* The parent doesn't need this anymore */

Ceci accomplit l'exécution:

/bin/echo "hello world" > ./output_file.txt

depuis le shell de commande.


5

Lorsqu'un processus utilise fork (), il crée une copie de lui-même et ces doublons deviennent l'enfant du processus. Le fork () est implémenté en utilisant l'appel système clone () sous linux qui retourne deux fois du noyau.

  • Une valeur différente de zéro (ID de processus de l'enfant) est renvoyée au parent.
  • Une valeur de zéro est renvoyée à l'enfant.
  • Dans le cas où l'enfant n'est pas créé avec succès en raison de problèmes tels que la mémoire insuffisante, -1 est renvoyé au fork ().

Comprenons cela avec un exemple:

pid = fork(); 
// Both child and parent will now start execution from here.
if(pid < 0) {
    //child was not created successfully
    return 1;
}
else if(pid == 0) {
    // This is the child process
    // Child process code goes here
}
else {
    // Parent process code goes here
}
printf("This is code common to parent and child");

Dans l'exemple, nous avons supposé que exec () n'est pas utilisé dans le processus enfant.

Mais un parent et un enfant diffèrent dans certains des attributs PCB (bloc de contrôle de processus). Ceux-ci sont:

  1. PID - L'enfant et le parent ont un ID de processus différent.
  2. Signaux en attente - L'enfant n'hérite pas des signaux en attente de Parent. Il sera vide pour le processus enfant lors de sa création.
  3. Verrous de mémoire - L'enfant n'hérite pas des verrous de mémoire de son parent. Les verrous de mémoire sont des verrous qui peuvent être utilisés pour verrouiller une zone de mémoire et cette zone de mémoire ne peut pas être échangée sur le disque.
  4. Verrous d'enregistrement - L'enfant n'hérite pas des verrous d'enregistrement de son parent. Les verrous d'enregistrement sont associés à un bloc de fichier ou à un fichier entier.
  5. L'utilisation des ressources de processus et le temps processeur consommé sont définis sur zéro pour l'enfant.
  6. L'enfant n'hérite pas non plus des minuteries du parent.

Mais qu'en est-il de la mémoire de l'enfant? Un nouvel espace d'adressage est-il créé pour un enfant?

Les réponses en non. Après le fork (), le parent et l'enfant partagent l'espace d'adressage mémoire du parent. Sous Linux, ces espaces d'adressage sont divisés en plusieurs pages. Uniquement lorsque l'enfant écrit sur l'une des pages de mémoire parent, une copie de cette page est créée pour l'enfant. Ceci est également connu sous le nom de copie à l'écriture (copie des pages parentes uniquement lorsque l'enfant y écrit).

Comprenons copie sur écriture avec un exemple.

int x = 2;
pid = fork();
if(pid == 0) {
    x = 10;
    // child is changing the value of x or writing to a page
    // One of the parent stack page will contain this local               variable. That page will be duplicated for child and it will store the value 10 in x in duplicated page.  
}
else {
    x = 4;
}

Mais pourquoi la copie sur écriture est-elle nécessaire?

Une création de processus typique a lieu via la combinaison fork () - exec (). Comprenons d'abord ce que fait exec ().

Le groupe de fonctions Exec () remplace l'espace d'adressage de l'enfant par un nouveau programme. Une fois que exec () est appelé dans un enfant, un espace d'adressage séparé sera créé pour l'enfant qui est totalement différent de celui du parent.

S'il n'y avait pas de mécanisme de copie sur l'écriture associé à fork (), des pages dupliquées auraient été créées pour l'enfant et toutes les données auraient été copiées sur les pages de l'enfant. L'allocation d'une nouvelle mémoire et la copie de données est un processus très coûteux (prend du temps au processeur et d'autres ressources système). Nous savons également que dans la plupart des cas, l'enfant va appeler exec () et que cela remplacerait la mémoire de l'enfant par un nouveau programme. Donc, la première copie que nous avons faite aurait été un gaspillage si la copie sur écriture n'était pas là.

pid = fork();
if(pid == 0) {
    execlp("/bin/ls","ls",NULL);
    printf("will this line be printed"); // Think about it
    // A new memory space will be created for the child and that   memory will contain the "/bin/ls" program(text section), it's stack, data section and heap section
else {
    wait(NULL);
    // parent is waiting for the child. Once child terminates, parent will get its exit status and can then continue
}
return 1; // Both child and parent will exit with status code 1.

Pourquoi le parent attend-il un processus enfant?

  1. Le parent peut attribuer une tâche à son enfant et attendre qu'il termine sa tâche. Ensuite, il peut effectuer d'autres travaux.
  2. Une fois l'enfant terminé, toutes les ressources associées à l'enfant sont libérées à l'exception du bloc de contrôle de processus. Maintenant, l'enfant est dans l'état de zombie. En utilisant wait (), le parent peut se renseigner sur l'état de l'enfant puis demander au noyau de libérer le PCB. Dans le cas où le parent n'utilise pas wait, l'enfant restera dans l'état zombie.

Pourquoi l'appel système exec () est-il nécessaire?

Il n'est pas nécessaire d'utiliser exec () avec fork (). Si le code que l'enfant exécutera se trouve dans le programme associé au parent, exec () n'est pas nécessaire.

Mais pensez aux cas où l'enfant doit exécuter plusieurs programmes. Prenons l'exemple du programme shell. Il prend en charge plusieurs commandes telles que find, mv, cp, date, etc. Serait-il juste d'inclure le code de programme associé à ces commandes dans un programme ou de laisser l'enfant charger ces programmes dans la mémoire si nécessaire?

Tout dépend de votre cas d'utilisation. Vous avez un serveur Web qui a donné une entrée x qui renvoie le 2 ^ x aux clients. Pour chaque requête, le serveur Web crée un nouvel enfant et lui demande de calculer. Allez-vous écrire un programme séparé pour calculer cela et utiliser exec ()? Ou vous allez simplement écrire du code de calcul dans le programme parent?

Habituellement, la création d'un processus implique une combinaison d'appels fork (), exec (), wait () et exit ().


4

Les exec(3,3p)fonctions remplacent le processus actuel par un autre. Autrement dit, le processus en cours s'arrête et un autre s'exécute à la place, reprenant certaines des ressources du programme d'origine.


6
Pas assez. Il remplace la mémoire image actuelle par une nouvelle image mémoire. Le processus est le même processus avec le même pid, le même environnement et la même table de descripteurs de fichiers. Ce qui a changé, c'est la totalité de la mémoire virtuelle et de l'état du processeur.
JeremyP

@JeremyP "Le même descripteur de fichier" était important ici, il explique comment la redirection fonctionne dans les shells! J'étais perplexe sur la façon dont la redirection peut fonctionner si l'exécutif écrase tout! Merci
FUD
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.