6

在 APUE 第 8.3 节fork function中,关于父进程和子进程之间的文件共享,
它说:It is important that the parent and the child share the same file offset.

在第 8.9 节中Race Conditions,有一个例子:父母和孩子都写入
一个在调用 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);
 }
}
4

5 回答 5

6

parent 和 child 在内核中共享相同的文件表条目,其中包括偏移量。因此,如果没有一个或两个进程关闭和重新打开文件,父子进程就不可能有不同的偏移量。因此,父级的任何写入都使用此偏移量并修改(增加)偏移量。然后孩子的任何写入都使用新的偏移量,并修改它。一次写一个字符会加剧这种情况。

在我的 write(2) 手册页中:“文件偏移量的调整和写入操作作为原子步骤执行。”

因此,您可以保证没有一个人(父母或孩子)写的东西会写在另一个人的上面。您还可以注意到,如果您要一次编写(2)整个句子(在一次调用 write(2) 中),则可以保证句子将一起写成一个片段。

在实践中,许多系统以这种方式写入日志文件。许多相关进程(同一个父进程的子进程)将具有由父进程打开的文件描述符。只要他们每个人一次写入一整行(一次调用 write(2)),日志文件就会按照您的意愿读取。一次写一个字符不会有相同的保证。使用输出缓冲(例如,stdio)将同样消除保证。

于 2009-10-28T15:23:05.673 回答
4

好吧,我错了。

所以,他们正在共享一个偏移量,但其他一些奇怪的事情正在发生。如果他们不共享偏移量,您将获得如下所示的输出:

this is a long long output from chredt

因为每个都会从它自己的偏移量 0 开始写入,并一次推进一个字符。在到达句子的最后一个单词之前,他们不会开始对要写入文件的内容发生冲突,这最终会交错。

所以,他们共享一个偏移量。

但奇怪的是,偏移量似乎并没有自动更新,因为两个进程的输出都没有完整显示。就像一个的某些部分正在覆盖另一个的某些部分,即使它们也提前了偏移量,所以这种情况总是不会发生。

如果未共享偏移量,则文件中的字节数将与两个字符串中最长的字节数完全相同。

如果偏移量是自动共享和更新的,那么最终文件中的字节数与两个字符串加在一起的字节数完全相同。

但是你最终会在文件中得到一些介于两者之间的字节,这意味着偏移量是共享的,而不是原子更新的,这很奇怪。但这显然就是发生的事情。多么奇怪。

  1. 进程 A 将 offset 读入 A.offset
  2. 进程 B 将 offset 读入 B.offset
  3. 进程 A 在 A.offset 处写入字节
  4. 进程 A 设置 offset = A.offset + 1
  5. 进程 B 在 B.offset 写入字节
  6. 进程 A 将 offset 读入 A.offset
  7. 进程 B 设置 offset = B.offset + 1
  8. 进程 A 在 A.offset 处写入字节
  9. 进程 A 设置 offset = A.offset + 1
  10. 进程 B 将 offset 读入 B.offset
  11. 进程 B 在 B.offset 写入字节
  12. 进程 B 设置 offset = B.offset + 1

这大概就是事件的顺序。多么奇怪。

存在 pread 和 pwrite 系统调用,因此两个进程可以在特定位置更新文件,而无需争辩谁的全局偏移值获胜。

于 2009-10-28T09:49:01.707 回答
2

好吧,我调整了代码以在 vanilla GCC/glibc 上编译,这是一个示例输出:

thhis isias a l long oulout futput frd
 parent

而且我认为这支持了文件位置共享的并且它受制于竞赛的想法,这就是它如此奇怪的原因。请注意,我显示的数据有 47 个字符。这比任何一条消息的 38 或 39 个字符都多,并且少于两条消息的 77 个字符——我能看到发生这种情况的唯一方法是,如果进程有时竞相更新文件位置——它们每个都写一个字符,他们每个人都尝试增加位置,但是由于比赛,只发生了一次增加,并且一些字符被覆盖。

支持证据:man 2 lseek在我的系统上说清楚

请注意,由 dup(2) 或 fork(2) 创建的文件描述符共享当前文件位置指针,因此对此类文件的查找可能会受到竞争条件的影响。

于 2009-10-28T10:14:24.490 回答
1

如果我从我的操作系统类中正确回忆,分叉确实给了孩子它自己的偏移量(尽管它从与父母相同的位置开始),它只是保持相同的打开文件表。虽然,我正在阅读的大部分内容似乎都不是这样。

于 2009-10-28T09:30:21.660 回答
1

使用 pwrite,因为当同一资源(write())被多个进程共享时,有时会出现竞争条件,因为 write 完成后不会离开文件 pos=0,例如您最终位于文件中间,因此文件指针(fd)指向这个位置,如果其他进程想要做某事,那么它会产生或工作不是它想要做的,因为文件描述符将在分叉之间共享!

试着给我反馈

于 2010-10-27T14:01:50.810 回答