这种文件locking方法可以接受吗?

我们有10个Linux机器,每周必须运行100个不同的任务。 这些计算机主要在晚上在家工作。 我的一个同事正在开发一个项目,通过使用Python自动启动任务来优化运行时间。 他的程序将读取任务列表,抓取一个打开的任务,将该任务标记为正在进行的文件中,然后一旦任务完成,在文件中将任务标记为完成。 任务文件将在我们的networking上安装。

我们意识到不build议让一个程序的多个实例访问同一个文件,但是我们没有看到任何其他选项。 当他在寻找一种防止两台计算机同时写入文件的方法时,我提出了一种我自己的方法,比我们在网上find的方法更容易实现。 我的方法是检查文件是否存在,如果不存在则等待几秒钟,如果存在则临时移动文件。 我写了一个脚本来testing这个方法:

#!/usr/bin/env python import time, os, shutil from shutil import move from os import path fh = "testfile" fhtemp = "testfiletemp" while os.path.exists(fh) == False: time.sleep(3) move(fh, fhtemp) f = open(fhtemp, 'w') line = raw_input("type something: ") print "writing to file" f.write(line) raw_input("hit enter to close file.") f.close() move(fhtemp, fh) 

在我们的testing中,这个方法已经工作了,但是我想知道是否可能有一些我们没有看到的问题。 我意识到,两台电脑同时运行exists()可能会导致灾难。 两台计算机不可能同时达到这一点,因为任务在20分钟到8小时之间。

你已经基本上开发了二进制信号量(或互斥体)的文件系统版本。 这是一个用于锁定的研究很好的结构,所以只要你的实现细节正确,它应该工作。 诀窍是获得“测试和设置”的操作,或者在你的情况下“检查存在和移动”是真正的原子。 为此,我会使用这样的东西:

 lock_acquired = False while not lock_acquired: try: move(fh, fhtemp) except: sleep(3) else: lock_acquired = True # do your writing move(fhtemp, fh) lock_acquired = False 

这个程序在大多数情况下都能正常工作,但是如前所述,如果另一个进程在检查其存在和move的调用之间移动文件,则可能会有问题。 我想你可以解决这个问题,但我个人建议坚持一个经过充分测试的互斥体算法。 (我已经把Andrew Tanenbaum的Modern Operating Systems的代码示例翻译/移植了出来,但是我可能会在转换中引入错误 – 只是公正的警告)

顺便说一句,在Linux上的open功能的手册页提供了这个文件锁定的解决方案:

使用lockfile执行原子文件锁定的解决方案是在同一个文件系统上创建一个唯一的文件(例如,包含主机名和pid),使用link(2)创建一个到lockfile的链接。 如果link()返回0,则锁定成功。 否则,在唯一文件上使用stat(2)来检查它的链接数是否增加到2,在这种情况下,锁也是成功的。

要在Python中实现,可以这样做:

 # each instance of the process should have a different filename here process_lockfile = '/path/to/hostname.pid.lock' # all processes should have the same filename here global_lockfile = '/path/to/lockfile' # create the file if necessary (only once, at the beginning of each process) with open(process_lockfile, 'w') as f: f.write('\n') # or maybe write the hostname and pid # now, each time you have to lock the file: lock_acquired = False while not lock_acquired: try: link(process_lockfile, global_lockfile) except: lock_acquired = (stat(process_lockfile).st_nlinks == 2) else: lock_acquired = True # do your writing unlink(global_lockfile) lock_acquired = False 

如果你不跟踪你的move电话,看看他们是否成功,你永远不会知道你是否是受害者的时间窗口。 请记住,如果有什么可能出错,它将在最坏的时候

而不是使用文件的内容作为标志,也许你可以使用文件名本身? 对于每个任务,将文件“task_waiting_to_run”重命名为“task_running”至“task_complete”。 如果从“task_waiting_to_run”重命名为“task_running”失败,那意味着另一个框首先到达那里。

编辑 :这也是常见的做法,以确定重命名文件的过程。 这样,如果进程在恢复到原来的名称之前死亡,就可以跟踪文件的所有权并决定是否干预。

我插入(勉强测试) ossocket调用来添加此功能。 使用风险自负。


如果两个进程竞争重命名文件,那么首先检查它的存在不会阻止竞争状态; 它只会延迟发生的时间。

如果文件不存在, shutil.move的文档(可悲的是)并不明确抛出一个IOError,但这似乎是一个合理的期望 – 我发现它确实发生在实践中:

 import shutil import os import socket oldname = "foobar.txt" newname = (oldname + "." + socket.gethostbyaddr(socket.gethostname())[0] + "." + str(os.getpid())) i_win = True try: shutil.move(oldname, newname) except IOError, e: print "File does not exist" i_win = False except Exception, e: print e i_win = False if i_win: print "I got it!" 

这意味着只有一个进程可以认为它已经成功地重命名了文件。

文件移动/重命名通常是在大多数操作系统上的原子操作,所以它可能是一个可行的解决方案。

但是,您需要在move添加异常检查并open调用,但是如果其他进程在存在检查和move之间移动文件(或move未完成)。

编辑

总结适合的工作流程:

  1. 问题从A move到A. [myID]
  2. 尝试open A. [myID]
  3. 如果#1或#2失败,我们没有得到锁; 等一下,然后回到#1。 否则,我们得到了锁定,继续。
  4. 进行修改。
  5. 问题从A. [myID]移到A.(不应该失败。)这将释放锁定。

[myID]一个好的选择是进程的PID(如果在多个系统上运行,可能还包括主机)。

在我看来,如果你改变了你的数据结构,你就付出了太多的努力来完成一些简单的事情。 现在你有一个包含任务列表的文件。

如何使任务排队目录而不是每个待处理任务是文件? 然后这个过程就像从“Pending”目录中选择一个任务一样简单, 移动到目录(比如说)“Running”,完成后将任务文件移动到目录“Completed”。 由于文件移动是原子操作,不会有竞争条件(如果移动失败,意味着另一个工作人员只是抢先它,所以拿起下一个任务)。

另外,检查进度与在其中一个目录上发出ls一样简单:-)

依靠网络文件系统进行锁定是一个困扰系统多年的问题(并且通常不会如你所期望的那样工作)

为什么不使用被设计为明确的多用户和事务性的东西,就像数据库系统一样? (我个人喜欢Postgres …)

这可能有点矫枉过正,但是对于这样的事情,这些工作通常很容易理解。 这也使得扩展更容易,以后添加新的功能。