后端开发中有时会遇到这种状况:进程运行中偶现,重启进程问题就消失;或者是,进程必定要运行一段时间才会出现问题;又或是,极难复现的问题出现了,然而已有的log不足以定位html
对于这些状况,尽管大部分时候,咱们能够经过在可能的地方加log,而后重启进程等待问题复现,但这样相对被动。咱们都知道若是要调试C/C++程序,gdb attach上进程就能够,而python虽然有类似的工具pdb,但它没法附加到一个进程上,必需要用pdb启动进程,在实际环境中显然无论用,那么python是否有相似的办法来改变运行中进程的代码
呢?这样咱们就能够经过实时加log来定位问题,这样几乎能够解决python层面的任何问题python
能够参考两篇文章:linux
https://mozillazg.com/2018/07...
https://mozillazg.com/2017/07...git
简单来讲,能够直接用gdb使用相似调试c程序的方式,但要求python进程是使用python-debug这种版本的python,一样不够实用。这里介绍博客中提到的“纯gdb”的方式,经过github上一个开源python包pyrasite
,本质上是经过gdb的-eval-command
和它的PyRun_SimpleString
来向进程注入代码。github
这个库有一些附加功能,能够经过它的文档去了解。这里只说实现进程注入的核心,是其中一个很短的文件injector.py
,这里去掉了原文件中用于windows平台的一段代码,咱们这里只考虑linux,核心代码以下:shell
import os import subprocess import platform def inject(pid, filename, verbose=False, gdb_prefix=''): """Executes a file in a running Python process.""" filename = os.path.abspath(filename) gdb_cmds = [ 'PyGILState_Ensure()', 'PyRun_SimpleString("' 'import sys; sys.path.insert(0, \\"%s\\"); ' 'sys.path.insert(0, \\"%s\\"); ' 'exec(open(\\"%s\\").read())")' % (os.path.dirname(filename), os.path.abspath(os.path.join(os.path.dirname(__file__), '..')), filename), 'PyGILState_Release($1)', ] p = subprocess.Popen('%sgdb -p %d -batch %s' % (gdb_prefix, pid, ' '.join(["-eval-command='call %s'" % cmd for cmd in gdb_cmds])), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() if verbose: print(out) print(err)
这个函数作的事很简单,不难看懂,因此,咱们须要作的就是调用这个函数,传入pid和文件名,文件是一个你要对这个进程执行的python代码。如今咱们运行一个很简单的python进程test.py
:windows
import time def b(): print('b') while 1: b() time.sleep(1)
而后建立一个文件patch.py
:后端
print('injecting') def newb(): print('new b') b = newb
在injector.py
的末尾加上一段,以便接收命令行调用:函数
import sys pid = sys.argv[1] filename = sys.argv[2] inject(int(pid), filename)
经过ps aux|grep test.py
查看上面进程的pid,而后执行python injector.py pid patch.py
,为方便反复测试能够这样:工具
pid=`ps aux | grep test.py | grep -v grep | awk '{print $2}'`;python injector.py $pid patch.py;echo $pid injected
输出以下:
至此就实现了进程注入。
注意点:
classA.method = new_method
会将变化应用到全部实例,注意对类方法来讲在patch.py
中定义时也要加上self
参数patch.py
中,咱们能够直接对b
赋值,由于咱们gdb进入一个进程后,所在的上下文环境就是该进程的入口模块,能够经过打印globals()
来看到有哪些全局变量,这些就是能够直接访问的对象。若是是在一个普通的业务进程中,必然有大量import
,这种状况下你须要import相应模块再对该模块的函数或类进行修改,如import x.y.z as z; z.b = newb
A
使用了from B import func
,那么若是你想改变A
中运行的func
,须要import A; A.func = newfunc
,像这样改变B
是没有用的:import B; B.func = newfunc
,由于from .. import ..
会将对象复制一份到本地命名空间。反之,若是A
是使用import B
并经过B.func
进行调用,那么就应当import B
进行修改while True
,那么改变这个函数是没有用的,显然要应用改变的对象须要对象下一次被调用,这个不难理解可是容易漏想到