[APUE]在fork之后,父进程和子进程共享相同的文件偏移量吗?

8
在APUE第8.3节的fork函数中,关于父进程和子进程之间文件共享的问题,它说:“父进程和子进程共享相同的文件偏移量非常重要。”
在第8.9节的竞争条件中,有一个例子:父进程和子进程都写入一个在调用fork函数之前打开的文件。该程序包含竞争条件,因为输出取决于内核运行进程的顺序以及每个进程运行的时间长度。
但是,在我的测试代码中,输出是重叠的。
“[Langzi@Freedom apue]$ cat race.out this is a long long outputhis is a long long output from parent”
看起来父进程和子进程具有单独的文件偏移量,而不是共享相同的偏移量。
我的代码有错误吗?还是我误解了共享偏移量的意义?任何建议和帮助将不胜感激。
以下是我的代码:
#include "apue.h"
#include <fcntl.h>

void charatatime(int fd, char *);

int main()
{
 pid_t pid;
 int fd;
 if ((fd = open("race.out", (O_WRONLY | O_CREAT |  O_TRUNC),
     S_IRUSR | S_IWUSR)) < 0)
  err_sys("open error");

 if ((pid = fork()) < 0)
  err_sys("fork error");
 else if (pid == 0)
  charatatime(fd, "this is a long long output from child\n");
 else
  charatatime(fd, "this is a long long output from parent\n");

 exit(0);
}


void charatatime(int fd, char *str)
{
 // try to make the two processes switch as often as possible
 // to demonstrate the race condition.
 // set synchronous flag for fd
 set_fl(fd, O_SYNC);
 while (*str) {
  write(fd, str++, 1);
  // make sure the data is write to disk
  fdatasync(fd);
 }
}
5个回答

6
父进程和子进程在内核中共享相同的文件表条目,其中包括偏移量。因此,如果父进程和子进程没有一个或两个进程关闭并重新打开文件,则不可能具有不同的偏移量。因此,父进程的任何写操作都使用此偏移量并修改(增加)偏移量。然后,子进程的任何写操作都使用新的偏移量,并对其进行修改。逐个字符地写入会加剧这种情况。
从我的write(2)手册页面上可以看到:"文件偏移量的调整和写操作将作为一个原子步骤执行"。
因此,您可以确保来自一个进程(父进程或子进程)的任何写入都不会覆盖另一个进程的写入。您还可以注意,如果您一次性(在一次write(2)调用中)写入整个句子,则保证该句子将一起写入,成为一个整体。
在实践中,许多系统以这种方式编写日志文件。许多相关进程(同一父进程的子进程)将具有由父进程打开的文件描述符。只要它们每个人都一次性写入一整行(使用一次write(2)调用),则日志文件将按照您想要的方式读取。逐个字符地写入将没有相同的保证。使用输出缓冲(例如,使用stdio)也将消除这些保证。

1
我认为以下内容仅适用于使用O_APPEND标志打开的文件。从我的write(2)手册中可以看到:“文件偏移量的调整和写操作是作为一个原子步骤执行的。”正如输出所示,不能保证写操作是原子操作。 - OnTheEasiestWay
无论 O_APPEND 是否为真,只要文件是以写入模式打开的,我从 write(2) 引用的内容都是正确的。写操作本身和文件偏移量的调整是原子性的,因此如果写操作发生在文件末尾,则偏移量将调整为新的文件末尾,并且所有操作都在一个操作中完成。仅当文件被多个进程独立打开时,才需要使用 O_APPEND,而在这种情况下,每次写操作会导致文件偏移量进行两次调整——一次在写操作之前,一次在写操作之后,且所有操作都是原子性的。 - Rob F
原始问题描述了在fork(2)之前的open(2),在这种情况下,文件偏移量由父进程和子进程共享(以及任何进一步的子进程)。一旦家族中的任何特定进程关闭文件,它将不再将其传递给其子进程,尽管这不会影响其在任何其他进程中的状态。 - Rob F
同样在原问题中,字符是逐个写入的--也就是说,对于每个写入的字符都有一个单独的write(2)调用。当只写入单个字符时,无法确定write(2)调用的原子性。如果整个缓冲区/字符串在每个进程中通过单个write(2)调用被写入,则write(2)调用的原子性将变得明显。 - Rob F
1
根据您所说,a)文件是以写入模式打开的,b)文件偏移量由父进程和子进程共享,c)写入操作本身和文件偏移量的调整是原子性的。为什么最终结果会重叠?也许您在最新评论中漏掉了一些单词,所以我无法正确理解您的意思。写一个字符的写入调用是否是原子操作?还是只有使用写入调用写入整个缓冲区/字符串是原子操作? - OnTheEasiestWay

4

好的,我错了。

所以,他们共享一个偏移量,但还有其他奇怪的事情发生。如果他们没有共享偏移量,你会得到以下输出:

this is a long long output from chredt

因为每个进程都从自己的偏移量0开始写,并逐个字符地推进。他们在写入文件的最后一个单词之前不会开始发生冲突,这会导致交错的结果。
所以,它们共享一个偏移量。
但奇怪的是,似乎没有原子更新偏移量,因此两个进程的输出都没有完整显示。就像其中一个部分覆盖了另一个部分一样,即使它们也推进了偏移量,以便始终不会发生这种情况。
如果偏移量不是共享的,则文件中的字节数将与两个字符串中最长的字符串的长度完全相同。
如果偏移量是共享的并且原子更新,则文件中的字节数将与两个字符串的总和完全相同。
但是,您最终得到的文件字节数介于两者之间,这意味着偏移量是共享的但不是原子更新的,这就很奇怪。但显然就是这种情况。多么奇怪啊。
以下是事件序列的大致内容:
1. 进程A读取偏移量到A.offset 2. 进程B读取偏移量到B.offset 3. 进程A在A.offset处写入字节 4. 进程A设置偏移量= A.offset + 1 5. 进程B在B.offset处写入字节 6. 进程A读取偏移量到A.offset 7. 进程B设置偏移量= B.offset + 1 8. 进程A在A.offset处写入字节 9. 进程A设置偏移量= A.offset + 1 10. 进程B读取偏移量到B.offset 11. 进程B在B.offset处写入字节 12. 进程B设置偏移量= B.offset + 1
这大致就是事件序列。非常奇怪。 pread和pwrite系统调用的存在是为了使两个进程能够在特定位置更新文件,而不会发生竞争,看谁赢得全局偏移量的值。

1
谢谢你的回答,现在我理解了。 由于文件大小大于两个进程写入文件的较长字符串,因此应共享偏移量。 我尝试使用O_APPEND标志打开文件,结果如预期。不同之处在于O_APPEND标志使以下两个步骤成为原子操作:
  1. 将偏移量设置为新文件大小,即使文件大小已更改。
  2. 向文件写入。 由于两个进程中没有竞争关系,因此结果是正确的。
- OnTheEasiestWay
但是使用O_APPEND标志时,无论两个写入同一文件的进程是否为父子关系,结果都应该是正确的。如果这两个进程两次打开文件,则结果也是正确的。 O_APPEND标志就像pread和pwrite系统调用一样,它们都是原子操作。 由于存在竞争条件,因此我们必须像APUE所说的那样使用某种形式的信号传递。 - OnTheEasiestWay
O_APPEND更像是每次向文件写入时原子地执行lseek(fd,0,SEEK_END)和写入操作。 - Omnifarious

2

好的,我把代码进行了调整,使其能够在普通的GCC/glibc上编译,这里是一个示例输出:

thhis isias a l long oulout futput frd
 parent

我认为这支持了一个观点,即文件位置被共享,并且它受到竞争的影响,这就是为什么它非常奇怪的原因。请注意,我展示的数据有47个字符。那比任何单个消息的38或39个字符多,比两条消息合在一起的77个字符少--我唯一能想到的方式是,如果进程有时会竞争更新文件位置--它们每个人都写一个字符,他们每个人都试图增加位置,但由于竞争,只有一个增量发生,一些字符被覆盖。
支持证据: 我系统上的man 2 lseek清楚地指出:
请注意,由dup(2)或fork(2)创建的文件描述符共享当前文件位置指针,因此对这样的文件进行寻址可能会受到竞争条件的影响。

感谢您的回答。它让我清楚地理解了这个问题。 - OnTheEasiestWay

1
使用pwrite,因为当多个进程共享同一资源(write())时,write有时会出现竞争条件,因为写入完成后不会将文件位置留在0处。例如,您可能会停在文件的中间,因此文件指针(fd)指向该位置,如果其他进程想要做某事,则会产生或不按预期工作,因为文件描述符将被共享在分叉中!!
请尝试并给我反馈

1
如果我从操作系统课程中记得正确的话,forking确实会给子进程分配自己的偏移量(尽管它从父进程的相同位置开始),但它仍然保持相同的打开文件表。虽然,我正在阅读的大部分内容似乎都在说明相反的情况。

在open的手册页中,它说文件偏移量和文件状态标志存储在“打开文件描述符”中。 这个“打开文件描述符”是你上面提到的“打开文件表”吗? - OnTheEasiestWay
我相信是这样的。在UNIX中,每个进程都保留着自己的文件描述符表,而内核则有它自己的打开文件表(以及像文件链接计数之类的东西)。所以我猜这取决于实现,无论是操作系统将偏移量存储在处理器的打开文件表中,还是在内核中。也许最初的UNIX标准就像APUE所说的那样,而更现代的实现则为每个进程提供了它自己的偏移量。 - David Brown
不,现代实现中偏移量保留在内核文件表中。 - Omnifarious
1
现代实现、原始实现和每个正确的实现。很多Unix *依赖于全局文件描述符。它是进程本地的libc级别FILE *,但它们只是代理文件描述符。 - hobbs

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