Windows 进程溯源(谁启动的?为什么杀了又活?)
0. 三条铁律(踩过坑,先看这个)
-
禁止靠窗口标题推断"是谁启动的"
tasklist /V的窗口标题只说明"这个进程显示什么标题",不能说明"谁启动了它"。
→ 真实案例:看到窗口标题是自家程序名,就断定"用户刚刚启动了它",结论完全错误(实际是守护脚本自动重启的,用户什么都没做)。
→ 必须查创建时间。 -
"杀了又活"先找守护脚本,别硬杀
Windows 上常见 keep-alive 写法:
bat :loop "C:\path\to\python.exe" -u script.py >> log.out 2>&1 timeout /t 5 /nobreak >nul goto loop
→ 必须先杀守护脚本的 cmd 窗口,再杀目标进程。顺序反了永远杀不干净(5 秒后又起来)。
→ 识别线索:目标进程的父进程是cmd.exe,就去翻对应的 .bat 内容。 -
强杀(
taskkill /F)不会走 try/finally
进程的清理逻辑(删 pid 文件、还原代理、写退出日志)全被跳过 → 手动清理残留。
1. 一键排查(推荐直接用脚本)
本 skill 附 scripts/proc_info.py,纯 stdlib + ctypes,不需要管理员权限:
# 列出关心的进程:PID / 父链 / 创建时间 / 窗口标题
python scripts/proc_info.py
# 只看某类进程
python scripts/proc_info.py --name python.exe,cmd.exe,node.exe
# 追溯某个 pid 的来源(父链一路到根)
python scripts/proc_info.py --who 11864
输出示例:
python.exe(16944) [窗口:抖贝控券] 启动=2026-09-11 15:23:05 ← cmd.exe(120) ← explorer.exe(3964)
python.exe(11864) [窗口:—] 启动=2026-09-12 09:06:38 ← cmd.exe(900) ← explorer.exe(3964)
读法:
- 父链到 explorer.exe = 桌面双击启动(或某个 bat)
- 父链到 cmd.exe = 某个 .bat 启动 → 去看那个 bat 的内容
- 两个进程创建时间相同 = 同一批启动(例如同一个 bat 的连续步骤)
- 创建时间是昨晚/很久以前 = 不是刚刚启动的(别误判成用户操作)
2. 手工查(脚本不够用时)
import ctypes, ctypes.wintypes as w
from datetime import datetime, timezone, timedelta
k = ctypes.windll.kernel32
class FILETIME(ctypes.Structure):
_fields_ = [('dwLowDateTime', w.DWORD), ('dwHighDateTime', w.DWORD)]
def proc_creation_time(pid):
"""拿进程创建时间(本地时间)。ctypes 实现,无需管理员。"""
h = k.OpenProcess(0x1000, False, pid) # PROCESS_QUERY_LIMITED_INFORMATION
if not h:
return None
c, e, kt, u = FILETIME(), FILETIME(), FILETIME(), FILETIME()
ok = k.GetProcessTimes(h, ctypes.byref(c), ctypes.byref(e),
ctypes.byref(kt), ctypes.byref(u))
k.CloseHandle(h)
if not ok:
return None
v = (c.dwHighDateTime << 32) | c.dwLowDateTime
dt = datetime(1601, 1, 1) + timedelta(microseconds=v // 10) # FILETIME 起点
return dt.replace(tzinfo=timezone.utc).astimezone().replace(tzinfo=None)
def proc_tree():
"""返回 {pid: (ppid, exe_name)},用于追溯父链。"""
class PE32W(ctypes.Structure):
_fields_ = [('dwSize', w.DWORD), ('cntUsage', w.DWORD),
('th32ProcessID', w.DWORD),
('th32DefaultHeapID', ctypes.c_void_p),
('th32ModuleID', w.DWORD), ('cntThreads', w.DWORD),
('th32ParentProcessID', w.DWORD),
('pcPriClassBase', ctypes.c_long), ('dwFlags', w.DWORD),
('szExeFile', ctypes.c_wchar * 260)]
snap = k.CreateToolhelp32Snapshot(0x2, 0) # TH32CS_SNAPPROCESS
e = PE32W(); e.dwSize = ctypes.sizeof(PE32W)
out = {}
while k.Process32NextW(snap, ctypes.byref(e)):
out[e.th32ProcessID] = (e.th32ParentProcessID, e.szExeFile)
k.CloseHandle(snap)
return out
3. 干净的收工顺序(模板)
# 1) 先查:有没有守护脚本?(看目标进程的父进程是不是 cmd.exe,再读那个 bat)
# 2) 先杀守护脚本(cmd 窗口),连带它的子进程树
taskkill /PID <守护cmd的pid> /T /F
# 3) 再杀目标进程(可能已随树死亡,报"找不到进程"属正常)
taskkill /PID <目标pid> /T /F
# 4) 手动清理残留(强杀不走 finally)
# - pid 文件、锁文件、代理设置、临时标志文件
# 5) 等 5~10 秒后复查:确认没有"新实例"冒出来
第 5 步不能省 —— 它是验证守护脚本真的死了的唯一手段。顺手写个复查:
import subprocess, time
time.sleep(8)
r = subprocess.run(['tasklist', '/FI', 'IMAGENAME eq python.exe'],
capture_output=True, text=True, encoding='mbcs', errors='ignore')
print(r.stdout) # 确认没有新的目标进程
4. 附带坑
| 坑 | 说明 |
|---|---|
| PowerShell 工具输出被吞 | 本机实测 Get-CimInstance / Format-Table 常常返回空 → 改用 Python ctypes 更可靠 |
wmic / reg.exe 被黑名单拦 |
安全策略拦截 → 用 Python (ctypes / winreg) 替代 |
| 窗口标题会骗人 | 自家程序和第三方程序可能同名(如都叫「抖贝控券」)→ 看进程名(python.exe vs 抖贝控券.exe)+ 创建时间 |
tasklist 编码 |
加 encoding="mbcs", errors="ignore",否则中文 Windows 上乱码 |
5. 排查决策树
进程在跑,但"不知道谁起的" / "杀了又活"
│
├─ 查创建时间(proc_creation_time)
│ ├─ 很久以前 → 不是刚启动的,别当成"用户操作"
│ └─ 刚刚 → 继续
│
├─ 查父链(proc_tree)
│ ├─ 父是 explorer.exe → 桌面双击(bat/快捷方式/手工)
│ ├─ 父是 cmd.exe → 去读那个 bat 的内容
│ │ └─ 含 ":loop" + "goto loop" → ★ keep-alive 守护脚本
│ │ → 必须"先杀守护、再杀目标"
│ └─ 父是某编辑器/IDE/终端 → 那边启动的
│
└─ 杀完之后
└─ 等 8 秒复查 → 又冒出来?守护脚本没杀干净