Por que "ps ax" não encontra um script bash em execução sem o cabeçalho "#!"?

13

Quando executo esse script, pretendia ser executado até ser morto ...

# foo.sh

while true; do sleep 1; done

... Não consigo encontrá-lo 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

... mas se eu apenas adicionar o #!cabeçalho " " comum ao script ...

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

while true; do sleep 1; done

... então o script se torna localizável pelo mesmo 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

Porque isto é assim?
Esta pode ser uma pergunta relacionada: " #" pensei que era apenas um prefixo de comentário e, se for, " #! /usr/bin/bash" nada mais é do que um comentário. Mas " #!" carrega algum significado maior do que apenas um comentário?

StoneThrow
fonte
Qual Unix você está usando?
Kusalananda
@Kusalananda - Linux linuxbox 3.11.10-301.fc20.x86_64 # 1 SMP Qui Dez 5 14:01:17 UTC 2013 x86_64 x86_64 x86_64 GNU / Linux
StoneThrow

Respostas:

13

Quando o shell interativo atual estiver bashe você executar um script sem #!linha, basho script será executado. O processo aparecerá na ps axsaída como justo bash.

$ cat foo.sh
# foo.sh

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

$ ./foo.sh
55411

Em outro terminal:

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

Palavras-chave:


As seções relevantes formam o bashmanual:

Se esta execução falhar porque o arquivo não está no formato executável e o arquivo não é um diretório, é considerado um script de shell , um arquivo contendo comandos de shell. Um subshell é gerado para executá-lo. Esse subshell se reinicializa, de modo que o efeito é como se um novo shell tivesse sido chamado para manipular o script , com a exceção de que os locais dos comandos lembrados pelo pai (veja o hash abaixo em SHELL BUILTIN COMMANDS) são retidos pelo filho.

Se o programa for um arquivo começando com #!, o restante da primeira linha especificará um intérprete para o programa. O shell executa o intérprete especificado em sistemas operacionais que não manipulam esse formato executável. [...]

Isso significa que executar ./foo.shna linha de comando, quando foo.shnão possui uma #!linha, é o mesmo que executar os comandos no arquivo em uma subshell, ou seja, como

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

Com uma #!linha adequada apontando para /bin/bash, por exemplo , é como fazer

$ /bin/bash foo.sh
Kusalananda
fonte
Eu acho que sigo, mas o que você diz também é verdade no segundo caso: o bash também executa o script no segundo caso, como pode ser observado quando psmostra o script sendo executado como " /usr/bin/bash ./foo.sh". Portanto, no primeiro caso, como você diz, o bash executará o script, mas esse script não precisa ser "passado" para o executável do bash bifurcado, como no segundo caso? (e se sim, imagino que seria possível encontrar o cachimbo para grep ...?)
stonethrow
@StoneThrow Veja a resposta atualizada.
Kusalananda
"... exceto que você obtém um novo processo" - bem, você obtém um novo processo de qualquer maneira, exceto que $$ainda aponta para o antigo no caso de subshell ( echo $BASHPID/ bash -c 'echo $PPID').
Michael Homer
@MichaelHomer Ah, obrigado por isso! Atualizará.
Kusalananda
12

Quando um script de shell começa #!, essa primeira linha é um comentário no que diz respeito ao shell. No entanto, os dois primeiros caracteres são significativos para outra parte do sistema: o kernel. Os dois caracteres #!são chamados de shebang . Para entender o papel do shebang, você precisa entender como um programa é executado.

A execução de um programa a partir de um arquivo requer ação do kernel. Isso é feito como parte da execvechamada do sistema. O kernel precisa verificar as permissões do arquivo, liberar os recursos (memória, etc.) associados ao arquivo executável atualmente em execução no processo de chamada, alocar recursos para o novo arquivo executável e transferir o controle para o novo programa (e mais coisas que Eu não vou mencionar). A execvechamada do sistema substitui o código do processo em execução no momento; há uma chamada de sistema separada forkpara criar um novo processo.

Para fazer isso, o kernel precisa suportar o formato do arquivo executável. Este arquivo deve conter o código da máquina, organizado da maneira que o kernel entende. Um script de shell não contém código de máquina, portanto, não pode ser executado dessa maneira.

O mecanismo shebang permite que o kernel adie a tarefa de interpretar o código para outro programa. Quando o kernel vê que o arquivo executável começa #!, ele lê os próximos caracteres e interpreta a primeira linha do arquivo (menos o #!espaço inicial e opcional) como um caminho para outro arquivo (mais argumentos, que não serão discutidos aqui) ) Quando o kernel é dito para executar o arquivo /my/script, e ele vê que o arquivo começa com a linha #!/some/interpreter, o kernel executa /some/interpretercom o argumento /my/script. Cabe então /some/interpreterdecidir que /my/scriptesse arquivo de script deve ser executado.

E se um arquivo não contiver código nativo em um formato que o kernel compreenda e não iniciar com um shebang? Bem, o arquivo não é executável e a execvechamada do sistema falha com o código de erro ENOEXEC(erro de formato executável).

Este pode ser o fim da história, mas a maioria dos shells implementa um recurso de fallback. Se o kernel retornar ENOEXEC, o shell examinará o conteúdo do arquivo e verificará se ele se parece com um script de shell. Se o shell achar que o arquivo se parece com um script de shell, ele será executado sozinho. Os detalhes de como isso acontece depende do shell. Você pode ver um pouco do que está acontecendo adicionando ps $$no seu script e muito mais assistindo ao processo em strace -p1234 -f -eprocessque 1234 é o PID do shell.

No bash, esse mecanismo de fallback é implementado chamando fork mas não execve. O processo bash filho limpa seu estado interno sozinho e abre o novo arquivo de script para executá-lo. Portanto, o processo que executa o script ainda está usando a imagem do código do bash original e os argumentos da linha de comando originais passados ​​quando você chamou o bash originalmente. O ATT ksh se comporta da mesma maneira.

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

Dash, ao contrário, reage ENOEXECchamando /bin/shcom o caminho para o script passado como argumento. Em outras palavras, quando você executa um script sem barra a partir do traço, ele se comporta como se o script tivesse uma linha shebang #!/bin/sh. Mksh e zsh se comportam da mesma maneira.

% dash
$ ./foo.sh
  PID TTY      STAT   TIME COMMAND
21427 pts/2    S+     0:00 /bin/sh ./foo.sh
Gilles 'SO- parar de ser mau'
fonte
Ótimo, compreenda a resposta. Uma pergunta RE: a implementação de fallback que você explicou: suponho que, como um filho bashé bifurcado, ele tem acesso à mesma argv[]matriz que seu pai, que é como ele sabe "os argumentos originais da linha de comando passados ​​quando você chamou o bash originalmente" e se então, é por isso que o filho não recebe o script original como argumento explícito (daí o fato de ele não ser encontrado pelo grep) - é preciso?
StoneThrow
1
Você pode realmente desativar o comportamento shebang do kernel (o BINFMT_SCRIPTmódulo controla isso e pode ser removido / modularizado, embora geralmente esteja vinculado estaticamente ao kernel), mas não vejo por que você desejaria, exceto talvez em um sistema incorporado . Como solução alternativa para essa possibilidade, bashpossui um sinalizador de configuração ( HAVE_HASH_BANG_EXEC) para compensar!
ErikF
2
@StoneThrow Não é tanto que o child bash “conhece os argumentos originais da linha de comando”, pois não os modifica. Um programa pode modificar quais psrelatórios são os argumentos da linha de comando, mas apenas até certo ponto: ele precisa modificar um buffer de memória existente, não pode aumentar esse buffer. Portanto, se o bash tentasse modificá-lo argvpara adicionar o nome do script, nem sempre funcionaria. A criança não é "aprovada em um argumento" porque nunca há uma execvechamada de sistema na criança. É apenas a mesma imagem do processo bash que continua sendo executada.
Gilles 'SO- stop be evil'
-1

No primeiro caso, o script é executado por um filho extraído do seu shell atual.

Você deve primeiro executar echo $$e, em seguida, examinar um shell que tenha o ID do processo do seu shell como ID do processo pai.

esperto
fonte