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
)
Lorsque vous appelez une méthode de la famille exec
name__, elle ne crée pas de nouveau processus. Au lieu de cela, exec
remplace 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 grep
en utilisant exec. bash
est 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 bash
n’existera plus. Par conséquent, vous ne pouvez plus revenir au terminal après avoir exécuté la commande grep
name__. 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.
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).
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.
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 bash
name__).
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 stdout
aka fd 1 (ainsi, si write end est à fd 4, nous faisons dup2(4,1)
). Lorsque exec()
pour générer df
se produit, le processus enfant ne pensera plus à son stdout
et 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 grep
name__, à l'exception de fork()
, prenons la fin de la lecture avec fd 3 et dup(3,0)
avant de générer grep
avec 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 source
name__. Les sous-coquilles nécessitent fork()
.
En bref, il s’agit d’un mécanisme nécessaire et utile.
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 bash
name__) 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.
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.