¿Cómo trata Linux los scripts de shell?

22

Para esta pregunta, consideremos un script de shell bash, aunque esta pregunta debe ser aplicable a todos los tipos de script de shell.

Cuando alguien ejecuta un script de shell, ¿Linux carga todo el script a la vez (tal vez en la memoria) o lee los comandos del script uno por uno (línea por línea)?

En otras palabras, si ejecuto un script de shell y lo elimino antes de que se complete la ejecución, ¿finalizará la ejecución o continuará como está?

usuario registrado
fuente
3
Intentalo. (Continuará.)
devnull
1
@devnull en realidad hay una pregunta interesante aquí. De acuerdo, si continuará o no es trivial para probar, pero hay diferencias entre los archivos binarios (que se cargan en la memoria) y los scripts con una línea shebang, o los scripts sin una línea shebang.
terdon
1
Te puede interesar esta respuesta
terdon
23
Para el propósito de su intención real, de eliminar el script de shell durante su ejecución, no importa si se lee todo de una vez o línea por línea. En Unix, un inodo en realidad no se elimina (incluso si no hay enlaces desde ningún directorio) hasta que se cierra el último archivo abierto. En otras palabras, incluso si su shell lee en el script de shell línea por línea durante la ejecución, todavía es seguro eliminarlo. La única excepción es si su shell es del tipo que cierra y vuelve a abrir el script de shell cada vez, pero si lo hace, tiene problemas mucho mayores (de seguridad).
Chris Jester-Young

Respuestas:

33

Si lo usa strace, puede ver cómo se ejecuta un script de shell cuando se ejecuta.

Ejemplo

Digamos que tengo este script de shell.

$ cat hello_ul.bash 
#!/bin/bash

echo "Hello Unix & Linux!"

Ejecutándolo usando strace:

$ strace -s 2000 -o strace.log ./hello_ul.bash
Hello Unix & Linux!
$

Echar un vistazo dentro del strace.logarchivo revela lo siguiente.

...
open("./hello_ul.bash", O_RDONLY)       = 3
ioctl(3, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7fff0b6e3330) = -1 ENOTTY (Inappropriate ioctl for device)
lseek(3, 0, SEEK_CUR)                   = 0
read(3, "#!/bin/bash\n\necho \"Hello Unix & Linux!\"\n", 80) = 40
lseek(3, 0, SEEK_SET)                   = 0
getrlimit(RLIMIT_NOFILE, {rlim_cur=1024, rlim_max=4*1024}) = 0
fcntl(255, F_GETFD)                     = -1 EBADF (Bad file descriptor)
dup2(3, 255)                            = 255
close(3)     
...

Una vez que el archivo ha sido leído, se ejecuta:

...
read(255, "#!/bin/bash\n\necho \"Hello Unix & Linux!\"\n", 40) = 40
rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 3), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc0b38ba000
write(1, "Hello Unix & Linux!\n", 20)   = 20
rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
read(255, "", 40)                       = 0
exit_group(0)                           = ?

En lo anterior, podemos ver claramente que todo el script parece estar siendo leído como una sola entidad y luego ejecutado allí. Por lo tanto, "aparecerá" al menos en el caso de Bash en el que lee el archivo y luego lo ejecuta. ¿Crees que podrías editar el script mientras se está ejecutando?

NOTA: ¡no lo hagas! Siga leyendo para comprender por qué no debe meterse con un archivo de script en ejecución.

¿Qué pasa con otros intérpretes?

Pero tu pregunta está un poco fuera de lugar. No es Linux el que necesariamente carga el contenido del archivo, es el intérprete el que carga el contenido, por lo que depende realmente de cómo el intérprete implemente si carga el archivo por completo o en bloques o líneas a la vez.

Entonces, ¿por qué no podemos editar el archivo?

Sin embargo, si usa un script mucho más grande, notará que la prueba anterior es un poco engañosa. De hecho, la mayoría de los intérpretes cargan sus archivos en bloques. Esto es bastante estándar con muchas de las herramientas de Unix donde cargan bloques de un archivo, lo procesan y luego cargan otro bloque. Puede ver este comportamiento con estas preguntas y respuestas de U&L que escribí hace un tiempo sobre grep, tituladas: ¿Cuánto texto consume grep / egrep cada vez? .

Ejemplo

Digamos que hacemos el siguiente script de shell.

$ ( 
    echo '#!/bin/bash'; 
    for i in {1..100000}; do printf "%s\n" "echo \"$i\""; done 
  ) > ascript.bash;
$ chmod +x ascript.bash

Resultando en este archivo:

$ ll ascript.bash 
-rwxrwxr-x. 1 saml saml 1288907 Mar 23 18:59 ascript.bash

Que contiene el siguiente tipo de contenido:

$ head -3 ascript.bash ; echo "..."; tail -3 ascript.bash 
#!/bin/bash
echo "1"
echo "2"
...
echo "99998"
echo "99999"
echo "100000"

Ahora, cuando ejecute esto utilizando la misma técnica anterior con strace:

$ strace -s 2000 -o strace_ascript.log ./ascript.bash
...    
read(255, "#!/bin/bash\necho \"1\"\necho \"2\"\necho \"3\"\necho \"4\"\necho \"5\"\necho \"6\"\necho \"7\"\necho \"8\"\necho \"9\"\necho \"10\"\necho 
...
...
\"181\"\necho \"182\"\necho \"183\"\necho \"184\"\necho \"185\"\necho \"186\"\necho \"187\"\necho \"188\"\necho \"189\"\necho \"190\"\necho \""..., 8192) = 8192

Notarás que el archivo se está leyendo en incrementos de 8 KB, por lo que Bash y otros shells probablemente no cargarán un archivo en su totalidad, sino que los leerán en bloques.

Referencias

slm
fuente
@terdon: sí, recuerdo haber visto esas preguntas y respuestas antes.
slm
55
Con un script de 40 bytes, claro, se lee en un bloque. Pruebe con un script> 8kB.
Gilles 'SO- deja de ser malvado'
Nunca lo intenté, pero creo que la eliminación de archivos no se realiza realmente hasta que todos los procesos cierran el descriptor de archivo asociado con el archivo eliminado, por lo que bash podría continuar leyendo desde el archivo eliminado.
Farid Nouri Neshat
@Gilles: sí, agregué un ejemplo, estaba llegando a él.
slm
2
Este comportamiento depende de la versión. Probé con bash versión 3.2.51 (1) -release, y descubrí que no se almacenó más allá de la línea actual (ver esta respuesta de stackoverflow ).
Gordon Davisson
11

Esto depende más del shell que del sistema operativo.

Dependiendo de la versión, kshlea el script a pedido por bloque de 8k o 64k bytes.

bashlea el guión línea por línea. Sin embargo, dado que las líneas de hecho pueden ser de longitud arbitraria, se lee cada vez 8176 bytes desde el comienzo de la siguiente línea para analizar.

Esto es para construcciones simples, es decir, un conjunto de comandos simples.

Si se utilizan comandos estructurados de shell ( un caso que la respuesta aceptada no se tiene en cuenta ) como un for/do/donebucle, un case/esacinterruptor, un documento aquí, un subshell entre paréntesis, una definición de función, etc. y cualquier combinación de lo anterior, los intérpretes de shell se leen hasta el final de la construcción para asegurarse primero de que no haya un error de sintaxis.

Esto es algo ineficiente ya que el mismo código puede leerse una y otra vez una gran cantidad de veces, pero mitigado por el hecho de que este contenido normalmente se almacena en caché.

Cualquiera que sea el intérprete de shell, es muy imprudente modificar un script de shell mientras se ejecuta, ya que el shell es libre de leer nuevamente cualquier parte del script y esto puede conducir a errores de sintaxis inesperados si no está sincronizado.

Tenga en cuenta también que bash podría bloquearse con una violación de segmentación cuando no puede almacenar una construcción de script demasiado grande, ksh93 puede leer sin problemas.

jlliagre
fuente
7

Eso depende de cómo funciona el intérprete que ejecuta el script. Todo lo que hace el núcleo es notar que el archivo a ejecutar comienza #!, esencialmente ejecuta el resto de la línea como un programa y le da el ejecutable como argumento. Si el intérprete enumerado allí lee ese archivo línea por línea (como hacen los shells interactivos con lo que escribe), eso es lo que obtiene (pero las estructuras de bucle de varias líneas se leen y se guardan para que se repitan); si el intérprete extrae el archivo en la memoria, lo procesa (tal vez lo compila en una representación intermedia, como lo hacen Perl y Pyton), el archivo se lee por completo antes de ejecutarse.

Si elimina el archivo mientras tanto, el archivo no se elimina hasta que el intérprete lo cierre (como siempre, los archivos desaparecen cuando la última referencia, ya sea una entrada de directorio o un proceso que lo mantiene abierto) desaparece.

vonbrand
fuente
4

El archivo 'x':

cat<<'dog' >xyzzy
LANG=C
T=`tty`
( sleep 2 ; ls -l xyzzy >$T ) &
( sleep 4 ; rm -v xyzzy >$T ) &
( sleep 4 ; ls -l xyzzy >$T ) &
echo alive. ; sleep 1
echo alive. ; sleep 1
echo alive. ; sleep 1
echo alive. ; sleep 1
echo alive. ; sleep 1
echo alive. ; sleep 1
echo alive. ; sleep 1
echo alive. ; sleep 1
dog

sh xyzzy

La carrera:

~/wrk/tmp$ sh x
alive.
alive.
alive.
-rw-r--r-- 1 yeti yeti 287 Mar 23 16:57 xyzzy
alive.
removed `xyzzy'
ls: cannot access xyzzy: No such file or directory
alive.
alive.
alive.
alive.
~/wrk/tmp$ _

IIRC un archivo no se elimina mientras un proceso lo mantenga abierto. La eliminación solo elimina el DIRENT dado.


fuente