在Linux环境下使用Python多进程编程时, 有时通过Ctrl+C中断主进程后子进程残留. 本文罗列几种处理方案.
文章目录
- 一 Python代码层面的解决方案
- 1.1 使用守护进程 (Daemon) 属性
- 1.2 捕获KeyboardInterrupt异常
- 1.3 使用进程池管理
- 1.4 使用psutil库精确控制
- 二 Linux系统层面的Shell脚本方案
- 2.1 基于进程树的终止
- 2.2 使用进程组管理
- 2.3 基于/proc目录的脚本
- 三 高级进程管理工具集成
- 3.1 使用Supervisor
- 3.2 Systemd单元文件配置
- 四 信号处理机制解析
- 4.1 信号传播机制
- 4.2 可靠信号处理实现
- 五 方案对比
- 六 诊断工具与调试技巧
一 Python代码层面的解决方案
1.1 使用守护进程 (Daemon) 属性
将子进程设置为守护进程后, 主进程退出时会自动终止所有守护子进程. 这是最轻量级的解决方案.
from multiprocessing import Process
import timedef worker():while True:print("子进程运行中...")time.sleep(1)if __name__ == '__main__':p = Process(target=worker)p.daemon = True # 设置守护属性p.start()time.sleep(5) # 主进程结束后自动终止子进程
关键点:
daemon=True
必须在start()
前设置- 守护进程无法创建自己的子进程 (避免孤儿进程)
1.2 捕获KeyboardInterrupt异常
通过异常处理机制显式终止子进程:
import multiprocessing
import timedef worker():while True:print("子进程运行中...")time.sleep(1)if __name__ == '__main__':try:p = multiprocessing.Process(target=worker)p.start()while True:time.sleep(1)except KeyboardInterrupt:p.terminate()p.join()print("所有子进程已终止")
优势:
- 支持自定义清理逻辑 (如关闭数据库连接)
- 可处理多个子进程的批量终止
1.3 使用进程池管理
concurrent.futures.ProcessPoolExecutor
相比原生multiprocessing.Pool
对信号处理更友好:
from concurrent.futures import ProcessPoolExecutor
import timedef worker():while True:print("子进程运行中...")time.sleep(1)if __name__ == '__main__':with ProcessPoolExecutor() as executor:future = executor.submit(worker)try:while not future.done():time.sleep(1)except KeyboardInterrupt:executor.shutdown(wait=False)
特点:
- 自动处理进程组终止
wait=False
参数确保立即终止
1.4 使用psutil库精确控制
通过跨平台进程管理库实现更精细的操作:
import psutil
import osdef kill_children():current = psutil.Process(os.getpid())for child in current.children(recursive=True):child.terminate()psutil.wait_procs(current.children(recursive=True), timeout=5)
应用场景:
- 需要递归终止孙子进程时
- 跨平台兼容性要求高的项目
二 Linux系统层面的Shell脚本方案
也可以通过Linux方法, 编写合适的shell脚本干掉子进程.
2.1 基于进程树的终止
通过pstree
获取进程树结构后批量终止:
#!/bin/bash
MAIN_PID=$(pgrep -f "main.py") # 获取主进程PID
pkill -P $MAIN_PID # 终止所有子进程
kill $MAIN_PID # 终止主进程
原理:
pgrep -f
匹配完整命令行-P
参数指定父进程
2.2 使用进程组管理
通过创建进程组实现一键终止:
#!/bin/bash
set -m # 启用作业控制
python main.py & # 后台运行
PID=$!
kill -- -$(ps -o pgid= $PID) 2>/dev/null # 终止整个进程组
优势:
- 避免子进程残留
- 兼容僵尸进程处理
2.3 基于/proc目录的脚本
递归解析进程树结构:
#!/bin/bash
killtree() {local _pid=$1for _child in $(ps -o pid --ppid ${_pid} --no-headers); dokilltree ${_child}donekill -9 ${_pid} 2>/dev/null
}MAIN_PID=$(pgrep -f "main.py")
killtree $MAIN_PID
特点:
- 支持多层级进程终止
- 兼容性优于
pkill
三 高级进程管理工具集成
3.1 使用Supervisor
配置示例 (/etc/supervisor/conf.d/app.conf) :
[program:myapp]
command=python main.py
process_name=%(program_name)s_%(process_num)02d
numprocs=4 # 启动4个子进程
autostart=true
autorestart=unexpected
stopasgroup=true # 终止进程组
killasgroup=true
优势:
- 统一管理进程组
- 提供Web管理界面
- 自动日志轮转
3.2 Systemd单元文件配置
创建服务文件 (/etc/systemd/system/myapp.service) :
[Unit]
Description=My Python App
After=network.target[Service]
Type=notify
ExecStart=/usr/bin/python /opt/app/main.py
KillMode=process # 可选control-group/mixed
TimeoutStopSec=30
Restart=on-failure[Install]
WantedBy=multi-user.target
控制命令:
systemctl kill --kill-who=all myapp.service # 终止所有相关进程
适用场景:
- 需要深度系统集成的服务
四 信号处理机制解析
4.1 信号传播机制
信号类型 | 默认行为 | 推荐处理方式 |
---|---|---|
SIGINT | 终止进程 | 注册handler清理子进程 |
SIGTERM | 终止进程 | 同上 |
SIGKILL | 立即终止 | 无法捕获, 慎用 |
SIGCHLD | 忽略 | 用于回收僵尸进程 |
4.2 可靠信号处理实现
import signal
import osdef handler(signum, frame):print(f"收到信号{signum}, 开始清理...")# 递归终止子进程os.system("pkill -P {}".format(os.getpid()))exit(0)signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGTERM, handler)
五 方案对比
场景 | 推荐方案 | 优点 | 注意事项 |
---|---|---|---|
快速开发原型 | Daemon属性+异常捕获 | 实现简单, 无外部依赖 | 需处理进程残留风险 |
长期运行服务 | Supervisor/systemd | 完整的生命周期管理 | 需要学习配置语法 |
复杂多层级进程 | psutil递归终止 | 精确控制进程树 | 需安装第三方库 |
临时调试环境 | Shell脚本终止进程组 | 无需修改代码 | 可能误杀同名进程 |
高并发任务 | ProcessPoolExecutor | 内置优雅终止机制 | Python3.7+特性支持更完善 |
六 诊断工具与调试技巧
-
进程树可视化:
pstree -aps <PID> # 显示完整命令行参数
-
僵尸进程检测:
ps aux | grep 'Z' # STAT列显示为Z
-
信号跟踪:
strace -p <PID> -e trace=signal
-
Python调试模式:
from multiprocessing import util util.log_to_stderr(util.SUBDEBUG) # 输出详细日志