【问题标题】:Reading input using fgets returns duplicate lines in C使用 fgets 读取输入会返回 C 中的重复行
【发布时间】:2017-10-16 10:35:03
【问题描述】:

我正在试验一些用于 shell 实现的 C 代码,发现 fgets() 在我 fork 一个进程后返回重复的行,我无法理解,如果能提供任何帮助,我将不胜感激。

我的问题是:分叉是否会更改父进程中任何打开文件中的偏移量?这似乎发生在我的程序中。

来自@Vadim Ponomarev 下方的答案和我的理解: fgets() 不是线程安全的(或者严格来说是线程安全的,但是 fork 一个进程会导致 stdin 以某种方式被初始化,从而导致共享文件偏移量的变化)。

代码如下:

int main() {

  char buf[200];
  int r;
  pid_t pid = 0;

  while(getcmd(buf, 200, pid) >= 0) {
    fprintf(stderr, "current pid: %d\n", getpid());
    pid = fork();
    // Without forking the fgets() reads all lines normally
    if(pid == 0)
      exit(0);

    wait(&r);
  }

  return 0;
}

getcmd() 函数只是一个包装器:

int
getcmd(char *buf, int nbuf, pid_t pid)
{
  memset(buf, 0, nbuf);
  if (fgets(buf, nbuf, stdin) == NULL) {
    fprintf(stderr, "EOF !!!\n");
    return -1;
  }
  fprintf(stderr, "pid: %d -- getcmd buf ======= --> %s\n", getpid(), buf);
  return 0;
}

我还有一个带有一些随机文本的输入文件 temp

line 1
line 2
line 3

编译后,我运行 a.out ,输出显示打印了 6 行,通常有些行是重复的。但是如果我删除该行

pid = fork()
...

然后输出就正常了(只是把所有的行一一显示,也就是说 fgets() 被调用了 3 次)。

知道出了什么问题吗?

输出(这是得到的):

pid: 10361 -- getcmd buf ======= --> line1

current pid: 10361
pid: 10361 -- getcmd buf ======= --> line2

current pid: 10361
pid: 10361 -- getcmd buf ======= --> line3

current pid: 10361
pid: 10361 -- getcmd buf ======= --> line2

current pid: 10361
pid: 10361 -- getcmd buf ======= --> line3

current pid: 10361
pid: 10361 -- getcmd buf ======= --> line3

current pid: 10361
EOF !!!

我希望看到这个:

current pid: 10361
pid: 10361 -- getcmd buf ======= --> line1

current pid: 10361
pid: 10361 -- getcmd buf ======= --> line2

current pid: 10361
pid: 10361 -- getcmd buf ======= --> line3

EOF

可供参考的可编译版本:

#include <stdio.h>
#include <stdlib.h>
#include <wait.h>
#include <zconf.h>
#include <unistd.h>
#include <memory.h>

int
getcmd(char *buf, int nbuf, pid_t pid)
{
  memset(buf, 0, nbuf);
  if (fgets(buf, nbuf, stdin) == NULL) {
    fprintf(stderr, "EOF !!!\n");
    return -1;
  }
  fprintf(stderr, "pid: %d -- getcmd buf ======= --> %s\n", getpid(), buf);
  return 0;
}

int main() {

  char buf[200];
  int r;
  pid_t pid = 0;

  while(getcmd(buf, 200, pid) >= 0) {
    fprintf(stderr, "current pid: %d\n", getpid());
    pid = fork();
    // Without forking the fgets() reads all lines normally
    if(pid == 0)
      exit(0);

    wait(&r);
  }

  return 0;
}

谢谢!

【问题讨论】:

  • 您能否编辑您的问题以显示实际(和预期)输出(完整,复制粘贴为文本)?还请包括一个实际的Minimal, Complete, and Verifiable Example,我们可以轻松地复制和测试自己。
  • @Someprogrammerdude 嗨,我添加了输出。总之,我没想到会看到重复的行被读取。
  • 父子共享同一个文件描述符,你对stdin的fork有什么期望?
  • @Ôrel 感谢您提供信息。但是在我的代码中,我在创建孩子后立即终止。这将如何更改父级中的文件偏移量?
  • 这应该可以按预期工作。你确定你编译了发布的代码吗?看起来你在测试中删除了对exit的调用。

标签: c shell process io fgets


【解决方案1】:
  1. 已经提到父子节点共享文件描述符 0 (stdin) 的当前位置
  2. 似乎流(stdin、stdout、stderr)的 libc 运行时初始化包含一些改变当前标准输入位置的东西:

    > strace -f ./a.out < temp 2>&1 | less
    ....
    write(2, "pid: 29487 -- getcmd buf ======="..., 45pid: 29487 -- getcmd buf ======= --> line 1
    clone(child_stack=0,flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD,child_tidptr=0x7f34940f19d0) = 29488
    Process 29488 attached
    [pid 29487] wait4(-1,  <unfinished ...>
    [pid 29488] lseek(0, -14, SEEK_CUR)     = 7
    [pid 29488] exit_group(0)               = ?
    [pid 29488] +++ exited with 0 +++
    <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29488
    

请注意子级 (pid 29488) 中的 lseek(0, -14, SEEK_CUR)

  1. 因此,在我的环境(openSUSE Leap 42.2,glibc-2.22-4.3.1)中,程序无限循环,根本没有 EOF

  2. 在示例中将 fgets() 更改为 read()

    ....
    if (read(0, buf, nbuf) == 0) {
    ....
    while(getcmd(buf, 7, pid) >= 0) {
    ....
    

程序按预期运行(三行和 EOF)

  1. 然后再次运行 strace -f - 没有更多的 lseek() !

  2. 结论 - 似乎在多进程环境中必须非常谨慎地使用流函数(在 stdio.h 中声明),因为有许多副作用(如本例所示)

【讨论】:

  • 感谢您的精彩回答!
  • 其实我也试过用 pos = lseek(0, 0, SEEK_CUR) 在分叉前获取当前偏移量,用 lseek(0, pos, SEEK_SET) 在 wait() 后重置文件位置。它也被证明是有效的。
【解决方案2】:

我从this thread 中找到了使用fgets() 的解决方案,它谈到了同样的问题,tldr:

exit 刷新子进程中的 stdio 缓冲区。 ... 有关更多详细信息,这里是与 POSIX 对应的链接 参考,第 2.5.1 章:

http://pubs.opengroup.org/onlinepubs/007904875/functions/xsh_chap02_05.html

因此行为是未定义的,因此可以改变 在 glibc 2.19 和 2.24 之间。

修复:

正如上面的链接所写,修复代码有两种解决方案:

if(fork() == 0) { fclose(fd);退出(1); }

if(fork() == 0) { _exit(1); }

【讨论】:

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