如何确保Python多进程调用Docker运行ROS2节点时可靠处理SIGINT
解决Python multiprocessing + Docker + ROS2节点的SIGINT(Ctrl-C)清理问题
问题根源
- Docker的
-it参数会为每个容器分配独立TTY,导致SIGINT信号只能传递到当前前台容器,后台容器无法收到终止信号。 - multiprocessing子进程的信号处理与父进程隔离,父进程收到Ctrl-C中断
join()后,子进程可能仍在运行,Docker容器未被正确终止。 - 即使使用
--init参数,TTY隔离依然会阻碍信号传递到容器内的ROS2节点。
解决方案
方案1:改进multiprocessing + subprocess的信号处理
修改原代码,解决TTY绑定和信号传递问题:
import subprocess import multiprocessing import signal import os def run_node(node): # 替换-it为-t,避免独立TTY导致的信号隔离;添加--rm确保容器退出即删除 proc = subprocess.Popen( ["docker", "run", "-t", "--rm", "--init", "osrf/ros:foxy-desktop", "bash", "-c", f"ros2 run demo_nodes_py {node}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) try: proc.wait() except KeyboardInterrupt: # 捕获中断信号,主动终止Docker进程 proc.terminate() proc.wait() def handle_sigint(signum, frame): # 父进程统一处理SIGINT,终止所有子进程 for child_proc in multiprocessing.active_children(): child_proc.terminate() # 等待所有子进程完全退出 for child_proc in multiprocessing.active_children(): child_proc.join() os._exit(0) if __name__ == '__main__': # 注册SIGINT信号处理器,确保父进程优先捕获中断 signal.signal(signal.SIGINT, handle_sigint) nodes = [ multiprocessing.Process(target=run_node, args=(node,)) for node in ["add_two_ints_server", "add_two_ints_client"] ] for node in nodes: node.start() for node in nodes: node.join()
关键改动说明:
- 移除
-it改用-t:避免每个容器占用独立TTY,让信号能通过父进程的subprocess传递到容器。 - 使用
subprocess.Popen替代subprocess.call:允许主动终止Docker进程,而非阻塞等待。 - 父进程注册SIGINT处理器:统一管理所有子进程的终止,确保没有残留。
- 添加
--rm参数:即使容器异常退出,也会自动删除,避免无用容器残留。
方案2:使用Docker Compose统一管理容器
这种方案更简洁,让Docker Compose负责容器生命周期管理,Python只需要启动和停止Compose进程:
- 编写
docker-compose.yml:
version: '3' services: add_two_ints_server: image: osrf/ros:foxy-desktop command: bash -c "ros2 run demo_nodes_py add_two_ints_server" init: true add_two_ints_client: image: osrf/ros:foxy-desktop command: bash -c "ros2 run demo_nodes_py add_two_ints_client" init: true depends_on: - add_two_ints_server
- 编写Python控制代码:
import subprocess import signal import os def handle_sigint(signum, frame): # 收到Ctrl-C时,调用docker-compose down终止所有容器 subprocess.run(["docker-compose", "down"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) os._exit(0) if __name__ == '__main__': signal.signal(signal.SIGINT, handle_sigint) # 启动所有容器 compose_proc = subprocess.Popen(["docker-compose", "up"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) compose_proc.wait()
优势:
- Docker Compose会统一处理所有容器的信号传递和终止逻辑,无需手动管理多个进程。
- 避免multiprocessing带来的信号隔离问题,代码更简洁可靠。
内容的提问来源于stack exchange,提问作者fortytwo
相关产品推荐
相关产品推荐

