带重定向的fgets()调用获取异常数据流

fgets() call with redirection get abnormal data stream

我正准备用C语言写一个shell。下面是源代码:

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>

int
getcmd(char *buf, int nbuf)
{
  memset(buf, 0, nbuf);
  fgets(buf, nbuf, stdin);
  printf("pid: %d, ppid: %d\n", getpid(), getppid());
  printf("buf: %s", buf);
  if(buf[0] == 0) {// EOF
    printf("end of getcmd\n");
    return -1;
  }
  return 0;
}

int
main(void)
{
  static char buf[100];
  int fd, r, ret;

  // Read and run input commands.
  while((ret = getcmd(buf, sizeof(buf))) >= 0){
    if(fork() == 0)
      exit(0);
    wait(&r);
  }
  exit(0);
}

当我执行已编译的可执行文件并将标准输入重定向到名为 t.sh 的文件时,其内容为“1111\n2222\n”,如 ./myshell < t.sh,输出为:

pid: 2952, ppid: 2374
buf: 1111
pid: 2952, ppid: 2374
buf: 2222
pid: 2952, ppid: 2374
buf: 2222
pid: 2952, ppid: 2374
buf: end of getcmd

显然,getcmd() 函数得到 3 行(1111、2222、2222),而 t.sh 中只有 2 行。当在 t.sh.

中放置更多行时,这些情况会变得更糟

而主进程是唯一执行getcmd的进程,我们可以通过pid的输出来判断。

对了,我发现去掉wait(&r)这行代码,输出就可以正常了。

wait 确保 child 进程在 parent 完成文件之前有时间到达 运行。如果我 strace Linux 下的文件,我得到

% strace -f ./a.out
[lots of stuff]
wait4(-1, strace: Process 29317 attached
 <unfinished ...>
[pid 29317] lseek(0, -2, SEEK_CUR)      = 0
[pid 29317] exit_group(0)               = ?
[pid 29317] +++ exited with 0 +++
<... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29317
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=29317, si_uid=1000, si_status=0
    _utime=0, si_stime=0} ---
[lots of stuff]

child 进程 倒回 标准输入作为 fork 之后的第一个操作之一,之后它将立即退出。具体来说,它将 fgets 从流中读入的字节数倒回到缓冲区中,但仍然 未使用 libc 在 fork 后自动执行此操作。我还看到 child 进程刷新 stdout.

我不知道该怎么想...但很明显,如果你想写一个shell,你一定不能与标准流交互<stdio.h> 。如果 lseek 没有 发生,那么 child 进程将看到多达 4095 个字节的 stdin 被跳过!您必须始终仅使用 <unistd.h> 中的 readwrite。或者,在从 stdin:

读取任何内容之前,将以下调用添加到 main 的开头可能会很幸运
if (setvbuf(stdin, NULL, _IONBF, 0) != 0) {
    perror("setvbuf:");
   exit(1);
}

这会将 stdin 流设置为 非缓冲模式 ,因此不应读取太多。然而,Linux manual page for fgets 说:

It is not advisable to mix calls to input functions from the stdio library with low-level calls to read(2) for the file descriptor associated with the input stream; the results will be undefined and very probably not what you want.

顺便说一句,如果 stdin 来自管道,则无法重现:

% echo -e '1\n2' | ./a.out  
pid: 498, ppid: 21285
buf: 1
pid: 498, ppid: 21285
buf: 2
pid: 498, ppid: 21285
buf: end of getcmd

但这自然会使另一个问题可见 - child 看到输入被跳过。


P.S.

您从不检查 fgets 的 return 值,因此您不知道何时发生读取错误。

If a read error occurs during the operation, the array contents are indeterminate and a null pointer is returned.