Python3中如何可靠获取sudo启动的目标进程PID以实现进程终止
这个问题我之前也碰到过——当用subprocess.Popen执行带sudo的命令时,返回的PID确实是sudo进程本身,而不是你真正想控制的目标进程。直接调用terminate()或者kill()只会杀掉sudo进程,目标进程会被系统init进程接管继续运行;但如果Python进程退出,sudo进程也会跟着退出,目标进程就会终止,这是因为sudo默认会在父进程退出时清理自己的子进程,但手动发送信号给sudo时,它不会主动把信号转发给子进程。
下面给你几个可靠的解决方案,按推荐程度排序:
方案1:用psutil遍历进程树(最推荐)
psutil是Python生态里非常好用的进程管理库,它可以轻松获取进程的父子关系,完美解决这个问题——我们只需要找到你启动的sudo进程的直接子进程,那就是目标进程。
首先安装psutil:
pip install psutil
然后修改你的测试代码:
import subprocess import time import psutil import signal cmd = ["sudo", "testscript.sh"] def get_target_pid(sudo_pid): try: # 获取sudo进程对象 sudo_process = psutil.Process(sudo_pid) # 只获取直接子进程(避免递归拿到更深的子进程) children = sudo_process.children(recursive=False) if children: return children[0].pid return None except psutil.NoSuchProcess: # 说明sudo进程已经退出了 return None def myfunction(action, process=None): if action == "start": process = subprocess.Popen(cmd) # 给一点时间让目标进程启动(避免sudo还没拉起子进程就去查) time.sleep(0.1) return process if action == "stop": target_pid = get_target_pid(process.pid) if target_pid: try: # 先尝试优雅终止(SIGTERM) target_process = psutil.Process(target_pid) target_process.terminate() # 等待0.5秒看是否正常终止,不行就强制杀(SIGKILL) time.sleep(0.5) if target_process.is_running(): target_process.kill() print(f"目标进程 {target_pid} 已成功终止") except psutil.NoSuchProcess: print("目标进程已经不存在") # 最后杀掉sudo进程本身 process.terminate() process = myfunction("start") time.sleep(5) myfunction("stop", process)
这个方案的优势在于:完全绑定到你启动的那个sudo进程,不会误杀其他同名进程,而且是纯Python实现,跨平台性好,不需要依赖外部命令。
方案2:用唯一环境变量标记目标进程
如果不想引入psutil依赖,也可以通过给目标进程设置唯一环境变量的方式来定位它。我们在启动时给目标进程加一个独一无二的标记,之后通过这个标记查找PID:
import subprocess import time import os import signal # 生成一个唯一标记:当前Python进程PID + 时间戳,确保不会重复 unique_tag = f"MY_UNIQUE_PROCESS_MARK={os.getpid()}_{int(time.time())}" # 用sudo -E保留环境变量,然后通过bash -c传递标记给目标脚本 cmd = ["sudo", "-E", "bash", "-c", f"{unique_tag} ./testscript.sh"] def get_target_pid(): # 用pgrep查找包含唯一标记的进程 result = subprocess.run( ["pgrep", "-f", unique_tag], capture_output=True, text=True ) if result.returncode == 0: # 取出所有匹配的PID,取最后一个(最新启动的) pids = result.stdout.strip().split('\n') return int(pids[-1]) if pids else None return None def myfunction(action, process=None): if action == "start": process = subprocess.Popen(cmd) time.sleep(0.1) return process if action == "stop": target_pid = get_target_pid() if target_pid: try: os.kill(target_pid, signal.SIGTERM) time.sleep(0.5) if os.path.exists(f"/proc/{target_pid}"): os.kill(target_pid, signal.SIGKILL) print(f"目标进程 {target_pid} 已终止") except OSError: print("目标进程已经不存在") process.terminate() process = myfunction("start") time.sleep(5) myfunction("stop", process)
这个方法依赖pgrep命令(Linux/macOS默认都有),唯一要注意的是标记要足够唯一,避免和其他进程的命令行参数冲突。
方案3:让sudo启动后立即退出(不推荐)
如果你的目标进程启动后不需要再持有sudo权限,可以用sudo -b让sudo启动进程后后台运行并退出,这样subprocess拿到的sudo进程会很快结束,目标进程变成孤儿进程。但这种方式你还是需要额外手段追踪目标PID,而且如果目标进程启动失败,你很难感知到,所以一般不推荐。
内容的提问来源于stack exchange,提问作者patman

