达梦dmPython安装后,为什么 python -m 失败?一个第三方库导致的 Python 启动链路失败
这个问题我当时绕了很久。
表面现象是:
$ python3 -m pip install xxx
/usr/bin/python3: No module named install
或者:
$ python3 -m pip -V
/usr/bin/python3: No module named -V
看起来像是:
python -m 的参数解析坏了
但真正的问题不是 pip,也不是 -m 本身。
真正的问题是:
dmPython 安装时带了一个 `.pth` 文件,这个 `.pth` 在 Python 解释器启动中途执行了 `os.execve`,把 `-m` 模式下本来就不完整的 `sys.argv` 又拿去重拼了一次。
这篇真正想讲清楚的,不只是“怎么修”。
而是:
• .pth 为什么会影响 python -m
• 解释器为什么会把模块名单独拿走
• 为什么最后只吃掉了一个参数,而不是把后面的全吃掉
一、现场现象:import dmPython 正常,但 python -m pip 坏了
项目背景是信创迁移。
业务系统从 MySQL 迁到达梦,安装 dmPython 后:
• import dmPython 正常
• python3 -m pip 突然报错
也就是说,坏掉的不是 dmPython 本身能不能 import。
坏掉的是:
整个 python -m 启动链路
这就说明问题大概率不在业务模块,而是在:
解释器初始化阶段
二、strace 最先把问题指到了 .pth
先用 strace 跟踪:
$ strace -f python3 -m pip 2>&1 | grep pth
openat(AT_FDCWD, "/site-packages/dmPython.pth", O_RDONLY) = 3
然后看文件内容:
$ cat /site-packages/dmPython.pth
import dmdpi
继续看 dmdpi.py,发现关键逻辑大概是:
import os, sys
if 'LD_LIBRARY_PATH' not in os.environ:
os.environ['LD_LIBRARY_PATH']= dpi_path
ifsys.argv[0] == '':
os.execve(sys.executable,[sys.executable, ...], os.environ)
else:
os.execve(sys.executable,[sys.executable] + sys.argv, os.environ)
到这里,问题其实已经比较清楚了:
1. .pth 会在解释器启动时自动执行
2. 它触发了 import dmdpi
3. dmdpi.py 又在启动中途执行了 os.execve
也就是说,Python 解释器本来正在启动,结果中途被人用一份新的命令行又重新启动了一次。
三、关键原理:python -m 里的模块名,本来就不是普通参数
这里最容易误解。
很多人会下意识觉得:
python3 -m pip install xxx
不就是一串普通参数吗?
其实不是。
这里的 pip 不是“传给程序的普通参数”。
它是:
解释器自己接下来要执行的目标。
你可以把 Python 启动时先要决定的事情理解成:
• 我是执行脚本文件?
• 还是执行 -c 后面的代码?
• 还是执行 -m 后面的模块?
• 还是进入交互式?
所以在:
python3 -m pip install xxx
这条命令里,解释器会先把它拆成三部分:
• 解释器选项:-m
• 执行目标:pip
• 传给 pip 的参数:install xxx
这就是为什么解释器必须先把 pip 单独解析出来。
因为如果不先拿出来,解释器后面根本不知道:
到底该运行哪个模块
也不知道之后的 install xxx 应该交给谁。
所以 -m 的语法,本质上就是:
-m 只消费后面紧跟着的 1 个参数,作为模块名
剩下的参数保留给这个模块自己使用
这也是为什么后来“只错位了一个参数”。
因为解释器本来就只会单独拿走一个:
pip
后面的 install xxx 本来就是留给 pip 的,不归解释器消费。
四、解释器真正的启动顺序,问题就卡在这里
python -m 的关键不只是“拿走模块名”。
还在于:
模块真正执行,不是发生在解释器最开始。
大致顺序可以先粗暴理解成这样:
1. C 层解析命令行
2. 发现是 -m,把模块名 pip 单独保存起来
3. 初始化解释器
4. import site
5. site 处理 .pth
6. .pth 触发 import dmdpi
7. dmdpi.py 执行 os.execve
8. 如果没被打断,最后才由 runpy 把保存下来的 pip 当作 __main__ 跑起来
这里最关键的是第 2 步和第 8 步。
模块名 pip 已经被解释器保存下来了。
但在 site/.pth 执行的时候,pip 还没真正开始跑。
所以这时候如果你去看 sys.argv,你看到的不是完整命令,而是:
['-m', 'install', 'xxx']
也就是说:
• pip 已经被解释器拿走
• install xxx 还留在 sys.argv 里,准备之后交给 pip
这不是异常。
这就是 -m 模式在启动中途本来的样子。
五、问题就出在:dmdpi.py 用这份“中途状态”的 sys.argv 重拼命令
现在再回来看这段逻辑:
os.execve(sys.executable, [sys.executable] + sys.argv, os.environ)
它的问题不是单纯“重启 Python”。
而是:
它拿来重启的参数列表,根本不是原始命令行,而是解释器启动到一半时的 `sys.argv`。
原始命令本来是:
python3 -m pip install xxx
但 site 阶段的 sys.argv 已经变成:
['-m', 'install', 'xxx']
于是它重拼出来的就变成了:
python3 -m install xxx
这时候解释器第二次启动,再按 -m 的规则解析:
• -m 还是解释器选项
• install 被当成新的模块名
• xxx 成了传给 install 的参数
如果你原来跑的是:
python3 -m pip -V
那第二次重拼后就会变成:
python3 -m -V
于是错误就成了:
No module named -V
所以这里不是“后面的参数为什么没被吃掉”。
而是:
解释器按规则本来就只会消费 `-m` 后面的一个参数作为模块名。原来的 `pip` 丢了以后,后面紧跟着的那个参数就自动顶上来,成了新的模块名。
六、怎么验证 site 阶段看到的 sys.argv 确实已经缺了模块名?
我当时的验证方式很直接。
写一个 usercustomize.py:
import os
import sys
os.environ["ARGV_DEBUG"] = str(sys.argv)
然后执行:
$ python3 -m pip --version
再看结果:
['-m', '--version']
这就证明了两件事:
1. pip 确实已经不在 sys.argv 里
2. site 阶段拿到的 sys.argv 本来就是“少了模块名”的中间态
所以 dmdpi.py 再拿它去 execve,天然就会错位。
七、为什么 import dmPython 本身又没事?
因为这次事故里,真正有问题的不是:
dmPython.so 能不能找到自己的依赖库
而是:
它为了设置动态库路径,在解释器启动阶段额外搞了一次 execve
而实际排下来,dmPython.so 自己已经能通过内置的 RPATH 找到依赖库。
也就是说,根本不需要:
• .pth 在 site 阶段自动 import dmdpi
• dmdpi.py 去改 LD_LIBRARY_PATH
• 再 os.execve 把解释器重新拉起一次
所以 import dmPython 没问题,不代表这套启动链是安全的。
只能说明:
真正出事的是解释器启动流程,不是模块导入结果本身
八、修复很简单,问题却很典型
修复方式其实很直接:
rm /site-packages/dmPython.pth
删掉以后:
• python3 -m pip 恢复正常
• import dmPython 仍然正常
后面达梦新版也改成了 wheel 形式,不再依赖:
• .pth
• dmdpi.py
• 启动阶段 os.execve
这说明旧方案的问题不在于“写法不优雅”。
而在于:
它把解释器还没启动完的中间状态,当成了可以无脑复用的最终命令行。
最后一句
这次问题的根因,不是 pip 坏了,也不是 `-m` 机制坏了,而是 `.pth` 在 `site` 初始化阶段执行了 `os.execve`,错误地用中间态 `sys.argv` 重建了命令行。`python -m` 的模块名本来就会被解释器单独拿走,后面的参数本来就是留给模块自己的;一旦你在这个阶段重启解释器,原来的模块名丢了,后面紧跟着的那个参数就会自动顶上来,变成新的模块名。