然后我們在Interactive Python prompt中測試了一下:
>>> import subprocess >>> subprocess.check_call("false") 0
而在其他機器運行相同的代碼時, 卻正確的拋出了錯誤:
>>> subprocess.check_call("false") Traceback (most recent call last): File "", line 1, in File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 542, in check_call raise CalledProcessError(retcode, cmd) subprocess.CalledProcessError: Command 'false' returned non-zero exit status 1
看來是subprecess誤以為子進程成功的退出了導致的原因.
深入分析
第一眼看上去, 這一問題應該是Python自身或操作系統引起的. 這到底是怎么發生的? 于是我的同事查看了subprocess的wait()方法:
def wait(self): """Wait for child process to terminate. Returns returncode attribute.""" while self.returncode is None: try: pid, sts = _eintr_retry_call(os.waitpid, self.pid, 0) except OSError as e: if e.errno != errno.ECHILD: raise # This happens if SIGCLD is set to be ignored or waiting # for child processes has otherwise been disabled for our # process. This child is dead, we can't get the status. pid = self.pid sts = 0 # Check the pid and loop as waitpid has been known to return # 0 even without WNOHANG in odd situations. issue14396. if pid == self.pid: self._handle_exitstatus(sts) return self.returncode
可見, 如果os.waitpid的ECHILD檢測失敗, 那么錯誤就不會被拋出. 通常, 當一個進程結束后, 系統會繼續記錄其信息, 直到母進程調用wait()方法. 在此期間, 這一進程就叫"zombie". 如果子進程不存在, 那么我們就無法得知其是否成功還是失敗了.
以上代碼還能解決另外一個問題: Python默認認為子進程成功退出. 大多數情況下, 這一假設是沒問題的. 但當一個進程明確表明忽略子進程的SIGCHLD時, waitpid()將永遠是成功的.
回到原來的代碼中
我們是不是在我們的程序中明確設置忽略SIGCHLD? 不太可能, 因為我們使用了大量的子進程, 但只有極少數情況下才出現同樣的問題. 再使用git grep后, 我們發現只有在一段獨立代碼中, 我們忽略了SIGCHLD. 但這一代嗎根本就不是程序的一部分, 只是引用了一下.
一星期后
一星期后, 這一錯誤又再一次發生. 并且通過簡單的調試, 在debugger中重現了該錯誤.
經過一些測試, 我們確定了正是由于程序忽略了SIGCHLD才引起的這一bug. 但這是怎么發生的呢?
我們查看了那段獨立代碼, 其中有一段:
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
我們是不是無意間import了這段代碼到程序中? 結果顯示我們的猜測是正確的. 當import了這段代碼后, 由于以上語句是在這一module的頂層, 而不是在一個function中, 導致了它的運行, 忽略了SIGCHLD, 從而導致了子進程錯誤沒有被拋出!
新聞熱點
疑難解答