这篇讲两个我在单机量化系统上真实踩过的进程管理坑,都在 Windows 环境下、都与"判断一个进程还活着吗"这个动作有关。第一个坑差点把我正在跑的测试直接打断;第二个坑让守护程序误判"服务都活着"。修复代码都在文里,可以直接抄。
os.kill(pid, 0) 不是探活,是发 Ctrl-C现场:我给守护逻辑写了标准的"探活"代码——查文件拿到进程号,然后 os.kill(pid, 0),Python 里这是跨平台惯例(POSIX 下信号 0 表示"只检查不送达")。写完跑测试:测试进程直接被中断退出,像被人按了 Ctrl-C。
排查:在 Windows 上,os.kill(pid, sig) 的语义和 POSIX 完全不同——它没有"信号 0 探测"这个概念,信号值 0 被解释为 CTRL_C_EVENT,并且会发给同控制台进程组。也就是说:你以为在"轻轻敲门",实际在给整组进程群发 Ctrl-C。测试进程作为同组进程,直接被打断。
复现(10 行代码):
import os # 危险示范:在 Windows 下执行会向同控制台进程组发送 CTRL_C_EVENT os.kill(os.getpid(), 0) # 别在真实测试里跑这行!
修复:用 ctypes 调 Win32 API 做只读探活(无新依赖,纯标准库):
def pid_alive(pid: int) -> bool:
if pid <= 0:
return False
if os.name == "nt":
import ctypes
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
STILL_ACTIVE = 259
k32 = ctypes.windll.kernel32
handle = k32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
if not handle: # 打不开 ⇒ 进程不存在
return False
try:
code = ctypes.c_ulong()
k32.GetExitCodeProcess(handle, ctypes.byref(code))
return code.value == STILL_ACTIVE
finally:
k32.CloseHandle(handle)
# POSIX 侧(生产 Linux)继续用信号 0 语义
try:
os.kill(pid, 0)
except PermissionError:
return True
except OSError:
return False
return True
要点:OpenProcess 只请求查询权限(不碰进程本身)、GetExitCodeProcess 返回 STILL_ACTIVE 即活着——纯只读,绝不发送任何东西。
现场:我的守护程序(每分钟跑一次)判断"盯盘服务死没死"用的是心跳文件——服务每轮更新 watcher.heartbeat 的时间戳,守护看到"≤120 秒内更新过"就认为活着。
然后我手动杀掉盯盘进程做演练:守护巡检输出 state: ok——它认为服务还活着。原因是杀进程那一刻,心跳文件才写入几十秒,还在 120 秒的新鲜窗口里。
修复:pid 文件权威 + 状态先记录"是谁"再判断"活没活":
1. 每个服务启动时写 var/run/<name>.pid;
2. 存活判定顺序:有 pid 文件 → 以 pid 为准(进程死了就是死了,心跳再新鲜也忽略);无 pid(旧式启动)→ 才用心跳/端口兜底;
3. 拉起动作也统一走"检测→拉起→等被拉起进程写 pid"的闭环。
修复后的真实演练数据(日志实录):
杀 watch 进程(pid 11076)→ 守护巡检 → 立即拉起新进程(pid 7004)→ 巡检 ok
心跳兜底并没有废弃——它负责另一个场景:某次启动方式没写 pid 文件时,能靠它恢复感知;但只要有 pid 文件,就绝不与它"商量"。
{
"checked_at": "2026-10-09T16:42:19",
"services": {
"watch": {"state": "ok"},
"web": {"state": "ok"},
"content": {"state": "started", "pid": 24240}
}
}
state 三值:ok(活着)/ started(本次已拉起)/ start-error(拉起失败,带了原因)——巡检结果里不允许出现"应该 ok",每一个判断都要落到可验证的字段上。
1. Windows 上永远不要用 os.kill(pid, 0) 探活——用 OpenProcess + GetExitCodeProcess;
2. 存活判定要有"权威源":pid 文件优先,心跳/端口只是兜底;两套判据并存时,写明优先级;
3. 拉起后要验证拉起成功(等被拉起进程写 pid、有超时),而不是"命令没报错就算成功";
4. 守护动作留痕:每次"拉起/失败"写一行结构化日志,正常轮次不刷屏——出问题时日志要能直接回答"几点几分拉起了谁"。
两个坑合起来是一句话:进程管理的每个判断都必须是"可验证的事实",不能是"通常成立的经验"。os.kill 的跨平台差异、心跳的新鲜窗口,都是那种"通常成立"的经验——它们平时不响,出事的时候正好是你最需要守护程序靠谱的时候。
本站内容为个人研究记录与方法分享,模拟,不构成投资建议;据此操作风险自负。
Joyous 的技术笔记 · 沪ICP备2026053770号
隐私最小化:不设 cookie、无表单、无第三方脚本;访问统计仅取服务端日志(≤30 天,仅聚合),不导出 IP。