分析运行中的 Python 进程详细解析
在Java中打印当前线程的方法栈,可以用kill-3命令向JVM发送一个OS信号,JVM捕捉以后会自动dump出来;当然,也可以直接使用jstack工具完成,这些方法好几年前我在这篇性能分析的文章中介绍过。这样的需求可以说很常见,比如定位死锁,定位一个不工作的线程到底卡在哪里,或者定位为什么CPU居高不下等等问题。
现在工作中我用的是Python,需要线上问题定位的缘故,也有了类似的需求——想要知道当前的Python进程“在干什么”。但是没有了JVM的加持,原有的命令或者工具都不再适用。传统的gdb的debug大法在线上也不好操作。于是我寻找了一些别的方法,来帮助定位问题,我把它们记录在这里。
signal
在代码中,我们可以使用signal为进程预先注册一个信号接收器,在进程接收到特定信号的时候,可以打印方法栈:
importtraceback,signal classDebugger(): def__init__(self,logger): self._logger=logger deflog_stack_trace(self,sig,frame): d={'_frame':frame} d.update(frame.f_globals) d.update(frame.f_locals) messages="Signalreceived.Stacktrace:\n" messages+=''.join(traceback.format_stack(frame)) self._logger.warn(messages) deflisten(self): signal.signal(signal.SIGUSR1,self.log_stack_trace)
通过调用上面的listen方法(比如newDebug(logger).listen()),就将一个可以接收SIGUSR1并打印方法栈的接收器注册到当前进程了。这里是打印方法栈,但是实际上可以做任何事,因为方法执行的当前,上下文已经跑到进程里面了。
那么怎么向进程发送信号呢?和JVM的方法类似,可以通过操作系统命令来发送:
kill-30pid
这里的信号为什么是30?这是因为SIGUSR1被当前操作系统定义成30(请注意不同的操作系统这个映射表是可能不同的),这点可以通过mansignal查看:
NoNameDefaultActionDescription SIGHUPterminateprocessterminallinehangup SIGINTterminateprocessinterruptprogram SIGQUITcreatecoreimagequitprogram SIGILLcreatecoreimageillegalinstruction SIGTRAPcreatecoreimagetracetrap SIGABRTcreatecoreimageabortprogram(formerlySIGIOT) SIGEMTcreatecoreimageemulateinstructionexecuted SIGFPEcreatecoreimagefloating-pointexception SIGKILLterminateprocesskillprogram SIGBUScreatecoreimagebuserror SIGSEGVcreatecoreimagesegmentationviolation SIGSYScreatecoreimagenon-existentsystemcallinvoked SIGPIPEterminateprocesswriteonapipewithnoreader SIGALRMterminateprocessreal-timetimerexpired SIGTERMterminateprocesssoftwareterminationsignal SIGURGdiscardsignalurgentconditionpresentonsocket SIGSTOPstopprocessstop(cannotbecaughtorignored) SIGTSTPstopprocessstopsignalgeneratedfromkeyboard SIGCONTdiscardsignalcontinueafterstop SIGCHLDdiscardsignalchildstatushaschanged SIGTTINstopprocessbackgroundreadattemptedfromcontrolterminal SIGTTOUstopprocessbackgroundwriteattemptedtocontrolterminal SIGIOdiscardsignalI/Oispossibleonadescriptor(seefcntl(2)) SIGXCPUterminateprocesscputimelimitexceeded(seesetrlimit(2)) SIGXFSZterminateprocessfilesizelimitexceeded(seesetrlimit(2)) SIGVTALRMterminateprocessvirtualtimealarm(seesetitimer(2)) SIGPROFterminateprocessprofilingtimeralarm(seesetitimer(2)) SIGWINCHdiscardsignalWindowsizechange SIGINFOdiscardsignalstatusrequestfromkeyboard SIGUSR1terminateprocessUserdefinedsignal1 SIGUSR2terminateprocessUserdefinedsignal2
当然,也可以写一点点python脚本来发送这个信号:
importos,signal os.kill($PID,signal.SIGUSR1)
原理是一样的。
strace
如果进程已经无响应了,或者上面的信号接收器没有注册,那么就要考虑别的方法来或者“进程在干什么”这件事情了。其中,一个有用的命令是strace:
strace-ppid
比如,我自己写了一个测试脚本t.py,使用python执行,然后调用sleep,再给它发送一个SIGUSR1的消息,它打印方法栈并退出。这整个过程,我使用strace可以得到这样的结果:
strace-p9157 strace:Process9157attached select(0,NULL,NULL,NULL,{9999943,62231})=?ERESTARTNOHAND(Toberestartedifnohandler) ---SIGUSR1{si_signo=SIGUSR1,si_code=SI_USER,si_pid=9273,si_uid=9007}--- rt_sigreturn({mask=[]})=-1EINTR(Interruptedsystemcall) stat("t.py",{st_mode=S_IFREG|0644,st_size=1281,...})=0 open("t.py",O_RDONLY)=3 fstat(3,{st_mode=S_IFREG|0644,st_size=1281,...})=0 fstat(3,{st_mode=S_IFREG|0644,st_size=1281,...})=0 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0)=0x7f631e866000 read(3,"importtraceback,signal,time\n"...,8192)=1281 read(3,"",4096)=0 close(3)=0 munmap(0x7f631e866000,4096)=0 stat("t.py",{st_mode=S_IFREG|0644,st_size=1281,...})=0 write(1,"Signalreceived.Stacktrace:\n"...,134)=134 write(1,"\n",1)=1 rt_sigaction(SIGINT,{SIG_DFL,[],SA_RESTORER,0x7f631e06f5d0},{0x7f631e392680,[],SA_RESTORER,0x7f631e06f5d0},8)=0 rt_sigaction(SIGUSR1,{SIG_DFL,[],SA_RESTORER,0x7f631e06f5d0},{0x7f631e392680,[],SA_RESTORER,0x7f631e06f5d0},8)=0 exit_group(0)=? +++exitedwith0+++
可以看到从straceattached开始,到进程退出,所有重要的调用都被打印出来了。
在iOS下,没有strace,但是可以使用类似的(更好的)命令dtruss。
lsof
lsof可以打印某进程打开的文件,而Linux下面一切都是文件,因此查看打开的文件列表有时可以获取很多额外的信息。比如,打开前面提到的这个测试进程:
lsof-p16872 COMMANDPIDUSERFDTYPEDEVICESIZE/OFFNODENAME Python16872xxxcwdDIR1,526881113586/Users/xxx Python16872xxxtxtREG1,55174410627527/System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python Python16872xxxtxtREG1,55276810631046/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload/_locale.so Python16872xxxtxtREG1,56595210631134/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload/time.so Python16872xxxtxtREG1,584144010690598/usr/lib/dyld Python16872xxxtxtREG1,5117007974410705794/private/var/db/dyld/dyld_shared_cache_x86_64h Python16872xxx0uCHR16,20t39990649/dev/ttys002 Python16872xxx1uCHR16,20t39990649/dev/ttys002 Python16872xxx2uCHR16,20t39990649/dev/ttys002
它有几个参数很常用,比如-i,用来指定网络文件(如果是“-i:端口号”这样的形式还可以指定端口)。
总结
以上所述是小编给大家介绍的分析运行中的Python进程,希望对大家有所帮助,如果大家有任何疑问欢迎给我留言,小编会及时回复大家的!