分类: Linux, Python, 操作系统

APUE杂记:解释器文件

一个非常常见的Python脚本如下:

一直以来从来没考虑过为什么在脚本的第一行要写上 #!/usr/bin/env python 这样的注释,通常的解释是这样写就知道用什么来解释这个文件了,但是也没有深究为什么。其实这是一个Unix解释器文件的写法。

对这种文件的解释是内核作为exec系统调用处理的一部分完成的。当我们执行这个脚本时,实际上发生的过程是这样的:

  1. sh程序执行fork系统调用生成子进程
  2. 子进程执行exec系统调用,执行/home/Documents/kongfy/args.py a b c
  3. 内核执行exec,发现该文件不是可执行格式(如ELF),作为解释器文件执行,使用第一行的解释器执行解释器,并将原参数进行位移后附加在后面(在此处内核使用pathname代替argv[0]),实际执行的程序变为/usr/bin/env python /home/Documents/kongfy/args.py a b c

当不添加解释器注释时,错误输出如下:

也就是说当内核找不到解释器文件的时候使用用户默认的sh执行脚本,自然会报错。

在实际解释器中执行该文件时,这一行作为注释不产生效果,也就是说,难道这就是为什么脚本语言中多采用#作为注释符的原因?


拨乱反正

一种常见的误解是#!的开头是由bash等shell程序解释的,下面从几个方面验证解释器文件确实是由内核解释的。

shell

使用bash解释带有#!的python脚本文件:

可见bash并不能理解解释器文件,#!在bash看来只是普通的注释而已。

strace

通过strace跟踪执行脚本文件时使用的系统调用:

如果脚本文件真的是由shell解释执行的,则不应该会产生对于./args.py的execve系统调用,后面的一连串系统调用都是由/usr/bin/env生成的寻找python位置的调用。

man

执行man 2 execve查看execve系统调用的手册文档:

Interpreter scripts
An interpreter script is a text file that has execute permission enabled and whose first line is of the form:

#! interpreter [optional-arg]

The interpreter must be a valid pathname for an executable which is not itself a script. If the filename argument of execve() specifies an
interpreter script, then interpreter will be invoked with the following arguments:

interpreter [optional-arg] filename arg…

where arg… is the series of words pointed to by the argv argument of execve().

很明显,执行解释器文件是execve系统调用工作的一部分。

kernal code

实际上,对开源项目最明显最直接的验证方法必须是“read the fucking code”,只是Linux内核源码并不是那么直观,所以如果只想知道结果的话建议跳过这一小节,下面的相关内核源码贴给和我一样喜欢追根溯源的傻boy们。
注:代码摘自Linux内核2.6.32.63版本的x86部分

当在用户空间执行系统调用陷入内核后,通过syscall_table找到并调用函数sys_execve,函数简单的从用户空间拷贝了filename字符串,调用do_execve函数:

这个函数很长,你可以慢慢品读,注意中间调用了search_binary_handler函数,该函数负责寻找实际实行该文件的方式:

Linux中有多个可执行的格式,这个函数就是在这些格式中循环查找,其中一个就是script格式,对应的执行代码:

执行过程和APUE中描述的流程一致,Done。


参考资料

  • Unix环境高级编程
  • Linux man page
  • 胡萝卜

    这个应该是跟具体的shell解释器相关的,只是现在流行的bash、zsh、fish都是支持这种写法,第一行的 #! 这两个符号就是告诉shell应该调用后面的来执行,shell也只会判断第一行的第一个和第二个是这两个字符才行。

    • 你说的这是一种常见的误区,这个功能是系统调用的工作而非shell,shell的工作只是fork、exec、waitpid,我增加了验证部分,你可以看一下。