後端開發中有時會遇到這種狀況:進程運行中偶現,重啓進程問題就消失;或者是,進程必定要運行一段時間纔會出現問題;又或是,極難復現的問題出現了,然而已有的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
,那麼改變這個函數是沒有用的,顯然要應用改變的對象須要對象下一次被調用,這個不難理解可是容易漏想到