1.1 踩坑案例
踩坑的程序是個常駐的Agent類管理進程, 包括但不限於以下類型的任務在執行:python
- a. 多線程的網絡通訊包處理
- 和控制Master節點交互
- 有固定Listen端口
- b. 按期做業任務, 經過subprocess.Pipe執行shell命令
- c. etc
發現坑的過程頗有意思:shell
- 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具體執行的庫代碼以下:bash
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
中的坑就被結實的踩上了.code
1.4 坑後擴展
1.4.1 擴展知識
本節擴展知識包括二個部分:htm
- Linux系統中, 子進程通常會繼承父進程的哪些信息
- Agent這種常駐進程選擇>60000端口的意義
擴展知識留到下篇末尾講述, 感興趣的能夠自行搜索
1.4.1 技術關鍵字
- Linux系統進程
- Linux隨機端口選擇
- 程序多線程執行
- Shell執行
1.5 填坑總結
- 子進程會繼承父進程的資源信息
若是隻kill某進程的父進程, 集成了父進程資源的子進程會繼續佔用父進程的資源不釋放, 包括但不限於
- listened port
- opened fd
- etc
Python Popen使用上, shell的bool狀態決定了進程kill的邏輯, 須要根據場景選擇使用方式
建議你們也看一下這篇文章的姊妹篇, 此篇是子孫進程沒法 kill/殺死的終極解法 [Python 踩坑之旅進程篇其三pgid是個什麼鬼 (子進程\子孫進程沒法kill 退出的解法)] (http://www.javashuo.com/article/p-nxquorsz-ck.html)