subprocess.Popen close_fds=True导致执行命令耗时长
一、背景
在使用subprocess.Popen调用系统命令时,如果配置close_fds=True,命令执行耗时会显著加长。
首先,我们看下一个普通的ls命令,耗时只需要0.002秒。
[root@wenf-dev subprocess_test]# time ls
subprocess_close_fds.py
real 0m0.002s # 直接执行,耗时0.002s
user 0m0.000s
sys 0m0.002s
然后,subprocess.Popen,配置close_fds=True,执行ls命令,耗时需要整整0.06秒(0.002秒的30倍)。
[root@wenf-dev subprocess_test]# cat subprocess_close_fds.py
import subprocess
import time
start = time.time()
_PIPE = subprocess.PIPE
obj = subprocess.Popen(["ls"],
stdin=_PIPE,
stdout=_PIPE,
stderr=_PIPE,
close_fds=True)
result = obj.communicate()
print "cost time: %s" % (time.time() - start)
[root@wenf-dev subprocess_test]# python subprocess_close_fds.py
cost time: 0.0697808265686
最后,我们把close_fds=False,耗时又再次只需要0.002秒了。
[root@wenf-dev subprocess_test]# cat subprocess_close_fds.py
import subprocess
import time
start = time.time()
_PIPE = subprocess.PIPE
obj = subprocess.Popen(["ls"],
stdin=_PIPE,
stdout=_PIPE,
stderr=_PIPE,
close_fds=False)
result = obj.communicate()
print "cost time: %s" % (time.time() - start)
[root@wenf-dev subprocess_test]# python subprocess_close_fds.py
cost time: 0.0025429725647 # 设置close_fds=False,执行耗时0.002s
二、问题定位
subprocess.Popen执行系统命令时,总的来说分为七步:
-
创建四个管道,分别用于标准输入(p2c_pipe,fd 0)、标准输出(c2p_pipe,fd 1)、标准错误(err_pipe,fd 2)以及os.execvp执行失败的异常(execvp_err_pipe)
-
fork子进程
-
如果close_fds=True,子进程关闭除了0,1,2之外的所有fd
-
子进程os.execvp执行系统命令
-
父进程持续从管道execvp_err_pipe中读取数据
a. 有异常时,execvp_err_pipe读取到子进程返回的异常信息,结束循环
b. 子进程成功执行os.execvp,关闭管道execvp_err_pipe,os.read返回空,结束循环
-
父进程通过poll/select持续监听c2p_pipe、err_pipe两个fd,收到数据后则结束循环
-
父进程调用waitpid(子进程PID),获取子进程返回码,整改过程结束
subprocess.Popen调用系统命令比较慢的原因就在于第三步关闭fd耗时。
在第三步中,关闭的fd数量直接取决于进程的最大文件句柄数。每个进程的最大文件句柄数,默认值是1024。
通过ulimit -n可以查看当前配置。
一些应用会把这个值调高成百万级别,甚至千万级别。
这时候,下面的close_fd函数耗时将直线上升,最终导致subprocess.Popen调用系统命令耗时比较长。
参考:https://bugs.python.org/issue11284
try:
MAXFD = os.sysconf("SC_OPEN_MAX")
except:
MAXFD = 256
def _close_fds(self, but):
if hasattr(os, 'closerange'):
os.closerange(3, but)
os.closerange(but + 1, MAXFD)
else:
for i in xrange(3, MAXFD):
if i == but:
continue
try:
os.close(i)
except:
pass
那子进程为什么需要关闭fd呢 ?
这是因为,子进程会继承父进程所有的fd信息。
引用《UNIX系统编程手册》24.2节
执行 fork()时,子进程会获得父进程所有文件描述符的副本。这些副本的创建方式类似于
dup(),这也意味着父、子进程中对应的描述符均指向相同的打开文件句柄(即 open file
description,详见 5.4 节译注)。正如 5.4 节所述,打开文件句柄包含有当前文件偏移量(由 read()、
write()和 lseek()修改)以及文件状态标志(由 open()设置,通过 fcntl()的 F_SETFL 操作改变)。
一个打开文件的这些属性因之而在父子进程间实现了共享。举例来说,如果子进程更新了文
件偏移量,那么这种改变也会影响到父进程中相应的描述符。
三、解决方案
既然耗时的长短跟最大文件句柄数有关,那么我们只要避免配置过大的值,就能避免遇上这个问题。
echo "root soft nofile 655350" >> /etc/security/limits.conf
echo "root hard nofile 655350" >> /etc/security/limits.conf

浙公网安备 33010602011771号