一、背景
我有个批处理脚本如下:
@echo off
chcp 936 & cls
cd /D F:\Chen\python3\ExciseC
set fdate=%date:~0,4%%date:~5,2%%date:~8,2%
python main.py >> crawl_record_%fdate%.log 2>&1
for /F %%f in ('dir crawl_record_*.log /B ^| find /V "%fdate%"') do move %%f archived\logs
其中,for /F
语句是将除当天之外的log文件,移动到archived\logs
,然后这个批处理是在任务计划中定期跑的。
运行一段时间后,偶尔发现仍有当天之外的log文件未被移动到archived\logs
,后来定位发现,文件是被python.exe
程序给占用了,推测可能是python main.py >> crawl_record_%fdate%.log 2>&1
这个条语句在执行过程中非正常结束(如重启、断网等),导致重定向符后的文件被锁定,然后第2天,这个文件无法正常移动。
然后我的需求出现了:找到占用这个文件的进程,然后杀死它,释放被占用的文件,再进行移动操作该文件。(这些步骤都要在批处理内完成,而非图形界面操作)
二、解决问题
1. 查找占用指定文件的进程pid
这里使用handle.exe
命令程序,handle.exe
是Sysinternals Suite
中的进程实用程序。
它可用于显示系统中任何进程的打开句柄的信息。可以使用它来查看打开了文件的程序,或者查看程序的所有句柄的对象类型和名称。
帮助文档:https://learn.microsoft.com/en-us/sysinternals/downloads/handle
下载地址:https://download.sysinternals.com/files/Handle.zip
下载并解压出 handle.exe
/ handle64.exe
应用程序,然后把它放到path环境变量里的某个路径下。
handle /v name /nobanner
/v
使输出结果为逗号分隔符的csv格式,name
要搜索的被进程打开的文件名称(接受片段),/nobanner
不显示启动banner和版权信息。更多信息请查看handle /?
帮助信息。
输出结果结构如下所示:
C:\Users\cyinl>handle /v /nobanner crawl_record_20230722.log
Process,PID,Type,Handle,Name
python.exe,23156,File,0x000001E0,F:\test\crawl_record_20230722.log
注意:
tasklist
也可以查看进程pid,但是它只能按照 exe/dll 模块名来查找,命令格式:
tasklist /M 模块名
,像示例中的crawl_record_20230722.log
这种文件,是查不到相关进程的。
2. 终止进程
handle.exe
可以终止进程,格式:
handle /nobanner /p PID /c <句柄号>
但是,句柄号不太好拿到,如下:
C:\Users\cyinl>handle /nobanner /p 2315640: File D:\Chen\MySoft\Python\Python3.7.71D0: File C:\Windows\System32\zh-CN\KernelBase.dll.mui1E0: File F:\test\crawl_record_20230722.log208: File C:\Windows\System32\zh-CN\kernel32.dll.mui
第1行的40,应该就是要关闭的进程文件的句柄号,但是能确保要关闭的就是第1行的句柄号???有些困惑。
taskkill
命令也可以终止进程,格式:
taskkill /f /pid <进程号>
/f
表示强制终止进程,/pid
指定要终止进程的pid进程号
输出结果如下示例:
C:\Users\cyinl>taskkill /f /pid 23156
成功: 已终止 PID 为 23156 的进程。
3. 实现代码
有handle
、taskkill
两个命令,改进批处理,来解决问题了。具体实现如下:
@echo off
chcp 936 & cls
cd /D F:\Chen\python3\ExciseC
set fdate=%date:~0,4%%date:~5,2%%date:~8,2%
python main.py >> crawl_record_%fdate%.log 2>&1
for /F %%f in ('dir crawl_record_*.log /B ^| find /V "%fdate%"') do move %%f archived\logsREM After the above command is executed, if the files are successfully moved, they will not enter the loop of this command
for /F %%f in ('dir crawl_record_*.log /B ^| find /V "%fdate%"') do (echo the file to move is locked: %%ffor /F "skip=1 tokens=2 delims=," %%p in ('handle /v %%f /nobanner') do taskkill /F /PID %%pREM If the move command is executed immediately after the process is deleted, it will fail. Therefore, a 5-second delay is given heretimeout /T 5 /nobreak > nulmove %%f archived\logs
)
解释:
- 如果第1个
for /F
能正常移动文件的话,就不会进入第2个for /F
;- 第2个
for /F
用来查找被占用的log文件,嵌套的第3个for /F
获取进程pid,其中skip
表示跳过第1行,delims=,
表示使用,
将数据按列分隔,tokens=2
表示取第2列。然后do
结构体内使用taskkill
命令将查到的pid对应的进程终止掉。- 测试发现,终止进程后,不能立刻去移动文件,否则可能会失败,因此这里用
timeout
命令给了5秒延时,然后再去移动文件。
三、拓展
手动查找并终止进程
1. 使用系统自带的资源监视器
1)在任务栏搜索资源监视器
,切换到CPU
选项卡,在关联的句柄
-搜索句柄
输入框中,输入被占用的文件名称;
2)在搜索结果中,选中需要终止的进程,右键,选择终止进程
即可
2. 使用Process Explorer
工具
你也可以使用 Process Explorer ,它是基于handle
的GUI版本,Process Explorer
可查看有关哪些句柄和DLL进程已打开或加载的信息。
下载地址:https://download.sysinternals.com/files/ProcessExplorer.zip
解压后直接打开procexp.exe
/procexp64.exe
即可使用。
1)点击搜索
图标,输入待查找的文件名称;
2)在搜索结果中,点击搜索结果,会自动定位到相关进程以及文件句柄;
3)右键对应的进程-kill Process
或者 右键对应的文件句柄-close Handle