Python的fcntl无法按预期锁定

26
在基于Debian的操作系统(Ubuntu,Debian Squeeze)上,我正在使用Python(2.7,3.2)中的fcntl来锁定文件。根据我所了解的内容,fnctl.flock以一种方式锁定文件,如果另一个客户端想要锁定同一文件,则会引发异常。
我建立了一个小例子,我希望它能抛出异常,因为我先锁定文件,然后立即尝试再次锁定:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import fcntl
fcntl.flock(open('/tmp/locktest', 'r'), fcntl.LOCK_EX)
try:
    fcntl.flock(open('/tmp/locktest', 'r'), fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError:
    print("can't immediately write-lock the file ($!), blocking ...")
else:
    print("No error")

但是这个例子只会打印出“无错误”。

如果我将这段代码分成两个同时运行的客户端(一个锁定然后等待,另一个尝试在第一个锁定已经激活后再次锁定),我得到了相同的行为 - 没有任何影响。

这种行为的解释是什么?

编辑

根据nightcracker的要求进行更改,此版本也会打印出“无错误”,尽管我不会期望那样:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import fcntl
import time
fcntl.flock(open('/tmp/locktest', 'w'), fcntl.LOCK_EX | fcntl.LOCK_NB)
try:
    fcntl.flock(open('/tmp/locktest', 'w'), fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError:
    print("can't immediately write-lock the file ($!), blocking ...")
else:
    print("No error")

3
关于同一进程内文件锁的问题有一个注意点。在单个进程内,线程会共享文件锁。 - Bouke
叮叮叮!谢谢,Bouke。那就是我的问题! - Bill Evans at Mariposa
7个回答

18

虽然这是一篇旧的帖子,但如果有其他人看到此处,以下是我的解答:

>>> fcntl.flock(open('test.flock', 'w'), fcntl.LOCK_EX)
>>> fcntl.flock(open('test.flock', 'w'), fcntl.LOCK_EX | fcntl.LOCK_NB)
# That didn't throw an exception

>>> f = open('test.flock', 'w')
>>> fcntl.flock(f, fcntl.LOCK_EX)
>>> fcntl.flock(open('test.flock', 'w'), fcntl.LOCK_EX | fcntl.LOCK_NB)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: [Errno 35] Resource temporarily unavailable
>>> f.close()
>>> fcntl.flock(open('test.flock', 'w'), fcntl.LOCK_EX | fcntl.LOCK_NB)
# No exception

看起来在第一个案例中,文件在第一行之后就关闭了,可能是因为文件对象无法访问。关闭文件会释放锁定。


2
请注意,自Python 3.3起,fcntl模块中的操作会引发OSError而不是IOError。 - Qlimax

16
我遇到了同样的问题...我通过将打开的文件保存在另一个变量中来解决它:
不起作用:
fcntl.lockf(open('/tmp/locktest', 'w'), fcntl.LOCK_EX | fcntl.LOCK_NB)

作品:

lockfile = open('/tmp/locktest', 'w')
fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)

我认为第一个不起作用是因为打开的文件会被垃圾回收关闭释放锁定


6
请注意,您正在使用lockf,而原帖中的OP使用的是flock。这两个实现非常不同!名称不好,很难抓住;) - Jatin Kumar

12

明白了。我的脚本错误在于每次调用时我都创建了一个新的文件描述符:

fcntl.flock(open('/tmp/locktest', 'r'), fcntl.LOCK_EX | fcntl.LOCK_NB)
(...)
fcntl.flock(open('/tmp/locktest', 'r'), fcntl.LOCK_EX | fcntl.LOCK_NB)

相反,我必须将文件对象分配给一个变量,然后尝试锁定:

f = open('/tmp/locktest', 'r')
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
(...)
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)

接下来我也得到了我想要看到的异常:IOError: [Errno 11] Resource temporarily unavailable。现在我必须考虑在哪些情况下使用fcntl才有意义。


26
要明确的是,错误不在于每次调用都创建一个文件描述符,而是先前的文件描述符已被垃圾回收(并且先前的锁也随之消失)。如果您将这两个文件描述符保存到不同的变量中,脚本将正常工作。 - Dustin Boswell
2
这个答案是误导性的。上面@Dustin的评论是正确的。 - Sam Watkins

6
有两个需要注意的地方。根据文档:
  1. 当操作为LOCK_SHLOCK_EX时,还可以与LOCK_NB按位OR以避免在获取锁时阻塞。如果使用LOCK_NB并且无法获取锁,则会引发IOError,并且异常将具有设置为EACCESEAGAIN(取决于操作系统; 为了可移植性,请检查这两个值)的errno属性。

    您忘记设置LOCK_NB

  2. 在至少某些系统上,仅当文件描述符引用打开用于写入的文件时,才能使用LOCK_EX

    您已经打开了一个用于读取的文件,在您的系统上可能不支持LOCK_EX


谢谢。但是这两个更改(在第一个锁定中添加fcntl.LOCK_NB并且以写模式打开文件)并没有改变行为,仍然收到“无错误”的消息。 - Wolkenarchitekt
1
@ifischer:在你的系统上,C语言中的 fcntl 是否有效?你使用的Python版本是什么? - orlp
尝试了Python 2.7.1和Python 3.2。稍后会尝试C语言。 - Wolkenarchitekt

3

如果需要了解不同的锁定方案的更多详细信息,请参考此文章
至于您的第二个问题,使用fcntl可以在不同进程之间获取锁(为了简单起见,可以使用lockf)。在Linux上,lockf只是fcntl的一个包装器,两者都与(pid,inode)配对相关联。
1. 使用fcntl.fcntl提供跨进程的文件锁定。

import os
import sys
import time
import fcntl
import struct


fd = open('/etc/mtab', 'r')
ppid = os.getpid()
print('parent pid: %d' % ppid)
lockdata = struct.pack('hhllh', fcntl.F_RDLCK, 0, 0, 0, ppid)
res = fcntl.fcntl(fd.fileno(), fcntl.F_SETLK, lockdata)
print('put read lock in parent process: %s' % str(struct.unpack('hhllh', res)))
if os.fork():
    os.wait()
    lockdata = struct.pack('hhllh', fcntl.F_UNLCK, 0, 0, 0, ppid)
    res = fcntl.fcntl(fd.fileno(), fcntl.F_SETLK, lockdata)
    print('release lock: %s' % str(struct.unpack('hhllh', res)))
else:
    cpid = os.getpid()
    print('child pid: %d' % cpid)
    lockdata = struct.pack('hhllh', fcntl.F_WRLCK, 0, 0, 0, cpid)
    try:
        fcntl.fcntl(fd.fileno(), fcntl.F_SETLK, lockdata)
    except OSError:
        res = fcntl.fcntl(fd.fileno(), fcntl.F_GETLK, lockdata)
        print('fail to get lock: %s' % str(struct.unpack('hhllh', res)))
    else:
        print('succeeded in getting lock')

2. 使用 fcntl.lockf

import os
import time
import fcntl

fd = open('/etc/mtab', 'w')
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
if os.fork():
    os.wait()
    fcntl.lockf(fd, fcntl.LOCK_UN)
else:
    try:
        fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except IOError as e:
        print('failed to get lock')
    else:
        print('succeeded in getting lock')

0

您需要传入文件描述符(可通过调用文件对象的fileno()方法获得)。当在单独的解释器中运行相同代码时,下面的代码会抛出IOError异常。

>>> import fcntl
>>> thefile = open('/tmp/testfile')
>>> fd = thefile.fileno()
>>> fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)

这应该是不必要的。根据文档,在文件描述符fd上执行锁操作op(接受提供fileno()方法的__file对象)。 - orlp
应用这个并不改变行为,仍然得到“无错误”的结果,与我预期的相反。 - Wolkenarchitekt
1
@ifischer:奇怪,我将上面的代码粘贴到Ubuntu机器上的两个Python解释器中,第一个完成了,第二个抛出了异常。 - Vatine

-2

尝试:

global f
f = open('/tmp/locktest', 'r')

当文件关闭时,锁将消失。


2
如果是两个不同的进程,那么使用它绝对没有意义。应该使用fcntl.flock。 - theBuzzyCoder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接