在C语言中使用fgets读取输入会返回重复的行

5
我正在尝试编写一个shell实现的C代码,并发现在fork进程后,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 < temp,输出显示打印了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;
}

谢谢!


请问您能否编辑您的问题以显示实际(和期望的)输出(完整地复制粘贴为文本)?同时,请包括一个实际的最小完整可验证示例,这样我们就可以轻松地复制并测试。 - Some programmer dude
父进程和子进程共享相同的文件描述符,您对stdin在fork中的预期是什么? - Ôrel
2
@Ôrel 感谢您提供的信息。但是在我的代码中,我是在创建子进程后立即终止的。这样父进程中的文件偏移量会发生什么变化呢? - Vitt Volt
1
这应该按预期工作。你确定编译了发布的代码吗?看起来你在测试中删除了对 exit 的调用。 - Jean-Baptiste Yunès
谢谢您发布链接。那正是我遇到的同样问题。我对C语言不是很熟悉,也许这不是我应该采取的方法? - Vitt Volt
显示剩余9条评论
2个回答

4
  1. it was already mentioned that parent and child are sharing current position for file descriptor 0 (stdin)
  2. seems that libc runtime initialization for streams (stdin, stdout, stderr) contains some stuff changing current stdin position:

    > 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. as a result, in my environment (openSUSE Leap 42.2, glibc-2.22-4.3.1) the program loops infinitely and there is no EOF at all

  2. changed fgets() to read() in the example

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

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

  1. 再次运行strace -f - 在子进程中不再有lseek()!!

  2. 结论 - 似乎在多进程环境中必须非常谨慎地使用流函数(在stdio.h中声明),因为存在许多副作用(就像在这个例子中一样)


感谢您的出色回答! - Vitt Volt
实际上,我也尝试使用pos = lseek(0,0,SEEK_CUR)在fork之前获取当前偏移量,并使用lseek(0,pos,SEEK_SET)在wait()之后重置文件位置。它也被证明是有效的。 - Vitt Volt

1

我在这个帖子中找到了一个关于使用fgets()的解决方案,它讨论了同样的问题,tldr:

退出会清除子进程中的stdio缓冲区。 ... 更多细节请参考POSIX参考链接,第2.5.1章节:

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

因此行为是未定义的,因此允许在glibc 2.19和2.24之间更改。

修复方法:

如上面的链接所述,有两种解决方案可用于修复代码:

如果(fork() == 0) { fclose(fd); exit(1); }

或者

如果(fork() == 0) { _exit(1); }


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