Linux 如何处理 shell 脚本?

Reg*_*ser 31 linux shell shell-script

对于这个问题,让我们考虑一个 bash shell 脚本,尽管这个问题必须适用于所有类型的 shell 脚本。

当有人执行 shell 脚本时,Linux 是一次性加载所有脚本(可能是到内存中)还是逐行读取脚本命令

换句话说,如果我执行一个shell脚本并在执行完成之前将其删除,执行会被终止还是会继续执行?

slm*_*slm 40

如果您使用,strace您可以看到 shell 脚本在运行时是如何执行的。

例子

假设我有这个 shell 脚本。

$ cat hello_ul.bash 
#!/bin/bash

echo "Hello Unix & Linux!"
Run Code Online (Sandbox Code Playgroud)

运行它使用strace

$ strace -s 2000 -o strace.log ./hello_ul.bash
Hello Unix & Linux!
$
Run Code Online (Sandbox Code Playgroud)

查看strace.log文件内部会发现以下内容。

...
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)     
...
Run Code Online (Sandbox Code Playgroud)

一旦文件被读入,它就会被执行:

...
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)                           = ?
Run Code Online (Sandbox Code Playgroud)

在上面我们可以清楚地看到整个脚本似乎被作为一个实体读入,然后在那里执行。所以它至少在 Bash 的情况下会“出现”,它会读入文件,然后执行它。所以你认为你可以在脚本运行时编辑它?

注意:不要,但是!请继续阅读以了解为什么您不应该弄乱正在运行的脚本文件。

其他口译员呢?

但你的问题有点偏离。不一定要加载文件内容的不是 Linux,而是加载内容的解释器,因此无论是完全加载文件还是以块或行加载文件,这实际上取决于解释器的实现方式。

那么为什么我们不能编辑文件呢?

但是,如果您使用更大的脚本,您会注意到上述测试有点误导。事实上,大多数解释器以块的形式加载他们的文件。这是许多 Unix 工具的标准,它们加载文件块,处理它,然后加载另一个块。您可以通过我前段时间写的 U&L Q&A 看到这种行为grep,标题为:grep/egrep 每次消耗多少文本?.

例子

假设我们制作了以下 shell 脚本。

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

导致此文件:

$ ll ascript.bash 
-rwxrwxr-x. 1 saml saml 1288907 Mar 23 18:59 ascript.bash
Run Code Online (Sandbox Code Playgroud)

其中包含以下类型的内容:

$ head -3 ascript.bash ; echo "..."; tail -3 ascript.bash 
#!/bin/bash
echo "1"
echo "2"
...
echo "99998"
echo "99999"
echo "100000"
Run Code Online (Sandbox Code Playgroud)

现在,当您使用与上面相同的技术运行它时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
Run Code Online (Sandbox Code Playgroud)

您会注意到文件是以 8KB 的增量读入的,因此 Bash 和其他 shell 可能不会完整加载文件,而是分块读入。

参考

  • 对于 40 字节的脚本,当然,它是在一个块中读取的。尝试使用 >8kB 的脚本。 (7认同)
  • 此行为取决于版本。我用 bash 版本 3.2.51(1)-release 进行了测试,发现它没有缓冲超过当前行(参见 [this stackoverflow answer](http://stackoverflow.com/questions/21096478/overwrite-executing- bash 脚本文件#21100710))。 (2认同)

jll*_*gre 12

这更依赖于外壳而不是依赖于操作系统。

根据版本,ksh按 8k 或 64k 字节块按需读取脚本。

bash逐行阅读脚本。然而,考虑到行可以是任意长度,它每次从下一行的开头读取 8176 个字节进行解析。

这适用于简单的结构,即一套简单的命令。

如果使用 shell 结构化命令(接受的答案未考虑的情况),如for/do/done循环、case/esac开关、here 文档、括号括起来的子 shell、函数定义等以及上述任何组合,shell 解释器会读取到构造结束时首先要确保没有语法错误。

这在某种程度上是低效的,因为相同的代码可以被反复读取很多次,但由于该内容通常被缓存这一事实而缓解了这种情况。

无论 shell 解释器是什么,在执行 shell 脚本时修改它是非常不明智的,因为 shell 可以自由地再次读取脚本的任何部分,如果不同步,这可能导致意外的语法错误。

另请注意,当 bash 无法存储过大的脚本结构时,它可能会因分段违规而崩溃,ksh93 可以完美读取。


von*_*and 7

这取决于运行脚本的解释器如何工作。内核所做的只是注意要执行的文件以 开头#!,本质上将行的其余部分作为程序运行,并将可执行文件作为参数提供给它。如果那里列出的解释器逐行读取该文件(就像交互式 shell 对您键入的内容所做的那样),这就是您得到的(但多行循环结构被读取并保留以供重复);如果解释器将文件放入内存中,对其进行处理(可能将其编译为中间表示,如 Perl 和 Pyton 所做的那样),则文件在执行前会被完整读取。

如果您同时删除该文件,则该文件不会被删除,直到解释器关闭它(一如既往,当最后一个引用,无论是目录条目还是保持打开的进程)消失时,文件都会消失。