Python:仅向子进程发SIGTERM却终止父进程的问题排查
问题
我是Python信号处理新手,用FastAPI搭建了一个服务器。其中/run接口会启动一个耗时子进程任务,通过pathos.multiprocessing.ProcessingPool调用子进程的run_loop函数;/stop接口读取pid.txt中的子进程PID,发送SIGTERM信号终止任务。
我在子进程的run_loop函数中设置了SIGTERM信号处理器,触发时抛出SystemError,期望仅终止子进程并让父进程返回值114514,但实际触发/stop接口后,子进程虽按预期终止,整个FastAPI服务器(父进程)也随之停止。
主进程代码(test.py)
from fastapi import FastAPI from fastapi.responses import JSONResponse from pathos.multiprocessing import ProcessingPool import os import uvicorn import signal from sub import run_loop app = FastAPI() def run_py(**kwargs): i = kwargs['i'] try: out = ProcessingPool().map(run, [[i]])[0] except SystemError as err: out = 114514 return out def run(args): re_fig = run_loop(args[0]) return re_fig @app.post("/run") def run_main(): i = 1 out = run_py(i=i) return JSONResponse({"code": 200, "message": out}) @app.post("/stop") def stop(): with open("./pid.txt", "r") as f: pid = int(f.readline()) os.kill(pid, signal.SIGTERM) if __name__ == "__main__": uvicorn.run(app='test:app', host="0.0.0.0", port=1145, reload=False)
子进程代码(sub.py)
import signal import os import time def loop(i): pid = os.getpid() with open("./pid.txt", "w") as f: f.write(str(pid)) while i < 30: i += 1 print(f"sleep {i}") return i def run_loop(i): def sigterm_handler(_signo, _stack_frame): print("stop!!!!!!!!!!!!") raise SystemError signal.signal(signal.SIGTERM, sigterm_handler) i = loop(i) return i
终端输出
INFO: Started server process [108395] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:1145 (Press CTRL+C to quit) sleep 2 sleep 3 sleep 4 sleep 5 sleep 6 stop!!!!!!!!!!!! INFO: 127.0.0.1:57480 - "POST /stop HTTP/1.1" 200 OK INFO: 127.0.0.1:57476 - "POST /run HTTP/1.1" 200 OK INFO: Shutting down INFO: Waiting for application shutdown. INFO: Application shutdown complete. INFO: Finished server process [108395]
如何实现仅终止子进程而不关闭整个服务器?
解决方案
问题根源
SystemError属于Python内置的严重异常类型,子进程抛出该异常后,会被ProcessingPool传递到主进程,触发Python默认的异常处理逻辑,进而导致FastAPI服务器终止。另外,主进程未对SIGTERM信号做防护,若信号误发送到主进程也会直接终止服务。
修改步骤
- 替换异常类型:用自定义异常替代
SystemError,避免触发Python的严重异常处理流程 - 加固主进程信号防护:给主进程添加
SIGTERM空处理器,防止误杀 - 优化异常捕获逻辑:确保父进程能精准捕获子进程的终止信号异常,返回指定值
修改后的代码
子进程代码(sub.py)
import signal import os import time # 定义自定义终止异常 class TaskStoppedError(Exception): pass def loop(i): pid = os.getpid() with open("./pid.txt", "w") as f: f.write(str(pid)) while i < 30: i += 1 print(f"sleep {i}") time.sleep(1) # 增加睡眠,确保任务有足够时间被终止 return i def run_loop(i): def sigterm_handler(_signo, _stack_frame): print("stop!!!!!!!!!!!!") raise TaskStoppedError("任务被手动终止") signal.signal(signal.SIGTERM, sigterm_handler) try: return loop(i) except TaskStoppedError: # 重新抛出异常,确保父进程能捕获 raise
主进程代码(test.py)
from fastapi import FastAPI from fastapi.responses import JSONResponse from pathos.multiprocessing import ProcessingPool import os import uvicorn import signal from sub import run_loop, TaskStoppedError app = FastAPI() def run_py(**kwargs): i = kwargs['i'] try: out = ProcessingPool().map(run, [[i]])[0] except TaskStoppedError: out = 114514 # 保留兜底异常捕获,处理其他意外情况 except Exception: out = 114514 return out def run(args): return run_loop(args[0]) @app.post("/run") def run_main(): i = 1 out = run_py(i=i) return JSONResponse({"code": 200, "message": out}) @app.post("/stop") def stop(): try: with open("./pid.txt", "r") as f: pid = int(f.readline()) os.kill(pid, signal.SIGTERM) return JSONResponse({"code": 200, "message": "已发送终止信号"}) except FileNotFoundError: return JSONResponse({"code": 400, "message": "未找到运行中的任务"}) except ProcessLookupError: return JSONResponse({"code": 400, "message": "任务进程不存在"}) if __name__ == "__main__": # 给主进程添加SIGTERM空处理器,防止误触发终止 def ignore_sigterm(_signo, _frame): pass signal.signal(signal.SIGTERM, ignore_sigterm) uvicorn.run(app='test:app', host="0.0.0.0", port=1145, reload=False)
效果说明
- 子进程触发
SIGTERM后抛出自定义异常,父进程捕获后返回114514,不会触发主进程的终止逻辑 - 主进程的
SIGTERM空处理器确保即使信号误发送到主进程,服务也不会停止 loop函数增加time.sleep(1),让任务有足够时间被终止,便于测试验证
内容的提问来源于stack exchange,提问作者Tony ZHAO
相关产品推荐
相关产品推荐

