1. 程式人生 > >Python踩坑之旅其一殺不死的Shell子程序

Python踩坑之旅其一殺不死的Shell子程序

目錄

  • 1.1 踩坑案例
  • 1.2 填坑解法
  • 1.3 坑位分析
  • 1.4 坑後擴充套件
    • 1.4.1 擴充套件知識
    • 1.4.1 技術關鍵字
  • 1.5 填坑總結

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的邏輯, 需要根據場景選擇使用方式