【问题标题】:Pipe function not executing properly管道功能未正确执行
【发布时间】:2016-09-14 18:57:01
【问题描述】:

我已经构建了以下程序来尝试在我自己的 shell 中进行管道传输。 StringArray 只是我构建的 char**。代码运行良好,但是当我输入cat txt.txt | grep a 时,屏幕上没有任何内容。调试时,我看到代码似乎停在 152 左右(打印输出命令所在的位置),pid==0i==0

对于上下文,我在检测到管道后在另一个函数中调用此函数。

void doPipe(StringArray sa) 
{
    printf("In 69\n"); 
    int filedes[2]; // pos. 0 output, pos. 1 input of the pipe
    int filedes2[2];

    int num_cmds = 0;

    char *command[256];

    pid_t pid;

    int err = -1;
    int end = 0;

    // Variables used for the different loops
    int i = 0;
    int j = 0;
    int k = 0;
    int l = 0;

    // First we calculate the number of commands (they are separated
    // by '|')
    while (sa[l] != NULL){
        if (strcmp(sa[l],"|") == 0){
            num_cmds++;
        }
        l++;
    }
    num_cmds++;

    // Main loop of this method. For each command between '|', the
    // pipes will be configured and standard input and/or output will
    // be replaced. Then it will be executed
    while (sa[j] != NULL && end != 1){
        k = 0;
        // We use an auxiliary array of pointers to store the command
        // that will be executed on each iteration
        while (strcmp(sa[j],"|") != 0){
            command[k] = sa[j];
            j++;    
            if (sa[j] == NULL){
                // 'end' variable used to keep the program from entering
                // again in the loop when no more arguments are found
                end = 1;
                k++;
                break;
            }
            k++;
        }
        // Last position of the command will be NULL to indicate that
        // it is its end when we pass it to the exec function
        command[k] = NULL;
        j++;        
        printf("In 121\n"); 

        // Depending on whether we are in an iteration or another, we
        // will set different descriptors for the pipes inputs and
        // output. This way, a pipe will be shared between each two
        // iterations, enabling us to connect the inputs and outputs of
        // the two different commands.
        if (i % 2 != 0){
            pipe(filedes); // for odd i
        }else{
            pipe(filedes2); // for even i
        }

        pid=fork();

        if(pid==-1){            
            if (i != num_cmds - 1){
                if (i % 2 != 0){
                    close(filedes[1]); // for odd i
                }else{
                    close(filedes2[1]); // for even i
                } 
            }           
            printf("Child process could not be created\n");
            return;
        }
        if(pid==0){
            printf("In 148\n"); 

            // If we are in the first command
            if (i == 0){
                printf("In 152\n"); 

                dup2(filedes2[1], STDOUT_FILENO);
            }
            // If we are in the last command, depending on whether it
            // is placed in an odd or even position, we will replace
            // the standard input for one pipe or another. The standard
            // output will be untouched because we want to see the 
            // output in the terminal
            else if (i == num_cmds - 1){
                printf("In 162\n"); 

                if (num_cmds % 2 != 0){ // for odd number of commands
                    dup2(filedes[0],STDIN_FILENO);
                    printf("In 166\n"); 

                }else{ // for even number of commands
                    dup2(filedes2[0],STDIN_FILENO);
                    printf("In 166\n"); 

                }
            // If we are in a command that is in the middle, we will
            // have to use two pipes, one for input and another for
            // output. The position is also important in order to choose
            // which file descriptor corresponds to each input/output
            }else{ // for odd i
                if (i % 2 != 0){
                    dup2(filedes2[0],STDIN_FILENO); 
                    dup2(filedes[1],STDOUT_FILENO);
                }else{ // for even i
                    dup2(filedes[0],STDIN_FILENO); 
                    dup2(filedes2[1],STDOUT_FILENO);                    
                } 
            }

            if (execvp(command[0],command)==err){
                kill(getpid(),SIGTERM);
            }       
        }

        // CLOSING DESCRIPTORS ON PARENT
        if (i == 0){
            close(filedes2[1]);
        }
        else if (i == num_cmds - 1){
            if (num_cmds % 2 != 0){                 
                close(filedes[0]);
            }else{                  
                close(filedes2[0]);
            }
        }else{
            if (i % 2 != 0){                    
                close(filedes2[0]);
                close(filedes[1]);
            }else{                  
                close(filedes[0]);
                close(filedes2[1]);
            }
        }

        waitpid(pid,NULL,0);

        i++;    
    }


}

【问题讨论】:

  • 父母似乎在等待每个孩子退出,然后再分叉下一个。这在几个层面上都是错误的。它需要启动所有个孩子,然后等待所有孩子。
  • execvp()失败的情况下,为什么孩子给自己发SIGTERM?只需exit() 会容易得多。另外,您可以根据需要测试execvp() 的返回值,但您不需要这样做,因为它只有在失败时才会返回。
  • 您似乎有printf() 调用进行调试,您在其中硬编码行号。出于调试目的,打印语句没有问题,但您应该考虑使用 __LINE__ 宏而不是硬编码行号。
  • 你在没有检查返回值的情况下进行了很多系统调用。你真的应该检查每一个。预处理器可以帮助您做到这一点,同时最大限度地减少代码的丑化。
  • 在两个文件描述符数组之间来回翻转是很聪明的,但反复测试i 的奇偶性以确定将哪个用于任何特定目的不是。通过为文件描述符创建一个 2x2 数组并战略性地使用指针,您可以大大简化所有这些操作。

标签: c shell pipe piping


【解决方案1】:

您的一个大问题可能是在管道构造的每次迭代中执行waitpid。等待应该在最后完成(记住列表中的 pid)。

我在理解您的代码时遇到了一些困难,所以我做了一些简化和清理工作。尤其是在任何地方都使用if (i % 2 ...) 会让事情变得更加困难。

我已经清理并修复了代码。我添加了一个结构以使事情更易于管理[请原谅无端的样式清理]:

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

typedef struct {
    int pipe_fildes[2];
} pipectl_t;

#define CLOSEME(_fd) \
    do { \
        close(_fd); \
        _fd = -1; \
    } while (0)

void
doPipe(char **sa)
{
    pipectl_t pipes[2];
    pipectl_t *pipein;
    pipectl_t *pipeout;
    pipectl_t *pipetmp;

    int num_cmds = 0;

    char *command[256];
    pid_t pidlist[256];

    pid_t pid;

    int err = -1;
    int end = 0;

    // Variables used for the different loops
    int icmd = 0;
    int j = 0;
    int k = 0;
    int l = 0;

    // First we calculate the number of commands (they are separated
    // by '|')
    for (int l = 0;  sa[l] != NULL;  ++l) {
        if (strcmp(sa[l], "|") == 0)
            num_cmds++;
    }
    num_cmds++;

    for (int ipipe = 0;  ipipe <= 1;  ++ipipe) {
        pipes[ipipe].pipe_fildes[0] = -1;
        pipes[ipipe].pipe_fildes[1] = -1;
    }

    pipein = &pipes[0];
    pipeout = &pipes[1];

    // Main loop of this method. For each command between '|', the
    // pipes will be configured and standard input and/or output will
    // be replaced. Then it will be executed
    while (sa[j] != NULL && end != 1) {
        // We use an auxiliary array of pointers to store the command
        // that will be executed on each iteration
        k = 0;
        while (strcmp(sa[j], "|") != 0) {
            command[k] = sa[j];
            j++;
            k++;
            if (sa[j] == NULL) {
                // 'end' variable used to keep the program from entering
                // again in the loop when no more arguments are found
                end = 1;
                break;
            }
        }

        // Last position of the command will be NULL to indicate that
        // it is its end when we pass it to the exec function
        command[k] = NULL;

        j++;

        // swap input and output, so previous child's output becomes the new
        // child's input
        // NOTE: by doing this here, in one place, we eliminate all the i % 2
        // if statements
        pipetmp = pipein;
        pipein = pipeout;
        pipeout = pipetmp;

        // are we the last command?
        int lastflg = (icmd == (num_cmds - 1));

        // last command does _not_ have an output pipe, so don't create one
        if (! lastflg)
            pipe(pipeout->pipe_fildes);

        pid = fork();

        // NOTE: fork failure almost never happens and is fatal
        if (pid == -1) {
            printf("Child process could not be created\n");
            return;
        }

        // process child
        if (pid == 0) {
            // NOTE: after we've dup'ed a file descriptor, we close it

            // first command does _not_ have a pipe for input
            if (icmd > 0)
                dup2(pipein->pipe_fildes[0],STDIN_FILENO);
            CLOSEME(pipein->pipe_fildes[0]);

            // last command does _not_ have a pipe for output
            if (! lastflg)
                dup2(pipeout->pipe_fildes[1],STDOUT_FILENO);
            CLOSEME(pipeout->pipe_fildes[1]);

            // close the parent sides of the pipes (in this child)

            // close previous child's output descriptor (the feed for our input)
            CLOSEME(pipein->pipe_fildes[1]);

            // close next child's input descriptor (our feed for its input)
            CLOSEME(pipeout->pipe_fildes[0]);

            if (execvp(command[0], command) == err) {
#if 0
                kill(getpid(), SIGTERM);
#else
                exit(1);
#endif
            }
        }

        // close all input descriptors for _this_ child
        CLOSEME(pipein->pipe_fildes[0]);
        CLOSEME(pipein->pipe_fildes[1]);

        // close output side of _this_ child's output pipe [which becomes next
        // child's input pipe]
        CLOSEME(pipeout->pipe_fildes[1]);

        pidlist[icmd] = pid;

        icmd++;
    }

    // wait for all pids _after_ the entire pipeline is constructed
    for (int icmd = 0;  icmd < num_cmds;  ++icmd)
        waitpid(pidlist[icmd], NULL, 0);
}

// main -- main program
int
main(int argc,char **argv)
{
    char *cp;
    char *bp;
    char buf[1000];
    char **av;
    char *avlist[256];

    --argc;
    ++argv;

    for (;  argc > 0;  --argc, ++argv) {
        cp = *argv;
        if (*cp != '-')
            break;

        switch (cp[1]) {
        default:
            break;
        }
    }

    while (1) {
        printf("> ");
        fflush(stdout);

        cp = fgets(buf,sizeof(buf),stdin);
        if (cp == NULL)
            break;

        av = avlist;
        bp = buf;
        while (1) {
            cp = strtok(bp," \t\r\n");
            bp = NULL;

            if (cp == NULL)
                break;

            *av++ = cp;
        }
        *av = NULL;

        doPipe(avlist);
    }

    return 0;
}

更新:

当我运行此代码时,相同的命令cat txt.txt | grep a 似乎只执行第一个命令,而不是管道之后的第二个命令。 (它会剔除 txt 文件,但不会 grep)

我在发布之前测试了整个程序。我刚刚使用cat/grep 命令重新测试。它有效,但我的程序没有改变。

任何想法为什么会发生这种情况?我在我的代码中实现了你的 doPipe 方法,并传入了我的 StringArray sa,它也只是一个 char **。

我的建议是:

  1. 验证我未更改的版本是否适合您。
  2. doPipe 上使用gdb 断点并查看参数。对于这两个程序,它们应该是相同的。
  3. 如果StringArray 真的是char **,请在您的版本中替换它以确保它没有区别。那是void doPipe(char **sa),看看你的代码是否还能编译。在断点处的gdb 中,您应该可以在两个程序上执行ptype sa
  4. StringArray 在我看来有点“Java 风格”:-) 我会避免使用它,尤其是在这里,因为 execvp 想要 char **
  5. 验证sa 是否正确终止NULL。如果它不是管道中的最后一个命令,则可能是虚假/垃圾,并且对失败的execvp 的错误检查不是那么可靠。
  6. 验证num_cmds 是否相同。
  7. 试试cat txt.txt | grep a | sed -e s/a/b/。如果你得到catgrep,但没有得到sed,这意味着num_cmds 不正确
  8. 验证调用者对缓冲区的解析是否将"|" 放入单独的标记中。也就是说,此代码适用于cat txt.txt | grep a,但它适用于:cat txt.txt|grep a

更新 #2:

顺便说一句,如果您的管道代码仍然无法工作(例如,最后一个命令没有执行),请检查最后一个标记是否有换行符(即换行符没有被剥离正确)。

我已经尝试了所有这些,但仍然无法让我的重定向代码使用它。本质上,我很困惑在这段代码中我应该在哪里检查 ''

进行一般解析以支持重定向(例如&lt;&gt;)、管道(例如|)、每行多个命令(例如;)、嵌入式子shell(例如(echo the date is ; date)、和独立的工作(例如&amp;)可能需要一点小心,你需要一个多层次的方法。

我怀疑在管道和/或重定向工作之后,您的任务是实现更多的 shell 语法。我以前做过这个,所以,与其让你零零碎碎地想办法,这就是你需要做的......

您需要逐个字符地扫描输入缓冲区并将标记保存到也具有类型的“标记”结构中。您将需要这些结构的链接列表。更多内容请参见下文。

遇到带引号的字符串时,需要去掉引号:"abc" --> abc,注意转义的引号:"ab\"c --> ab"c

此外,您必须小心引用的字符串与 [perl 调用的]“bareword”字符串相邻:echo abc。如果我们有abc"d ef"ghi,则需要将其连接成单个字符串标记:abcd efghi

还必须考虑重定向器上的反斜杠。 echo abc &gt; def 是将abc 放入文件def 的重定向。但是,echo abc \&gt; def 应该只将abc &gt; def 直接输出到标准输出。其他“标点符号”上的反斜杠类似。

您还必须处理标点符号没有 周围有空格的事实。也就是说,echo abc&gt;def 必须像处理 echo abc &gt; def 一样处理。

此外,带引号的字符串中的标点符号应被视为在上面进行了转义。也就是说,echo abc "&gt;" def不是重定向,[再次]应该被视为一个简单的命令。

另外,如果当前行以反斜杠结束(例如\&lt;newline&gt;),这意味着下一行是“延续”行。您应该去掉反斜杠和换行符。然后,阅读另一行并继续构建令牌列表。

此外,虽然&amp; 可以用于分离作业,例如:date &amp;,但它也可以是重定向的一部分,例如gcc -o myshell myshell.c 2&gt;&amp;1 &gt;logfile

好的,所以要管理所有这些,我们需要令牌类型和令牌结构:

// token types
typedef enum {
    TOKEN_NORMAL,                       // simple token/string
    TOKEN_QUO1,                         // quoted string
    TOKEN_QUO2,                         // quoted string
    TOKEN_SEMI,                         // command separater (e.g. ;)
    TOKEN_OREDIR,                       // output redirector (e.g. >)
    TOKEN_IREDIR,                       // input redirector (e.g. <)
    TOKEN_PIPE,                         // pipe separater (e.g. |)
    TOKEN_AMP                           // an & (can be detach or redirect)
} toktype_t;

// token control
typedef struct token token_t;
struct token {
    token_t *tok_next;                  // forward link
    token_t *tok_prev;                  // backward link
    toktype_t tok_type;                 // token type
    char tok_str[256];                  // token value
};

// token list
typedef struct tlist tlist_t;
struct token {
    tlist_t *tlist_next;                // forward link
    tlist_t *tlist_prev;                // backward link

    token_t *tlist_head;                // pointer to list head
    token_t *tlist_tail;                // pointer to list tail
};

最初,在解析输入行 [注意延续] 之后,我们有一个 tlist

如果列表中包含 ; 分隔符,我们会在它们上拆分以创建子列表。然后我们循环子列表并按顺序执行命令。

查看子命令时,如果它以&amp; 结尾,则该命令必须分离运行。我们注意到这一点并将其从列表的后面弹出。

好的,现在我们有一个可能是以下形式的列表:

cat < /etc/passwd | grep root | sed -e s/root/admin/ > /tmp/out

现在,我们对| 进行进一步拆分,因此我们有一个包含三个元素的列表:

cat < /etc/passwd
grep root
sed -e s/root/admin/ > /tmp/out

实际上,这些“行”中的每一个都是tlist,这是一个二维列表:

list_of_tlists:
  |
  |
tlist[0] --> cat --> < --> /etc/passwd
  |
  |
tlist[1] --> grep --> root
  |
  |
tlist[2] --> sed --> -e --> s/root/admin/ --> > /tmp/out

在创建管道时,我们会记录重定向并根据需要执行文件 open 而不是 pipe

好的,这就是摘要。

在这里查看我的答案:Implementing input/output redirection in a Linux shell using C 以获得完整和完整的实施。

在该页面上,有用于执行重定向的代码。通过将该代码与我在此处发布的代码合并,它可能适用于包含管道。

那个 OP 在做重定向 管道时寻求帮助。

旁注: 那时,出现了一连串的 shell 实现问题。所以,我最终制作了一个完整的外壳,几乎可以做所有事情。但是,该版本太大而无法在 SO 上发布。因此,在该页面中,找到我发布的 pastebin 链接。它有完整的源代码。它可以下载、构建和运行。

你可能不想直接使用那个代码,但它应该会给你一些想法。此外,完整版的功能可能与我上面描述的有所不同。

【讨论】:

  • 当我运行此代码时,相同的命令“cat txt.txt | grep a”似乎只执行第一个命令,而不是管道之后的第二个。 (它排除了 txt 文件,但没有 grep)任何想法为什么会发生这种情况?我在我的代码中实现了你的 doPipe 方法,并传入了我的 StringArray sa,它也只是一个 char **。
  • 我已经尝试了所有这些,但仍然无法让我的重定向代码使用它。本质上,我很困惑我应该在这段代码中的哪个位置检查'',然后添加我的 if(in) { if ((fd0 = open(input, O_RDONLY, 0))
猜你喜欢
  • 2018-11-16
  • 2013-07-27
  • 1970-01-01
  • 2021-11-05
  • 2019-06-25
  • 2020-12-13
  • 1970-01-01
  • 2020-02-14
  • 1970-01-01
相关资源
最近更新 更多