¿Por qué "ps ax" no encuentra un script bash en ejecución sin el encabezado "#!"?

13

Cuando ejecuto este script, tenía la intención de ejecutar hasta que lo mataran ...

# foo.sh

while true; do sleep 1; done

... No puedo encontrarlo 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

... pero si solo agrego el " #!" encabezado común al script ...

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

while true; do sleep 1; done

... entonces el script se puede encontrar con el mismo 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

¿Por qué esto es tan?
Esta puede ser una pregunta relacionada: pensé que " #" era solo un prefijo de comentario, y si es así " #! /usr/bin/bash" no es más que un comentario. ¿Pero " #!" tiene algún significado mayor que un simple comentario?

Lanzamiento De Piedra
fuente
¿Qué Unix estás usando?
Kusalananda
@Kusalananda - Linux linuxbox 3.11.10-301.fc20.x86_64 # 1 SMP jue 5 de diciembre 14:01:17 UTC 2013 x86_64 x86_64 x86_64 GNU / Linux
StoneThrow

Respuestas:

13

Cuando el shell interactivo actual es bash, y ejecuta un script sin línea #!, entonces bashejecutará el script. El proceso se mostrará en la ps axsalida como justo bash.

$ cat foo.sh
# foo.sh

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

$ ./foo.sh
55411

En otra terminal:

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

Relacionado:


Las secciones relevantes forman el bashmanual:

Si esta ejecución falla porque el archivo no está en formato ejecutable y el archivo no es un directorio, se supone que es un script de shell , un archivo que contiene comandos de shell. Se genera un subshell para ejecutarlo. Este subshell se reinicializa a sí mismo, de modo que el efecto es como si se hubiera invocado un nuevo shell para manejar el script , con la excepción de que el hijo retiene las ubicaciones de los comandos recordados por el padre (vea el hash a continuación en SHELL BUILTIN COMMANDS).

Si el programa es un archivo que comienza #!, el resto de la primera línea especifica un intérprete para el programa. El shell ejecuta el intérprete especificado en los sistemas operativos que no manejan este formato ejecutable por sí mismos. [...]

Esto significa que ejecutar ./foo.shen la línea de comandos, cuando foo.shno tiene una línea #!, es lo mismo que ejecutar los comandos en el archivo en una subshell, es decir, como

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

Con una línea adecuada #!apuntando a /bin/bash, por ejemplo , es como hacer

$ /bin/bash foo.sh
Kusalananda
fuente
Creo que sigo, pero lo que dices también es cierto en el segundo caso: bash también ejecuta el script en el segundo caso, como se puede observar cuando psmuestra el script que se ejecuta como " /usr/bin/bash ./foo.sh". Entonces, en el primer caso, como usted dice, bash ejecutará el script, pero ¿no sería necesario "pasar" ese script al ejecutable bifurcado bifurcado, como en el segundo caso? (y si es así, me imagino que se podría encontrar con la tubería a grep ...?)
StoneThrow
@StoneThrow Ver respuesta actualizada.
Kusalananda
"... excepto que obtienes un nuevo proceso" - bueno, obtienes un nuevo proceso de cualquier manera, excepto que $$todavía apunta al anterior en el caso de subshell ( echo $BASHPID/ bash -c 'echo $PPID').
Michael Homer
@MichaelHomer Ah, ¡gracias por eso! Actualizará.
Kusalananda
12

Cuando comienza un script de shell #!, esa primera línea es un comentario en lo que respecta al shell. Sin embargo, los dos primeros caracteres son significativos para otra parte del sistema: el núcleo. Los dos personajes #!se llaman shebang . Para comprender el papel del shebang, debe comprender cómo se ejecuta un programa.

Ejecutar un programa desde un archivo requiere una acción del núcleo. Esto se realiza como parte de la execvellamada al sistema. El núcleo debe verificar los permisos del archivo, liberar los recursos (memoria, etc.) asociados al archivo ejecutable que se está ejecutando actualmente en el proceso de llamada, asignar recursos para el nuevo archivo ejecutable y transferir el control al nuevo programa (y más cosas que No lo mencionaré). La execvellamada al sistema reemplaza el código del proceso actualmente en ejecución; Hay una llamada forkal sistema por separado para crear un nuevo proceso.

Para hacer esto, el núcleo debe admitir el formato del archivo ejecutable. Este archivo tiene que contener código de máquina, organizado de manera que el núcleo lo entienda. Un script de shell no contiene código de máquina, por lo que no se puede ejecutar de esta manera.

El mecanismo shebang permite que el núcleo difiera la tarea de interpretar el código a otro programa. Cuando el núcleo ve que el archivo ejecutable comienza con #!, lee los siguientes caracteres e interpreta la primera línea del archivo (menos el #!espacio inicial y opcional) como una ruta a otro archivo (más argumentos, que no discutiré aquí) ) Cuando se le dice al núcleo que ejecute el archivo /my/script, y ve que el archivo comienza con la línea #!/some/interpreter, el núcleo se ejecuta /some/interpretercon el argumento /my/script. Entonces depende de /some/interpreterdecidir que /my/scriptes un archivo de script que debe ejecutar.

¿Qué sucede si un archivo no contiene código nativo en un formato que el kernel entienda y no comience con un shebang? Bueno, entonces el archivo no es ejecutable y la execvellamada al sistema falla con el código de error ENOEXEC(error de formato ejecutable).

Este podría ser el final de la historia, pero la mayoría de los shells implementan una función alternativa. Si el kernel regresa ENOEXEC, el shell mira el contenido del archivo y comprueba si parece un script de shell. Si el shell cree que el archivo parece un script de shell, lo ejecuta por sí mismo. Los detalles de cómo hacerlo depende del shell. Puede ver algo de lo que está sucediendo agregando ps $$su secuencia de comandos, y más mirando el proceso con strace -p1234 -f -eprocess1234 donde es el PID del shell.

En bash, este mecanismo de respaldo se implementa llamando forkpero no execve. El proceso bash secundario borra su estado interno por sí mismo y abre el nuevo archivo de script para ejecutarlo. Por lo tanto, el proceso que ejecuta el script sigue utilizando la imagen del código bash original y los argumentos de la línea de comando original pasados ​​cuando invocó bash originalmente. ATT ksh se comporta de la misma manera.

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

Dash, en contraste, reacciona ENOEXECllamando /bin/shcon la ruta al guión pasado como argumento. En otras palabras, cuando ejecuta un script shebangless desde el tablero, se comporta como si el script tuviera una línea shebang #!/bin/sh. Mksh y zsh se comportan de la misma manera.

% dash
$ ./foo.sh
  PID TTY      STAT   TIME COMMAND
21427 pts/2    S+     0:00 /bin/sh ./foo.sh
Gilles 'SO- deja de ser malvado'
fuente
Genial, comprende la respuesta. Una pregunta RE: la implementación alternativa que explicó: Supongo que dado que un hijo bashestá bifurcado, tiene acceso a la misma argv[]matriz que su padre, que es cómo conoce "los argumentos de la línea de comando original pasados ​​cuando invocó bash originalmente", y si entonces, esta es la razón por la cual el niño no pasa el guión original como un argumento explícito (de ahí que el grep no pueda encontrarlo), ¿es eso correcto?
StoneThrow
1
En realidad, puede desactivar el comportamiento shebang del kernel (el BINFMT_SCRIPTmódulo controla esto y se puede eliminar / modularizar, aunque generalmente está vinculado estáticamente al kernel), pero no veo por qué querría hacerlo, excepto quizás en un sistema integrado . Como una solución para esta posibilidad, bashtiene un indicador de configuración ( HAVE_HASH_BANG_EXEC) para compensar!
ErikF
2
@StoneThrow No es tanto que el bash secundario "conozca los argumentos originales de la línea de comandos", sino que no los modifique. Un programa puede modificar lo que psinforma como argumentos de línea de comando, pero solo hasta cierto punto: tiene que modificar un búfer de memoria existente, no puede agrandar este búfer. Entonces, si bash intentara modificarlo argvpara agregar el nombre del script, no siempre funcionaría. Al niño no se le "pasa un argumento" porque nunca hay una execvellamada al sistema en el niño. Es la misma imagen del proceso bash que sigue ejecutándose.
Gilles 'SO- deja de ser malvado'
-1

En el primer caso, el script es ejecutado por un hijo bifurcado de su shell actual.

Primero debe ejecutar echo $$y luego echar un vistazo a un shell que tiene el ID de proceso de su shell como ID de proceso principal.

astuto
fuente