Cpython源码分析03(*)_简要总结下Cpython是如何执行python test.py

当我们通过命令行传入参数的方式调用 python 解释器去运行一个模块的时候,比如: $ python test.py 图2.1中所示的过程将开始进行。(当然这只是其中一种运行 Python 程序的方式比如也可以在交互模式下单行运行,对于交互模式,这里暂时不做讨论。) 基于Python3.7
在这里插入图片描述

  1. Python 可执行程序是一个用 C 语言编写的程序。当它被执行的时候,所发生的事情其实就和其他 C 语言程序(比如 Linux 内核或是一个简单的 hello world 程序)差不多。请花一点时间来理解一下,Python 可执行程序只是一个用来运行程序的程序,可以结合 C 与 汇编或者 LLVM 的关系来理解。当我们使用解释器运行一个模块的时候,最开始会运行一个与平台相关的标准初始化过程。
    所有讨论都假设在类 Unix 操作系统中,Windows 下会有些许不同。

  2. C 运行时环境会包办了所有的初始化操作,像是加载库、检查与设置环境变量等等。然后 python 可执行程序的 main 方法开始运行就像任何其他普通的 C 程序那样。

  3. python 的 main 函数位于 Programs/python.c文件中。 main 函数会调用位于 Modules/main.c中的 Py_Main 函数,它处理解释器的初始化过程,包括:解析命令行参数、设置程序的 flags、读取环境变量、运行 hook、哈希初始化等等。作为初始化过程的一部分, Python/pylifecycle.c中的 Py_Initialize 函数会被调用,它会初始化两个比较重要的数据结构:解释器状态与线程状态。
    看一眼解释器状态与线程状态的定义,它能给我们一些关于它们功能的信息。这两个数据结构由一些指向特定字段的指针组成,它们带有程序执行所需要的一些信息。首先我们来看解释器状态在源码中的定义(Include/pystate.h),以下是其中比较重要的部分:

    //解释器状态数据结构定义如下
    typedef struct _is {
        struct _is *next;
        struct _ts *tstate_head;
    
        PyObject *modules;
        PyObject *modules_by_index;
        PyObject *sysdict;
        PyObject *builtins;
        PyObject *importlib;
    
        PyObject *codec_search_path;
        PyObject *codec_search_cache;
        PyObject *codec_error_registry;
    
        int codecs_initialized;
        int fscodec_initialized;
    
        PyObject *builtins_copy;
    } PyInterpreterState;
    

    Python 程序员应该或多或少会对这些字段名字中的词有一些认识比如:sysdict,builtins,codec 等。

    • *next 字段为一个指向另一个解释器实例的引用(译注:多个解释器可以同时存在于一个进程当中,这个特性有望用于解决 CPython 的 GIL 问题,参考 PEP554)
    • *tstate_head 字段指向运行的主线程。当程序开启多线程时,解释器被它开启的所有线程所共享。线程状态随后将被讨论。
    • modules, modules_by_index, sysdict, builtins 以及 importlib 从它们的名字就能理解。它们都被定义为 PyObject 的实例,在 Python 虚拟机中 PyObject 为最基本的对象类型。
    • codec* 相关的字段持有用于定位、加载编码方式(encodings)的信息,对于字节解码(decoding bytes)非常重要。
  4. 程序的运行必须在一个线程之中进行,thread state structure 包含了一个线程执行 python code object 所需的信息,关于 thread state 定义的其中一部分代码如下(Include/pystate.h):

    //线程状态数据结构定义如下
    typedef struct _ts {
    
        struct _ts *prev;
        struct _ts *next;
        PyInterpreterState *interp;
    
        struct _frame *frame;
        int recursion_depth;
        char overflowed; /* The stack has overflowed. Allow 50 more calls
                            to handle the runtime error. */
        char recursion_critical; /* The current calls must not cause
                                    a stack overflow. */
        int stackcheck_counter;
    
        /* 'tracing' keeps track of the execution depth when tracing/profiling.
           This is to prevent the actual trace/profile code from being recorded in
           the trace/profile. */
        int tracing;
        int use_tracing;
        
        Py_tracefunc c_profilefunc;
        Py_tracefunc c_tracefunc;
        PyObject *c_profileobj;
        PyObject *c_traceobj;
    
        /* The exception currently being raised */
        PyObject *curexc_type;
        PyObject *curexc_value;
        PyObject *curexc_traceback;
    
        PyObject *dict;  /* Stores per-thread state */
    
        int gilstate_counter;
    } PyThreadState;
    

    关于这两个数据结构,后面的章节中还会有进一步讨论。初始化过程还会设置一些重要的机制,比如基本的 stdio等。

  5. 一旦完成了所有的初始化,Py_Main 函数会调用 pymain_run_file函数,随后发生调用:

    PyRun_AnyFileExFlags -> 
    PyRun_SimpleFileExFlags -> 
    PyRun_FileExFlags -> 
    PyParser_ASTFromFileObject
    

    PyRun_SimpleFileExFlags 函数调用中,main namespace 将被创建,文件内容将在其中被运行。它也将检查文件的 pyc 版本是否存在(pyc 文件是一个储存了已经被执行过的py文件编译后的代码(字节码)的文件)。当文件的 pyc 版本存在的时候,将会尝试以二进制形式读取并执行它。而当 pyc 文件不存在的时候,会顺着 PyRun_FileExFlags 向下调用, PyParser_ASTFromFileObject函数会接着调用 PyParser_ParseFileObject函数,它会读取被执行模块的内容并建立 parse tree,然后将 parse tree 传递给 PyAST_FromNodeObject根据 parse tree 创建 AST。(如果你阅读这些代码,你会看到很多的 Py_INCREF 和 Py_DECREF。这些内存管理函数会在后面详细讨论。CPython 通过引用计数管理对象的生命周期。当一个新的对象引用产生的时候,计数通过Py_INCREF增加,当一个引用离开作用域的时候会通过Py_DECREF减少。)

    AST 生成后会被传给 run_mod函数,这个函数会接着调用 PyAST_CompileObject 根据 AST 生成 code object。并且在调用 PyAST_CompileObject 生成字节码的时候会经过一个简单的 peephole optimizer 进行字节码优化。随后 run_mod 会调用 PyEval_EvalCode 随之产生函数调用:

    PyEval_EvalCode ->
    PyEval_EvalCodeEx ->
    _PyEval_EvalCodeWithName ->
    PyEval_EvalFrameEx
    
    • PyEval_EvalFrameEx 中的解释器循环(interpreter loop)才会实际进行对 code object 的运行处理。它不只是将 code object 作为参数而是使用一个 frame object,它有一个字段用来保存到 code object 的引用。简单来说,解释器循环会不断读取储存在一个指令数组中的下一个指令进行执行,增加或者删除位于栈上的对象,直到没有新的指令或者中途发生异常中断了循环。
    • 头文件 Include/opcode.h 中包含了 python 虚拟机支持的 instruction/opcodes 清单。其实 opcodes 的概念很容易理解,在上面的例子中有4条 instruction:LOAD_FAST 将它的参数对应的值(这里对应x)加载到栈上。python 虚拟机是基于栈的 (stack based),也就是说 opcode 进行求值的对象以及求值得到的结果都会保存在栈上。BINARY_MULTIPLY opcode 会将前两条指令的结果从栈中弹出(pop),执行二进制乘法运算然后将结果放(push)回栈顶。RETURN_VALUE 指令会从栈中弹出设置为返回值并中断解释器循环。

    当所有的指令都被执行以后,Py_Main 将继续执行一些清理(clean up)过程。就像 Py_Initialize 所做的那样,Py_Finalize 会被调用完成一些清理任务,比如等待线程退出、调用 exit hooks、清理解释器分配的仍然被使用的内存等等。

上面我们在一个较高的层面上对 python 解释器如何执行程序进行了描述,但还有大量的细节没有被讨论,接下来的文章中我们将深入进去提供更多的细节信息。
参考视频
参考课程
参考文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值