社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
某Team在用Python开发一些代码,涉及子进程以及设法消除僵尸进程的需求。实践中他们碰上Python程序非预期退出的现象。最初他们决定 用GDB调试Python解释器,查看exit()的源头。我听了之后,觉得这个问题应该用别的调试思路。帮他们排查这次程序故障时,除去原始问题,还衍 生了其他问题。
这次的问题相比西安研发中心曾经碰上的Python信号处理问题,有不少基础知识、先验知识是共用的,此处不做再普及,感兴趣的同学可以翻看我以前发过的文章。
下文是一次具体的调试、分析记录。为了简化现场、方便调试,已将原始问题、衍生问题浓缩成DebugPythonWithGDB_6.py、DebugPythonWithGDB_7.py。
$ vi DebugPythonWithGDB_6.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
#!/usr/bin/env python # -*- encoding: utf-8 -*- import sys, os, signal, subprocess, shlex, traceback def on_SIGCHLD ( signum, frame ) : print "[on_SIGCHLD" sys.stdout.write( "signum = %un" % signum ) traceback.print_stack( frame ) print os.waitpid( -1, os.WNOHANG ) """ try : print os.waitpid( -1, os.WNOHANG ) except OSError : sys.stdout.write( 'Line[%u]: OSErrorn' % sys.exc_info()[2].tb_lineno ) """ print "on_SIGCHLD]" def do_more ( count ) : print '[do_more() begin %u]' % count os.system( r'printf "Child = %un" $$;/bin/sleep 1' ) """ # # 这里存在竞争条件,可以增加触发OSError异常的概率 # os.system( r'printf "Child = %un" $$;/bin/sleep 1' ) os.system( r'printf "Child = %un" $$;/bin/sleep 1' ) os.system( r'printf "Child = %un" $$;/bin/sleep 1' ) os.system( r'printf "Child = %un" $$;/bin/sleep 1' ) """ print '[do_more() end %u]' % count def main ( prog, args ) : if 0 == len( args ) : print 'Usage: %s ' % prog else : sys.stdout.write( "Parent = %un" % os.getpid() ) # # 本例中,即使有下列代码,Ctrl-C仍然无效。 # signal.signal( signal.SIGINT, signal.SIG_DFL ) # # signal.signal( signal.SIGCHLD, signal.SIG_IGN ) # signal.signal( signal.SIGCHLD, on_SIGCHLD ) # count = 0 while True : # # 本例中父进程只是一个调度框架,不需要与子进程进行通信,因此不 # 需要特别处理"stdin=None, stdout=None, stderr=None"。 # child = subprocess.Popen ( # # 不要直接用args[0].split(),它在处理单、双引号时不是我们 # 期望的行为。考虑这种例子,ls -l "/tmp/non exist" # shlex.split( args[0] ), # # all file descriptors except 0, 1 and 2 will be closed # before the child process is executed # close_fds = True, cwd = "/tmp" ) sys.stdout.write( "Child = %un" % child.pid ) # # child.send_signal( signal.SIGTERM ) # child.terminate() # child.kill() # # child.wait() # do_more( count ) count += 1 if '__main__' == __name__ : try : main( os.path.basename( sys.argv[0] ), sys.argv[1:] ) except KeyboardInterrupt : pass |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
$ python DebugPythonWithGDB_6.py 'python -c "import time;time.sleep(3600)"' Parent = 10244 Child = 10245 [do_more() begin 0] [on_SIGCHLD signum = 17 File "DebugPythonWithGDB_6.py", line 81, in main( os.path.basename( sys.argv[0] ), sys.argv[1:] ) File "DebugPythonWithGDB_6.py", line 76, in main do_more( count ) File "DebugPythonWithGDB_6.py", line 20, in do_more print '[do_more() begin %u]' % count (10245, 9) on_SIGCHLD] Child = 10246 [on_SIGCHLD signum = 17 File "DebugPythonWithGDB_6.py", line 81, in main( os.path.basename( sys.argv[0] ), sys.argv[1:] ) File "DebugPythonWithGDB_6.py", line 76, in main do_more( count ) File "DebugPythonWithGDB_6.py", line 21, in do_more os.system( r'printf "Child = %un" $$;/bin/sleep 1' ) Traceback (most recent call last): File "DebugPythonWithGDB_6.py", line 81, in main( os.path.basename( sys.argv[0] ), sys.argv[1:] ) File "DebugPythonWithGDB_6.py", line 76, in main do_more( count ) File "DebugPythonWithGDB_6.py", line 21, in do_more os.system( r'printf "Child = %un" $$;/bin/sleep 1' ) File "DebugPythonWithGDB_6.py", line 10, in on_SIGCHLD print os.waitpid( -1, os.WNOHANG ) OSError: [Errno 10] No child processes |
流程进入on_SIGCHLD(),但os.waitpid()抛出OSError异常。帮助里写的是,如果系统调用 waitpid()返回-1,就抛出异常: An OSError is raised with the value of errno when the syscall returns -1. 10245号子进程在on_SIGCHLD()里waitpid()成功,(10245, 9)中的9表示该进程是被SIGKILL干掉的,符合预期。 10246号子进程是do_more()里的os.system()产生的shell进程,它结束时向10244号父进程投递了SIGCHLD信号。 on_SIGCHLD()里waitpid()时,已经在别处wait*()过,10246号子进程已经彻底消失,系统调用waitpid()返回 -1,Python函数os.waitpid()抛出异常。 整个过程非常复杂,用伪代码描述如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
do_more() os.system() posix_system() // posixmodule.c __libc_system() // weak_alias (__libc_system, system) do_system() // sysdeps/posix/system.c /* * SIG_IGN * * Ctrl-C暂时失效 */ sigaction( SIGINT, &sa, &intr ) /* * 屏蔽(阻塞)SIGCHLD信号 */ sigaddset( &sa.sa_mask, SIGCHLD ) sigprocmask( SIG_BLOCK, &sa.sa_mask, &omask ) fork() 子进程(10246号子进程) /* * 恢复原有SIGINT信号处理方式 */ sigaction( SIGINT, &intr, (struct sigaction *)NULL ) /* * 调用"sh -c ..." */ execve() [shell子进程结束,向DebugPythonWithGDB_6.py投递SIGCHLD] [由于SIGCHLD信号已被屏蔽(阻塞),其保持在内核态的未决信号链上] 父进程(10244号父进程) /* * 同步调用,会阻塞。不是在信号句柄中异步调用。 * * 10246号子进程在此被wait*()回收后彻底消失 */ waitpid( pid, &status, 0 ) /* * 恢复原有SIGINT信号处理方式 */ sigaction( SIGINT, &intr, (struct sigaction *)NULL ) /* * 取消对SIGCHLD的屏蔽(阻塞) */ sigprocmask( SIG_SETMASK, &omask, (sigset_t *)NULL ) [SIGCHLD信号的屏蔽(阻塞)被取消] [DebugPythonWithGDB_6.py的C级信号句柄signal_handler()安排"延迟调用"后返回] [DebugPythonWithGDB_6.py的on_SIGCHLD()此时并未得到执行,因为built-in函数os.system()尚未返回] /* * built-in函数os.system()返回后,10244号父进程开始处理"延迟调用",调用 * Python级信号句柄。这个SIGCHLD信号是10246号子进程投递过来的。 * * DebugPythonWithGDB_6.py的on_SIGCHLD()得到执行 */ on_SIGCHLD() /* * 调用waitpid( -1, &status, WNOHANG ),试图处理10246号子进程。 * * 10246号子进程已为前述waitpid( pid, &status, 0 )所处理,此处系统调用 * 返回-1,导致os.waitpid()抛出OSError异常。 */ os.waitpid( -1, os.WNOHANG ) |
整个过程之所以如此复杂,主要是因为Python的信号处理机制比较复杂,让已经非常复杂的Linux信号机制再添变数。参看:
1 2 |
《2.50 对Python解释器进行调试》 《22.0 Linux信号机制》 |
就本例而言,为了确保DebugPythonWithGDB_6.py不因OSError异常而终止,只需在on_SIGCHLD()中调用os.waitpid()时捕捉OSError异常:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!