如何通过使用try语句避免竞争条件?
在判断一个文件是否存在时,使用try语句是怎么避免“竞争条件”的呢?
我问这个问题是因为有一个点赞很多的回答(更新:它已经被删除)似乎暗示使用os.path.exists()
会创造出一种机会,而这种机会在其他情况下是不存在的。
给出的例子是:
try:
with open(filename): pass
except IOError:
print 'Oh dear.'
但我不明白这和下面的情况相比,怎么就避免了竞争条件:
if not os.path.exists(filename):
print 'Oh dear.'
调用os.path.exists(filename)
是怎么让攻击者能够对文件做一些他们原本做不了的事情呢?
3 个回答
我觉得你问的是一种特定的竞争条件,具体情况是这样的:
- 文件被打开了
- 上下文切换,文件被删除了
- 上下文切换回来,尝试对“打开的”文件进行操作
在这种情况下,你的“保护”方式是把所有处理文件的代码放在一个 try
块里。如果在任何时候文件变得无法访问或者损坏,你的文件操作就可以通过 catch
块优雅地失败,也就是说不会导致程序崩溃。
当然,现代操作系统其实不会让这种情况发生。当一个文件被“删除”时,实际上在所有打开的文件句柄都被关闭(释放)之前,删除操作是不会真正执行的。
这里有一个使用的例子:
try:
with open('filename') as f:
do_stuff_that_depends_on_the_existence_of_the_file(f)
except IOError as e:
print 'Trouble opening file'
如果你以任何方式打开文件,操作系统会确保这个文件是存在的,否则就会报错。如果你是独占访问,其他想要访问这个文件的程序要么会被你阻止,要么会阻止你。
try
只是用来检测打开文件时是否出错或成功,因为在 Python 中,文件输入输出的接口通常没有返回码(而是用异常来处理)。所以,真正回答你的问题,不是 try
避免了竞争条件,而是 open
。在 C 语言中(Python 是基于 C 的),情况基本相同,但没有异常处理。想了解更多,可以查看 这个链接。
注意,你可能想在 try
块中执行依赖于文件访问的代码。一旦你关闭了文件,它的存在就不再有保障了。
调用 os.path.exists
只是给你一个瞬间的快照,告诉你文件可能存在也可能不存在,而一旦 os.path.exists
返回,你就不知道文件的真实状态了。恶意代码或意外的逻辑可能会在你不注意的时候删除或更改文件。这就像你开车前先转头确认路上是否畅通,一旦你转回去,就只能猜测你不再看着的地方发生了什么。保持文件打开可以确保状态的一致性,这是开车时无法做到的,无论是好是坏。:)
你提到的检查文件不存在而不是使用 try/open
的建议仍然不够,因为 os.path.exists
只是快照性质的。不幸的是,我不知道有什么方法可以在所有情况下防止文件在目录中被创建,所以我认为最好是检查文件是否确实存在,而不是它是否缺失。
所谓的竞争条件,其实就是你的程序和其他一些操作文件的代码之间的竞争(竞争条件总是需要至少两个并行的进程或线程,想了解更多可以看看这个链接)。这意味着,使用open()
而不是exists()
可能只在两种情况下真的有帮助:
- 你在检查一个文件的存在性,而这个文件是由某个后台进程创建或删除的(不过,如果你是在一个网络服务器里运行,这通常意味着有很多你的进程在并行处理HTTP请求,所以在网络应用中,即使没有其他程序,竞争条件也是可能发生的)。
- 可能有一些恶意程序在运行,试图通过在你期望文件存在的时刻删除它来让你的代码崩溃。
exists()
只是进行了一次简单的检查。如果文件存在,它可能在exists()
返回True
后的一微秒内就被删除了。如果文件不存在,它可能会立即被创建。
但是,open()
不仅仅是检查文件是否存在,它还会打开文件(并且这两个操作是原子性的,也就是说在检查和打开之间不会发生其他事情)。通常情况下,文件在被某人打开时是不能被删除的。这意味着在with
语句块内,你可以完全放心:文件现在确实存在,因为它已经被打开了。不过,这种情况只在with
语句块内成立,文件在with
块结束后仍然可能会被立即删除,把需要文件存在的代码放在with
里可以确保这段代码不会失败。