这是一些用于执行管道的中等通用但简单的代码,我正在调用一个程序pipeline
。它是一个单一文件中的 SSCCE,尽管我将文件stderr.h
和stderr.c
作为库中的单独文件与我的所有程序链接。(实际上,我的“真实”stderr.c
和中有一组更复杂的函数stderr.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()
大部分重复了 inexec_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信号概念部分讨论了 SIGCHLD:
在 SIG_DFL 下:
如果默认操作是忽略信号,则信号的传递对进程没有影响。
在 SIG_IGN 下:
如果 SIGCHLD 信号的动作设置为 SIG_IGN,则调用进程的子进程在终止时不应变成僵尸进程。如果调用进程随后等待其子进程,并且该进程没有转化为僵尸进程的未等待子进程,则它将阻塞直到其所有子进程终止,并且wait()、waitid()和waitpid()将失败并将 errno 设置为[ECHILD]
.
的描述<signal.h>
有一个信号的默认处置表,对于 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 等待每个进程(cmd1
、cmd2
和cmd3
),而不是只等待最后一个进程(cmd3
),否则整个管道会在输出到达末尾之前关闭。我很难找到等待他们的好方法;我的老师说要使用 WNOHANG。
我不确定我是否理解这个问题。使用我提供的代码,cmd3
是 的父级cmd2
,并且cmd2
是cmd1
3-命令管道中的父级(并且 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 参数由以下标志中的零个或多个的按位或构成,在标头中定义:
...
WNOHANGwaitpid()
如果 pid 指定的子进程之一不能立即获得状态,则该函数不应暂停调用线程的执行。
...
这意味着如果您正在使用进程组并且 shell 知道管道正在哪个进程组中运行(例如,因为管道被第一个进程放入其自己的进程组),那么父进程可以等待适当的孩子终止。
...漫无边际...我认为这里有一些有用的信息;我可能应该写更多,但我的大脑一片空白。