后台开发离不开调试代码,有时线上问题,生产环境等无法调试,这时候就需要进行进程调试。python开发相对于c/c++开发,调试过程少很多,但是也有一些常见的情况,比如:生产环境问题,进程不能轻易重启,偶尔问题重现,但是现有的日志没有足以定位进程运行时间长到某个状态,需要根据这个状态进行调试等。对于这些情况,虽然大多数时候,我们可以在可能的情况下添加日志,然后重启进程等待问题再次出现,但这样比较被动。我们都知道,如果要调试C/C++程序,直接用gdb附加进程即可。python虽然有类似的工具pdb,但是不能附加到一个进程上。必须使用pdb启动进程,在实际环境中显然行不通,那么python是否有类似的方法来更改正在运行的python进程的代码?如果能调试python进程,几乎可以解决python层面的任何问题。可以参考两篇文章:https://mozillazg.com/2018/07...https://mozillazg.com/2017/07...总之直接用gdb就可以使用类似调试的方法c程序,但是要求python进程使用python-debug等版本的python是不够实用的。这里我介绍一下博客中提到的“puregdb”方法,通过github上的一个开源python包pyrasite,本质上是通过gdb的-eval-command及其PyRun_SimpleString向进程中注入代码。这个库有一些额外的特性,可以通过它的文档来学习。这里只讲进程注入的核心,就是一个很短的文件injector.py。这里去掉了原文件中windows平台的一段代码。我们这里只考虑linux。核心代码如下:importosimportsubprocessimportplatformdefinject(pid,filename,verbose=False,gdb_prefix=''):"""ExecutesafileinarunningPythonprocess."""filename=os.path.abspath(文件名)gdb_cmds=['PyGILState_Ensure()','PyRun_SimpleString("''importsys;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'"%cmdforcmdingdb_cmds])),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)out,err=p.communicate()如果冗长:print(out)print(err)函数很简单,也不难理解,我们只需要调用这个函数,传入pid和文件名,文件就是你要在这个进程上执行的python代码现在我们运行一个非常简单的python进程test.py:importtimedefb():print('b')while1:b()time.sleep(1)然后创建一个文件patch.py??:print('injecting')defnewb():print('newb')b=newb在injector.py末尾添加一个部分来接收命令行调用:importsyspid=sys.argv[1]filename=sys.argv[2]inject(int(pid),filename)通过psaux|greptest.py查看上述进程的pid,然后执行pythoninjector.pypidpatch.py??。为了方便重复测试,可以这样:pid=`psaux|grep测试.py|grep-vgrep|awk'{print$2}'`;pythoninjector.py$pidpatch.py??;echo$pidinjected输出如下:至此,进程注入已经实现。注意:修改类或类方法等同于函数。更改类方法时,直接使用类名classA.method=new_method将更改应用到所有实例。注意,对于实例方法,也必须在patch.py??中定义,在patch.py??中加入self参数,我们可以直接给b赋值,因为我们的gdb进入一个进程后,它所在的上下文就是入口进程的模块,通过打印globals()我们可以看到存在哪些全局变量,这些都是可以直接访问的对象。如果是在一个普通的业务流程中,肯定会有大量的导入。这种情况下,需要导入相应的模块,然后修改模块的函数或类,如importx.y.zasz;z.b=newb.需要特别注意的是,如果一个模块A使用了fromBimportfunc,那么如果要改变A中运行的func,需要导入模块A,修改模块A中的func。修改B是没有用的,因为from..import..将对象的副本复制到本地命名空间。反之,如果A使用importB,通过B.func调用,那么就应该修改模块B中的func。如果一个函数内部有阻塞操作,比如socket监听,或者持续运行whileTrue循环等,那么修改这个函数本身是不会生效的(但是修改循环体内调用的函数还是有用的),因为要改变的对象显然需要下次调用该对象(这里是True所在的函数),这个容易理解但容易遗漏
