Cpython源码分析02_Python代码是怎么运行起来的


说明:如果没有特殊说明,均基于window平台讨论

1.python代码运行时的入口

  1. 入口文件为python.c,当宏MS_WINDOWS存在时,即为window平台编译的python时的入口
    /* Minimal main program -- everything is loaded from the library */
    
    #include "Python.h"
    
    #ifdef MS_WINDOWS
    int
    wmain(int argc, wchar_t **argv) //等价于int wmain(int argc, wchar_t *argv[]) 
    {
    	printf("hellow world\n");//这里我们加一句调试语句可以来验证window平台确实是走的这个分支
        return Py_Main(argc, argv);
    }
    #else
    int
    main(int argc, char **argv) //等价于int main(int argc, char *argv[])
    {
        return _Py_UnixMain(argc, argv);
    }
    #endif
    
    

2.window与Linux入口出的区别

  1. 从上面的之前的分析可以看到window与Linux的区别在于,数组argv,Linux平台argv中的元素为char*类型,window为wchar_t *。
  2. 如果忘记了char与wchar_t类型的区别可以去查阅相关资料复习一下,这里只做一些简单的回顾,wchar_t的宽度属于编译器的特性,且可以小到8位。所以程序若需要跨过所有C和C++ 编译器的可携性,就不应使用wchar_t存储Unicode文字。wchar_t类型是为存储编译器定义的宽字符,在部分编译器中,其可以是Unicode字符。

3.继续前进,生成_Py_Main对象,并做简单初始化

  1. 源文件为main.c
  2. _Py_Main主要用于将数据传递给子函数。
  3. 代码如下图,可以看到这个过程主要是_Py_Main的初始化,主要是use_bytes_argv、argc、argv的初始化。其中的printf的内容是我自己调试时候打印的。在这里插入图片描述

4.继续前进,我们来到了pymain_mian

  1. 源文件位于main.c

  2. 源码如下

    static int
    pymain_main(_PyMain *pymain)
    {
        int res = pymain_init(pymain);
        if (res == 1) {
            goto done;
        }
    
        pymain_run_python(pymain);
    
        if (Py_FinalizeEx() < 0) {
            /* Value unlikely to be confused with a non-error exit status or
               other special meaning */
            pymain->status = 120;
        }
    
    done:
        pymain_free(pymain);
    
        return pymain->status;
    }
    
  3. 可以看到,pymain_main函数主要做的事情为初始化python的核心配置即pymain_init,以及真正将python的源代码run起来的pymain_run_python函数。

5.pymain_init都干了些什么事情呢

  1. 代码如下,我们在后面着重介绍一下运行时runtime的初始化过程
static int
pymain_init(_PyMain *pymain)
{
    _PyCoreConfig local_config = _PyCoreConfig_INIT;
    _PyCoreConfig *config = &local_config;

    /* 754 requires that FP exceptions run in "no stop" mode by default,
     * and until C vendors implement C99's ways to control FP exceptions,
     * Python requires non-stop mode.  Alas, some platforms enable FP
     * exceptions by default.  Here we disable them.
     */
#ifdef __FreeBSD__
    fedisableexcept(FE_OVERFLOW);
#endif

    config->_disable_importlib = 0;
    config->install_signal_handlers = 1;
    _PyCoreConfig_GetGlobalConfig(config);

    int res = pymain_cmdline(pymain, config);
    if (res < 0) {
        _Py_FatalInitError(pymain->err);
    }
    if (res == 1) {
        pymain_clear_config(&local_config);
        return res;
    }

    pymain_init_stdio(pymain);

    PyInterpreterState *interp;
    pymain->err = _Py_InitializeCore(&interp, config);
    if (_Py_INIT_FAILED(pymain->err)) {
        _Py_FatalInitError(pymain->err);
    }

    pymain_clear_config(&local_config);
    config = &interp->core_config;

    if (pymain_init_python_main(pymain, interp) < 0) {
        _Py_FatalInitError(pymain->err);
    }

    if (pymain_init_sys_path(pymain, config) < 0) {
        _Py_FatalInitError(pymain->err);
    }
    return 0;
}
  1. 可以看到首先是定义了一个类型为_PyCoreConfig的local_config对象,接着定义了一个类型为_PyCoreConfig *的config指针,接着初始化了_disable_importlib与install_signal_handlers的值,接着config进入了_PyCoreConfig_GetGlobalConfig中,这个函数主要是对需要忽略的环境和字符集做一些初始化,具体可以转到定义查看。

  2. 接着是pymain_cmdline函数,它的主要作用是将配置读入_PyCoreConfig 和_PyMain,初始化LC_CTYPE 语言环境和 Py_DecodeLocale(),主要有配置内容有命令行参数,全局环境变量,以及Py_xxx 全局配置变量。

  3. 在pymain_cmdline中比较重要的是还会设置内存分配器,截取部分代码如下图,其中PyMem_SetAllocator的定义位于obmalloc.c文件中
    在这里插入图片描述

  4. 还有一个_Py_InitializeCore函数,用来初始化运行时python解释器的核心,它的定义位于pylifestyle.c文件中。

  5. 语句 PyInterpreterState *interp;可以去查看一下PyInterpreterState结构体的定义。

  6. 其中filename是在哪里进行分析且赋值给py_main->filename的呢,在解析命令行参数函数pymain_parse_cmdline_impl中,具体调用链如下,其中pymain_init里面内容很多这里就不过多的介绍了,感兴趣的可以去仔细研读源码

    pymain_init->
    pymain_cmdline->
    pymain_cmdline_impl->
    pymain_read_conf->
    pymain_read_conf_impl ->
    pymain_parse_cmdline->
    pymain_parse_cmdline_impl
    
  7. 最重要的GIL全局锁的初始化就隐藏在pymain_cmdline_impl中的_PyRuntime_Initialize即为运行时的初始化,_PyRuntime_Initialize的函数的定义位于pylifestyle.c中。

    _PyRuntime_Initialize的源码如下

    _PyInitError
    _PyRuntime_Initialize(void)
    {
        /* XXX We only initialize once in the process, which aligns with
           the static initialization of the former globals now found in
           _PyRuntime.  However, _PyRuntime *should* be initialized with
           every Py_Initialize() call, but doing so breaks the runtime.
           This is because the runtime state is not properly finalized
           currently. */
    	/*XXX 我们在这个过程中只初始化一次,这与现在在 _PyRuntime 中找到
    	的以前全局变量的静态初始化一致。 但是,_PyRuntime *应该*在每次 
    	Py_Initialize() 调用时初始化,但这样做会破坏运行时。这是因为当前
    	未正确确定运行时状态
    	*/
        static int initialized = 0;//用来标记是否已经被初始化过了
        if (initialized) {
            return _Py_INIT_OK();
        }
        initialized = 1;
    
        return _PyRuntimeState_Init(&_PyRuntime);
    }
    

    重点来看_PyRuntimeState_Init函数中的_PyRuntimeState_Init_impl函数,源代码位于文件pystate.c

    static _PyInitError
    _PyRuntimeState_Init_impl(_PyRuntimeState *runtime)
    {
        memset(runtime, 0, sizeof(*runtime));
    
        _PyGC_Initialize(&runtime->gc);//初始化垃圾回收器
        _PyEval_Initialize(&runtime->ceval);//下面重点关注
    
        runtime->gilstate.check_enabled = 1;
    
        /* A TSS key must be initialized with Py_tss_NEEDS_INIT
           in accordance with the specification. */
        //根据规范,必须使用 Py_tss_NEEDS_INIT 初始化 TSS 密钥
        Py_tss_t initial = Py_tss_NEEDS_INIT;
        runtime->gilstate.autoTSSkey = initial;
    	//PyThread_allocate_lock分配线程锁,源码位于thread_pthread.h文件
        runtime->interpreters.mutex = PyThread_allocate_lock();
        if (runtime->interpreters.mutex == NULL) {
            return _Py_INIT_ERR("Can't initialize threads for interpreter");
        }
        runtime->interpreters.next_id = -1;
    
        return _Py_INIT_OK();
    }
    
    

    _PyEval_Initialize用来初始化ceva运行时的状态,源码如下,位于文件ceval.c

    void
    _PyEval_Initialize(struct _ceval_runtime_state *state)
    {
    	//初始化递归调用的深度,宏Py_DEFAULT_RECURSION_LIMIT默认1000,要小于该值,在python中可以用sys.setrecursionlimit(1000000)来修改这一个限制
        state->recursion_limit = Py_DEFAULT_RECURSION_LIMIT;
        //检查递归限制
        _Py_CheckRecursionLimit = Py_DEFAULT_RECURSION_LIMIT;
        //初始化gil锁
        _gil_initialize(&state->gil);
    }
    

    接着看下_gil_initialize的定义

    #define DEFAULT_INTERVAL 5000
    
    static void _gil_initialize(struct _gil_runtime_state *state)
    {
        _Py_atomic_int uninitialized = {-1};
        //locked检查是否已采用 GIL(如果未初始化,则为 -1)。 
        //这是原子性的,因为它可以在 ceval.c 中不加锁的情况下读取。
        state->locked = uninitialized;
        //interval是表示gil锁切换的间隔时间,单位是微秒,默认5000微妙
        state->interval = DEFAULT_INTERVAL;
    }
    

6.继续前进,进入4步骤中的pymain_run_python看看

  1. 我们可以在main.c中看到该函数的定义,如下,其中的printf为我调试时候所打印
    在这里插入图片描述

  2. 从上图中我们可以看到python真正执行的时候分为三种大的模式,为什么说是三种大的模式呢,因为其实在pymain_run_filename这种模式下是包括了命令行的交互式环境和我们python test.py这两种模式的,我们可以来验证一下,将上图加了调试代码后重新编译一下Cpython后来实验一下,实验结果如下:
    在这里插入图片描述

7.重点分析一下pymain_run_filename,既通过交互式环境或者文件执行代码的过程

  1. pymain_run_filename的相关分析,见如下源码的注释

    static void
    pymain_run_filename(_PyMain *pymain, PyCompilerFlags *cf)
    {	/*进来后首先会判断,文件名是否为空,如果文件名为空
    	且stdin_is_interactive为真,则可以判断当前环境为交
    	互式环境
    	*/
        if (pymain->filename == NULL && pymain->stdin_is_interactive) {
            Py_InspectFlag = 0; /* do exit on SystemExit */
    		//pymain_run_startup函数的主要作用是检查当前系统或当前命令行窗口是否设置了PYTHONSTARTUP这个环境变量,
    		//至于PYTHONSTARTUP环境变量是做什么的,它主要是用来在交互式环境下,提前导入一些常用的模块,这样就可
    		//以在我们切入到交互式环境后就可以不用手动导入模块,就可以使用对应模块相关的接口了,也可以理解为预加
    		//载模块,有点类似在__init__.py中导入模块后,被其它模块引入的效果
            pymain_run_startup(cf);
    		//pymain_run_interactive_hook函数,这个里面会检查钩子函数__interactivehook__是否能正常调用,如果不能
    		//正常调用,则进入交互式环境会失败,报Failed calling sys.__interactivehook__的错误
            pymain_run_interactive_hook();
        }
    
    	//这里还会检查一次,我们在pymain_init中初始化的模块搜索路径是否为空
        if (pymain->main_importer_path != NULL) {
            pymain->status = pymain_run_main_from_importer(pymain);
            return;
        }
    
        FILE *fp;
    	//如果filename不为空,则说明当前是执行的文件,python test.py 或者python test.pyc
        if (pymain->filename != NULL) {
        	//pymain_open_filename返回对应文件指针,即文件首地址
            fp = pymain_open_filename(pymain);
            if (fp == NULL) {
                return;
            }
        }
        else {
    		printf("come from Interactive window\n");//测试用
            fp = stdin; //fp的值来自交互式环境的标准输入,可以理解为文件描述符
        }
    
        pymain->status = pymain_run_file(fp, pymain->filename, cf);
    
  2. 接着我们来到pymain_run_file函数,顾名思义,用来跑文件的,这里不管是交互式环境还是真正的文件,都统统抽象为文件。

    static int
    pymain_run_file(FILE *fp, const wchar_t *filename, PyCompilerFlags *p_cf)
    {
        PyObject *unicode, *bytes = NULL;
        const char *filename_str;
        int run;
    
        /* call pending calls like signal handlers (SIGINT) */
    	//Py_MakePendingCalls是用来调用被挂起的调用,比如信号处理程序
        if (Py_MakePendingCalls() == -1) {
            PyErr_Print();
            return 1;
        }
    
        if (filename) {
    		//将文件名转为unicode对象
            unicode = PyUnicode_FromWideChar(filename, wcslen(filename));
            if (unicode != NULL) {
    			//如果unicode对象不为空,则再将unicode对象转为bytes对象
                bytes = PyUnicode_EncodeFSDefault(unicode);
                Py_DECREF(unicode);//减少对象unicode的引用计数。 对象必须不为 NULL
            }
            if (bytes != NULL) {
                filename_str = PyBytes_AsString(bytes);//将bytes转为字符串
            }
            else {
                PyErr_Clear();
                filename_str = "<encoding error>";
            }
        }
        else {
            filename_str = "<stdin>";
        }
    	//PyRun_AnyFileExFlags作用是解析来自文件的输入并执行它,即真正的执行模块
        run = PyRun_AnyFileExFlags(fp, filename_str, filename != NULL, p_cf);
        Py_XDECREF(bytes);//减少对象bytes的引用计数。 对象可以为 NULL
        return run != 0;
    }
    
  3. 接着我们去PyRun_AnyFileExFlags看看

    int
    PyRun_AnyFileExFlags(FILE *fp, const char *filename, int closeit,
                         PyCompilerFlags *flags)
    {
    	//fp为文件指针、filename为转为字符串后的文件名、closeit为最原始的宽字符文件名是否为空、flags编译器标志
        if (filename == NULL)
            filename = "???";
        //用来判断是否为交互式环境
        if (Py_FdIsInteractive(fp, filename)) {
    		printf("this is Interactive\n");
    		/*这里的交互式终端就是我们的python回车后的命令行,
    		PyRun_InteractiveLoopFlags里面其实是一个
    		do...while循环一直在那里接受我们交互式环境中的输入,
    		直到收到EOF(即python代码中的exit())就表示要退出
    		交互式环境了,具体的更详细的过程可以跳转到该函数的定
    		义中去了解*/
            int err = PyRun_InteractiveLoopFlags(fp, filename, flags);
            if (closeit)
                fclose(fp);
            return err;
        }
        else
    		printf("this is Real file\n");
            return PyRun_SimpleFileExFlags(fp, filename, closeit, flags);
    }
    
  4. 重点看看PyRun_SimpleFileExFlags都做了些什么
    PyRun_SimpleFileExFlags首先会判断文件是不是pyc文件,即是否为字节码文件,如果是则到run_pyc_file函数,且不会去解析抽象语法树AST了,如果不是pyc文件则到PyRun_FileExFlags函数

    PyObject *
    PyRun_FileExFlags(FILE *fp, const char *filename_str, int start, PyObject *globals,
                      PyObject *locals, int closeit, PyCompilerFlags *flags)
    {
        PyObject *ret = NULL;
        mod_ty mod;
        PyArena *arena = NULL;
        PyObject *filename;
    	//解码文件名
        filename = PyUnicode_DecodeFSDefault(filename_str);
        if (filename == NULL)
            goto exit;
    	// /* 为编译阶段申请一块内存池 */
        arena = PyArena_New();
        if (arena == NULL)
            goto exit;
    	/*PyParser_ASTFromFileObject从文件对象中解析抽象语法树,
    	该函数调用Parser/parsetok.c中的PyParser_ParseFileObject()
    	函数中的解析器构造节点对象,并调用AST树构造函数PyAST_FromNodeObject()逐节点对象创建AST树。
    	*/
        mod = PyParser_ASTFromFileObject(fp, filename, NULL, start, 0, 0,
                                         flags, NULL, arena);
        if (closeit)
            fclose(fp);
        if (mod == NULL) {
            goto exit;
        }
    	//run_mod执行之前出来的抽象语法树,两个功能第一个生成代码对象 (PyAST_CompileObject()),第二个调用解释器循环 (PyEval_EvalCode())。
        ret = run_mod(mod, filename, globals, locals, flags, arena);
    
    exit:
        Py_XDECREF(filename);
        if (arena != NULL)
    		//释放之前申请的内存池
            PyArena_Free(arena)
        return ret;
    }
    
  5. 让我们PyAST_CompileObject()先进入,这个函数在Python/compile.c. 它有两个重要的函数调用PySymtable_BuildObject()和compiler_mod()

    PySymtable_BuildObject()用于生成符号表,它在Python/symtable.c第 251行定义。

    compiler_mod()将 AST 转换为 CFG(控制流图),其中它里面还有一个很关键的函数调用compiler_body()它用来生成字节码,compiler_body有一个如下的代码段。

    for (; i < asdl_seq_LEN(stmts); i++)
            VISIT(c, stmt, (stmt_ty)asdl_seq_GET(stmts, i));
    
     在这里,我们观察到我们遍历 ASDL 语句并调用宏VISIT,然后调用compiler_visit_expr(c, node).
    

    字节码的发射由以下宏处理:

    • ADDOP()
      添加指定的操作码
    • ADDOP_I()
      添加一个带参数的操作码
    • ADDOP_O(struct compiler *c, int op, PyObject *type, PyObject obj)
      根据
      PyObject 序列对象中指定的位置添加具有适当参数的操作码PyObject,但不处理损坏的名称;这用于当您需要对对象(例如全局变量、常量或参数)进行命名查找时使用,这些对象无法进行名称修改并且名称的范围是已知的
    • ADDOP_NAME()
      就像ADDOP_O,但也处理了名称修改;用于基于名称的属性加载或导入
    • ADDOP_JABS()
      创建到基本块的绝对跳转
    • ADDOP_JREL()
      创建到基本块的相对跳转

    几个将发出字节码并命名为 的辅助函数,该函数compiler_xx()在何处提供xx帮助(列表、boolop 等)

  6. 第五步已经将文件对象转为了字节码
    一旦字节码生成,下一步就是由解释器执行程序。回到文件Python/pythonrun.c,然后我们调用该函数PyEval_EvalCode(),它是 to 的包装函数PyEval_EvalCodeEx(),并且是另一个包装函数_PyEval_EvalCodeWithName()

    让我们检查一下文件中定义的框架对象的结构Include/frameobject.h:

    typedef struct _frame {
        PyObject_VAR_HEAD
        struct _frame *f_back;      /* previous frame, or NULL */
        PyCodeObject *f_code;       /* code segment */
        PyObject *f_builtins;       /* builtin symbol table (PyDictObject) */
        PyObject *f_globals;        /* global symbol table (PyDictObject) */
        PyObject *f_locals;         /* local symbol table (any mapping) */
        PyObject **f_valuestack;    /* points after the last local */
        /* Next free slot in f_valuestack.  Frame creation sets to f_valuestack.
           Frame evaluation usually NULLs it, but a frame that yields sets it
           to the current stack top. */
        PyObject **f_stacktop;
        PyObject *f_trace;          /* Trace function */
        char f_trace_lines;         /* Emit per-line trace events? */
        char f_trace_opcodes;       /* Emit per-opcode trace events? */
    
        /* Borrowed reference to a generator, or NULL */
        PyObject *f_gen;
    
        int f_lasti;                /* Last instruction if called */
        /* Call PyFrame_GetLineNumber() instead of reading this field
           directly.  As of 2.3 f_lineno is only valid when tracing is
           active (i.e. when f_trace is set).  At other times we use
           PyCode_Addr2Line to calculate the line from the current
           bytecode index. */
        int f_lineno;               /* Current line number */
        int f_iblock;               /* index in f_blockstack */
        char f_executing;           /* whether the frame is still executing */
        PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
        PyObject *f_localsplus[1];  /* locals+stack, dynamically sized */
    } PyFrameObject;
    

在_PyEval_EvalCodeWithName(),它将创建一个框架_PyFrame_New_NoTrack(),并在底部调用该函数PyEval_EvalFrameEx()。

PyEval_EvalFrameEx()然后将调用eval_frame()PyThreadState 上的函数,它只是函数_PyEval_EvalFrameDefault()。这个函数也可以称为Python的虚拟机。

跟踪到函数_PyEval_EvalFrameDefault(),然后我们可以在第 930 行观察到一个无限循环(Cpython版本为3.7.8),然后它将生成操作码。我们可以跟踪它,你会看到它切换到相应的操作码块。

例如,运行 with 的代码a = 100将首先使用LOAD_CONST,然后LOAD_NAME,然后依此类推,我们可以用 观察python -m dis test.py。

总结

Python 程序 由 解释器 python 命令执行, Python 解释器中包含一个 编译器 和一个 虚拟机 。 Python 解释器执行 Python 程序时,分为以下两步:

  1. 编译器 将 .py 文件中的 Python 源码编译成 字节码 ;
  2. 虚拟机 逐行执行编译器生成的 字节码 ;

Python 源码的编译结果是代码对象 PyCodeObject ,对象中保存了 字节码 、 常量 以及 名字 等信息,代码对象与源码作用域一一对应。 Python 将编译生成的代码对象保存在 .pyc 文件中,以避免不必要的重复编译,提高效率。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值