Atomically open and lock file

5

我有一个名为foo.hex的文件,被两个进程访问。其中一个进程具有O_RDONLY访问权限,另一个进程则具有O_RDWR访问权限。

在首次启动系统时,读取进程在写入进程初始化文件之前不应访问该文件。

因此,我编写了以下内容来初始化文件。

fd = open("foo.hex", O_RDWR|O_CREAT, 0666);
flock(fd, LOCK_EX);

init_structures(fd);

flock(fd, LOCK_UN);

这仍然存在一个可能性,即读者进程在文件初始化之前访问该文件。

我找不到一种以原子方式open()flock()的方法。除了互斥锁,还有哪些可能性以最少的开销优雅地实现我的目标(因为它只在系统第一次启动时使用一次)?


这不是你实际的问题,但你可能想在写入程序中使用 open("foo.hex", O_RDWR|O_CREAT|O_EXCL, 0666) - zwol
@zwol 对,我实际上是用 O_CREAT、0666 打开它的,但这并不能改变竞争条件的问题。 - Frode Akselsen
2
请记住:flock()锁定是建议性的,而不是强制性的,因此即使您在一个进程中应用了锁定,如果它忽略了锁定,另一个进程仍然可以继续执行其操作。 - Jonathan Leffler
@JonathanLeffler 感谢您的提示,那正是我在读取器方面出错的地方。最初,我认为只需要在创建/初始化期间保护文件,但现在我将基于John的方法实现更通用的解决方案。 - Frode Akselsen
4个回答

6
让作者创建一个名为“foo.hex.init”的文件,然后在将其重命名为“foo.hex”之前进行初始化。这样,读者就永远看不到未初始化的文件内容。

1
这就是做法。在编写过程中重命名文件以保护它们非常普遍。例如,Web浏览器在下载文件时会这样做,以确保您不会尝试打开一半的文件。文件锁定充满了危险,应尽可能避免使用。 - John Zwinck
1
关于重命名。假设程序开始之前存在“foo.hex”,并且编写者创建了“foo.hex.init”。为了重命名,难道不需要在将“foo.hex.init”重命名为“foo.hex”之前删除(或重命名为“some_tmp_name”)“foo.hex”吗?如果是这样的话,那么是否会留下一个没有“foo.hex”的时间窗口 - 这可能会给读者带来麻烦? - chux - Reinstate Monica
3
在符合POSIX标准的系统上,rename("foo.hex.init", "foo.hex")是必需的以原子方式替换foo.hex -- 不会存在观察到foo.hex不存在或具有错误内容的任何时刻。然而,在Windows上,必须先删除旧文件,并且确实存在一个无法避免的竞争窗口,在这个窗口期中,可能会观察到foo.hex不存在。如果我正在编写一个关心这一点的程序,我会让读者在放弃之前旋转几次。 - zwol
3
需要使用原子替换逻辑的代码通常使用 POSIX link() 函数(而不是 rename())。 - Nominal Animal
@NominalAnimal link()无法用于目录。您确定POSIX的rename()不是原子操作吗?POSIX标准规定如果new参数指定的链接已存在,则必须删除它,并将old重命名为new。在这种情况下,名为new的链接将在整个重命名操作期间对其他线程可见,并引用操作开始之前由new或old所引用的文件。 这样可以带来额外的好处:如果rename()函数因除了EIO以外的任何原因而失败,则任何名为new的文件都不会受到影响。 - Andrew Henle
显示剩余2条评论

2

另一种方法是删除现有文件,重新创建文件并禁止任何进程访问该文件,然后在写入文件后更改文件权限:

unlink("foo.hex");
fd = open("foo.hex", O_RDWR|O_CREAT|O_EXCL, 0);

init_structures(fd);

fchmod(fd, 0666);

如果您正在以root身份运行,则可能不起作用。(无论如何,您都不应该这样做...)

这将防止任何进程在进行unlink()调用后使用旧数据。根据您的要求,这可能或可能不值得花费额外的读者代码来处理文件不存在或在初始化新文件时无法访问的情况。

个人而言,在init_structures()需要显著的时间并且实际上有一个硬性要求不使用新数据一旦可用之前的旧数据时,我会使用rename( "foo.hex.init", "foo.hex" )解决方案。但是有时重要的人不喜欢在任何部分新数据可用的情况下使用旧数据,并且他们真的不理解,“如果读取器进程早两毫秒启动,它仍然会使用旧数据”。


1

另一种方法是让读取进程稍微休眠一下,如果发现文件尚不存在或为空,则重试。

int open_for_read(const char *fname)
{
    int retries = 0;

    for (;;) {
        int fd = open(fname, O_RDONLY);
        if (fd == -1) {
            if (errno != ENOENT) return -1;
            goto retry;
        } 
        if (flock(fd, LOCK_SH)) {
            close(fd);
            return -1;
        }

        struct stat st;
        if (fstat(fd, &st)) {
            close(fd);
            return -1;
        }
        if (st.st_size == 0) {
            close(fd);
            goto retry;
        }
        return fd;

    retry:
        if (++retries > MAX_RETRIES) return -1;
        sleep(1);
    }
    /* not reached */
}

您需要在写入端使用类似的代码,这样如果写入者输掉比赛,就不必重新启动。


0

有许多方法可以进行进程间通信

也许可以使用一个命名的信号量,在打开和初始化文件之前由写入进程锁定?然后,读取进程也可以尝试锁定信号量,如果成功并且文件不存在,则解锁信号量,等待一段时间后重试。

但最简单的方法,特别是如果文件将由写入进程每次重新创建,已经在John Zwinck的答案中了。


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