pyinstaller打包pywin32服务时multiprocessing子进程无法正常运行问题
问题表现
- 基于pywin32开发的服务程序经PyInstaller打包后,前台运行时
multiprocessing创建的子进程可正常执行日志写入、休眠逻辑,注册为Windows服务后子进程完全失效,事件日志中看不到子进程输出。
根因说明
问题由三个典型的Windows服务+Python多进程兼容坑导致:
multiprocessing.freeze_support()调用位置错误。Windows平台下multiprocessing默认使用spawn模式启动子进程,会重新拉起主exe程序,通过特殊命令行参数区分主/子进程流程。freeze_support()的作用是识别子进程启动参数,直接跳转执行子进程目标函数,你将这行代码放在服务类的实例方法main()中,子进程启动时根本无法执行到这行逻辑,会直接进入服务启动流程,因缺少服务控制管理器上下文直接崩溃。- Windows服务运行在Session 0隔离会话,子进程直接继承父进程的服务控制管理器句柄、日志句柄时,会因为会话权限隔离无法正常调用系统接口,轻则日志写入失败,重则子进程直接启动异常退出。
- 原服务停止逻辑缺少跨进程同步事件,停止服务时无法通知子进程退出,容易出现服务卡住、子进程残留的问题。
修复步骤
1. 调整代码结构
首先将multiprocessing.freeze_support()移动到程序入口最顶层,在所有业务逻辑、服务初始化代码之前执行,同时显式指定Windows平台下多进程启动模式为spawn,避免默认逻辑兼容问题。
修复后的完整可运行代码如下:
import sys import time import traceback import multiprocessing import win32serviceutil import win32service import servicemanager # !!!必须在所有代码最顶层调用,不能放到函数/类方法内部 multiprocessing.freeze_support() # Windows环境强制指定spawn启动模式,规避服务环境兼容问题 if sys.platform == "win32": multiprocessing.set_start_method("spawn", force=True) def log_and_sleep(): # 子进程内独立调用日志接口,不依赖父进程继承的句柄 try: servicemanager.LogInfoMsg("Child process started") except Exception: pass # 兜底用文件日志,避免Session 0权限导致事件日志写入失败 # 注意路径选择服务账号有权限写入的目录,比如ProgramData下的专属目录 log_path = "C:\\ProgramData\\TestService\\child.log" with open(log_path, "a", encoding="utf-8") as f: f.write(f"Child process running at {time.ctime()}\n") time.sleep(10) def execute(stop_event): proc = multiprocessing.Process(target=log_and_sleep) proc.start() # 循环等待,支持服务停止时主动终止子进程 while proc.is_alive(): proc.join(timeout=1) if stop_event.is_set(): proc.terminate() proc.join() break class TestService(win32serviceutil.ServiceFramework): _svc_name_ = "Test_SERVICE" _svc_display_name_ = "Test_SERVICE" _svc_description_ = "Any Description" def __init__(self, args): super().__init__(args) # 新增停止事件,用于跨进程同步退出信号 self.stop_event = multiprocessing.Event() def SvcStop(self): self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) # 触发停止信号 self.stop_event.set() self.ReportServiceStatus(win32service.SERVICE_STOPPED) servicemanager.LogMsg( servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STOPPED, (self._svc_name_, "Service stopped") ) def SvcDoRun(self): servicemanager.LogMsg( servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTING, (self._svc_name_, "Service started") ) try: execute(self.stop_event) except Exception: servicemanager.LogMsg( servicemanager.EVENTLOG_ERROR_TYPE, servicemanager.PYS_SERVICE_STOPPED, (self._svc_name_, traceback.format_exc()) ) if __name__ == "__main__": if len(sys.argv) == 1: # 无命令行参数时由Windows服务管理器拉起 if not hasattr(sys, "frozen"): print("Error: Must run as PyInstaller packaged executable when started by service manager\n") win32serviceutil.usage() sys.exit(1) servicemanager.Initialize() servicemanager.PrepareToHostSingle(TestService) servicemanager.StartServiceCtrlDispatcher() else: # 命令行模式下处理安装、启动、停止、卸载等指令 win32serviceutil.HandleCommandLine(TestService)
2. 调整PyInstaller打包参数
打包时需要注意以下配置,避免依赖缺失导致运行失败:
- 必须添加隐藏导入参数:
--hidden-import=win32timezone,否则服务启动时会报缺少模块错误 - 不要添加
-w/--windowed无控制台参数,服务运行需要控制台上下文,使用默认控制台模式打包即可 - 提前创建日志存放目录(比如示例中的
C:\ProgramData\TestService),给服务运行账号授予写入权限,避免文件日志写入失败 - 如果子进程用到了第三方依赖,按需添加
--hidden-import参数确保依赖被正确打包
3. 服务安装与验证
- 用管理员权限打开命令提示符,执行
你的程序.exe install安装服务 - 执行
你的程序.exe start启动服务,或在服务管理器中手动启动 - 验证点:
- 服务状态保持“正在运行”,不会启动后立刻退出
- Windows事件日志中可看到服务启动、子进程启动的相关日志
- 配置的文件日志路径下能看到子进程写入的日志记录
- 执行
你的程序.exe stop时服务可正常停止,无残留子进程
内容的提问来源于stack exchange,提问作者li-la-lama
相关产品推荐
相关产品推荐

