多个进程可以使用fopen追加文件而不会存在并发问题吗?

17

我有一个以附加模式打开文件的进程。在这种情况下,它是一个日志文件。示例代码:

int main(int argc, char **argv) {
    FILE *f;
    f = fopen("log.txt", "a");
    fprintf(f, "log entry line");
    fclose(f);
}

两个问题:

  1. 如果我有多个进程向同一个文件追加日志,每个日志行是否会独立显示,还是它们可能被交错在一起,因为进程上下文切换了?
  2. 如果许多进程需要访问文件,这种写入是否会阻塞,从而导致并发问题?

我正在考虑使用最简单的方式实现此操作,或者使用zeromq通过管道将日志条目推送到日志收集器。

我曾考虑过syslog,但我不想在软件中出现任何平台依赖性。

默认平台是Linux。

6个回答

11
我不知道 fopenfprintf,但你可以使用 O_APPENDopen 文件。然后每个 write 将顺利写入文件的末尾(不会与其他写入混合在一起)。
实际上,查看标准

打开的流所关联的文件描述符应当分配并以以下标志调用 open() 开启:

a or ab          O_WRONLY|O_CREAT|O_APPEND
所以我猜只要使用 a 打开文件,多个进程就可以安全地使用 fprintf 写入文件。

2
你仍然需要将缓冲切换为无或行。使用完全缓冲可能会在条目中间用尽缓冲区并调用写入。 - Jan Hudec
@Jan Hudec 没错。使用 write(2) 直接写入的另一个原因。 - cnicutar

8
标准(适用于open/write,而不是fopen/fwrite)规定:

如果文件状态标志的O_APPEND标记被设置,则在每次写操作之前,文件偏移量应设置为文件末尾,并且在更改文件偏移量和写操作之间不得进行任何介入的文件修改操作。

要使用fprintf(),必须在文件上禁用缓冲。


1
重要提示:这是针对open/write的,而不是针对fopen/fwrite。 - Jan Hudec
正确,但write() "位于"fwrite()fprintf()之下,即被它们使用。如果禁用缓冲,则甚至会1:1使用它。(但最好明确说明,这样你就是对的。) - glglgl
不需要禁用缓冲以使其工作。实际上,行缓冲有助于确保一次写入整行。 - Per Johansson
1
@glglgl:是的,“write”位于“fwrite”的下层,但没有什么能说明“fwrite”会将数据一次性传递给“write”。实际上,如果完全关闭缓冲或启用行缓冲且行长度不超过缓冲区大小,则会这样做。 - Jan Hudec
尽管写操作确保了原子性,但是两个进程的写操作顺序取决于操作系统。因此,即使进程1在进程2之前调用write()函数,写入数据的顺序也取决于操作系统。为了保证顺序,使用互斥锁是最好的选择。 - Bill

6
你肯定有平台依赖性,因为Windows无法处理多个进程向同一文件追加的情况。
关于同步问题,我认为行缓冲输出可以在大多数情况下节省您的时间,即根据我的简短基于shell的测试,超过99.99%的短日志行应该是完整的,但不是每次都是。显式语义绝对是更可取的,由于您无法以系统无关的方式编写此黑客系统,我建议使用syslog方法。

谢谢 - 这正是我想知道的。Windows 的多次追加是我稍微担心的地方。 - Deleted

3
当你的进程要写如下内容时:

当你的进程将要写一些类似于:

"Here's process #1"
"Here's process #2"

你可能会得到类似这样的结果:
"Hehere's process #2re's process #1"

你需要将它们同步。

3

编辑以明确回答您的问题:

  1. 如果我有多个进程向同一个文件追加,每个日志行是否会清晰可见或者它们会混杂在一起,因为进程切换?

是的,每个日志行将会完整出现,因为根据msdn/vs2010:

"此函数[即fwrite()]锁定调用线程,并且因此是线程安全的。如需非锁定版本,请参阅_fwrite_nolock。"

GNU手册页面上也暗示了相同的内容:

"— Function: size_t fwrite (const void *data, size_t size, size_t count, FILE *stream)

This function writes up to count objects of size size from the array data, to the stream stream. The return value is normally count, if the call succeeds. Any other value indicates some sort of error, such as running out of space. 

— 函数:size_t fwrite_unlocked(const void * data,size_t size,size_t count,FILE * stream)

该函数是用于将数据块写入文件的。它与fwrite函数非常相似,但是不会对文件进行加锁。如果您需要高效地写入大量数据,可以考虑使用此函数。

The fwrite_unlocked function is equivalent to the fwrite function except that it does not implicitly lock the stream.

This function [i.e., fwrite_unlocked( )] is a GNU extension. "
  1. 如果有很多进程需要访问该文件,这个写操作会被阻塞吗?这样会导致并发问题吗?

是的,从问题1的意涵可以得出这个答案。


谢谢回复。我可能会遇到并发的问题,所以锁定会导致问题。我已经点赞了,因为使用案例仍然有效且描述清晰。再次感谢! - Deleted
哦,我终于明白了。你所说的“并发问题”,是指当进程1试图写入日志文件时,进程2会被阻塞。我编辑了我的答案,明确回答了这两个问题。 - Pete Wilson
那就是这个了 :) - 无论如何还是谢谢你的答案。我会在低并发需求下使用它。 - Deleted

1

如果不进行某种形式的同步,日志行可能会重叠。因此,回答第二个问题,这取决于您如何实现锁定和记录代码。如果您只是锁定、写入文件并解锁,如果有很多进程尝试同时访问该文件,可能会导致问题。


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