Perché "ps ax" non trova uno script bash in esecuzione senza "#!" intestazione?


13

Quando eseguo questo script, destinato a funzionare fino a quando non viene ucciso ...

# foo.sh

while true; do sleep 1; done

... Non riesco a trovarlo usando ps ax:

>./foo.sh

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

... ma se aggiungo semplicemente l' #!intestazione " " comune allo script ...

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

while true; do sleep 1; done

... quindi lo script diventa reperibile con lo stesso pscomando ...

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

Perché è così?
Questa potrebbe essere una domanda correlata: ho pensato che " #" fosse solo un prefisso di commento, e in tal caso " #! /usr/bin/bash" non è altro che un commento. Ma " #!" ha un significato maggiore di come solo un commento?


Quale Unix stai usando?
Kusalananda

@Kusalananda - Linux linuxbox 3.11.10-301.fc20.x86_64 # 1 SMP gio 5 dic 14:01:17 UTC 2013 x86_64 x86_64 x86_64 GNU / Linux
StoneThrow

Risposte:


13

Quando la shell interattiva corrente è bashe si esegue uno script senza linea #!, quindi bashverrà eseguito lo script. Il processo verrà visualizzato ps axnell'output come solo bash.

$ cat foo.sh
# foo.sh

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

$ ./foo.sh
55411

In un altro terminale:

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

Relazionato:


Le sezioni pertinenti formano il bashmanuale:

Se l'esecuzione non riesce perché il file non è in formato eseguibile e il file non è una directory, si presume che sia uno script shell , un file contenente comandi shell. Viene generata una subshell per eseguirla. Questa subshell si reinizializza se stessa, in modo che l'effetto sia come se una nuova shell fosse stata invocata per gestire lo script , con l'eccezione che le posizioni dei comandi ricordate dal genitore (vedere l'hash sotto in SHELL BUILTIN COMMANDS) vengono mantenute dal figlio.

Se il programma è un file che inizia con #!, il resto della prima riga specifica un interprete per il programma. La shell esegue l'interprete specificato su sistemi operativi che non gestiscono da soli questo formato eseguibile. [...]

Ciò significa che l'esecuzione ./foo.shsulla riga di comando, quando foo.shnon ha una linea, equivale #!a eseguire i comandi nel file in una subshell, ovvero come

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

Con una linea corretta che #!punta ad esempio /bin/bash, è come fare

$ /bin/bash foo.sh

Penso di seguirlo, ma ciò che dici è vero anche nel secondo caso: bash esegue anche lo script nel secondo caso, come si può osservare quando psmostra lo script in esecuzione " /usr/bin/bash ./foo.sh". Quindi nel primo caso, come dici tu, bash eseguirà lo script, ma quello script non dovrebbe essere "passato" all'eseguibile bash biforcato, come nel secondo caso? (e se è così, immagino che sarebbe rintracciabile con la pipa per grep ...?)
StoneThrow

@StoneThrow Vedi la risposta aggiornata.
Kusalananda

"... tranne per il fatto che si ottiene un nuovo processo" - beh, si ottiene un nuovo processo in entrambi i modi, tranne per il fatto che $$punta ancora a quello vecchio nel caso subshell ( echo $BASHPID/ bash -c 'echo $PPID').
Michael Homer,

@MichaelHomer Ah, grazie per quello! Si aggiorna.
Kusalananda

12

Quando inizia uno script di shell #!, quella prima riga è un commento per quanto riguarda la shell. Tuttavia i primi due caratteri sono significativi per un'altra parte del sistema: il kernel. I due personaggi #!sono chiamati shebang . Per capire il ruolo dello shebang, devi capire come viene eseguito un programma.

L'esecuzione di un programma da un file richiede un'azione da parte del kernel. Questo viene fatto come parte della execvechiamata di sistema. Il kernel deve verificare le autorizzazioni del file, liberare le risorse (memoria, ecc.) Associate al file eseguibile attualmente in esecuzione nel processo di chiamata, allocare le risorse per il nuovo file eseguibile e trasferire il controllo al nuovo programma (e altre cose che Non menzionerò). La execvechiamata di sistema sostituisce il codice del processo attualmente in esecuzione; c'è una chiamata di sistema separata forkper creare un nuovo processo.

Per fare ciò, il kernel deve supportare il formato del file eseguibile. Questo file deve contenere codice macchina, organizzato in modo da comprendere il kernel. Uno script di shell non contiene codice macchina, quindi non può essere eseguito in questo modo.

Il meccanismo shebang consente al kernel di differire il compito di interpretare il codice su un altro programma. Quando il kernel vede che inizia il file eseguibile #!, legge i seguenti caratteri e interpreta la prima riga del file (meno lo #!spazio iniziale e facoltativo) come un percorso verso un altro file (più argomenti, che non tratterò qui ). Quando viene detto al kernel di eseguire il file /my/scripte vede che il file inizia con la linea #!/some/interpreter, il kernel viene eseguito /some/interpretercon l'argomento /my/script. Sta quindi /some/interpretera decidere che /my/scriptè un file di script che dovrebbe eseguire.

Cosa succede se un file non contiene né un codice nativo in un formato comprensibile per il kernel e non inizia con uno shebang? Bene, allora il file non è eseguibile e la execvechiamata di sistema non riesce con il codice di errore ENOEXEC(errore di formato eseguibile).

Questa potrebbe essere la fine della storia, ma la maggior parte delle shell implementa una funzionalità di fallback. Se il kernel ritorna ENOEXEC, la shell esamina il contenuto del file e controlla se sembra uno script di shell. Se la shell pensa che il file assomigli a uno script di shell, lo esegue da solo. I dettagli di come ciò dipende dalla shell. Puoi vedere qualcosa di ciò che sta accadendo aggiungendo ps $$nel tuo script e altro osservando il processo in strace -p1234 -f -eprocesscui 1234 è il PID della shell.

In bash, questo meccanismo di fallback è implementato chiamando forkma non execve. Il processo bash figlio cancella il suo stato interno da solo e apre il nuovo file di script per eseguirlo. Pertanto, il processo che esegue lo script utilizza ancora l'immagine del codice bash originale e gli argomenti della riga di comando originali passati quando è stato richiamato originariamente bash. ATT ksh si comporta allo stesso modo.

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

Dash, al contrario, reagisce ENOEXECchiamando /bin/shil percorso dello script passato come argomento. In altre parole, quando si esegue uno script shebangless dal trattino, si comporta come se lo script avesse una riga shebang #!/bin/sh. Mksh e zsh si comportano allo stesso modo.

% dash
$ ./foo.sh
  PID TTY      STAT   TIME COMMAND
21427 pts/2    S+     0:00 /bin/sh ./foo.sh

Ottima risposta comprensiva. Una domanda RE: l'implementazione del fallback che hai spiegato: suppongo che poiché un bambino bashè biforcato, abbia accesso allo stesso argv[]array del suo genitore, che è come sa "gli argomenti della riga di comando originali passati quando hai invocato bash originariamente", e se quindi, questo è il motivo per cui al bambino non viene trasmessa la sceneggiatura originale come argomento esplicito (quindi perché non è reperibile dal grep) - è esatto?
StoneThrow

1
In realtà puoi disattivare il comportamento del shebang del kernel (il BINFMT_SCRIPTmodulo controlla questo e può essere rimosso / modularizzato, anche se di solito è staticamente collegato al kernel), ma non vedo perché lo vorresti mai, tranne forse in un sistema incorporato . Come soluzione alternativa per questa possibilità, bashha un flag di configurazione ( HAVE_HASH_BANG_EXEC) per compensare!
ErikF,

2
@StoneThrow Non è tanto che il figlio bash "conosce gli argomenti della riga di comando originale", in quanto non li modifica. Un programma può modificare ciò che psriporta come argomenti della riga di comando, ma solo fino a un certo punto: deve modificare un buffer di memoria esistente, non può ingrandire questo buffer. Quindi se bash provasse a modificarne l' argvaggiunta per aggiungere il nome dello script, non funzionerebbe sempre. Il bambino non "passa un argomento" perché non c'è mai una execvechiamata di sistema nel bambino. È solo la stessa immagine di processo bash che continua a funzionare.
Gilles 'SO- smetti di essere malvagio' l'

-1

Nel primo caso, lo script viene eseguito da un figlio biforcuto dalla shell corrente.

Dovresti prima eseguirlo echo $$e quindi dare un'occhiata a una shell che ha l'ID di processo della shell come ID di processo padre.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.