在 C 中实现 shell 并需要帮助处理输入/输出重定向

Posted

技术标签:

【中文标题】在 C 中实现 shell 并需要帮助处理输入/输出重定向【英文标题】:Implementing shell in C and need help handling input/output redirection 【发布时间】:2012-07-15 22:55:43 【问题描述】:

第二轮

看了一些答案,我修改后的代码是:

int pid = fork();

if (pid == -1) 
    perror("fork");
 else if (pid == 0)    

    if (in)  //if '<' char was found in string inputted by user
        int fd0 = open(input, O_RDONLY, 0);
        dup2(fd0, STDIN_FILENO);
        close(fd0);
        in = 0;
    

    if (out)  //if '>' was found in string inputted by user
        int fd1 = creat(output, 0644);
        dup2(fd1, STDOUT_FILENO);
        close(fd1);
        out = 0;
       

    execvp(res[0], res);
    perror("execvp");
    _exit(1);
 else 
    waitpid(pid, 0, 0);
    free(res);

它有效,但似乎没有重新连接标准输出或类似的东西。这是执行:

SHELL$ cat > file
hello, world
this is a test
SHELL$ cat < file //no output
SHELL$ ls //no output

'' 都可以工作,但是执行后没有输出。


第一轮

我一直在用 C 编写一个相对简单的 shell,但我在实现输入 () 重定向时遇到了麻烦。帮我找出以下代码中的问题:

int fd;
int pid = fork();
int current_out;

if (in)  //if '<' char was found in string inputted by user
    fd = open(input, O_RDONLY, 0);
    dup2(fd, STDIN_FILENO);
    in = 0;
    current_out = dup(0);


if (out)  //if '>' was found in string inputted by user
    fd = creat(output, 0644);
    dup2(fd, STDOUT_FILENO);
    out = 0;
    current_out = dup(1);


if (pid == -1) 
    perror("fork");
 else if (pid == 0)        
    execvp(res[0], res);
    perror("execvp");
    _exit(1);
 else 
    waitpid(pid, 0, 0);
    dup2(current_out, 1);
    free(res);

我可能有一些不必要的材料,因为我一直在尝试不同的东西来让它发挥作用。我不确定出了什么问题。

【问题讨论】:

current_out 感觉很格格不入;例如,dup(0) 复制了标准输入描述符,而不是标准输出。你确定你已经正确解析了ininput 吗?在opendup2 周围添加一些调试printf() 调用——或者使用strace(1) 来观察你正在进行的系统调用。 对输入和输入非常有信心。对处理输入重定向的任何事情都没有信心;这对我来说非常新鲜和陌生。 您不应该在某些时候使用“0”而在其他时候使用 STDIN_FILENO。选择一个或另一个。我不确定您为什么将 dup(0) 保存为 current_out。当然 current_in 更好(尽管老实说,保留另一个 ref 并没有任何帮助)。运行 strace -o/tmp/tr -f myshell 'cat &lt; /etc/passwd' 并向我们展示 open 和 fork 之间的系统调用将使事情变得更加清晰,然后向我们展示孩子(猫)做了什么也会很好。 【参考方案1】:

重定向后打开的文件描述符过多。让我们剖析这两段:

if (in)  //if '<' char was found in string inputted by user
    fd = open(input, O_RDONLY, 0);
    dup2(fd, STDIN_FILENO);
    in = 0;
    current_in = dup(0);  // Fix for symmetry with second paragraph


if (out)  //if '>' was found in string inputted by user
    fd = creat(output, 0644);
    dup2(fd, STDOUT_FILENO);
    out = 0;
    current_out = dup(1);

我将成为慈善家,并忽略您忽略错误的事实。但是,您需要对系统调用进行错误检查。

在第一段中,您打开一个文件并在变量fd 中捕获文件描述符(很可能是3)。然后将文件描述符复制到标准输入 (STDIN_FILENO)。但请注意,文件描述符 3 仍处于打开状态。然后你做一个dup(0)(为了一致性,应该是STDIN_FILENO),得到另一个文件描述符,也许是4。所以你有文件描述符0、3和4指向同一个文件(实际上,相同打开文件描述——注意打开文件描述不同于打开文件描述符)。如果您对current_in 的意图是保留(父)shell 的标准输入,则必须在执行覆盖输出的dup2() 之前执行dup()。但是,最好不要更改父 shell 的文件描述符;这比重新复制文件描述符的开销要小。

然后您或多或少地重复第二段中的过程,首先覆盖文件描述符 3 的唯一记录,该记录使用 fd = creat(...) 调用打开,但获得一个新的描述符,可能是 5,然后将其复制到标准输出中。然后执行dup(1),产生另一个文件描述符,可能是 6。

因此,您已将主 shell 的标准输入和标准输出重定向到文件(并且无法将它们恢复为原始值)。因此,您的第一个问题是您在 fork() 之前进行重定向;您应该在fork() 之后执行此操作——尽管当您在进程之间使用管道时,您需要在分叉之前创建管道。

您的第二个问题是您需要关闭过多的文件描述符,其中一个您不再有参考。

所以,你可能需要:

if ((pid = fork()) < 0)
    ...error...
else if (pid == 0)

    /* Be childish */
    if (in)
    
        int fd0 = open(input, O_RDONLY);
        dup2(fd0, STDIN_FILENO);
        close(fd0);
    

    if (out)
    
        int fd1 = creat(output , 0644) ;
        dup2(fd1, STDOUT_FILENO);
        close(fd1);
    
    ...now the child has stdin coming from the input file, 
    ...stdout going to the output file, and no extra files open.
    ...it is safe to execute the command to be executed.
    execve(cmd[0], cmd, env);   // Or your preferred alternative
    fprintf(stderr, "Failed to exec %s\n", cmd[0]);
    exit(1);

else

    /* Be parental */
    ...wait for child to die, etc...

在你做任何这些之前,你应该确保你已经刷新了 shell 的标准 I/O 通道,可能是使用fflush(0),这样如果分叉的孩子因为问题而写入标准错误,那里是没有多余的重复输出。

还要注意各种open() 调用应该进行错误检查。

【讨论】:

您可以通过在仅父代码中放置适当的printf()fflush() 调用来做一些简单的验证父shell 的标准输出是否混乱(例如,在您捕获退出之后孩子的状态,你可以打印出它的 pid 和它的退出状态)。你的原始代码有dup2(current_out, FILENO_STDOUT);你摆脱了吗? (答案可能是肯定的,但如果你不这样做,它可能会解释你的问题。) 是的,我确实摆脱了它。父母正在打印到控制台,而孩子似乎是问题所在。 好的,看起来这是一个简单的修复。 “in”和“out”在子进程中被设置为 0,但是父进程仍然存储了我的无限 while 循环控制 shell 的最后一次迭代中的先前值。【参考方案2】:

重定向后打开的文件描述符过多。你需要的代码就是这个。

    if (pid == 0)
          /* for the child process:         */

    // function for redirection ( '<' , '>' )

    int fd0,fd1,i,in=0,out=0;
    char input[64],output[64];

    // finds where '<' or '>' occurs and make that argv[i] = NULL , to ensure that command wont't read that

    for(i=0;argv[i]!='\0';i++)
    
        if(strcmp(argv[i],"<")==0)
                
            argv[i]=NULL;
            strcpy(input,argv[i+1]);
            in=2;           
                       

        if(strcmp(argv[i],">")==0)
              
            argv[i]=NULL;
            strcpy(output,argv[i+1]);
            out=2;
                 
    

    //if '<' char was found in string inputted by user
    if(in)
       

        // fdo is file-descriptor
        int fd0;
        if ((fd0 = open(input, O_RDONLY, 0)) < 0) 
            perror("Couldn't open input file");
            exit(0);
                   
        // dup2() copies content of fdo in input of preceeding file
        dup2(fd0, 0); // STDIN_FILENO here can be replaced by 0 

        close(fd0); // necessary
    

    //if '>' char was found in string inputted by user 
    if (out)
    

        int fd1 ;
        if ((fd1 = creat(output , 0644)) < 0) 
            perror("Couldn't open the output file");
            exit(0);
                   

        dup2(fd1, STDOUT_FILENO); // 1 here can be replaced by STDOUT_FILENO
        close(fd1);
    

    execvp(*argv, argv);
    perror("execvp");
    _exit(1);

    // another syntax
    /*      if (!(execvp(*argv, argv) >= 0))      // execute the command  
            printf("*** ERROR: exec failed\n");
            exit(1);
     */ 



    else if((pid) < 0)
         
        printf("fork() failed!\n");
        exit(1);
    

    else                                   /* for the parent:      */

        while (!(wait(&status) == pid)) ; // good coding to avoid race_conditions(errors) 
    

【讨论】:

老兄,你不能对一个 4 岁的问题给出原始答案吗?甚至第一行也与接受的答案相同。你没有提供任何新东西。如果您想获得更好的声誉,请尝试回答一些尚未回答或已接受答案的问题。 不,最初的答案不完整。我花了很长时间才弄清楚,那里的“in”和“out”是什么? ,我的意思是我不知道它们是来自某个库的一些内置语法或以前硬编码的。因此@Matt 我用完整的代码回答了它。【参考方案3】:

这就是正在发生的事情。在您调用fork() 后,将执行两个与原始进程重复的进程。区别在于fork() 的返回值存储在pid 中。

然后两个进程(shell 和子进程)将它们的标准输入和标准输出重定向到相同的文件。我认为您试图将以前的 fd 保存在 current_out 中,但正如 Seth Robertson 指出的那样,这目前不起作用,因为正在保存错误的文件描述符。父级还恢复其标准输出,但不恢复标准输入。

您可以修复此错误,但可以做得更好。您实际上不必重定向父母的输出,只需重定向孩子的输出。所以只需先检查pid。那么也不需要恢复任何文件描述符。

【讨论】:

我认为你在询问者试图做的事情上是正确的,当然除了 dup2() 发生在保存 current_out 之前而不是之后。在孩子身上做这一切当然会使问题变得毫无意义,所以是最好的选择。 @SethRobertson 我错过了。我想我在脑海中做了一些自动更正:) 我会更新答案。

以上是关于在 C 中实现 shell 并需要帮助处理输入/输出重定向的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C 中实现我自己的基本 unix shell?

需要帮助在 Unity 3D 中实现移动输入

在下面所述的代码中实现错误处理时需要帮助[重复]

在 qt 中实现 GUI Shell

Shell脚本中实现hbase shell命令调用

Shell脚本中实现切换用户并执行命令操作