09月17, 2020

51. Python 是如何运行起来的?

对于 Python 是一门优雅的语言,这一点应该不会有太多人反对,随着我们对 CPython 实现的研究,不得不说 Python 的优雅一直渗透到它的底层,在 Python 的 C 实现这一层,几乎全部以 Python C API 实现,函数之间传递的是 Object *,Dict、Tuple、List 等对象,在 CPython 的 C 实现中更是随处可见。

image.png

如果将 Python 进行分层,最底层是基本的内存管理,之上就是 Python 强大的类型和面向对象系统,Python VM 甚至也是高度依赖于其自身的类型系统,在最顶层支撑了 Python 语言的用户接口。Python 语言实际上是对 Python 类型系统和 VM 的操作。

一切从 main 开始

所有 C 语言程序都必须有一个 main 函数,Python,更准确的说 CPython 也是一个程序,main 就是它运行的开端。找到这个开端,就算是理清 “头绪” 的第一步。

在 GDB 中设置 main 函数的断点,很容易找到 Python 的入口位置:

// Programs/python.c:16

int
main(int argc, char **argv)
{
    return Py_BytesMain(argc, argv);
}

非常简单,可以看到进入 main 函数之后,只有一个简短的 Py_BytesMain 函数,实际上这个函数可不简单,Python 的一切都在这个函数里,如果在交互模式下,这个函数也不会像想象中一样迅速返回。如果一直追踪下去,会遇到下面这个比较长的函数:

// Modules/main.c:551
static void
pymain_run_python(int *exitcode)
{
    // ··· ···

    // some if-else conditions
    else if (config->run_filename != NULL) {
        // 执行 py 文件
        *exitcode = pymain_run_file(config, &cf);
    }
    else {
        // 交互模式
        *exitcode = pymain_run_stdin(config, &cf);
    }

    // ··· ···
}

在这里 Python 的运行流程开始分道扬镳,执行文件,或者进入交互模式从标准输入读取指令,分别由不同的函数处理。Python 的初始化工作也大体结束,真正开门营业了。

如果留意源码中的 Object *,实际上此时 Python VM 还什么都没有干呢,但 Python 对象已经先跑起来了。Object 很早就存在于 Python 中,这也是在研究 VM 之前,破天荒的优先认识类型系统的原因。

py 文件的执行

Python 的交互模式可以认为是临时一行一行写代码,写一点执行一点。为了方便,我们现在先搞清楚 py 文件是怎么执行的就好了,也就是要让程序流运行到 pymain_run_file 里面去。方法也很简单,在 GDB 执行 run test.py 就可以了,这就相当于执行 python test.py。这个 test.py 是我临时写的一小段代码;

print("hello world")

"hello world" 是如此流行,我们也遵守这个习惯好了。

如果一切顺利,很快就会找到这里:

// Python/pythonrun.c:1039

PyObject *
PyRun_FileExFlags(FILE *fp, const char *filename_str, int start, PyObject *globals,
                  PyObject *locals, int closeit, PyCompilerFlags *flags)
{
    // ··· ···

    filename = PyUnicode_DecodeFSDefault(filename_str);

    // ··· ···

    // 从文件对象,构建 AST
    mod = PyParser_ASTFromFileObject(fp, filename, NULL, start, 0, 0,
                                     flags, NULL, arena);
    // 执行该语法数
    ret = run_mod(mod, filename, globals, locals, flags, arena);

    // ··· ···
}

// Python/pythonrun.c:1132

static PyObject *
run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
            PyCompilerFlags *flags, PyArena *arena)
{
    // ··· ···

    // 编译,生成 codeobject,co 是它的缩写
    co = PyAST_CompileObject(mod, filename, flags, -1, arena);

    // ··· ···

    // 执行 codeobject
    v = run_eval_code_obj(co, globals, locals);
    Py_DECREF(co);
    return v;
}

到了这里,我们可以暂时先不继续深入跟进,可以看到在 run_eval_code_obj 返回的时候,test.py 已经正确执行,打印了 "hello wold"。

如果算上 PyRun_FileExFlags 之前某处打开 py 文件,那么 py 文件的运行至少经历了这些步骤:

image.png

  • test.py 也就是源码文件,首先被打开并封装为 Python 的文件对象;
  • 之后构建 AST,即抽象语法树,编译原理是另外一个话题了,有兴趣可以阅读编译原理经典著作龙书、虎书、鲸书;
  • 再将 AST 转换为 Python 字节码,源码中已经可以看出,字节码保存在 codeobject 中;
  • 最终执行该字节码,结束。

这样一个简单的 py 文件就执行完毕了。

文件操作、编译原理等问题已经超出我们研究的范围,另外在生成 codeobject 前还有一个优化过程,我们也暂时搁置这一议题,后面我们将会重点研究 codeobject,以及 Python VM 到底是如何响应字节码的指令,最终把 Python “运行” 起来的。

本文链接:http://www.thinkinpython.com/post/deep_python_vm_51.html

-- EOF --