Linux 如何处理 shell 脚本?

Linux 如何处理 shell 脚本?

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

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

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

答案1

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

例子

假设我有这个 shell 脚本。

$ cat hello_ul.bash 
#!/bin/bash

echo "Hello Unix & Linux!"

使用以下命令运行它strace

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

查看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)     
...

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

...
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)                           = ?

在上面我们可以清楚地看到整个脚本似乎被作为单个实体读入,然后在那里执行。所以它会“出现”至少在 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

生成此文件:

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

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

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

现在,当您使用与上面相同的技术运行它时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

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

参考

答案2

这更多地依赖于 shell,而不是依赖于操作系统。

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

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

这是为了简单的结构,即一套简单的命令。

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

这在某种程度上是低效的,因为相同的代码可以被多次重复读取,但由于这些内容通常会被缓存,因此可以缓解这一问题。

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

还要注意,当 bash 无法存储过大的脚本构造 ksh93 可以完美读取时,它可能会因分段冲突而崩溃。

答案3

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

如果您同时删除该文件,则在解释器将其关闭之前,该文件不会被删除(与往常一样,当最后一个引用(无论是目录条目还是保持打开状态的进程)消失时,文件也会消失。

答案4

“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

运行:

~/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 只要进程保持文件打开状态,文件就不会被删除。删除只是删除给定的 DIRENT。

相关内容