要真正搞清"一条 Python 语句在 C 扩展里到底怎么跑",最好把源码、调用栈、GIL 释放点、网络收发点都"跟"一遍。下面以
MySQLdb.connection.query("SELECT ...")
为例,把从 Python 层到 C 层再到 libmysqlclient 的完整路径拆给你看。
(代码行号基于 mysqlclient-python 2.2.x,MySQL-Connector/C 8.0.x,CPython 3.11)
一、Python 层:MySQLdb/init.py
python
import _mysql # 这是 C 扩展模块
...
connect = _mysql.connect
Connection = _mysql.connection
MySQLdb 只是对 _mysql 做了一层薄薄的包装,真正的类叫 _mysql.connection,所以
MySQLdb.connection.query 其实就是
_mysql.connection.query。
二、C 扩展层:_mysql.c
模块初始化时把 connection 类型注册到 Python:
c
static PyTypeObject MyConnection_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "_mysql.connection",
.tp_methods = _mysql_ConnectionObject_methods,
...
};
.tp_methods 里有一项:
c
{"query", (PyCFunction)_mysql_ConnectionObject_query, METH_VARARGS,
PyDoc_STR("query(sql) -> None")},
因此 Python 虚拟机在执行
LOAD_ATTR + CALL_METHOD 指令时,通过 PyType 的 method resolution 直接找到
_mysql_ConnectionObject_query。
三、C 函数内部:_mysql_ConnectionObject_query
c
static PyObject *
_mysql_ConnectionObject_query(MyConnectionObject *self, PyObject *args)
{
char *sql;
int len;
if (!PyArg_ParseTuple(args, "s#", &sql, &len))
return NULL;
Py_BEGIN_ALLOW_THREADS /* 1. 主动释放 GIL */
int r = mysql_real_query(self->conn, sql, len);
Py_END_ALLOW_THREADS /* 2. 重新拿 GIL */
if (r) {
_mysql_Exception(self); /* 3. 把 mysql_error() 包装成 Python 异常 */
return NULL;
}
Py_RETURN_NONE;
}
关键三步:
- 释放 GIL → 其他 Python 线程可以跑;
- 调 libmysqlclient 的
mysql_real_query(); - 出错就把 C 的
mysql_error()/mysql_errno()转成_mysql.ProgrammingError/DatabaseError等 Python 异常。
四、libmysqlclient 层:mysql_real_query()
libmysqlclient 是 C 语言官方客户端库,内部做了:
- 把 SQL 按 MySQL 协议打成 TCP 包(
net_write_command()) - 阻塞
recv()等待 server 返回结果包 - 如果 SQL 是 SELECT,后续还会再调
mysql_store_result()/mysql_use_result()读结果集。
五、结果集怎么回到 Python?
如果你在 Python 里继续写:
python
cursor = conn.cursor()
cursor.execute("SELECT ...")
cursor.execute 会再调 _mysql.connection.query,然后:
_mysql_ConnectionObject_store_result()把MYSQL_RES *抓出来;- 用
PyList_New()/PyTuple_New()把每一行转成 Python 对象; - 把
MYSQL_FIELD的类型映射成 Python 类型(FIELD_TYPE_LONG→PyLong,FIELD_TYPE_DATETIME→PyDateTime 等)。
这一步在 持有 GIL 的情况下做,因为要把 C 数据喂给 Python 对象。
六、小结:一条语句的"旅程"
Python 字节码
→ LOAD_ATTR "query"
→ CALL_METHOD (进入 C)
→ PyArg_ParseTuple
→ Py_BEGIN_ALLOW_THREADS
→ libmysqlclient:mysql_real_query (阻塞等网络)
→ Py_END_ALLOW_THREADS
→ 出错则 _mysql_Exception
→ 返回 Py_None。
七、动手验证:gdb + ltrace 看现场
-
装 debug 符号
apt install python3-dbg libmysqlclient-dev mysql-client-core-8.0-dbg -
起 gdb
sqlgdb -ex r --args python3 -c " import MySQLdb, time c=MySQLdb.connect(user='root',passwd='***') c.query('SELECT 1') " -
下断点
(gdb) b _mysql_ConnectionObject_query
(gdb) b mysql_real_query -
单步
(gdb) n可以看到Py_BEGIN_ALLOW_THREADS宏展开后调用
PyThread_release_lock();
继续s能跟到net_write_command()→send()/recv()。
八、写个最小 C 扩展 Demo(把上面流程自己跑一遍)
c
// demo.c
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <mysql.h>
typedef struct {
PyObject_HEAD
MYSQL *mysql;
} DemoConn;
static PyObject *
DemoConn_query(DemoConn *self, PyObject *args)
{
char *sql;
Py_ssize_t len;
if (!PyArg_ParseTuple(args, "s#", &sql, &len))
return NULL;
Py_BEGIN_ALLOW_THREADS
int err = mysql_real_query(self->mysql, sql, len);
Py_END_ALLOW_THREADS
if (err) {
PyErr_Format(PyExc_RuntimeError, "mysql: %s", mysql_error(self->mysql));
return NULL;
}
Py_RETURN_NONE;
}
static PyMethodDef DemoConn_methods[] = {
{"query", (PyCFunction)DemoConn_query, METH_VARARGS, ""},
{NULL}
};
static PyTypeObject DemoConnType = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "demo.Connection",
.tp_basicsize = sizeof(DemoConn),
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_methods = DemoConn_methods,
};
static PyModuleDef demomodule = {
PyModuleDef_HEAD_INIT,
.m_name = "demo",
.m_size -1,
};
PyMODINIT_FUNC
PyInit_demo(void)
{
PyObject *m = PyModule_Create(&demomodule);
if (!m) return NULL;
if (PyType_Ready(&DemoConnType) < 0) return NULL;
PyModule_AddObject(m, "Connection", (PyObject*)&DemoConnType);
return m;
}
编译
makefile
python3 -m pip install mysqlclient # 确保有头文件
gcc -shared -fPIC $(python3-config --includes) \
-I/usr/include/mysql -L/usr/lib/x86_64-linux-gnu \
demo.c -lmysqlclient -o demo$(python3-config --extension-suffix)
测试
ini
python3 -c "
import demo, MySQLdb
c = demo.Connection() # 这里省掉了 connect 参数,仅演示
c.query('SELECT 1')
"
用 ltrace -e mysql_real_query python3 test.py 就能抓到库函数调用。
九、随手可记的"脑图"
objectivec
Python 语句
↓ 字节码 CALL_METHOD
C 扩展函数 (PyCFunction)
↓ Py_BEGIN_ALLOW_THREADS
libmysqlclient 阻塞网络
↓ Py_END_ALLOW_THREADS
结果 or 异常 → Python 对象