Pourquoi "ps ax" ne trouve-t-il pas un script bash en cours d'exécution sans le "#!" entête?

13

Lorsque j'exécute ce script, destiné à s'exécuter jusqu'à ce qu'il soit tué ...

# foo.sh

while true; do sleep 1; done

... je ne le trouve pas en utilisant ps ax:

>./foo.sh

// In a separate shell:
>ps ax | grep foo.sh
21110 pts/3    S+     0:00 grep --color=auto foo.sh

... mais si j'ajoute juste l'en- #!tête " " commun au script ...

#! /usr/bin/bash
# foo.sh

while true; do sleep 1; done

... alors le script devient détectable par la même pscommande ...

>./foo.sh

// In a separate shell:
>ps ax | grep foo.sh
21319 pts/43   S+     0:00 /usr/bin/bash ./foo.sh
21324 pts/3    S+     0:00 grep --color=auto foo.sh

Pourquoi cela est-il ainsi?
Cela peut être une question connexe: je pensais que " #" n'était qu'un préfixe de commentaire, et si c'est le cas " #! /usr/bin/bash" n'est lui-même rien de plus qu'un commentaire. Mais " #!" a-t-il une signification supérieure à celle d'un simple commentaire?

StoneThrow
la source
Quel Unix utilisez-vous?
Kusalananda
@Kusalananda - Linux linuxbox 3.11.10-301.fc20.x86_64 # 1 SMP Thu Dec 5 14:01:17 UTC 2013 x86_64 x86_64 x86_64 GNU / Linux
StoneThrow

Réponses:

13

Lorsque le shell interactif actuel est bash, et que vous exécutez un script sans #!ligne, puis bashexécutera le script. Le processus apparaîtra ps axcomme juste dans la sortie bash.

$ cat foo.sh
# foo.sh

echo "$BASHPID"
while true; do sleep 1; done

$ ./foo.sh
55411

Dans un autre terminal:

$ ps -p 55411
  PID TT  STAT       TIME COMMAND
55411 p2  SN+     0:00.07 bash

En relation:


Les sections pertinentes forment le bashmanuel:

Si cette exécution échoue parce que le fichier n'est pas au format exécutable et que le fichier n'est pas un répertoire, il est supposé être un script shell , un fichier contenant des commandes shell. Un sous-shell est généré pour l'exécuter. Ce sous-shell se réinitialise lui-même, de sorte que l'effet est comme si un nouveau shell avait été appelé pour gérer le script , à l'exception que les emplacements des commandes mémorisées par le parent (voir hachage ci-dessous sous SHELL BUILTIN COMMANDS) sont conservés par l'enfant.

Si le programme est un fichier commençant par #!, le reste de la première ligne spécifie un interpréteur pour le programme. Le shell exécute l'interpréteur spécifié sur les systèmes d'exploitation qui ne gèrent pas eux-mêmes ce format exécutable. [...]

Cela signifie que l'exécution ./foo.shsur la ligne de commande, lorsqu'elle foo.shn'a pas de #!ligne, équivaut à l'exécution des commandes du fichier dans un sous-shell, c'est-à-dire que

$ ( echo "$BASHPID"; while true; do sleep 1; done )

Avec une #!ligne appropriée pointant par exemple /bin/bash, c'est comme faire

$ /bin/bash foo.sh
Kusalananda
la source
Je pense que je suis, mais ce que vous dites est également vrai dans le deuxième cas: bash exécute également le script dans le second cas, comme on peut le constater lorsque psle script s'exécute en tant que " /usr/bin/bash ./foo.sh". Donc, dans le premier cas, comme vous le dites, bash exécutera le script, mais ce script n'aurait-il pas besoin d'être «passé» à l'exécutable bash fourchu, comme dans le second cas? (et si oui, j'imagine que ce serait trouvable avec la pipe à grep ...?)
StoneThrow
@StoneThrow Voir la réponse mise à jour.
Kusalananda
"... sauf que vous obtenez un nouveau processus" - eh bien, vous obtenez un nouveau processus de toute façon, sauf qu'il $$pointe toujours vers l'ancien dans le cas du sous-shell ( echo $BASHPID/ bash -c 'echo $PPID').
Michael Homer
@MichaelHomer Ah, merci pour ça! Mettra à jour.
Kusalananda
12

Lorsqu'un script shell commence par #!, cette première ligne est un commentaire en ce qui concerne le shell. Cependant, les deux premiers caractères sont significatifs pour une autre partie du système: le noyau. Les deux personnages #!s'appellent un shebang . Pour comprendre le rôle du shebang, vous devez comprendre comment un programme est exécuté.

L'exécution d'un programme à partir d'un fichier nécessite une action du noyau. Cela se fait dans le cadre de l' execveappel système. Le noyau doit vérifier les autorisations de fichier, libérer les ressources (mémoire, etc.) associées au fichier exécutable en cours d'exécution dans le processus d'appel, allouer des ressources pour le nouveau fichier exécutable et transférer le contrôle au nouveau programme (et plus de choses qui Je ne mentionnerai pas). L' execveappel système remplace le code du processus en cours d'exécution; il existe un appel système distinct forkpour créer un nouveau processus.

Pour ce faire, le noyau doit prendre en charge le format du fichier exécutable. Ce fichier doit contenir du code machine, organisé d'une manière que le noyau comprend. Un script shell ne contient pas de code machine, il ne peut donc pas être exécuté de cette façon.

Le mécanisme shebang permet au noyau de reporter la tâche d'interprétation du code à un autre programme. Lorsque le noyau voit que le fichier exécutable commence par#! , il lit les quelques caractères suivants et interprète la première ligne du fichier (moins l' #!espace de tête et facultatif) comme un chemin vers un autre fichier (plus les arguments, dont je ne parlerai pas ici ). Lorsque le noyau est invité à exécuter le fichier /my/scriptet qu'il voit que le fichier commence par la ligne #!/some/interpreter, le noyau s'exécute /some/interpreteravec l'argument /my/script. C'est ensuite /some/interpreterà décider qu'il /my/scripts'agit d'un fichier de script qu'il doit exécuter.

Que se passe-t-il si un fichier ne contient pas de code natif dans un format que le noyau comprend et ne commence pas par un shebang? Eh bien, le fichier n'est pas exécutable et l' execveappel système échoue avec le code d'erreur ENOEXEC(erreur de format exécutable).

Cela pourrait être la fin de l'histoire, mais la plupart des shells implémentent une fonction de secours. Si le noyau revient ENOEXEC, le shell regarde le contenu du fichier et vérifie s'il ressemble à un script shell. Si le shell pense que le fichier ressemble à un script shell, il l'exécute lui-même. Les détails de la façon dont cela fonctionne dépendent du shell. Vous pouvez voir ce qui se passe en ajoutant ps $$dans votre script, et plus en observant le processus avec strace -p1234 -f -eprocessoù 1234 est le PID du shell.

En bash, ce mécanisme de secours est implémenté en appelant fork mais pas execve. Le processus bash enfant efface lui-même son état interne et ouvre le nouveau fichier de script pour l'exécuter. Par conséquent, le processus qui exécute le script utilise toujours l'image de code bash d'origine et les arguments de ligne de commande d'origine passés lorsque vous avez appelé bash à l'origine. ATT ksh se comporte de la même manière.

% bash --norc
bash-4.3$ ./foo.sh 
  PID TTY      STAT   TIME COMMAND
21913 pts/2    S+     0:00 bash --norc

Dash, en revanche, réagit ENOEXECen appelant /bin/shavec le chemin d'accès au script passé en argument. En d'autres termes, lorsque vous exécutez un script sans shebang à partir du tableau de bord, cela se comporte comme si le script avait une ligne de shebang avec #!/bin/sh. Mksh et zsh se comportent de la même manière.

% dash
$ ./foo.sh
  PID TTY      STAT   TIME COMMAND
21427 pts/2    S+     0:00 /bin/sh ./foo.sh
Gilles 'SO- arrête d'être méchant'
la source
Grande réponse compréhensive. Une question RE: l'implémentation de secours que vous avez expliquée: je suppose qu'un enfant bashest forké, il a accès au même argv[]tableau que son parent, c'est ainsi qu'il connaît "les arguments de ligne de commande d'origine passés lorsque vous avez invoqué bash à l'origine", et si donc, c'est pourquoi l'enfant n'est pas passé le script d'origine comme argument explicite (d'où pourquoi il n'est pas trouvé par le grep) - est-ce exact?
StoneThrow
1
Vous pouvez réellement désactiver le comportement shebang du noyau (le BINFMT_SCRIPTmodule le contrôle et peut être supprimé / modularisé, bien qu'il soit généralement lié statiquement au noyau), mais je ne vois pas pourquoi vous voudriez, sauf peut-être dans un système embarqué . Pour contourner cette possibilité, basha un indicateur de configuration ( HAVE_HASH_BANG_EXEC) pour compenser!
ErikF
2
@StoneThrow Ce n'est pas tant que l'enfant bash «connaît les arguments de la ligne de commande d'origine», mais qu'il ne les modifie pas. Un programme peut modifier ce qui se psprésente comme arguments de ligne de commande, mais seulement jusqu'à un certain point: il doit modifier un tampon de mémoire existant, il ne peut pas agrandir ce tampon. Donc, si bash essayait de modifier son argvpour ajouter le nom du script, cela ne fonctionnerait pas toujours. L'enfant n'est pas «passé d'argument» car il n'y a jamais d' execveappel système chez l'enfant. C'est juste la même image de processus bash qui continue de fonctionner.
Gilles 'SO- arrête d'être méchant'
-1

Dans le premier cas, le script est exécuté par un enfant dérivé de votre shell actuel.

Vous devez d'abord exécuter echo $$, puis jeter un œil à un shell qui a l'ID de processus de votre shell comme ID de processus parent.

schily
la source