6404 字
32 分钟
CPython实现原理的简单分析

1. 前言#

Python 作为一种功能强大、简洁易学的编程语言,在人工智能领域发挥着至关重要的作用。 当我们讨论Python时,首先需要明了的是Python只是一个接口。有一个关于Python应该做什么以及怎么做的具体说明(就像其他任何接口一样),并且对应的有很多具体的实现(也像其他接口一样)。 目前主流的Python实现有:CPython(基于C),JPython(基于Java),IronPython(基于 .NET),RubyPython等等。 这么多不同语言的实现的意义是什么?:可以更好的和其他语言glue起来,比如JPython:

[Java HotSpot(TM) 64-Bit Server VM (Apple Inc.)] on java1.6.0_51
>>> from java.util import HashSet
>>> s = HashSet(5)
>>> s.add("Foo")
>>> s.add("Bar")
>>> s
[Foo, Bar]
NOTE

本文基于CPython版本3.12.4

python
/
cpython
Waiting for api.github.com...
00K
0K
0K
Waiting...

2. CPython的架构#

2.1 代码的流动过程#

你的代码从遇到到执行,其中经历了什么?python代码的执行需要经过一下流程: Python源码 -> Scanner -> Parser -> Compiler -> Code Evaluator

2.2 语法规范#

我们从语法开始分析一门语言。CPython 3.9起使用PEG(Parsing Expression Grammar)进行语法/词法分析。 Python的语法被定义在了Grammar/python.gram文件中,而token则被定义在了Grammar/Tokens中,举个例子:

LPAR '('
RPAR ')'
LSQB '['
RSQB ']'
LBRACE '{'
RBRACE '}'

事实上,你可以修改一下这个文件,创建一个属于你自己的Python版本(比如说()⬅这个作为list,[]⬅这个作为tuple) 然后重新生成peg:

Terminal window
regen-pegen:
@$(MKDIR_P) $(srcdir)/Parser
@$(MKDIR_P) $(srcdir)/Parser/tokenizer
@$(MKDIR_P) $(srcdir)/Parser/lexer
PYTHONPATH=$(srcdir)/Tools/peg_generator $(PYTHON_FOR_REGEN) -m pegen -q c \
$(srcdir)/Grammar/python.gram \
$(srcdir)/Grammar/Tokens \
-o $(srcdir)/Parser/parser.c.new
$(UPDATE_FILE) $(srcdir)/Parser/parser.c $(srcdir)/Parser/parser.c.new

2.3 输入Reader#

现在你已经了解了Python的语法,是时候探索一下代码是如何进入可执行状态的了。 在CPython 中运行Python代码的方式有很多,下面是一些最常用的方法:

  1. 通过python -c命令加一个Python字符串来运行
  2. 通过python -m命令加一个模块名称来运行一个模块
  3. 通过python <file>运行,其中<file>为文件的具体路径,并且文件中包含Python代码
  4. 通过标准输入将 Python 代码通过管道传输到可执行文件中,例如cat <file> | python
  5. 通过启动交互式解释器(REPL)并一次执行一个命令
  6. 使用C API,并用Python作为嵌入式环境

为了执行python代码,解释器需要三个部分:

  • 一个要执行的模块;
  • 用于保存变量等信息的状态;<------环境变量,运行参数等等
  • 配置项,例如开启了哪些选项。<-----编译选项

有了这三个组件,解释器就可以执行代码并且输出: 图片 注意,python执行的总是模块,任何代码在被执行之前,必须从输入编译成一个模块。 正如之前所讨论的那样,输入的类型可以有多种:

  • 本地的文件和包;
  • 输入/输出流,如:标准输入或内存管道;
  • 字符串。

2.3.1 读取文件和输入#

一旦CPython有了运行时配置和命令行参数,它就可以加载它所需要执行的代码,这个任务由Modules/main.c文件中的pymain_main()函数来完成。 命令行输入字符串 CPython 可以通过指定-c选项,从而通过命令行模式来执行一个小的Python应用,比如:

Terminal window
$ ./python -c "print(2 ** 2)"
4

首先,pymain_run_command()函数在Modules/main.c文件中被执行,其将命令行中通过-c传入的命令行作为一个wchar_t*类型的参数。完成此操作后,pymain_run_command()将会把Python字节对象传递给 PyRun_SimpleStringFlags()用于执行,它将一个字符串转换成Python模块,然后把它送去执行。

2.3.2 本地模块输入#

执行 Python 命令的另一种方式是通过-m选项与模块名一起使用. CPython导入一个标准库模块runpy并通过PyObject_Call()来执行它,导入的过程由一个名为PyImport_ImportModule()的C API函数完成。 为了运行目标模块,runpy 做了以下三件事:

  • 为你指定的模块名调用import()方法;
  • 将 name(模块名称)设置到名为main的命名空间;
  • main命名空间中执行模块。

2.3.3 来自脚本文件或标准输入的输入#

如果python执行时的第一个参数是一个文件名,例如python test.py,CPython将会打开一个文件句柄并将句柄传递给PyRun_SimpleFileExFlags(),这个方法可以处理三种类型的文件路径:

  1. 如果文件是.pyc文件,那么它将会调用run_pyc_file()
  2. 如果文件是脚本文件.py,那么它将会调用PyRun_FileExFlags()
  3. 如果文件是stdin,如:用户执行了<command> | python,那么它会将stdin作为一个文件句柄并调用PyRun_FileExFlags()

2.4 解析Parser#

在上部分中,我们知道了Python如何从各类来源中读取文本。接下来需要将其转换为编译器可以使用的结构,这个过程被称为解析(parsing)。 图片 CPython使用具象语法树(CST)和抽象语法树(AST)两级结构来分析代码。 图片 具体地说,解析过程分为两部分:

  1. 使用parser-tokenizer创建具象语法树
  2. 通过具象语法树创建出抽象语法树 其中parser为2.2中我们生成的Parser/parser.ctokenizerParser/tokenizer.cparser-tokenizerParser/parsetok.c

2.4.1 CST#

parser-tokenizer接收文本输入并循环执行分词器和解析器,直到光标到达文本末尾才结束(或者出现了一个语法错误)。 在执行前,parser-tokenizer会先创建出一个tok_state实例,分词器会将所有的状态存放到这个临时的数据结构中。分词器状态包括当前光标位置、行等信息。 parser-tokenizer通过调用tok_get()获取到下一个token。tokenizer将token ID传递给parser,parser使用parser-tokenizer生成的 DFA 在具象语法树上创建节点。 图片 举个例子,比如:将算术表达式 “a + 1” 转换成具象语法树,如下图所示: 图片

2.4.2 AST#

下一个阶段是将parser-tokenizer生成的具象语法树转换为能被执行且抽象层次更高的东西。 具象语法树是代码文件中文本的字面表示方式。Python的基本语法结构已经被解释过了,但你无法使用具象语法树来建立函数、作用域或者任何Python语言核心特性。 在代码被编译前,具象语法树需要转换为能表示 Python 实际结构的更高层次的结构。这个能表示具象语法树的结构被称为:抽象语法树(AST)。 比如:在AST中的二元运算操作被称为BinOp并被定义为一种表达式类型。其由三部分构成:

  1. left:运算的左侧部分;
  2. op:运算符,如:+、-、*;
  3. right:运算的右侧部分。

a+1AST可以这样表示: 图片 与AST相关的文件有:

  1. Python/ast.c抽象语法树的实现
  2. Parser/Python.asdl在领域特定语言ASDL 5中的抽象语法树节点类型和属性列表集合
  3. Include/Python-ast.h抽象语法树节点类型声明,由Parser/asdl_c.py文件生成

最终,Parser的输出类型是一个能表示Python模块的mod_ty。 同样的,在这部分我们关心的是执行流程,关于其中的实现细节会放在第三部分中。

2.5 编译Compiler#

完成Parser后,解释器拥有了一棵抽象语法树,其包含Python代码的操作、函数、类以及命名空间。 接下来,编译器的任务是将抽象语法树转换为CPU能理解的指令。 有关的文件:Python/compile.cPython/compile.h 此编译任务会被分成两个组件: 编译器:遍历抽象语法树并创建一个控制流图(CFG),此图表示执行的逻辑顺序; 汇编器:将 CFG 中的节点转换为能按顺序执行的语句,即字节码(bytecode)。 图片 经过Assembler之后,大伙平时(可能)见到过的code object就出现了 图片

首先,我们要先理解几个概念: 整个包装类(容器)被称为编译器状态,其中包含一个符号表; 符号表包含了许多变量名,并且可以选择包含子符号表; 编译器类型包含许多编译器单元; 每个编译器单元可以包含许多名称、变量名、常量以及 cell 变量; 编译器单元还包含许多基础帧块; 基础帧块包含许多指令。 图片 在编译器启动前,会创建出一个全局编译器状态。编译器状态中包含了编译器使用的属性,比如编译器标志、嵌套等级、优化等级、栈、指向__future__模块的指针以及PyArena*(内存分配区域的指针)。它还包括了到其他数据结构的链接,如符号表。 这里不讲__future__模块,因为在座的应该没人在用python2.x了

指令#

指令instr具有如下字段:

AttributeTypeDescription
i_jabsunsigned指定此跳转为绝对跳转的标志
i_jrelunsigned指定此跳转为相对跳转指令的标志
i_linenoint创建此指令的行号
i_opcodeunsigned char此指令表示的操作码编号(参见Include/Opcode.h)
i_opargint操作码参数
i_targetbasicblock*i_jrel 为 true 时指向目标 basicblock 的指针

基础帧块#

Python/flowgraph.c 基础帧块basicblock包含以下字段:

AttributeTypeDescription
b_iallocint指令数组长度(b_instr)
b_instrinstr *指向指令数组的指针
b_iusedint使用的指令数(b_instr)
b_listbasicblock *此编译单元中的 block 列表(倒序)
b_nextbasicblock*指向正常控制流到达的下一个 block 的指针
b_offsetint块的指令偏移量,由assemble_jump_offsets()计算得到
b_returnunsigned如果插入了RETURN_VALUE操作码,则为true
b_seenunsigned用于执行基础块的深度优先搜索
b_startdepthint进入块时的堆栈深度,由stackdepth()计算得到

编译单元#

每个代码块为一个编译单元,当前在操作哪一个unitcompiler_enter_scope()compiler_exit_scope()进行管理。

/* The following items change on entry and exit of code blocks.
They must be saved and restored when returning to a block.
*/
struct compiler_unit {···};

2.5.1 创建符号表#

在编译代码之前,complier会创建出一个符号表(symbol table)。 符号表的目的是提供命名空间、全局和局部变量的列表,供编译器用于引用和解析作用域。 与上一章所讲到的 AST 编译类似,PySymtable_BuildObject()mod_ty(也就是AST)的可能类型(Module、Interactive、Expression 和 FunctionType)之间切换并访问其中的每个语句,然后递归地探索 AST的节点和分支,并将条目添加到symtable中。 准备好符号表之后,就开始了正式的编译过程。

2.5.2 核心编译过程#

现在的PyAST_CompileObject()具有编译器状态、symtable 和 AST 形式的模块,实际的编译过程可以开始了。核心编译器有两个目的:

  • 将状态、symtableAST转换成控制流图(CFG);
  • 通过捕获逻辑或代码错误来保护执行阶段免受运行时异常的影响。

AST 模块编译的入口点是compiler_mod()函数,此函数根据模块类型切换到不同的编译器函数,然后循环遍历访问模块中的每个语句。 语句类型是通过调用asdl_seq_GET()确定的, 它会查看AST节点类型,然后调用VISIT宏为每个语句类型调用相应的函数: 小插曲:在这里,编译器用宏做了一个防止表达式作为左值的检查

#define asdl_seq_GET(S, I) _Py_RVALUE((S)->typed_elements[(I)])
// Prevent using an expression as a l-value.
// For example, "int x; _Py_RVALUE(x) = 1;" fails with a compiler error.
#define _Py_RVALUE(EXPR) ((void)0, (EXPR))
#define VISIT(C, TYPE, V) \
RETURN_IF_ERROR(compiler_visit_ ## TYPE((C), (V)));

比如说,如果当前处理的是一个语句,而语句是for类型,那么compiler_visit_stmt()会调用compile_for()。所有语句和表达式类型都有一个等效的compiler_*()函数。更直接的类型会创建内联字节码指令,而一些更复杂的语句类型会调用其他函数。 简单的说,这里发生了什么呢?compiler_mod()将AST中的节点转换为指令序列,填充到各自的基础帧块中,并将这些基础帧块连接起来,形成CFG(并进行优化)。

2.5.3 汇编过程#

一旦这些编译阶段完成,编译器就会拥有帧块列表,每个帧块都包含指令列表和指向下一个块的指针。汇编器(assembler)对基础帧块执行深度优先搜索(DFS),并将指令合并为单字节码序列。 汇编器 API 有一个入口点:assemble(),它具有以下职责:

  • 计算内存分配的块数;
  • 确保每个边界情况的块都会返回 None;
  • 解析任何被标记为相对的跳转语句偏移;
  • 调用dfs()对块执行深度优先搜索;
  • 向编译器发送所有指令;
  • 把编译器状态作为入参调用makecode()以生成PyCodeObject

2.5.4 创建Code Object#

makecode()的任务是检查编译器状态和汇编器的一些属性,并通过调用PyCode_New()将这些属性放入PyCodeObject中。变量名和常量会作为属性放到Code Object中。 字节码被送到PyCode_NewWithPosOnlyArgs()之前会被送到PyCode_Optimize()。此函数是Python/peephole.c中字节码优化过程的一部分。 窥孔(peephole)优化器检查字节码指令,并在某些情况下用其他指令替换它们。例如,有一个优化器,它可以删除return语句之后的任何无法访问的指令。

2.6 执行Execution#

到这里为止,我们已经了解了如何将Python代码解析为抽象语法树并将其编译成Code Object,这些Code Object包含了以字节码形式表示且相互独立的一系列操作。但要执行Code Object还缺少一项关键的内容,它们需要输入。在Python中,输入可能以局部变量或全局变量的形式出现。在本节中,我们将接触到一个名为值栈(value stack)的概念。在Code Object中,字节码操作会在值栈创建、修改并使用变量。 CPython 中执行代码的动作发生在一个核心循环中,这个循环又被称为求值循环 (evaluation loop)。CPython解释器将在该循环中解析并执行由序列化的.pyc文件或由编译器得到的Code Object: 以下是与求值循环相关的源文件:

  • Python/ceval.c 实现求值循环的核心代码
  • Python/ceval-gil.h GIL 的定义和控制算法 在这个过程中:
  • 求值循环将会获取一个Code Object并将其转换为一系列的Frame Object
  • 解释器至少需要一个线程
  • 每个线程都有自己的线程状态(Thread State)
  • Frame Object在被称为帧栈(Frame Stack)的栈中执行
  • 变量在值栈中被引用

2.6.1 构建线程状态#

需要先将帧(Frame)绑定到一个线程上,它才能够被执行。CPython可以在单个解释器中同时运行多个线程。解释器拥有解释器状态(Interpreter State),其中保存了由这些线程状态组成的链表。 CPython需要至少包含一个线程,而每个线程都有它自己的线程状态。 线程状态(PyThreadState)包含了30多个属性,主要包括:

  • 线程的唯一标识符;
  • 指向其他线程状态的链表;
  • 该线程状态由哪一个解释器状态生成;
  • 当前正在执行的帧;
  • 当前递归的深度;
  • 可选的追踪函数;
  • 当前正在处理的异常;
  • 当前正在处理的任意异步异常;
  • 产生多个异常时抛出的异常栈(例如在 except 代码块中触发的异常);
  • GIL 计数器;
  • 异步生成器计数器。

2.6.2 构建帧对象#

编译后的Code Object会被添加到Frame Object中。由于Frame Object是一种Python类型,因此它可以被C或Python引用。执行Code Object中的指令时还需要其他运行时数据,这些数据也包含在Frame Object中,例如局部变量、全局变量和内置模块。 Frame Object的类型是PyObject,它包含了以下属性:

AttributeTypeDescription
f_backPyFrameObject *指向栈中前一个帧的指针,如果是第一帧则值为 NULL
f_codePyCodeObject *需要执行的 code object
f_builtinsPyObject * (dict)内置(builtin)模块的符号表
f_globalsPyObject * (dict)全局变量的符号表(PyDictObject)
f_localsPyObject * (dict)局部变量的符号表
f_valuestackPyObject **指向最后一个局部变量的指针
f_stacktopPyObject **指向 f_valuestack 中下一个空闲的插槽(slot)
f_tracePyObject *指向自定义追踪函数的指针
f_trace_lineschar切换自定义追踪函数在行号级别进行追踪
f_trace_opcodeschar切换自定义追踪函数在操作码级别进行追踪
f_genPybject *借用的生成器引用或 NULL
f_lastiint上一条执行的指令
f_linenoint当前行号
f_iblockint当前帧在 f_blockstack 中的索引
f_executingchar标记帧是否仍在执行
f_blockstackPyTryBlock[]保存 for 块,try 块 和 loop 块的序列
f_localsplusPyObject *[]局部变量和栈的联合

图片

2.6.3 帧的执行#

如前面的编译器和AST部分所述,Code Object除了包含要执行的字节码的二进制编码,还包含了变量列表和符号表。Python中局部和全局变量的值在运行时才会确定,这些值由运行时函数、模块及代码块的调用方式决定。通过函数_PyEval_EvalCode()可以将这些变量的值添加到Frame Object中。除此之外,Frame Object还有一些其他的应用方式,例如协程装饰器会动态的生成一个以目标对象为变量的Frame ObjectPyEval_EvalFrameEx()是一个公共的API,它会调用解释器在eval_frame属性中配置的帧计算函数。基于PEP 523,在Python 3.7实现了帧计算的可插拔性(提供了C API,允许使用第三方代码自定义帧的计算函数)。 _PyEval_EvalFrameDefault()是CPython唯一自带的帧计算默认函数。这个函数是执行帧的关键,它将所有的东西组合到一起,让代码可以运行起来。同时这个函数经历了数十年的持续优化,因为即便是修改一行代码也会对CPython的性能产生巨大的影响。在CPython中,执行任何代码最终都要经过这个帧的求值函数。

2.6.4 值栈#

求值循环将从值栈获取输入从而真正的工作起来。 在核心的求值循环中会创建一个值栈。这个栈包含了一系列指向PyObject实例的指针,这些实例可以是变量、函数的引用(在Pythons中也是对象)或其他类型的Python对象。 求值循环中的字节码指令将从值栈中获取输入。 Python/generated_cases.c.h下:

TARGET(BINARY_OP_ADD_INT) {
frame->instr_ptr = next_instr;
next_instr += 2;
INSTRUCTION_STATS(BINARY_OP_ADD_INT);
static_assert(INLINE_CACHE_ENTRIES_BINARY_OP == 1, "incorrect cache size");
PyObject *right;
PyObject *left;
PyObject *res;
// _GUARD_BOTH_INT
right = stack_pointer[-1];
left = stack_pointer[-2];
{
DEOPT_IF(!PyLong_CheckExact(left), BINARY_OP);
DEOPT_IF(!PyLong_CheckExact(right), BINARY_OP);
}
/* Skip 1 cache entry */
// _BINARY_OP_ADD_INT
{
STAT_INC(BINARY_OP, hit);
res = _PyLong_Add((PyLongObject *)left, (PyLongObject *)right);
_Py_DECREF_SPECIALIZED(right, (destructor)PyObject_Free);
_Py_DECREF_SPECIALIZED(left, (destructor)PyObject_Free);
if (res == NULL) goto pop_2_error;
}
stack_pointer[-2] = res;
stack_pointer += -1;
DISPATCH();
}

比如说:

if left or right:
pass

编译器会把操作符or编译成BINARY_OR指令:

static int
binop(struct compiler *c, operator_ty op)
{
switch (op) {
case Add:
return BINARY_ADD;
...
case BitOr:
return BINARY_OR;
...

在求值循环中,BINARY_OR将从值栈中获取两个值作为左右操作数(left 和 right),随后以这两个对象作为参数调用函数PyNumber_Or():

...
case TARGET(BINARY_OR): {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *res = PyNumber_Or(left, right);
Py_DECREF(left);
Py_DECREF(right);
SET_TOP(res);
if (res == NULL)
goto error;
DISPATCH();
}

最后,将求得的结果res放在栈的顶部,覆写了当前栈顶的值。 每一个操作码都有预定义的栈操作,可以由Python/compile.c中的函数 stack_effect()计算得到。这个函数会返回操作码执行后的值栈中元素数目的增量。 这个增量可能是正值、负值或 0。在执行操作码时,若stack_effect()返回值(例如 +1)与值栈中的增量不匹配,就会抛出一个异常。 举个字节码中使用值栈例子:

import dis
g = 11111
def outer():
c = 22222
def inner():
l = 33333
print(g, c, l)
dis.dis(inner)
outer()

输出:

0 COPY_FREE_VARS 1
7 2 RESUME 0
8 4 LOAD_CONST 1 (33333)
6 STORE_FAST 0 (l)
9 8 LOAD_GLOBAL 1 (NULL + print)
18 LOAD_GLOBAL 2 (g)
28 LOAD_DEREF 1 (c)
30 LOAD_FAST 0 (l)
32 CALL 3
40 POP_TOP
42 RETURN_CONST 0 (None)

18,28,30部分就是将变量加载到值栈当中。

2.6.5 总结#

求值循环是编译后的Python代码和底层 C 的扩展模块、标准库及系统调用之间的接口。虽然CPython解释器有一个核心求值循环,但其实你可以同时执行多个循环,不管它们是并发的还是并行的。CPython可以有多个求值循环去同时执行系统上的多个帧。使用帧栈系统让CPython运行在多核或多个 CPU 上。除此之外,CPython的Frame Object API允许帧以异步编程的方式暂停或恢复执行。

2.7 内存管理#

使用值栈加载的变量还需要内存分配和管理。要让CPython高效地执行,必须要有可靠的内存管理机制。 由于CPython是基于 C 构建的,它也受到 C 中静态内存分配、自动内存分配和动态内存分配的约束。而 Python 语言的一些特性设计使得我们面临更多的挑战:

  • Python是一门动态类型的语言,变量的大小不能在编译阶段得到
  • 大多数Python的核心类型大小都是动态的,例如list类型可以是任意长度,dict类型可以有任意数量的键,甚至连int大小也不是固定的,用户不需要指定这些类型的大小
  • Python内的变量名可以复用于任意类型 为了解决这些问题,CPython十分依赖动态内存分配,同时借助垃圾回收(GC)和引用计数算法去保证分配的内存可以自动释放。 Python 对象的内存是通过一个统一API自动分配得到的,并不需要Python开发者自己去分配内存。这种设计也意味着CPython的标准库和核心模块都要使用该API去分配内存。 CPython 中使用了两种内存分配器:
  1. 操作系统层面的内存分配器malloc,主要用于原始内存作用域;
  2. CPython层面的内存分配器pymalloc,主要用于PyMem和对象内存作用域。 CPython的内存分配器位于系统的内存分配器之上,并拥有它自己的分配算法。该算法与系统内存分配器类似,不同之处在于它是为CPython定制的:
  • 大部分需要分配的内存都是小块且大小固定的内存,因为PyObject占16字节,PyASCIIObject占42字节,PyCompactUnicodeObject占72字节,PyLongObject占32字节;
  • pymalloc内存分配器最多只能分配256KB大小的内存,更大的内存需要交给系统的内存分配器去处理;
  • pymalloc内存分配器使用GIL而不是系统的线程安全性检查。 内存池算法有以下几种优点:
  • 这种算法在CPython的主要应用场景(小内存且生命周期较短的对象)下有更好的性能;
  • 这种算法使用了GIL而不是系统的线程锁检测;
  • 算法使用内存映射mmap()而不是堆上内存分配。

👀关于引用计数的实现 Python对象的基对象定义位于Include/object.h

/* Nothing is actually declared to be a PyObject, but every pointer to
* a Python object can be cast to a PyObject*. This is inheritance built
* by hand. Similarly every pointer to a variable-size Python object can,
* in addition, be cast to PyVarObject*.
*/
#ifndef Py_GIL_DISABLED
struct _object {
#if (defined(__GNUC__) || defined(__clang__)) \
&& !(defined __STDC_VERSION__ && __STDC_VERSION__ >= 201112L)
// On C99 and older, anonymous union is a GCC and clang extension
__extension__
#endif
#ifdef _MSC_VER
// Ignore MSC warning C4201: "nonstandard extension used:
// nameless struct/union"
__pragma(warning(push))
__pragma(warning(disable: 4201))
#endif
union {
Py_ssize_t ob_refcnt;
#if SIZEOF_VOID_P > 4
PY_UINT32_T ob_refcnt_split[2];
#endif
};
#ifdef _MSC_VER
__pragma(warning(pop))
#endif
PyTypeObject *ob_type;
};
#else
// Objects that are not owned by any thread use a thread id (tid) of zero.
// This includes both immortal objects and objects whose reference count
// fields have been merged.
#define _Py_UNOWNED_TID 0
// NOTE: In non-free-threaded builds, `struct _PyMutex` is defined in
// pycore_lock.h. See pycore_lock.h for more details.
struct _PyMutex { uint8_t v; };
struct _object {
// ob_tid stores the thread id (or zero). It is also used by the GC and the
// trashcan mechanism as a linked list pointer and by the GC to store the
// computed "gc_refs" refcount.
uintptr_t ob_tid;
uint16_t _padding;
struct _PyMutex ob_mutex; // per-object lock
uint8_t ob_gc_bits; // gc-related state
uint32_t ob_ref_local; // local reference count
Py_ssize_t ob_ref_shared; // shared (atomic) reference count
PyTypeObject *ob_type;
};
#endif

引用计数器ob_refcnt被放在基类之中

static void
pymain_run_python(int *exitcode)
{
PyObject *main_importer_path = NULL;
PyInterpreterState *interp = _PyInterpreterState_GET();
/* pymain_run_stdin() modify the config */
PyConfig *config = (PyConfig*)_PyInterpreterState_GetConfig(interp);
/* ensure path config is written into global variables */
if (_PyStatus_EXCEPTION(_PyPathConfig_UpdateGlobal(config))) {
goto error;
}
······
// import readline and rlcompleter before script dir is added to sys.path
pymain_import_readline(config);
PyObject *path0 = NULL;
if (main_importer_path != NULL) {
path0 = Py_NewRef(main_importer_path);
}
else if (!config->safe_path) {
int res = _PyPathConfig_ComputeSysPath0(&config->argv, &path0);
if (res < 0) {
goto error;
}
else if (res == 0) {
Py_CLEAR(path0);
}
}
// XXX Apply config->sys_path_0 in init_interp_main(). We have
// to be sure to get readline/rlcompleter imported at the correct time.
if (path0 != NULL) {
wchar_t *wstr = PyUnicode_AsWideCharString(path0, NULL);
if (wstr == NULL) {
Py_DECREF(path0);
goto error;
}
config->sys_path_0 = _PyMem_RawWcsdup(wstr);
PyMem_Free(wstr);
if (config->sys_path_0 == NULL) {
Py_DECREF(path0);
goto error;
}
int res = pymain_sys_path_add_path0(interp, path0);
Py_DECREF(path0);
if (res < 0) {
goto error;
}
}
······
pymain_repl(config, exitcode);
goto done;
error:
*exitcode = pymain_exit_err_print();
done:
_PyInterpreterState_SetNotRunningMain(interp);
Py_XDECREF(main_importer_path);
}

创建新对象时,使用Py_NewRef记录对象的新引用。离开作用域时,使用Py_DECREF释放对象的引用。这两个过程存在于所有的Python C API中。

static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op)
{
// Non-limited C API and limited C API for Python 3.9 and older access
// directly PyObject.ob_refcnt.
if (_Py_IsImmortal(op)) {
return;
}
_Py_DECREF_STAT_INC();
if (--op->ob_refcnt == 0) {
_Py_Dealloc(op);
}
}
#define Py_DECREF(op) Py_DECREF(_PyObject_CAST(op))

3. Reference#

Cpython-Internals

CPython实现原理的简单分析
https://blog.lesia.top/posts/a-brief-analysis-of-cpythons-implementation-principles/
作者
AcgZone
发布于
2024-10-13
许可协议
CC BY-NC-SA 4.0