web-dev-qa-db-fra.com

Pourquoi les obus appellent-ils fork ()?

Lorsqu'un processus est démarré à partir d'un Shell, pourquoi ce dernier se lance-t-il avant d'exécuter le processus?

Par exemple, lorsque l'utilisateur entre grep blabla foo, pourquoi le shell ne peut-il pas simplement appeler exec() sur grep sans un shell enfant?

En outre, lorsqu'un shell se place dans un émulateur de terminal à interface graphique, démarre-t-il un autre émulateur de terminal? (comme pts/13 à partir de pts/14)

32
user3122885

Lorsque vous appelez une méthode de la famille execname__, elle ne crée pas de nouveau processus. Au lieu de cela, execremplace la mémoire du processus actuel et le jeu d'instructions, etc., par le processus que vous souhaitez exécuter.

Par exemple, vous voulez exécuter grepen utilisant exec. bashest un processus (qui a une mémoire distincte, un espace d'adressage). Désormais, lorsque vous appelez exec(grep), exec remplacera la mémoire, l’espace adresse, le jeu d’instructions, etc. du processus en cours par des données grep's. Cela signifie que le processus bashn’existera plus. Par conséquent, vous ne pouvez plus revenir au terminal après avoir exécuté la commande grepname__. C'est pourquoi les méthodes de la famille exec ne reviennent jamais. Vous ne pouvez pas exécuter de code après exec; c'est inaccessible.

34
shantanu

Selon le pts, vérifiez-le vous-même: dans un shell, exécutez

echo $$ 

pour connaître votre identifiant de processus (PID), j'ai par exemple

echo $$
29296

Ensuite, lancez par exemple sleep 60 puis, dans un autre terminal

(0)samsung-romano:~% ps -edao pid,ppid,tty,command | grep 29296 | grep -v grep
29296  2343 pts/11   zsh
29499 29296 pts/11   sleep 60

Donc non, en général, vous avez le même terminal associé au processus. (Notez que ceci est votre sleep car il a votre Shell comme parent).

3
Rmano

TL; DR : il s’agit de la méthode optimale pour créer de nouveaux processus et garder le contrôle dans un shell interactif.

fork () est nécessaire pour les processus et les tubes

Pour répondre à la partie spécifique de cette question, si grep blabla foo devait être appelé via exec() directement dans le parent, ce parent existerait, et son PID avec toutes les ressources serait repris par grep blabla foo.

Cependant, parlons en général de exec() et de fork(). La raison principale d'un tel comportement est que fork()/exec() est la méthode standard pour créer un nouveau processus sous Unix/Linux et qu'il ne s'agit pas d'une chose spécifique à Bash. cette méthode est en place depuis le début et est influencée par cette même méthode à partir des systèmes d’exploitation déjà existants. Pour paraphraser quelque peu la réponse de goldilocks sur une question connexe, fork() pour créer un nouveau processus est plus facile car le noyau a moins de travail à faire en ce qui concerne l'allocation des ressources et beaucoup de propriétés (telles que les descripteurs de fichier). , environnement, etc.) - tous peuvent être hérités du processus parent (ici, de bashname__).

Deuxièmement, en ce qui concerne les shells interactifs, vous ne pouvez pas exécuter une commande externe sans forking. Pour lancer un exécutable qui réside sur le disque (par exemple, /bin/df -h), vous devez appeler l’une des fonctions de la famille exec(), telle que execve(), qui remplacera le parent avec le nouveau processus, prendra en charge son PID et ses descripteurs de fichier existants, etc. Pour le shell interactif, vous souhaitez que le contrôle revienne à l'utilisateur et laisse le shell interactif parent continuer. Ainsi, le meilleur moyen est de créer un sous-processus via fork() et de laisser ce processus être repris via execve(). Ainsi, le PID 1156 interactif du Shell engendrerait un enfant via fork() avec le PID 1157, puis appelerait execve("/bin/df",["df","-h"],&environment), ce qui ferait exécuter /bin/df -h avec le PID 1157. Désormais, le Shell n'a plus qu'à attendre que le processus se termine et lui renvoie le contrôle.

Dans le cas où vous devez créer un canal entre deux commandes ou plus, par exemple, df | grep, vous devez créer deux descripteurs de fichier (lecture et écriture en fin de canal provenant de pipe() syscall), puis laisser les deux nouveaux processus en hériter. . Cela consiste à créer un nouveau processus, puis à copier l'extrémité du canal d'écriture via l'appel dup2() sur son stdoutaka fd 1 (ainsi, si write end est à fd 4, nous faisons dup2(4,1)). Lorsque exec() pour générer dfse produit, le processus enfant ne pensera plus à son stdoutet lui écrira sans se rendre compte (à moins qu'il ne vérifie activement) que sa sortie passe réellement à un tuyau. Le même processus se produit pour grepname__, à l'exception de fork(), prenons la fin de la lecture avec fd 3 et dup(3,0) avant de générer grepavec exec(). Pendant tout ce temps, le processus parent est toujours là, attendant de reprendre le contrôle une fois le pipeline terminé.

Dans le cas de commandes intégrées, Shell n’a généralement pas fork(), à l’exception de la commande sourcename__. Les sous-coquilles nécessitent fork().

En bref, il s’agit d’un mécanisme nécessaire et utile.

Inconvénients de forking et optimisations

Maintenant, c'est différent pour les shells non interactifs , tel que bash -c '<simple command>'. Bien que fork()/exec() soit la méthode optimale dans laquelle vous devez traiter de nombreuses commandes, vous gaspillez des ressources lorsque vous n’avez qu’une seule commande. Pour citer Stéphane Chazelas de cet article :

Le fork est coûteux, en temps CPU, en mémoire, en descripteurs de fichiers alloués ... Avoir un processus Shell qui attend de recevoir un autre processus avant de quitter est un gaspillage de ressources. En outre, il est difficile de signaler correctement l'état de sortie du processus distinct qui exécuterait la commande (par exemple, lorsque le processus est supprimé).

Par conséquent, de nombreux shells (pas seulement bashname__) utilisent exec() pour permettre à bash -c '' d'être repris par cette commande simple et unique. Et précisément pour les raisons indiquées ci-dessus, il est préférable de minimiser les pipelines dans les scripts Shell. Souvent, vous pouvez voir les débutants faire quelque chose comme ceci:

cat /etc/passwd | cut -d ':' -f 6 | grep '/home'

Bien sûr, cela va fork() 3 processus. Ceci est un exemple simple, mais considérons un fichier volumineux, dans la plage de gigaoctets. Ce serait beaucoup plus efficace avec un processus:

awk -F':' '$6~"/home"{print $6}' /etc/passwd

Le gaspillage de ressources peut en réalité être une forme d’attaque par déni de service, et en particulier fork bombs sont créés via des fonctions Shell s’appelant en pipeline, qui se copient en plusieurs exemplaires. De nos jours, ceci est atténué par la limitation du nombre maximal de processus dans cgroups on systemd , qu'Ubuntu utilise également depuis la version 15.04.

Bien sûr, cela ne signifie pas que bricoler est simplement mauvais. C’est toujours un mécanisme utile, comme indiqué précédemment, mais si vous pouvez vous en tirer avec moins de processus, consécutivement moins de ressources et donc de meilleures performances, évitez fork() si possible.

Voir également

2
Sergiy Kolodyazhnyy

Pour chaque commande (exemple: grep) que vous lancez sur l'invite bash, vous avez réellement l'intention de démarrer un nouveau processus, puis de revenir à l'invite bash après exécution.

Si le processus Shell (bash) appelle exec () pour exécuter grep, le processus Shell sera remplacé par grep. Grep fonctionnera correctement, mais après exécution, le contrôle ne pourra plus retourner dans le shell car le processus bash est déjà remplacé.

Pour cette raison, bash appelle fork (), ce qui ne remplace pas le processus en cours.

1
FlowRaja