目录
说明:如果没有特殊说明,均基于window平台讨论
1.python代码运行时的入口
- 入口文件为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入口出的区别
- 从上面的之前的分析可以看到window与Linux的区别在于,数组argv,Linux平台argv中的元素为char*类型,window为wchar_t *。
- 如果忘记了char与wchar_t类型的区别可以去查阅相关资料复习一下,这里只做一些简单的回顾,wchar_t的宽度属于编译器的特性,且可以小到8位。所以程序若需要跨过所有C和C++ 编译器的可携性,就不应使用wchar_t存储Unicode文字。wchar_t类型是为存储编译器定义的宽字符,在部分编译器中,其可以是Unicode字符。
3.继续前进,生成_Py_Main对象,并做简单初始化
- 源文件为main.c
- _Py_Main主要用于将数据传递给子函数。
- 代码如下图,可以看到这个过程主要是_Py_Main的初始化,主要是use_bytes_argv、argc、argv的初始化。其中的printf的内容是我自己调试时候打印的。
4.继续前进,我们来到了pymain_mian
-
源文件位于main.c
-
源码如下
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; }
-
可以看到,pymain_main函数主要做的事情为初始化python的核心配置即pymain_init,以及真正将python的源代码run起来的pymain_run_python函数。
5.pymain_init都干了些什么事情呢
- 代码如下,我们在后面着重介绍一下运行时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;
}
-
可以看到首先是定义了一个类型为_PyCoreConfig的local_config对象,接着定义了一个类型为_PyCoreConfig *的config指针,接着初始化了_disable_importlib与install_signal_handlers的值,接着config进入了_PyCoreConfig_GetGlobalConfig中,这个函数主要是对需要忽略的环境和字符集做一些初始化,具体可以转到定义查看。
-
接着是pymain_cmdline函数,它的主要作用是将配置读入_PyCoreConfig 和_PyMain,初始化LC_CTYPE 语言环境和 Py_DecodeLocale(),主要有配置内容有命令行参数,全局环境变量,以及Py_xxx 全局配置变量。
-
在pymain_cmdline中比较重要的是还会设置内存分配器,截取部分代码如下图,其中PyMem_SetAllocator的定义位于obmalloc.c文件中
-
还有一个_Py_InitializeCore函数,用来初始化运行时python解释器的核心,它的定义位于pylifestyle.c文件中。
-
语句 PyInterpreterState *interp;可以去查看一下PyInterpreterState结构体的定义。
-
其中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
-
最重要的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看看
-
我们可以在main.c中看到该函数的定义,如下,其中的printf为我调试时候所打印
-
从上图中我们可以看到python真正执行的时候分为三种大的模式,为什么说是三种大的模式呢,因为其实在pymain_run_filename这种模式下是包括了命令行的交互式环境和我们python test.py这两种模式的,我们可以来验证一下,将上图加了调试代码后重新编译一下Cpython后来实验一下,实验结果如下:
7.重点分析一下pymain_run_filename,既通过交互式环境或者文件执行代码的过程
-
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);
-
接着我们来到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; }
-
接着我们去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); }
-
重点看看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; }
-
让我们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 等)
- ADDOP()
-
第五步已经将文件对象转为了字节码
一旦字节码生成,下一步就是由解释器执行程序。回到文件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 程序时,分为以下两步:
- 编译器 将 .py 文件中的 Python 源码编译成 字节码 ;
- 虚拟机 逐行执行编译器生成的 字节码 ;
Python 源码的编译结果是代码对象 PyCodeObject ,对象中保存了 字节码 、 常量 以及 名字 等信息,代码对象与源码作用域一一对应。 Python 将编译生成的代码对象保存在 .pyc 文件中,以避免不必要的重复编译,提高效率。