0x01 問題現象
寫的程序使用subprocess創建子進程運行其他程序,判斷其他程序運行完后進行處理。
在subprocess使用了shell=True,判斷用戶程序退出的代碼如下
while self.proc.poll() is None:
do_something
判斷子進程是否運行結束,程序在子進程運行結束后,代碼未向下繼續運行,而是卡在了這個循環中。
0x02 原因分析
百度后對shell參數的解釋如下:
shell=True參數會讓subprocess.Popen接受字符串類型的變量作為命令,并調用shell去執行這個字符串,當shell=False是,subprocess.Popen只接受數組變量作為命令,并將數組的第一個元素作為命令,剩下的全部作為該命令的參數。
通過查看服務器進程可以看到,仍然有進程存在,進程如下

為shell中運行的程序,由此可以得出,shell=true時,子進程在運行完后,shell并沒有退出,而是卡在shell命令中,可由進程看到。

補充:Python踩坑之旅其一殺不死的Shell子進程
1.1 踩坑案例
踩坑的程序是個常駐的Agent類管理進程, 包括但不限于如下類型的任務在執行:
a. 多線程的網絡通信包處理
和控制Master節點交互
有固定Listen端口
b. 定期作業任務, 通過subprocess.Pipe執行shell命令
c. etc
發現坑的過程很有意思:
a.重啟Agent發現Port被占用了
=> 立刻想到可能進程沒被殺死, 是不是停止腳本出問題
=> 排除發現不是, Agent進程確實死亡了
=> 通過 netstat -tanop|grep port_number 發現端口確實有人占用
=> 調試環境, 直接殺掉占用進程了之, 錯失首次發現問題的機會
b.問題在一段時間后重現, 重啟后Port還是被占用
定位問題出現在一個叫做xxxxxx.sh的腳本, 該腳本占用了Agent使用的端口
=> 奇了怪了, 一個xxx.sh腳本使用這個奇葩Port干啥(大于60000的Port, 有興趣的磚友可以想下為什么Agent默認使用6W+的端口)
=> review該腳本并沒有進行端口監聽的代碼
一拍腦袋, c.進程共享了父進程資源了
=> 溯源該腳本,發現確實是Agent啟動的任務中的腳本之一
=> 問題基本定位, 該腳本屬于Agent調用的腳本
=> 該Agent繼承了Agent原來的資源FD, 也就是這個port
=> 雖然該腳本由于超時被動觸發了terminate機制, 但terminate并沒有干掉這個子進程
=> 該腳本進程的父進程(ppid) 被重置為了1
d.問題****出在腳本進程超時kill邏輯
1.2 填坑解法
通過代碼review, 找到shell具體執行的庫代碼如下:
self._subpro = subprocess.Popen(
cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
preexec_fn=_signal_handle
)
# 重點是shell=True !
把上述代碼改為:
self._subpro = subprocess.Popen(
cmd.split(), stdout=subprocess.PIPE,
stderr=subprocess.PIPE, preexec_fn=_signal_handle
)
# 重點是去掉了shell=True
1.3 坑位分析
Agent會在一個新創建的threading線程中執行這段代碼, 如果線程執行時間超時(xx seconds), 會調用 self._subpro.terminate()終止該腳本.
表面正常:
啟用新線程執行該腳本
如果出現問題,執行超時防止hang住其他任務執行調用terminate殺死進程
深層問題:
Python 2.7.x中subprocess.Pipe 如果shell=True, 會默認把相關的pid設置為shell(sh/bash/etc)本身(執行命令的shell父進程), 并非執行cmd任務的那個進程
子進程由于會復制父進程的opened FD表, 導致即使被殺死, 依然保留了擁有這個Listened Port FD
這樣雖然殺死了shell進程(未必死亡, 可能進入defunct狀態), 但實際的執行進程確活著. 于是1.1中的坑就被結實的踩上了.
1.4 坑后擴展
1.4.1 擴展知識
本節擴展知識包括二個部分:
Linux系統中, 子進程一般會繼承父進程的哪些信息
Agent這種常駐進程選擇>60000端口的意義
擴展知識留到下篇末尾講述, 感興趣的可以自行搜索
1.4.1 技術關鍵字
Linux系統進程
Linux隨機端口選擇
程序多線程執行
Shell執行
1.5 填坑總結
1.子進程會繼承父進程的資源信息
2.如果只kill某進程的父進程, 集成了父進程資源的子進程會繼續占用父進程的資源不釋放, 包括但不限于
listened port
opened fd
etc
3.Python Popen使用上, shell的bool狀態決定了進程kill的邏輯, 需要根據場景選擇使用方式
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。如有錯誤或未考慮完全的地方,望不吝賜教。
您可能感興趣的文章:- Python實現系統交互(subprocess)
- Python實現subprocess執行外部命令
- Python中使用subprocess庫創建附加進程
- Python中Subprocess的不同函數解析
- python中subprocess實例用法及知識點詳解