【问题标题】:Is a sync/flush needed before writes to a locked file from multiple threads/processes in fopen a+ mode?在 fopen a+ 模式下从多个线程/进程写入锁定文件之前是否需要同步/刷新?
【发布时间】:2012-02-01 19:13:27
【问题描述】:

我正在从多个线程对单个文件执行 I/O。对此共享文件foo 的访问是通过建议文件锁(flock(2)LOCK_EX)控制的。 foofopen(3) 模式a+ 打开。选择a+ 是因为文档说明:

对文件的后续写入将始终以当时的当前结束 文件结尾,无论是否有任何干预 fseek(3) 或类似内容。

简化,操作将开始:

FILE *fp = fopen("foo", "a+");
...spawn threads...

继续写作:

flock(fileno(fp), LOCK_EX);
fwrite(buffer, buffer_size, 1, fp);
flock(fileno(fp), LOCK_UN);

我目前在fwrite(3) 之前没有任何fflush(3)fsync(2) 电话,我想知道我是否应该这样做。 fopen(3) a+ 模式在计算“当前EOF”时是否考虑了多个线程命中文件?我知道flock(2) 在有未完成的 I/O 时授予我锁可能没有问题。

在我有限的测试中(在多个线程中写很长的 ASCII 文本行,然后是换行符,持续几秒钟,然后确保结果文件中每行的字符数相等),我没有看到任何“损坏" 不使用 fflush(3)fsync(2) 时。它们的存在会大大降低 I/O 性能。

tl;博士: 使用文件锁时,在写入以a+ 模式打开的多个线程之间的共享文件之前,是否需要刷新流?多个分叉/不同的机器写入一个并行文件系统的文件?

可能相关: why fseek or fflush is always required between reading and writing in the read/write "+" modes

【问题讨论】:

    标签: c file concurrency locking fopen


    【解决方案1】:

    那是一种错误的锁。 flock 用于进程之间的锁定,而不是同一进程中的线程之间的锁定。来自man 2 flock

    如果 other 持有不兼容的锁,则调用 flock() 可能会阻塞 进程。 要发出非阻塞请求,请包含 LOCK_NB(通过 ORing) 进行上述任何操作。

    添加了重点。还有……

    一个进程只能持有一种类型的锁(共享或独占) 文件。 对已锁定文件的后续flock() 调用将转换 一个现有的锁到新的锁模式。

    您想改用flockfile(或另外,如果还使用多个进程)。 flockfile 函数用于控制从多个线程对FILE * 的访问。从手册页:

    stdio 函数是线程安全的。这是通过分配给 每个 FILE 对象都有一个 lockcount 和(如果 lockcount 非零)一个 own- 荷兰国际集团线程。对于每个库调用,这些函数都会等到 FILE 对象不再被其他线程锁定,然后锁定它,执行 请求 I/O,然后再次解锁对象。 (注意:此锁定与文件锁定无关 像flock(2) 和lockf(3) 这样的函数。)

    像这样:

    // in one of the threads...
    flockfile(fp);
    fwrite(..., fp);
    funlockfile(fp);
    

    好消息是,在glibc 上,如果每个关键部分中只有一个来自 stdio.h 的函数调用,则不需要锁定文件,因为 glibc 有一个锁定的 fwrite。但在其他平台上情况并非如此,锁定文件当然没有坏处。因此,如果您在 Linux 上运行,您永远不会注意到 flock 不会做您想做的事情,因为 fwrite 会自动完成。

    关于追加模式: 使用追加模式编写时不需要额外的刷新,除非你想确保打开相同文件的不同进程之间的顺序(或者一个进程具有多个句柄)同一个文件)。除非从文件中读取,否则不需要“a+”模式。

    flock的演示

    如果您不相信flock 不提供使用相同文件描述符的线程之间的线程安全,这里有一个演示程序。

    #include <stdio.h>
    #include <errno.h>
    #include <pthread.h>
    #include <string.h>
    #include <stdlib.h>
    #include <sys/file.h>
    
    static FILE *fp;
    static pthread_mutex_t mutex;
    static pthread_cond_t cond;
    int state;
    
    static void fail_func(int code, const char *func, int line)
    {
        fprintf(stderr, "%s:%d: error: %s\n", func, line, strerror(code));
        exit(1);
    }
    
    #define fail(code) fail_func(code, __FUNCTION__, __LINE__)
    
    void *thread1(void *p)
    {
        int r;
    
        // Lock file (thread 2 does not have lock yet)
        r = pthread_mutex_lock(&mutex);
        if (r) fail(r);
        r = flock(fileno(fp), LOCK_EX);
        if (r) fail(errno);
        puts("thread1: flock successful");
        state = 1;
        r = pthread_mutex_unlock(&mutex);
        if (r) fail(r);
    
        // Wake thread 2
        r = pthread_cond_signal(&cond);
        if (r) fail(r);
    
        // Wait for thread 2
        r = pthread_mutex_lock(&mutex);
        if (r) fail(r);
        while (state != 2) {
            r = pthread_cond_wait(&cond, &mutex);
            if (r) fail(r);
        }
        puts("thread1: exiting");
        r = pthread_mutex_unlock(&mutex);
        if (r) fail(r);
    
        return NULL;
    }
    
    void *thread2(void *p)
    {
        int r;
    
        // Wait for thread 1
        r = pthread_mutex_lock(&mutex);
        if (r) fail(r);
        while (state != 1) {
            r = pthread_cond_wait(&cond, &mutex);
            if (r) fail(r);
        }
    
        // Also lock file (thread 1 already has lock)
        r = flock(fileno(fp), LOCK_EX);
        if (r) fail(r);
        puts("thread2: flock successful");
    
        // Wake thread 1
        state = 2;
        puts("thread2: exiting");
        r = pthread_mutex_unlock(&mutex);
        if (r) fail(r);
        r = pthread_cond_signal(&cond);
        if (r) fail(r);
    
        return NULL;
    }
    
    int main(int argc, char *argv[])
    {
        pthread_t t1, t2;
        void *ret;
        int r;
    
        r = pthread_mutex_init(&mutex, NULL);
        if (r) fail(r);
        r = pthread_cond_init(&cond, NULL);
        if (r) fail(r);
        fp = fopen("flockfile.txt", "a");
        if (!fp) fail(errno);
        r = pthread_create(&t1, NULL, thread1, NULL);
        if (r) fail(r);
        r = pthread_create(&t2, NULL, thread2, NULL);
        if (r) fail(r);
        r = pthread_join(t1, &ret);
        if (r) fail(r);
        r = pthread_join(t2, &ret);
        if (r) fail(r);
        puts("done");
        return 0;
    }
    

    在我的系统上,它产生以下输出:

    thread1: 集群成功 线程2:集群成功 线程2:退出 线程1:退出 完毕

    请注意,线程 1 不会释放 flock,而线程 2 无论如何都可以获取它。使用条件变量可确保线程 1 在线程 2 获得锁之前不会退出。 这正是 flock 手册页所说的, 因为 flock 说锁是每个文件和每个进程的,而不是每个线程的。

    以原子方式附加到文件的摘要

    为了在进程和线程之间进行原子写入,您可以做以下两种简单的事情之一:

    • 使用write 并且写入不超过PIPE_BUF 字节。 PIPE_BUF 定义在 &lt;limits.h&gt; 中,在我的系统上是 4096。如果文件描述符在 O_APPEND 模式下打开,那么写入将自动到文件末尾,无论还有谁在写入文件(线程和/或进程)。

    • 使用writeflock。如果您曾经一次写入超过PIPE_BUF 个字节,这是您所有写入的唯一选择。同样,如果文件以O_APPEND 模式打开,则字节将转到文件末尾。这将自动发生,但仅从拥有flock 的每个人的角度来看。

    另外,

    • 如果您使用&lt;stdio.h&gt; 并在线程之间共享FILE *,您还需要从每个线程调用flockfile。如果您使用较低级别的 POSIX API (open/write/etc),则不需要这样做。如果您使用 glibc 并且每次写入都是一个函数调用(例如,您想要原子地 fputs),则也不需要这样做。

    • 如果只使用一个进程,则不需要flock

    【讨论】:

    • 我认为您正在阅读手册页中flock 的警告太多。即使在重新阅读之后,我仍然满意 flock 在线程之间可以正常工作。此外,flockfile 不会在多个进程之间工作,因为锁定在 FILE* 而不是文件本身。
    • 至于附加模式(原始问题),我不一定需要确保进程写入的顺序,但我绝对不能让一个进程写入它认为是 EOF,但是EOF 已更改,因为自上次 I/O 以来另一个线程/进程已扩展该文件。
    • @greg:我添加了一个演示,证明flock 没有做你想做的事。随意在您的计算机上运行它。我认为附加模式可以满足您的需求。您可能希望使用 POSIX 文件描述符,因为看起来stdio 提供的缓冲区正在妨碍您。阅读手册页man 2 open 并查看O_APPEND 的描述,这是fopen 用于aa+ 模式的内容。
    • @greg:至于flockfile,是的,我知道flockfile 是每个线程但不会跨进程锁定。由于您在帖子中没有提到流程,因此我没有涉及它。为了在线程和进程之间锁定,您需要同时使用flockfileflock。只使用其中一个是行不通的。
    • 谢谢。我需要仅使用 flock 来评估我在测试用例线程中的“幸运”程度。
    【解决方案2】:

    上面的答案不太对。

    1. write() 以追加模式写入文件在多线程和多进程之间是原子的,无论一次写入多少字节。见标准:http://pubs.opengroup.org/onlinepubs/009695399/functions/write.html 如果设置了文件状态标志的 O_APPEND 标志,则文件偏移量应在每次写入之前设置为文件末尾,并且在更改文件偏移量和写入操作之间不应发生中间文件修改操作。

    2. 如果 write() 到具有附加模式的 FIFO 或管道,PIPE_BUF 会限制原子写入的最大大小。

    3. stdio 库不保证多进程或多线程原子追加写入。因为每个 FILE* 都有自己的缓冲区。

    4. flockfile 仅在以下情况下起作用 1. 只有一个进程操作文件 2. 多线程使用一个 FILE* 写入文件。

    5. 所以,当多进程或多线程需要用stdio函数写文件时,使用advisory lock是唯一的选择,flock只能在linux下工作,使用fcntl是可移植的。

    【讨论】:

      猜你喜欢
      • 2014-09-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多