那天一个开发者跟我说,他写个自动化部署脚本,直接用os.system去调用外部压缩工具处理日志文件,结果程序卡在执行中,最后不得不重启服务器才恢复正常。那一刻大家都傻眼,这不是小问题,而是会让整台服务停摆的隐形炸弹,我当时心里堵得慌。
老办法很多人还在用,像os.system、os.popen,这两样有公开的缺陷,常被为os.system的三大核心问题。最直观的一条是命令注入风险,假如把用户输入拼到字符串里,碰到类似"../; rm -rf /"这样的输入,后果会很严重。51CTO博客上也写过,像Ultralytics这类现代项目直接禁用os.system,转而用subprocess。
死锁这个事儿更常见。子进程输出到管道,但主进程没及时读,管道缓冲区满了就阻塞,进程卡死。阿里云安全文档里有专门一节讲这个问题,建议使用communicate代替手动读管道。我的理解很直白,管道像水管,流不了水就塞住,大家都得等着。
Python从3.5开始把subprocess.run定为推荐用法,subprocess.run会阻塞直到外部命令结束,返回一个CompletedProcess对象。简单调用时,我会把命令和参数用列表传进去,像用['tar','-czf','log.tar.gz','/var/log']这种形式,避免字符串拼接带来的注入风险,实用又安全。
如果你需要实时查看输出,或者做复杂的管道通信,就该用Popen接口配合communicate来收发数据。Popen可以非阻塞地启动进程,配合超时和异常捕获,能把响应时间控制住。阿里云的例子里就提到,生产环境监控日志时用Popen能把延迟从几分钟降到秒级响应。
我跟那位开发者聊时,他说当晚重启服务器后就改了代码,改成subprocess.run加上timeout,他还发给我那段日志截图。我看着截图里有错误信息跟警告,心里一紧,觉得这种事不该等到停服才改。写脚本的人,你们都知道,线上出问题叫得最响的,就是那会儿运维被连轴喊起来的夜。
成三点实用规则给你参考。第一条,永远用列表形式传参,一条原则。第二条,禁用shell=True,除非你能完全控制所有输入。第三条,所有外部调用都设超时并捕获异常,别相信它会一直好着。记住这些,就能把常见问题扼杀在摇篮里。
再往深了学,你可以看asyncio.subprocess的异步方案,把外部命令放进事件循环里并发执行,适合需要同时管理几十个子进程的场景。现在很多团队把这当成下一步技能,毕竟单线程的阻塞太浪费资源。
写到这儿,我想提醒一句,技术细节不是学会一两条就完事,真正能把风险降下来是把规范写进CI和代码审查里。那位开发者后来把模板脚本发到团队仓库,规定必须用subprocess.run或Popen,并且在提交时跑静态检查。我看了那次提交记录,改动不到20行,但上线后同类故障率下降明显,大家都松了口气。
你在写部署脚本时,会先检查命令注入问题,还是先把超时和异常写上去?评论里说说你常用的防护办法,我想把大家的经验汇总起来。