Warum findet "ps ax" kein laufendes Bash-Skript ohne den "#!" - Header?

13

Wenn ich dieses Skript ausführe, das ausgeführt werden soll, bis es getötet wird ...

# foo.sh

while true; do sleep 1; done

... Ich kann es nicht finden mit ps ax:

>./foo.sh

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

... aber wenn ich nur den allgemeinen " #!" Header zum Skript hinzufüge ...

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

while true; do sleep 1; done

... dann wird das Skript mit dem gleichen psBefehl auffindbar ...

>./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

Warum ist das so?
Dies könnte eine verwandte Frage sein: Ich dachte, " #" sei nur ein Kommentarpräfix, und wenn ja, " #! /usr/bin/bash" ist es selbst nichts weiter als ein Kommentar. Aber " #!" hat es eine größere Bedeutung als nur einen Kommentar?

StoneThrow
quelle
Welches Unix benutzt du?
Kusalananda
@Kusalananda - Linux linuxbox 3.11.10-301.fc20.x86_64 # 1 SMP Do 5. Dezember 14:01:17 UTC 2013 x86_64 x86_64 x86_64 GNU / Linux
StoneThrow

Antworten:

13

Wenn die aktuelle interaktive Shell ausgeführt wird bashund Sie ein Skript ohne #!-line bashausführen , wird das Skript ausgeführt. Der Prozess wird in der ps axAusgabe als nur angezeigt bash.

$ cat foo.sh
# foo.sh

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

$ ./foo.sh
55411

In einem anderen Terminal:

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

Verbunden:


Die relevanten Abschnitte bilden das bashHandbuch:

Wenn diese Ausführung fehlschlägt, weil die Datei nicht im ausführbaren Format vorliegt und kein Verzeichnis ist, wird davon ausgegangen, dass es sich um ein Shell-Skript handelt , eine Datei, die Shell-Befehle enthält. Eine Subshell wird erzeugt , um sie auszuführen. Diese Subshell initialisiert sich von selbst neu, sodass der Effekt so ist, als ob eine neue Shell aufgerufen worden wäre, um das Skript zu handhaben , mit der Ausnahme, dass die Positionen der Befehle, an die sich die Eltern erinnern (siehe Hash unter SHELL BUILTIN COMMANDS), vom Kind beibehalten werden.

Wenn das Programm eine Datei ist #!, die mit beginnt , gibt der Rest der ersten Zeile einen Interpreter für das Programm an. Die Shell führt den angegebenen Interpreter auf Betriebssystemen aus, die dieses ausführbare Format nicht selbst verarbeiten. [...]

Dies bedeutet, dass das Ausführen ./foo.shin der Befehlszeile, wenn foo.shkeine #!-line vorhanden ist, dasselbe ist wie das Ausführen der Befehle in der Datei in einer Subshell, dh als

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

Mit einer richtigen #!-Linie auf zB /bin/bashist es so

$ /bin/bash foo.sh
Kusalananda
quelle
Ich glaube, ich folge, aber was Sie sagen, gilt auch für den zweiten Fall: bash führt das Skript auch im zweiten Fall aus, wie man sehen kann, wenn psdas Skript als " /usr/bin/bash ./foo.sh" ausgeführt wird. Im ersten Fall wird also, wie Sie sagen, Bash das Skript ausführen, aber müsste dieses Skript nicht, wie im zweiten Fall, an die ausführbare Bash-Verzweigungsdatei "übergeben" werden? (und wenn ja, ich stelle mir vor, es wäre mit der Pipe zu grep auffindbar ...?)
StoneThrow
@StoneThrow Siehe aktualisierte Antwort.
Kusalananda
"... mit der Ausnahme, dass Sie einen neuen Prozess erhalten" - nun, Sie erhalten einen neuen Prozess in beide Richtungen, mit der Ausnahme, dass dieser $$immer noch auf den alten in der Unterschale ( echo $BASHPID/ bash -c 'echo $PPID') verweist .
Michael Homer
@MichaelHomer Ah, danke dafür! Werde dich auf den neuesten Stand bringen.
Kusalananda
12

Wenn ein Shell-Skript mit beginnt #!, ist diese erste Zeile ein Kommentar für die Shell. Die ersten beiden Zeichen sind jedoch für einen anderen Teil des Systems von Bedeutung: den Kernel. Die beiden Charaktere #!werden Shebang genannt . Um die Rolle des Shebang zu verstehen, müssen Sie verstehen, wie ein Programm ausgeführt wird.

Das Ausführen eines Programms aus einer Datei erfordert eine Aktion des Kernels. Dies erfolgt im Rahmen des execveSystemaufrufs. Der Kernel muss die Dateiberechtigungen überprüfen, die Ressourcen (Speicher usw.) freigeben, die der aktuell im aufrufenden Prozess ausgeführten ausführbaren Datei zugeordnet sind, Ressourcen für die neue ausführbare Datei zuweisen und die Steuerung auf das neue Programm übertragen (und vieles mehr) Ich werde nicht erwähnen). Der execveSystemaufruf ersetzt den Code des aktuell ausgeführten Prozesses. Es gibt einen separaten Systemaufruf forkzum Erstellen eines neuen Prozesses.

Dazu muss der Kernel das Format der ausführbaren Datei unterstützen. Diese Datei muss Maschinencode enthalten, der so organisiert ist, dass der Kernel ihn versteht. Ein Shell-Skript enthält keinen Maschinencode und kann daher nicht auf diese Weise ausgeführt werden.

Der Shebang-Mechanismus ermöglicht es dem Kernel, die Interpretation des Codes in ein anderes Programm zu verschieben. Wenn der Kernel erkennt, dass die ausführbare Datei mit beginnt #!, liest er die nächsten Zeichen und interpretiert die erste Zeile der Datei (abzüglich des führenden #!und optionalen Leerzeichens) als Pfad zu einer anderen Datei (plus Argumente, auf die ich hier nicht näher eingehen werde) ). Wenn der Kernel aufgefordert wird, die Datei auszuführen /my/scriptund feststellt, dass die Datei mit der Zeile beginnt #!/some/interpreter, wird der Kernel /some/interpretermit dem Argument ausgeführt /my/script. Es liegt dann /some/interpreteran Ihnen zu entscheiden, /my/scriptdass es sich um eine Skriptdatei handelt, die ausgeführt werden soll.

Was ist, wenn eine Datei weder nativen Code in einem Format enthält, das der Kernel versteht, noch mit einem Shebang beginnt? Nun, dann ist die Datei nicht ausführbar und der execveSystemaufruf schlägt mit dem Fehlercode ENOEXEC(Fehler im ausführbaren Format) fehl .

Dies könnte das Ende der Geschichte sein, aber die meisten Shells implementieren eine Fallback-Funktion. Wenn der Kernel zurückkehrt ENOEXEC, überprüft die Shell den Inhalt der Datei und ob sie wie ein Shell-Skript aussieht. Wenn die Shell denkt, dass die Datei wie ein Shell-Skript aussieht, führt sie es selbst aus. Die Details dazu hängen von der Shell ab. Sie können sehen, was passiert, indem Sie ps $$in Ihrem Skript etwas hinzufügen , und mehr, indem Sie den Prozess beobachten, bei strace -p1234 -f -eprocessdem 1234 die PID der Shell ist.

In Bash wird dieser Fallback-Mechanismus durch Aufrufen implementiert, forkjedoch nicht durch Aufrufen execve. Der untergeordnete Bash-Prozess löscht seinen internen Status von selbst und öffnet die neue Skriptdatei, um sie auszuführen. Daher verwendet der Prozess, der das Skript ausführt, weiterhin das ursprüngliche Bash-Codebild und die ursprünglichen Befehlszeilenargumente, die beim ursprünglichen Aufrufen von Bash übergeben wurden. ATT ksh verhält sich genauso.

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

Im Gegensatz dazu reagiert Dash, ENOEXECindem /bin/shder Pfad zum Skript als Argument übergeben wird. Mit anderen Worten, wenn Sie ein Shebangless-Skript von Dash aus ausführen, verhält es sich so, als hätte das Skript eine Shebang-Zeile mit #!/bin/sh. Mksh und zsh verhalten sich genauso.

% dash
$ ./foo.sh
  PID TTY      STAT   TIME COMMAND
21427 pts/2    S+     0:00 /bin/sh ./foo.sh
Gilles 'SO - hör auf böse zu sein'
quelle
Tolle, verständliche Antwort. Eine Frage zu RE: Die von Ihnen erläuterte Fallback-Implementierung: Ich nehme an, dass ein bashuntergeordnetes Element, da es gegabelt ist, Zugriff auf dasselbe argv[]Array wie sein übergeordnetes Element hat, wodurch es "die ursprünglichen Befehlszeilenargumente kennt, die beim ursprünglichen Aufrufen von bash übergeben wurden" und if Aus diesem Grund wird dem Kind das ursprüngliche Skript nicht als explizites Argument übergeben (daher kann es vom grep nicht gefunden werden). Ist das korrekt?
StoneThrow
1
Sie können das Kernel-Shebang-Verhalten tatsächlich ausschalten (das BINFMT_SCRIPTModul steuert dies und kann entfernt / modularisiert werden, obwohl es normalerweise statisch mit dem Kernel verbunden ist), aber ich verstehe nicht, warum Sie es jemals wollen würden, außer vielleicht in einem eingebetteten System . Als Workaround für diese Möglichkeit bashhat man ein Konfigurationsflag ( HAVE_HASH_BANG_EXEC) zum ausgleichen!
ErikF
2
@StoneThrow Es ist nicht so sehr so, dass die untergeordnete Bash "die ursprünglichen Befehlszeilenargumente kennt", da sie diese nicht ändert. Ein Programm kann ändern, was psals Befehlszeilenargumente ausgegeben wird, aber nur bis zu einem gewissen Punkt: Es muss einen vorhandenen Speicherpuffer ändern, es kann diesen Puffer nicht vergrößern. Wenn Bash versucht, argvden Namen des Skripts zu ändern , funktioniert dies nicht immer. Dem untergeordneten Element wird kein Argument übergeben, da das untergeordnete Element keinen execveSystemaufruf enthält. Es ist genau dasselbe Bash-Prozess-Image, das weiterhin ausgeführt wird.
Gilles 'SO- hör auf böse zu sein'
-1

Im ersten Fall wird das Skript von einem abgespaltenen Kind aus Ihrer aktuellen Shell ausgeführt.

Sie sollten zuerst echo $$eine Shell ausführen und sich diese dann ansehen, deren übergeordnete Prozess-ID die Prozess-ID Ihrer Shell ist.

schily
quelle