最佳的锁文件方法

38

Windows有一种打开文件的选项,可以以独占访问权限打开文件。Unix没有。

为了确保对某个文件或设备的独占访问,Unix通常使用锁文件来实现,通常存储在/var/lock目录中。

C指令open("/var/lock/myLock.lock", O_RDWR|O_CREAT|O_EXCL, 0666)如果锁文件已经存在,则返回-1,否则创建它。该函数是原子性的,并确保不存在竞争条件。

当释放资源时,以下指令可删除锁文件:remove("/var/lock/myLock.lock")

这种方法有两个问题。

  1. 程序可能在没有删除锁的情况下终止。例如,因为被杀死,崩溃或其他原因。锁文件仍然存在,并且即使不再使用该资源,也会阻止对该资源的访问。

  2. 锁文件创建时具有组和全局写许可,但通常惯例是配置帐户使用权限掩码来清除组和全局写许可。因此,如果我们有一种可靠的方法确定锁已经孤立(未使用),则文件的非所有者用户将无法删除它。

值得一提的是,我使用锁文件来确保对连接到串行端口(/dev/ttyUSBx)的设备的独占访问。需要协作的建议方法是可以接受的。但应当确保在不同用户之间实现独占访问。

有没有比锁文件更好的同步方法?如何确定创建锁文件的进程是否仍在运行?如何使其他用户能够在未被使用时删除锁文件?

我想出的一个解决方案是将文件用作Unix套接字文件。如果该文件存在,请尝试使用该文件进行连接。如果失败,则可能认为该文件的所有者进程已死亡。这需要在所属进程上循环套接字accept()线程。不幸的是,该系统将不再是原子性的。


2
似乎没有人提到显而易见的答案,这个答案可能适用于你,也可能不适用。POSIX默认要求流操作是原子性的,并且有一个内部文件锁来处理这个问题。因此,如果您可以在单个fprintf中完成所有想要做的事情,那么您就完成了。 - EML
@EML:如果是正确的,那是很好的信息。你有可以参考的来源吗? - Gabriel Staples
6个回答

45
请看这个富有启发性的演示文稿:文件锁定技巧与陷阱
这篇简短的演讲介绍了文件锁定的一些常见陷阱以及几种更有效地使用文件锁定的有用技巧。 编辑:为了更精确地回答您的问题:
“是否有比锁定文件更好的同步方法?”
正如@Hasturkun已经提到并且上述演示所述,您需要使用的系统调用是flock(2)。如果你想要跨多个用户共享的资源已经基于文件(在你的情况下是/dev/ttyUSBx),那么你可以将flock应用于设备文件本身
“如何确定创建锁定文件的进程是否仍在运行?”
您不必确定这一点,因为即使进程被终止,带有flock的锁定也会在关闭与文件关联的文件描述符时自动释放。
“如何使另一个用户能够在未使用时删除锁定文件?”
如果您将锁定直接应用于设备文件本身,则无需删除该文件。即使您决定在/var/lock中锁定普通文件,使用flock也不需要删除文件以释放锁定。

1
我希望我能再次为您的答案投票,以表彰其额外的细节。 - Hasturkun

22

你应该使用 flock(),例如:

fd = open(filename, O_RDWR | O_CREAT, 0666); // open or create lockfile
//check open success...
rc = flock(fd, LOCK_EX | LOCK_NB); // grab exclusive lock, fail if can't obtain.
if (rc)
{
    // fail
}

1
很好。请注意,应该是 0666(八进制),而不是 666,并且还应该使用 O_EXCL 选项。文件权限问题呢?如果另一个用户创建了文件,则打开调用将失败,即使文件已经存在且正在使用中,因为请求了写访问权限。如果请求读取访问权限,是否可能在其上锁定? - chmike
1
糟糕,不应该使用O_EXCL来允许打开现有文件。尽管写入访问可能会导致它在文件对用户只读时失败。Flock与文件权限无关,即使文件是只读的也可以工作。umask()函数允许在创建文件时清除权限掩码。 - chmike
16
请注意(在Linux上),任何没有调用flock检查现有锁定的程序都可以随意读/写该文件,即使另一个进程拥有独占锁定。 - AndiDog
1
@mvp:不是这样的。我刚刚尝试了一下,使用O_RDONLY打开了一个位于只读挂载的ext4文件系统上的文件。它毫无问题地获取了锁定。(顺便说一句,对于全局互斥量,请阅读有关POSIX信号量的内容,通过sem_open - Hasturkun
1
@mvp: 我想指出,lockf() 需要可写的文件,但 flock() 不需要。fcntl() 使用 F_SETLK 时,对于读锁需要一个可读文件描述符,对于写锁则需要一个可写文件描述符。此外,flock() 在 NFS 上无法使用(但 fcntl() 可以)。 - Hasturkun
显示剩余3条评论

8

小心使用像某个答案中所提到的锁定和释放锁定函数的实现方式,即像这样:

int tryGetLock( char const *lockName )
{
    mode_t m = umask( 0 );
    int fd = open( lockName, O_RDWR|O_CREAT, 0666 );
    umask( m );
    if( fd >= 0 && flock( fd, LOCK_EX | LOCK_NB ) < 0 )
    {
        close( fd );
        fd = -1;
    }
    return fd;
}

并且:

void releaseLock( int fd, char const *lockName )
{
    if( fd < 0 )
        return;
    remove( lockName );
    close( fd );
}

问题在于releaseLock的remove调用引入了一种竞争情况的bug。假设有三个进程都在尝试以不同的时间获得独占锁:
  • 进程#1打开了锁文件,获取了flock并即将调用unlock函数,但还没有完成。
  • 进程#2调用open打开了指向lockName的文件,并获得了它的文件描述符,但还没有调用flock。也就是说,指向lockName的文件现在已经被打开两次了。
  • 进程#3尚未启动。
可能发生的情况是,进程#1在第一次调用remove()和close()(顺序无关紧要)之后,进程#2使用已经打开但不再与任何目录条目相关联的文件描述符调用flock。现在,如果启动进程#3,则其open()调用将创建lockName文件,并针对该文件获取锁定状态,因为该文件尚未被锁定。结果,进程#2和#3都认为他们拥有fileName上的锁 - & gt; 这会导致一个bug。
在实现中的问题在于remove()(或更多的unlink())仅从目录条目中取消链接名称 - 但是引用该文件的文件描述符仍然可以使用。可以创建另一个具有相同名称的文件,但是已经打开的文件描述符仍然指向不同的位置。
可以通过向锁定函数添加延迟来演示这一点:
int tryGetLock( char const *lockName)
{
    mode_t m = umask( 0 );
    int fd = open( lockName, O_RDWR|O_CREAT, 0666 );
    umask( m );
    printf("Opened the file. Press enter to continue...");
    fgetc(stdin);
    printf("Continuing by acquiring the lock.\n");
    if( fd >= 0 && flock( fd, LOCK_EX | LOCK_NB ) < 0 )
    {
        close( fd );
        fd = -1;
    }
    return fd;
}

static const char *lockfile = "/tmp/mylock.lock";

int main(int argc, char *argv[0])
{
    int lock = tryGetLock(lockfile);
    if (lock == -1) {
        printf("Getting lock failed\n");
        return 1;
    }

    printf("Acquired the lock. Press enter to release the lock...");
    fgetc(stdin);

    printf("Releasing...");
    releaseLock(lock, lockfile);
    printf("Done!\n");
    return 0;
  1. 尝试启动进程 #1,并按一次回车键来获取锁。
  2. 然后在另一个终端上启动进程 #2,
  3. 在运行进程 #1 的终端上按下回车键以释放锁定。4. 通过按一次回车键使进程 #2 获取锁以继续。
  4. 接下来打开另一个终端来运行进程 #3。在那里,按一次回车键以获取锁。

"不可能" 的事情发生了:进程 #2 和 #3 都认为它们拥有独占锁。

这在实践中可能很少见,至少对于通常的应用程序而言,但是实现并不正确。

此外,使用模式 0666 创建文件可能存在安全风险。

我没有 "评论的声望",而且这也是一个相当旧的问题,但人们仍然参考这个并做类似的事情,所以我作为答案添加这个注释。


目的是防止其他进程获取锁。正如您所指出的那样,该代码不是线程安全的。为了编写线程安全的代码,请使用互斥锁以避免竞态条件。 - chmike
基本上你可以完全不用删除锁文件,因为这会引入潜在的竞争问题(并且在许多情况下/tmp,/var/run等可能会被自动清理)。 - Hasturkun
我要补充一点,使用命名信号量(如sem_open)可能更容易处理,但它们不是锁文件(它们也不能在NFS上使用)。如果进程死亡,它们也不会自动释放。另一个选择是在共享内存上使用进程共享的鲁棒互斥锁(因为这些锁在所有者未解锁而终止时指示放弃)。 - Hasturkun
@chmike:使用多个进程的实现也并不完全正确。 - Miika Karanki
1
修复指针问题,可以按照其他人的建议,直接省略取消链接步骤。或者可以尝试类似于FreeBSD libutils flopen()实现中的方法:在flocking()之后,使用open返回的fd进行fstat(),然后使用锁定文件的文件名进行stat(),并检查两个答案是否指向相同的inode。 - Miika Karanki
umask 的作用是创建权限为 0 的文件。我应该写成 open(lockName, O_RDWR|O_CREAT, 0) 来明确表示。由于权限问题,其他进程无法打开该文件。只有 root 和所有者才能删除该文件。文件锁定是通过 open() 实现的。flock() 的作用是防止同一进程多次锁定文件。 - chmike

8

Hasturkun的回答让我找到了方向。

这是我使用的代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <fcntl.h>

/*! Try to get lock. Return its file descriptor or -1 if failed.
 *
 *  @param lockName Name of file used as lock (i.e. '/var/lock/myLock').
 *  @return File descriptor of lock file, or -1 if failed.
 */
int tryGetLock( char const *lockName )
{
    mode_t m = umask( 0 );
    int fd = open( lockName, O_RDWR|O_CREAT, 0666 );
    umask( m );
    if( fd >= 0 && flock( fd, LOCK_EX | LOCK_NB ) < 0 )
    {
        close( fd );
        fd = -1;
    }
    return fd;
}

/*! Release the lock obtained with tryGetLock( lockName ).
 *
 *  @param fd File descriptor of lock returned by tryGetLock( lockName ).
 *  @param lockName Name of file used as lock (i.e. '/var/lock/myLock').
 */
void releaseLock( int fd, char const *lockName )
{
    if( fd < 0 )
        return;
    remove( lockName );
    close( fd );
}

1
在“releaseLock”函数中,交换“remove”和“close”可能是值得的。 - martemiev
这段代码接近正确,但还不够完美,因此会出现错误并且不是线程安全的。你应该提到或修复它。在结尾处不应该删除锁文件,而是应该关闭它。锁文件的存在并不表示锁已被占用,而是取决于flock()是否成功。更好的方法是将拥有基于flock的锁的进程的PID写入文件中。文件中写入的PID的存在或不存在表明外部人员和其他人现在是否占用了锁。当然,flock()返回的内容是唯一的完美指示器。 - Gabriel Staples

2

对Hasturhun的回答进行补充。 不要使用锁定文件的存在或不存在作为指示器,您需要在文件不存在时创建锁定文件,然后对文件进行独占锁定。

这种方法的优点是,与许多其他程序同步方法不同,如果您的程序退出而没有解锁,则操作系统应该会为您整理好。

因此,程序结构将类似于:

1: open the lock file creating it if it doesn't exist
2: ask for an exclusive lock an agreed byte range in the lock file
3: when the lock is granted then
    4: <do my processing here>
    5: release my lock
    6: close the lock file
end

在步骤中:你可以选择阻塞等待锁的授权或立即返回。 你锁定的字节实际上不必存在于文件中。如果你能获得Marc J. Rochkind的Advanced Unix Programming一书的副本,他开发了一个完整的C库,使用这种方法提供了一种同步程序的方式,该方式由操作系统进行清理,即使程序退出。


1
我使用了chmike发布的代码,发现一个小问题。在打开锁文件时,我遇到了竞争问题。有时候,几个线程同时打开锁文件。
因此,我从“releaseLock()”函数中删除了“remove(lockName);”行。我不明白为什么,但这个操作在某种程度上有助于解决问题。
我一直在使用以下代码来测试锁文件。通过它的输出,可以看到几个线程同时开始使用一个锁。
void testlock(void) {
  # pragma omp parallel num_threads(160)
  {    
    int fd = -1; char ln[] = "testlock.lock";
    while (fd == -1) fd = tryGetLock(ln);

    cout << omp_get_thread_num() << ": got the lock!";
    cout << omp_get_thread_num() << ": removing the lock";

    releaseLock(fd,ln);
  }
}

1
我提供的代码不是线程安全的。它保护不同的进程来获取锁。在函数中添加互斥锁以使其线程安全。 - chmike

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