【问题标题】:C Minishell Adding PipelinesC Minishell 添加管道
【发布时间】:2012-11-18 03:28:13
【问题描述】:

所以我正在制作一个 UNIX minishell,并尝试添加管道,所以我可以这样做:

ps aux | grep dh | grep -v grep | cut -c1-5

但是,我无法将头绕在管道部分上。我替换了所有的“|”字符为 0,然后将每一行作为正常行运行。但是,我试图转移输出和输入。一个命令的输入需要是上一个命令的输出,一个命令的输出需要是下一个命令的输入。

我正在使用管道执行此操作,但是我不知道在哪里调用 pipe() 以及在哪里关闭它们。从主处理函数processline(),我有这段代码:

if((pix = findUnquotChar(line_itr, '|')))
{
    line_itr[pix++] = 0;
    if(pipe (fd) < 0) perror("pipe");
    processline(line_itr, inFD, fd[1], pl_flags);
    line_itr = &(line_itr[pix]);

    while((pix = findUnquotChar(line_itr, '|')) && pix < line_len)
    {
        line_itr[pix++] = 0;
        //? if(pipe (fd) < 0) perror("pipe");
        processline(line_itr, fd[0], fd[1] pl_flags);
        line_itr = &(line_itr[pix]);
    //? close(fd[0]);
    //? close(fd[1]);
    }
    return;
}

所以,我递归地(上面的代码在进程行中)在“|”之间发送命令由processline处理。你可以看到我在哪里注释掉了上面的代码,我不知道如何让它工作。 processline 的第 2 和第 3 个参数分别是 inputFD 和 outputFD,所以我需要处理一个命令,将输出写入管道,然后在下一个命令上再次调用 processline,但是这次上一个命令的输出是输入。但这似乎不起作用,因为每次我关闭 fd[0] 时,我都会丢失以前的输出。我是否需要两个可以来回翻转的独立管道?

我只是很难看到这如何用一个管道实现,如果你们需要任何其他信息,请询问。如果您想看一下,这是整个 processline 函数:

http://pastebin.com/YiEdaYdj

编辑:如果有人有一个实现管道的 shell 示例,我希望提供指向源代码的链接,但到目前为止,我无法在 google 上找到一个。

EDIT2:这是我的困境的一个例子:

echo a | echo b | echo c

所以首先我会这样调用 shell:

processline("echo a", 0, fd[1], flags);

....

processline("echo b", fd[0], NOT_SURE_GOES_HERE[1], flags);

....

processline("echo c", NOT_SURE_GOES_HERE[0], NOT_SURE_EITHER[1], flags);

这些中的每一个在每次迭代中发生一次,正如您所看到的,我无法弄清楚为第二次和第三次(等等)迭代的输入文件描述符和输出文件描述符传递什么.

【问题讨论】:

  • 您需要为命令行中的每个单独的| 符号单独调用pipe()
  • 如果我在 fd 上调用 pipe,那么它不会清除上一个命令的输出吗?如何将上一个命令的输出设置为下一个命令的输入,然后还设置下一个命令的输出?我无法解决这个问题,我如何从 fd[0] 读取,写入 fd[1],然后在下一个命令中以某种方式再次从 fd[0] 读取?
  • 您基本上只是将一个标准输出连接到下一个标准输入。例如参见:bash、ash、sh、toysh、busybox 有几个,包括 hush
  • 是的,我从理论上理解它,但我很难找到一个好的算法来在迭代每个子命令的循环内使用。我找不到使用单个管道的方法,如果我使用两个管道,我能做到的唯一方法是每次都将哪些文件描述符传递给我的 shell,这看起来不像最好的方法。谢谢:)

标签: c shell unix pipe pipeline


【解决方案1】:

这里有一些用于执行管道的通用但简单的代码,我称之为pipeline 的程序。它是一个单一文件中的 SSCCE,尽管我会将文件 stderr.hstderr.c 作为库中的单独文件与我的所有程序链接。 (实际上,我的“真实”stderr.cstderr.h 中有一组更复杂的函数,但这是一个很好的起点。)

代码以两种方式运行。如果您不提供任何参数,则它会运行一个内置管道:

who | awk '{print $1}' | sort | uniq -c | sort -n

这会计算每个人登录系统的次数,按会话数递增的顺序显示列表。或者,您可以使用一系列参数进行调用,这些参数是您要调用的命令行,使用带引号的管道 '|'(或 "|")分隔命令:

有效:

pipeline
pipeline ls '|' wc
pipeline who '|' awk '{print $1}' '|' sort '|' uniq -c '|' sort -n
pipeline ls

无效:

pipeline '|' wc -l
pipeline ls '|' '|' wc -l
pipeline ls '|' wc -l '|'

最后三个调用强制“管道作为分隔符”。该代码不会对每个系统调用进行错误检查;它会检查fork()execvp()pipe() 的错误,但会跳过检查dup2()close()。它不包括生成的命令的诊断打印; -x 选项到 pipeline 将是一个明智的添加,使其打印出它所做的事情的痕迹。它也不会以管道中最后一个命令的退出状态退出。

请注意,代码以一个孩子被分叉开始。子进程将成为管道中的最后一个进程,但首先创建一个管道并派生另一个进程以运行管道中的较早进程。相互递归函数不太可能是解决问题的唯一方法,但它们确实留下了最少的代码重复(早期的代码草稿中 exec_nth_command() 的内容在 exec_pipeline()exec_pipe_command() 中大部分重复)。

这里的进程结构使得原始进程只知道管道中的最后一个进程。可以重新设计事物,使原始进程是管道中每个进程的父进程,因此原始进程可以单独报告管道中每个命令的状态。我还没有修改代码以允许这种结构;它会更复杂一些,虽然不是那么可怕。

/* One way to create a pipeline of N processes */

/* stderr.h */
#ifndef STDERR_H_INCLUDED
#define STDERR_H_INCLUDED

static void err_setarg0(const char *argv0);
static void err_sysexit(char const *fmt, ...);
static void err_syswarn(char const *fmt, ...);

#endif /* STDERR_H_INCLUDED */

/* pipeline.c */
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
/*#include "stderr.h"*/

typedef int Pipe[2];

/* exec_nth_command() and exec_pipe_command() are mutually recursive */
static void exec_pipe_command(int ncmds, char ***cmds, Pipe output);

/* With the standard output plumbing sorted, execute Nth command */
static void exec_nth_command(int ncmds, char ***cmds)
{
    assert(ncmds >= 1);
    if (ncmds > 1)
    {
        pid_t pid;
        Pipe input;
        if (pipe(input) != 0)
            err_sysexit("Failed to create pipe");
        if ((pid = fork()) < 0)
            err_sysexit("Failed to fork");
        if (pid == 0)
        {
            /* Child */
            exec_pipe_command(ncmds-1, cmds, input);
        }
        /* Fix standard input to read end of pipe */
        dup2(input[0], 0);
        close(input[0]);
        close(input[1]);
    }
    execvp(cmds[ncmds-1][0], cmds[ncmds-1]);
    err_sysexit("Failed to exec %s", cmds[ncmds-1][0]);
    /*NOTREACHED*/
}

/* Given pipe, plumb it to standard output, then execute Nth command */
static void exec_pipe_command(int ncmds, char ***cmds, Pipe output)
{
    assert(ncmds >= 1);
    /* Fix stdout to write end of pipe */
    dup2(output[1], 1);
    close(output[0]);
    close(output[1]);
    exec_nth_command(ncmds, cmds);
}

/* Execute the N commands in the pipeline */
static void exec_pipeline(int ncmds, char ***cmds)
{
    assert(ncmds >= 1);
    pid_t pid;
    if ((pid = fork()) < 0)
        err_syswarn("Failed to fork");
    if (pid != 0)
        return;
    exec_nth_command(ncmds, cmds);
}

/* Collect dead children until there are none left */
static void corpse_collector(void)
{
    pid_t parent = getpid();
    pid_t corpse;
    int   status;
    while ((corpse = waitpid(0, &status, 0)) != -1)
    {
        fprintf(stderr, "%d: child %d status 0x%.4X\n",
                (int)parent, (int)corpse, status);
    }
}

/*  who | awk '{print $1}' | sort | uniq -c | sort -n */
static char *cmd0[] = { "who",                0 };
static char *cmd1[] = { "awk",  "{print $1}", 0 };
static char *cmd2[] = { "sort",               0 };
static char *cmd3[] = { "uniq", "-c",         0 };
static char *cmd4[] = { "sort", "-n",         0 };

static char **cmds[] = { cmd0, cmd1, cmd2, cmd3, cmd4 };
static int   ncmds = sizeof(cmds) / sizeof(cmds[0]);

static void exec_arguments(int argc, char **argv)
{
    /* Split the command line into sequences of arguments */
    /* Break at pipe symbols as arguments on their own */
    char **cmdv[argc/2];            // Way too many
    char  *args[argc+1];
    int cmdn = 0;
    int argn = 0;

    cmdv[cmdn++] = &args[argn];
    for (int i = 1; i < argc; i++)
    {
        char *arg = argv[i];
        if (strcmp(arg, "|") == 0)
        {
            if (i == 1)
                err_sysexit("Syntax error: pipe before any command");
            if (args[argn-1] == 0)
                err_sysexit("Syntax error: two pipes with no command between");
            arg = 0;
        }
        args[argn++] = arg;
        if (arg == 0)
            cmdv[cmdn++] = &args[argn];
    }
    if (args[argn-1] == 0)
        err_sysexit("Syntax error: pipe with no command following");
    args[argn] = 0;
    exec_pipeline(cmdn, cmdv);
}

int main(int argc, char **argv)
{
    err_setarg0(argv[0]);
    if (argc == 1)
    {
        /* Run the built in pipe-line */
        exec_pipeline(ncmds, cmds); 
    }
    else
    {
        /* Run command line specified by user */
        exec_arguments(argc, argv);
    }
    corpse_collector();
    return(0);
}

/* stderr.c */
/*#include "stderr.h"*/
#include <stdio.h>
#include <stdarg.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>

static const char *arg0 = "<undefined>";

static void err_setarg0(const char *argv0)
{
    arg0 = argv0;
}

static void err_vsyswarn(char const *fmt, va_list args)
{
    int errnum = errno;
    fprintf(stderr, "%s:%d: ", arg0, (int)getpid());
    vfprintf(stderr, fmt, args);
    if (errnum != 0)
        fprintf(stderr, " (%d: %s)", errnum, strerror(errnum));
    putc('\n', stderr);
}

static void err_syswarn(char const *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    err_vsyswarn(fmt, args);
    va_end(args);
}

static void err_sysexit(char const *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    err_vsyswarn(fmt, args);
    va_end(args);
    exit(1);
}

信号和 SIGCHLD

POSIX Signal Concepts 部分讨论了 SIGCHLD:

在 SIG_DFL 下:

如果默认操作是忽略信号,则信号的传递对进程没有影响。

在 SIG_IGN 下:

如果 SIGCHLD 信号的动作设置为 SIG_IGN,调用进程的子进程在终止时不应变成僵尸进程。如果调用进程随后等待其子进程,并且该进程没有转化为僵尸进程的未等待子进程,则它将阻塞直到其所有子进程终止,并且wait()waitid()waitpid() 应失败并将 errno 设置为 [ECHILD]

&lt;signal.h&gt; 的描述有一个信号的默认处置表,对于 SIGCHLD,默认是 I (SIG_IGN)。


我在上面的代码中添加了另一个函数:

#include <signal.h>

typedef void (*SigHandler)(int signum);

static void sigchld_status(void)
{
    const char *handling = "Handler";
    SigHandler sigchld = signal(SIGCHLD, SIG_IGN);
    signal(SIGCHLD, sigchld);
    if (sigchld == SIG_IGN)
        handling = "Ignored";
    else if (sigchld == SIG_DFL)
        handling = "Default";
    printf("SIGCHLD set to %s\n", handling);
}

我在调用 err_setarg0() 后立即调用它,它在 Mac OS X 10.7.5 和 Linux(RHEL 5、x86/64)上都报告了“默认”。我通过运行验证了它的操作:

(trap '' CHLD; pipeline)

在这两个平台上,报告“已忽略”,pipeline 命令不再报告孩子的退出状态;它没有得到它。

因此,如果程序忽略 SIGCHLD,它不会生成任何僵尸程序,但会一直等到它的“所有”子进程终止。也就是说,直到它的所有直接子节点都终止;一个进程不能等待它的孙子或更远的后代,也不能等待它的兄弟姐妹,也不能等待它的祖先。

另一方面,如果 SIGCHLD 的设置是默认值,则忽略该信号,并创建僵尸。

这是编写的这个程序最方便的行为。 corpse_collector() 函数有一个循环,用于收集任何子节点的状态信息。这段代码一次只有一个孩子;管道的其余部分作为管道中最后一个进程的子进程(子进程、子进程...)运行。


但是我遇到了僵尸/尸体问题。我的老师让我以与您相同的方式实现它,因为 cmd1 不是 cmd2 在以下情况下的父级:“cmd1 | cmd2 | cmd3”。除非我告诉我的 shell 等待每个进程(cmd1cmd2cmd3),而不是只等待最后一个进程(cmd3),否则整个管道会在输出到达之前关闭结尾。我很难找到等待他们的好方法;我的老师说要使用 WNOHANG。

我不确定我是否理解问题所在。使用我提供的代码,cmd3cmd2 的父级,cmd2 是三命令管道中 cmd1 的父级(并且 shell 是 cmd3 的父级),所以 shell只能等cmd3。我原来确实说过:

这里的进程结构使得原始进程只知道管道中的最后一个进程。可以重新设计事物,使原始进程是管道中每个进程的父进程,因此原始进程可以单独报告管道中每个命令的状态。我还没有修改代码以允许这种结构;它会更复杂一些,虽然不是那么可怕。

如果您的 shell 能够等待管道中的所有三个命令,则您必须使用替代组织。

waitpid() 描述包括:

pid 参数指定一组请求状态的子进程。 waitpid() 函数应该只返回这个集合中子进程的状态:

  • 如果 pid 等于 (pid_t)-1,则为任何子进程请求状态。在这方面,waitpid() 就等价于 wait()。

  • 如果 pid 大于 0,它指定请求状态的单个子进程的进程 ID。

  • 如果 pid 为 0,则请求其进程组 ID 等于调用进程的任何子进程的状态。

  • 如果 pid 小于 (pid_t)-1,则为进程组 ID 等于 pid 绝对值的任何子进程请求状态。

options 参数由以下标志中的零个或多个按位或构成,在标头中定义:

...

WNOHANG 如果 pid 指定的子进程之一不能立即获得状态,waitpid() 函数不应暂停调用线程的执行。

...

这意味着如果您正在使用进程组并且 shell 知道管道正在哪个进程组中运行(例如,因为管道被第一个进程放入自己的进程组),那么父进程可以等待让适当的孩子终止。

...漫无目的...我认为这里有一些有用的信息;我可能应该写更多,但我的大脑一片空白。

【讨论】:

  • 哇,感谢您抽出时间来做这件事。这帮助了我,我能够让管道工作......几乎。我什么都懂,但是我在僵尸/尸体方面遇到了麻烦。我的老师让我以与您相同的方式实现它,因为在以下情况下 cmd1 不是 cmd 2 的父级:“cmd1 | cmd2 | cmd3”。除非我告诉我的 shell 等待每个进程(cmd1、cmd2 和 cm3),而不是只等待最后一个进程(cmd3),否则整个管道会在输出到达末尾之前关闭。我很难找到一个等待他们的好方法,我的老师说要使用 WNOHANG。谢谢:)
  • 我在 Mac OS X 10.7.5 和 Linux(RHEL 5、x86/64)上的测试都没有问题。在 Mac 上,“who”管道生成单行输出;在 Linux 上,有 6 个用户登录机器(不同用户登录 4-18 次)。最后一个sort 在读取所有输入之前不会输出任何内容,因此这是一个非常敏感的测试。我也在努力补充答案。
  • 好吧,我已经读了一段时间了,我很困惑哪种方法最适合我的 shell。我之前弄错了,我的管道的工作方式与您的不同,因此在“cmd1 | cmd2 | cmd3”中,我的 shell 是所有 3 个的父级,除非它们是内置的,在这种情况下没有子级。我想我会尝试阻止 sigchld,所以我不会生成任何僵尸。这似乎是一个理想的解决方案,但它似乎太容易了,就像会有某种捕获之类的东西。它会牺牲效率吗?无论如何,感谢您抽出宝贵时间将其全部写出来。
  • 阻塞 SIGCHLD 的问题是您无法确定您的命令或管道是否成功;你所能做的就是告诉是否还有孩子。所以,通常一个 shell 不能承受 SIG_IGN SIGCHLD;它可以使用 SIG_DFL,也可以使用特定的处理函数来响应 SIGCHLD 信号。我会选择 SIG_DFL 直到我发现有必要这样做,但我还没有制作完整的外壳,所以我不确定是否有必要。
  • 我尝试了 SIG_IGN,它使得无法找到子进程的最后一个退出值,因为当我尝试等待(&status)时,它给了我:等待:没有子进程。我仍然对 SIG_DFL 有点困惑,它似乎根本没有做任何事情。这是我现在正在考虑的,我想我会尝试的。制作一个 sigchld 处理程序,并有一个全局 sig_atomic_t 变量,我将在每次调用处理程序时递增。然后当我完成执行并返回 shell 时,我将等待 n 次,收集最后一个孩子的退出值(在本例中为 cmd3)。这看起来合适吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-01-14
  • 2019-04-22
  • 2015-04-14
  • 1970-01-01
  • 2022-12-06
  • 2012-11-06
  • 1970-01-01
相关资源
最近更新 更多