当你面对一大堆代码无从下手的时候,先找找有没有main()函数。 ——沃·兹基·硕德
前排提醒:建议在打开代码库的状态下阅读本文。
这里先来介绍一下PEP 587。这个PEP主要规范化了整个Python初始化的过程,加入了PyConfig,PyStatus等结构,以及一些配套的函数。这个PEP在3.8被实装,所以以前的版本里应该没有那些东西。
Python启动之后,第一件事情就是初始化。Python的各个小系统也会在这时初始化。这也是我选择从这里开始的原因之一。
PyStatus大致可根据其_type分成以下几类:OK,EXCEPTION,EXIT。相关的struct定义卸载了Include/cpython/initconfig.h里,而相关的宏定义写在了Include/internel/pycore_initconfig.h里。
先说EXIT。这个状态只产生于Python/initconfig.c/config_parse_cmdline()这个函数。这个函数负责对python的命令行参数(argv)进行解析。其中会引发EXIT的情况有:--check-hash-based-pycs后的参数不是default,always,和never中的任何一个,返回值2。
当读到-h和-?时,在stdout输出帮助信息并退出,返回值0。
遇到不认识的参数时,在stderr输出帮助信息并退出,返回值2。
当读到--version或-V时,输出版本信息并退出,返回值0。
这一步发生在初始化的后期。
之后就是EXCEPTION。整个初始化过程中遇到别的错误一般都会返回EXCEPTION,比如内存分配失败等。
从pymain_init开始往下,先初始化Runtime。
先配置默认的内存管理界面Interface。Python有自己的内存管理机制,也有不同的Interface去管理内存分配。直接使用malloc()和free()这类函数并不利于内存的管理,因此CPython在实现时也抽象出了不同的Interface在不同的层面进行内存的分配管理。具体之后会讲。
然后在_PyRuntimeState_Init_impl中进行GC的初始化,设置递归深度(默认1000),初始化GIL锁,Locale设置和解释器线程以及管理跨线程数据的线程(主要是给他们申请锁)。
随后进行的是preconfig的初始化。这个东西主要记录了allocator、Locale和编码(是否开启UTF-8)。相关信息会从环境变量、程序参数等处读取。
补充说明一下关于allocator的问题。在对Python进行debug时,会需要通过类似valgrind这样的工具来检查内存泄漏。如果直接使用Python的自己的内存分配器会很难发现这些泄漏的情况。具体可以参考Misc/README.valgrind。
然后用preconfig和argv去初始化config。这边东西很杂,建议看这里的解释。(大概是他们也忍不了这么多这么杂的东西了,选择包装一下了)
用最后这份初始化完的config去初始化Python核心,并且把config中不再需要的项清理掉,整个初始化过程就结束了。
如果整个初始化过程没有遇到什么问题,Python就会进入Py_RunMain并真正开始运行。
简短的说一下Finalize。收尾工作主要靠Py_FinalizeEx()来进行。这边会尽可能清除掉所有的已分配内存并让程序回到初始化前的状态。对于CPython自己来说这个函数只会被调用一次,但对在自己的程序里使用Python的情况下,这个函数(以及Py_Initialize())如果被调用多次,可能会发生一些内存泄露。
如果想在自己的程序里使用Python Runtime的话,这篇文章主要对应的部分是这篇文档中关于Initialize和Finalize的部分。
如果没有什么意外的话,下个星期会讲关于语法处理的部分(Tokenize、Parser和Python的语法加速器)。
啊第一次写这种文章可能有些抓不住重点……以后会好起来的嗯。