如何通过使用try语句避免竞争条件?

31 投票
3 回答
15135 浏览
提问于 2025-04-17 14:00

在判断一个文件是否存在时,使用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 个回答

0

我觉得你问的是一种特定的竞争条件,具体情况是这样的:

  1. 文件被打开了
  2. 上下文切换,文件被删除了
  3. 上下文切换回来,尝试对“打开的”文件进行操作

在这种情况下,你的“保护”方式是把所有处理文件的代码放在一个 try 块里。如果在任何时候文件变得无法访问或者损坏,你的文件操作就可以通过 catch 块优雅地失败,也就是说不会导致程序崩溃。

当然,现代操作系统其实不会让这种情况发生。当一个文件被“删除”时,实际上在所有打开的文件句柄都被关闭(释放)之前,删除操作是不会真正执行的。

10

这里有一个使用的例子:

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 只是快照性质的。不幸的是,我不知道有什么方法可以在所有情况下防止文件在目录中被创建,所以我认为最好是检查文件是否确实存在,而不是它是否缺失。

37

所谓的竞争条件,其实就是你的程序和其他一些操作文件的代码之间的竞争(竞争条件总是需要至少两个并行的进程或线程,想了解更多可以看看这个链接)。这意味着,使用open()而不是exists()可能只在两种情况下真的有帮助:

  1. 你在检查一个文件的存在性,而这个文件是由某个后台进程创建或删除的(不过,如果你是在一个网络服务器里运行,这通常意味着有很多你的进程在并行处理HTTP请求,所以在网络应用中,即使没有其他程序,竞争条件也是可能发生的)。
  2. 可能有一些恶意程序在运行,试图通过在你期望文件存在的时刻删除它来让你的代码崩溃。

exists()只是进行了一次简单的检查。如果文件存在,它可能在exists()返回True后的一微秒内就被删除了。如果文件不存在,它可能会立即被创建。

但是,open()不仅仅是检查文件是否存在,它还会打开文件(并且这两个操作是原子性的,也就是说在检查和打开之间不会发生其他事情)。通常情况下,文件在被某人打开时是不能被删除的。这意味着在with语句块内,你可以完全放心:文件现在确实存在,因为它已经被打开了。不过,这种情况只在with语句块内成立,文件在with块结束后仍然可能会被立即删除,把需要文件存在的代码放在with里可以确保这段代码不会失败。

撰写回答