【问题标题】:Opening a file on Windows with exclusive locking in Python在 Windows 上使用 Python 中的独占锁定打开文件
【发布时间】:2020-06-27 18:25:53
【问题描述】:

我有一个与this 问题非常相似的问题,我需要满足以下条件:

  • 如果打开文件以供读取,则该文件只能被任何其他进程/程序打开以供读取
  • 如果打开文件以进行写入,则该文件只能被任何其他进程/程序打开以进行读取

链接问题中发布的解决方案使用第三方库,该库将任意.LOCK 文件添加到与相关文件相同的目录中。这是一个仅适用于正在使用该库的程序的解决方案,并且不会阻止任何其他进程/程序使用该文件,因为它们可能无法用于检查 .LOCK 关联。

本质上,我希望仅使用 Python 的标准库来复制 this 结果。

BLUF:需要一个特定于 Windows 的标准库实现,用于独占文件锁定

举一个问题集的例子,假设有:

  • 共享网络/驱动器上的 1 个文件
  • 2 个用户在不同的进程/程序上

假设用户 1 正在文件上运行程序 A,并在某个时间点执行以下操作:

with open(fp, 'rb') as f:
    while True:
        chunk = f.read(10)
        if chunk:
            # do something with chunk
        else:
            break 

因此他们一次遍历文件 10 个字节。

现在用户 2 稍后在同一个文件上运行程序 B:

with open(fp, 'wb') as f:
    for b in data:  # some byte array
        f.write(b)

在 Windows 上,有问题的文件会立即被截断,程序 A 停止迭代(即使没有完成),程序 B 开始写入文件。因此,我需要一种方法来确保文件不会以其他模式打开,如果之前打开会改变其内容。

我在看msvcrt 库,即msvcrt.locking() 接口。我成功的做法是确保可以锁定打开以供阅读的文件,但没有其他人可以读取该文件(因为我锁定了整个文件):

>>> f1 = open(fp, 'rb')
>>> f2 = open(fp, 'rb')
>>> msvcrt.locking(f1.fileno(), msvcrt.LK_LOCK, os.stat(fp).st_size)
>>> next(f1)
b"\x00\x05'\n"
>>> next(f2)
PermissionError: [Errno 13] Permission denied

这是一个可以接受的结果,只是不是最想要的。

在同一场景中,用户 1 运行程序 A,其中包括:

with open(fp, 'rb') as f
    msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, os.stat(fp).st_size)
    # repeat while block
    msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, os.stat(fp).st_size)

然后用户 2 稍后运行程序 B,出现相同的结果并且文件被截断。

此时,我希望有一种方法向用户 2 抛出错误,说明文件已打开以在其他地方读取,此时无法写入。但是如果用户 3 过来打开文件进行阅读,那就没有问题了。

更新:

一个潜在的解决方案是更改文件的权限(如果文件已在使用中,则捕获异常):

>>> os.chmod(fp, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
>>> with open(fp, 'wb') as f:
        # do something
PermissionError: [Errno 13] Permission denied <fp>

这感觉不是最好的解决方案(特别是如果用户没有权限甚至更改权限)。仍在寻找适当的锁定解决方案,但msvcrt 不会阻止文件被锁定以供读取时截断和写入。似乎仍然没有办法使用 Python 的标准库生成独占锁。

【问题讨论】:

  • 如果只是Windows,可以调用CreateFile(如PyWin32的win32file.CreateFile),将共享模式设置为所需的读/执行、写/追加、删除/重命名共享。通过msvcrt.open_osfhandle 用文件描述符包装它返回的文件句柄。然后通过open打开文件描述符。
  • @ErykSun 但这不是 Python 标准库实现吗?它需要 PyWin32。
  • 标准库有 ctypes。假设您设置了函数原型并正确处理错误和异常以使其具有惯用性,那么使用 ctypes 实现它需要做更多的工作。
  • @ErykSun 是的,这就是我目前正在走的路。
  • @ErykSun 虽然它按预期工作(我将发布一个解决方案),但奇怪的是CreateFileW 不会抛出FileNotFoundError。如果路径不存在,则返回-1,然后msvcrt.open_osfhandle 返回OSError: Bad file descriptor。根据 MSDN 文档,我原以为会引发前一个错误。

标签: python windows file locking


【解决方案1】:

对于那些对 Windows 特定解决方案感兴趣的人:

import os
import ctypes
import msvcrt
import pathlib

# Windows constants for file operations
NULL = 0x00000000
CREATE_ALWAYS = 0x00000002
OPEN_EXISTING = 0x00000003
FILE_SHARE_READ = 0x00000001
FILE_ATTRIBUTE_READONLY = 0x00000001  # strictly for file reading
FILE_ATTRIBUTE_NORMAL = 0x00000080  # strictly for file writing
FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000
GENERIC_READ = 0x80000000
GENERIC_WRITE = 0x40000000

_ACCESS_MASK = os.O_RDONLY | os.O_WRONLY
_ACCESS_MAP = {os.O_RDONLY: GENERIC_READ,
               os.O_WRONLY: GENERIC_WRITE
               }

_CREATE_MASK = os.O_CREAT | os.O_TRUNC
_CREATE_MAP = {NULL: OPEN_EXISTING,
               os.O_CREAT | os.O_TRUNC: CREATE_ALWAYS
               }

win32 = ctypes.WinDLL('kernel32.dll', use_last_error=True)
win32.CreateFileW.restype = ctypes.c_void_p
INVALID_FILE_HANDLE = ctypes.c_void_p(-1).value


def _opener(path: pathlib.Path, flags: int) -> int:

    access_flags = _ACCESS_MAP[flags & _ACCESS_MASK]
    create_flags = _CREATE_MAP[flags & _CREATE_MASK]

    if flags & os.O_WRONLY:
        share_flags = NULL
        attr_flags = FILE_ATTRIBUTE_NORMAL
    else:
        share_flags = FILE_SHARE_READ
        attr_flags = FILE_ATTRIBUTE_READONLY

    attr_flags |= FILE_FLAG_SEQUENTIAL_SCAN

    h = win32.CreateFileW(path, access_flags, share_flags, NULL, create_flags, attr_flags, NULL)

    if h == INVALID_FILE_HANDLE:
        raise ctypes.WinError(ctypes.get_last_error())

    return msvcrt.open_osfhandle(h, flags)


class _FileControlAccessor(pathlib._NormalAccessor):

    open = staticmethod(_opener)


_control_accessor = _FileControlAccessor()


class Path(pathlib.WindowsPath):

    def _init(self) -> None:

        self._closed = False
        self._accessor = _control_accessor

    def _opener(self, name, flags) -> int:

        return self._accessor.open(name, flags)

【讨论】:

  • 使用kernel32 = WinDLL('kernel32.dll', use_last_error=True)。将结果类型设置为指针(句柄):kernel32.CreateFileW.restype = ctypes.c_void_p。定义INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value。如果返回后者,则为raise ctypes.WinError(ctypes.get_last_error())
  • 不是一个锁定文件。共享模式是按打开的,而不是按进程的,因此如果您不共享写访问权限,则无法使用写访问权限重新打开文件,即使您自己的进程也不行。您的代码应直接使用此句柄,并通过msvcrt.open_osfhandle 包装在 fd 中。随后您可以通过内置 open 将 fd 作为文件对象打开。
  • 您的函数打开句柄,将其包装在 fd 中并返回文件对象应该在嵌套的 try-finally 块中执行最后两个步骤。如果open_osfhandle 失败,finally 处理程序应调用kernel32.CloseHandle(handle) 以避免泄漏句柄。如果open 失败,finally 处理程序应该调用os.close(fd)。在文件对象拥有句柄后,不要不要在句柄上调用CloseHandle 或在fd 上调用os.close。 Python 的 I/O 堆栈现在拥有它。
  • c_void_p 是从一个无符号整数指针值初始化的,它通常是一个内存地址或一个不透明的句柄。作为 64 位有符号整数,-1 在本机(即在 CPU 中的裸机级别)表示为 0xFFFF_FFFF_FFFF_FFFF,其中每个十六进制数字为 4 位,0xF0b1111。作为一个无符号值,这是 18446744073709551615。要了解为什么 -1 以这种方式存储为有符号 64 位整数,请阅读two's complement
  • 请注意,Python 的 int 类型本身不会将值存储为二进制补码,因为它是一个可变大小的“大整数”实现。但是,它的按位运算确实保留了人们对二进制补码的期望,例如-1 &amp; (2**64 - 1) == 18446744073709551615.
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多