使用 fork 和 pipe 模仿 linux pipe 命令
using a fork and pipe to mimic linux pipe command
我的目标是模仿 linux 管道命令,例如 ls |种类。而不是简单地排序。但不是输入 |用户类型:例如 "./program ls : sort" == "ls | sort"
我需要使用 fork() 和 pipe() 来完成此任务。我有一个 MRE 设置,一次只允许我 运行 一个命令,但我不知道如何将其设置为标准输出是第二个命令的标准输入。每当我尝试在父级中 dup() close() and exec()
时,似乎出了什么问题?我有一个设置,其中解析用户给出的输入,我为参数 A 获取 argA,其中包含 ls 或排序之类的命令,为参数 A 获取 ArgAP,以防用户想要指定 -lh 或 -r 等。同样的事情对于 argB.
我目前将此程序设置为硬编码以执行 bc 命令,但可以通过分配一些参数轻松修复。请帮助我,因为我坚持这个!
//################ #-for include
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>
#include <dirent.h>
//################
int main(int b, char ** locations) {
int ok = 0;
int dots = 0;
char argA[1000];
char argB[1000];
char argAP[1000];
char argBP[1000];
while (locations[ok] != NULL) {
//printf("%s \n", locations[ok]);
if (strcmp(locations[ok], ":") == 0) {
dots = 1;
}
ok++;
} //printf("%s %d \n", locations[2], b);
strcpy(argA, "");
strcpy(argB, "");
strcpy(argAP, "");
strcpy(argBP, "");
if (dots == 0) {
int x = 1;
strcat(argA, locations[x]);
strcat(argA, " ");
x++;
while (locations[x] != NULL) {
strcat(argAP, locations[x]);
strcat(argAP, " ");
x++;
}
printf("%s%s \n", argA, argAP);
}
if (dots == 1) {
int x = 1;
int compare = strcmp(locations[x], ":");
if (strcmp(locations[1], ":") == 0) {
printf("one arg\n");
strcat(argA, locations[x]);
strcat(argA, " ");
x++;
while (locations[x] != NULL) {
strcat(argAP, locations[x]);
strcat(argAP, " ");
x++;
}
printf("%s%s \n", argA, argAP);
} else {
printf("two args\n");
strcat(argA, locations[x]);
strcat(argA, " ");
compare = strcmp(locations[x], ":");
x++;
compare = strcmp(locations[x], ":");
while (compare != 0) {
printf("%d \n", x);
strcat(argAP, locations[x]);
strcat(argAP, " ");
compare = strcmp(locations[x], ":");
x++;
}
printf("argA: %s%s \n", argA, argAP);
x++;
strcat(argB, locations[x]);
strcat(argB, " ");
x++;
while (locations[x] != NULL) {
strcat(argBP, locations[x]);
strcat(argBP, " ");
x++;
}
printf("argB: %s%s \n", argB, argBP);
}
}
// fork/piping
int i, n;
int fd[2];
pipe(fd);
int rd = fd[0]; // rd points to 0 (read) in pipe
int wt = fd[1]; // wt points to 1 (write) in pipe
if (fork()) {
close(rd);
write(wt, "2*1*9*1", strlen("2*1*9*1"));
write(wt, "\n", 1);
close(wt);
exit(0);
} else {
close(wt);
close(0); // close zero
dup(rd); // dup rd into lowest possible orbit
close(rd);
execlp("bc", "bc", 0, NULL); // reading from zero means reading from rd!
exit(1);
}
return 0;
}
我已将需要帮助的部分注释掉。我如何使用一个命令的管道将结果通过 fork 提供给第二个命令?我认为这几乎是不可能的,因为通过我的尝试,我只能通过我在这里编写的这个设置获得一个命令。
原创
一旦要替换子进程的stdin
,需要使用dup2()
函数
这是解释为什么 dup()
函数永远无法满足您的目的的手册部分:
The dup() system call creates a copy of the file descriptor oldfd,
using the lowest-numbered unused file descriptor for the new
descriptor.
这里是解释为什么 dup2()
函数可以解决您的问题的手册部分:
The dup2() system call performs the same task as dup(), but instead
of using the lowest-numbered unused file descriptor, it uses the file
descriptor number specified in newfd.
要解决您的问题,请将 dup(rd)
调用替换为 dup2(rd, STDIN_FILENO)
。您也可以删除 close(0)
调用,一旦 dup2()
函数关闭 newfd
(如果它已在使用中)。
If the file descriptor newfd was previously open, it is silently
closed before being reused.
编辑#1
我之前写的并没有解决问题,一旦close(0); dup(rd);
就会和dup2(rd, 0)
一样的效果,下面说到this user。所以,我按原样编译了你的代码,在 运行 之后,我
有这样的结果:
$ gcc -std=c99 -o program program.c
$ ./program ls : sort
two args
argA: ls
argB: sort
18
$
如你所见,最后一行显示18
,2*1*9*1
的结果。
现在,请注意父进程在写入描述为 wt
的文件后立即退出 - bc
命令的新 stdin
在子进程中执行。这意味着父进程可能会在子进程完成之前退出。我强烈建议您在父进程退出之前使用 wait()
或 waitpid()
调用来测试您的代码。例如:
// (...)
if (fork()) {
close(rd);
write(wt, "2*1*9*1", strlen("2*1*9*1"));
write(wt, "\n", 1);
close(wt);
wait(NULL);
exit(0);
} else {
close(wt);
close(0); // close zero
dup(rd); // dup rd into lowest possible orbit
close(rd);
execlp("bc", "bc", NULL);
exit(1);
}
我还用 execlp("bc", "bc", NULL);
行替换了 execlp("bc", "bc", 0, NULL);
行。我删除的零等同于 NULL
,表示使用 execlp()
.
执行的命令的参数列表的末尾
编辑#2(实施)
阅读整个代码,我们可以将您的实现分为两部分:
- 正在解析程序的参数以适应
execlp()
函数的语法;
- 将第一个命令的结果作为输入来执行第二个命令的进程。
如果您阅读 exec()
函数系列的手册页,您会注意到函数 execvp()
在此程序中更有用,因为 [=37= 的第二个参数] 函数与程序的参数类型相同:一个以 NULL
结尾的字符串数组。
按照这些步骤,您可以轻松地解析程序的参数以适应 execvp()
:
- 遍历程序的参数;
- 找到管道符号的位置;
- 在该位置,放置
NULL
以表示第一个命令的参数结束;
- 将下一个位置的地址保存为第二个命令参数的开始。
解析程序的参数后,是时候创建管道并分叉进程了。在子进程中,在执行第一个命令之前,将 stdout
替换为管道的写入端。在父进程中,在执行第二个命令之前,将 stdin
替换为管道的读取端。
这是我编写的完整代码,运行 并经过测试:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#define PIPE_SYMBOL ":"
int main ( int argc , char **argv ) {
/* Validates the usage. At least is needed the program's name, two commands and the pipe symbol */
if ( argc < 4 ) {
fprintf(stderr, "usage: command-1 [args-1...] : command-2 [args-2...]\n");
return EXIT_FAILURE;
}
/* The start of the first comment is allways the start of the program arguments array */
char **command1 = &argv[1];
/* The start of the second command is undefined, once it depends where the pipe symbol is located */
char **command2 = NULL;
/* Finds the position of the pipe symbol */
for ( int i = 0 ; argv[i] != NULL ; i++ ) {
/* When found, ... */
if ( strcmp(PIPE_SYMBOL, argv[i]) == 0 ) {
/* ... replaces it for NULL, so the first command array is NULL terminated and... */
argv[i] = NULL;
/* ... the next position is the start of the second command */
command2 = &argv[i+1];
break;
}
}
/* If the pipe symbol is missing or if there is no command after the pipe symbol, bad usage */
if ( command2 == NULL || command2[0] == NULL ) {
fprintf(stderr, "usage: command-1 [args-1...] : command-2 [args-2...]\n");
return EXIT_FAILURE;
}
pid_t pid;
int pipefd[2];
if ( pipe(pipefd) == -1 ) {
perror("creating pipe");
return EXIT_FAILURE;
}
if ( (pid = fork()) == -1 ) {
perror("creating child process");
return EXIT_FAILURE;
}
/* Child process executes the first command */
if ( pid == 0 ) {
close(pipefd[0]);
close(STDOUT_FILENO);
dup(pipefd[1]);
close(pipefd[1]);
execvp(command1[0], command1);
perror("executing first command");
return EXIT_FAILURE;
}
/* Parent process executes the second command */
close(pipefd[1]);
close(STDIN_FILENO);
dup(pipefd[0]);
close(pipefd[0]);
execvp(command2[0], command2);
perror("executing second command");
return EXIT_FAILURE;
}
您应该始终尝试将问题拆分成更小的问题 sub-problems,您可以先解决并验证。
例如,考虑是否向您提供了以下 run.h:
#ifndef RUN_H
#define RUN_H
#include <sys/types.h>
/** Create a close-on-exec pipe with descriptors not standard streams
*
* Both read and write end of the pipe will be close-on-exec,
* and neither of them will be 0 (stdin), 1 (stdout), or 2 (stderr).
*
* @fd Read ([0]) and write ([1]) ends of the pipe
* @return 0 if success, -1 with errno set if error
*/
int safe_pipe(int fd[2]);
/* pgid: */
enum {
NEW_SESSION = -2,
NO_CHANGE = -1,
NEW_PGROUP = 0,
};
/* Descriptors: */
enum {
DEV_NULL = -1,
STANDARD_INPUT = 0,
STANDARD_OUTPUT = 1,
STANDARD_ERROR = 2,
};
/** Execute a binary in a child process
*
* This function uses a control pipe to check if the specified binary could be executed
* (started; not completed!), and to provide the errno number if not.
* It is careful to ensure even oddball descriptor configurations work.
* It is up to the parent to close any pipe descriptors specified after the call.
*
* @pathname Name or path to the binary to be executed.
* @args NULL-terminated array of command-line arguments.
* Note that args[0] is the command name itself.
* @pgid Process group to use: NEW_SESSION, NO_CHANGE, NEW_PGROUP,
* or a process group ID. NO_CHANGE runs the child process
* in the same session and process group as the current process.
* @infd Standard input descriptor. DEV_NULL, 0, or a pipe descriptor.
* @outfd Standard output descriptor. DEV_NULL, 1, or a pipe descriptor.
* @errfd Standard error descriptor. DEV_NULL, 2, or a pipe descriptor.
* @return PID of the child process, or -1 with errno set if an error occurs.
*/
pid_t run(const char *pathname, const char *args[], pid_t pgid, int infd, int outfd, int errfd);
/** Wait for all child processes to finish, and reap them
*
* errno is always set when this function returns:
* ECHILD if there are no more child processes
* EINTR if wait was interrupted by signal delivery
* or any other positive value returned by reaped().
*
* @reaped NULL, or a function called for each reaped child process.
* First parameter is the PID of the child process that exited,
* the second parameter is the "packed" exit status code; see man 2 wait.
* If reaped() returns a negative value, wait_all_children() will
* immediately return with that value, keeping the same errno value.
* If reaped() returns a positive value, wait_all_children() will
* immediately return with the reaped process count, and that value in errno.
* @return Number of processes reaped, or negative if an error occurs.
*/
int wait_all_children(int (*reaped)(pid_t, int));
#endif /* RUN_H */
及其知识共享零许可(随意使用!)实施,run.c:
// SPDX-License-Identifier: CC0-1.0
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include "run.h"
/* Create a close-on-exec pipe whose read and write ends do not
* overlap with standard descriptors (stdin, stdout, stderr).
* Returns 0 if success, -1 with errno set if error.
*/
int safe_pipe(int fd[2])
{
unsigned int close_mask = 0;
int err = 0;
int pfd[2];
fd[0] = -1;
fd[1] = -1;
if (pipe2(pfd, O_CLOEXEC) == -1) {
/* errno set by pipe2() */
return -1;
}
do {
/* Make sure read end does not shadow standard descriptors */
while (pfd[0] >= 0 && pfd[0] <= 2) {
close_mask |= 1 << pfd[0];
pfd[0] = fcntl(pfd[0], F_DUPFD_CLOEXEC, 3);
if (pfd[0] == -1) {
err = errno;
break;
}
}
if (err)
break;
/* Make sure write end does not shadow standard descriptors */
while (pfd[1] >= 0 && pfd[1] <= 2) {
close_mask |= 1 << pfd[1];
pfd[1] = fcntl(pfd[1], F_DUPFD_CLOEXEC, 3);
if (pfd[1] == -1) {
err = errno;
break;
}
}
if (err)
break;
/* Close any temporarily used descriptors. */
if (close_mask & (1<<0))
close(0);
if (close_mask & (1<<1))
close(1);
if (close_mask & (1<<2))
close(2);
/* Success! */
fd[0] = pfd[0];
fd[1] = pfd[1];
return 0;
} while (0);
/* Failed. Close all related descriptors. */
if (pfd[0] != -1)
close(pfd[0]);
if (pfd[1] != -1)
close(pfd[1]);
if (close_mask & (1<<0))
close(0);
if (close_mask & (1<<1))
close(1);
if (close_mask & (1<<2))
close(2);
errno = err;
return -1;
}
/* Open the null device to a specific descriptor.
*/
static int dev_null(int to_descriptor)
{
int fd;
if (to_descriptor == -1) {
errno = EBADF;
return -1;
}
close(to_descriptor);
fd = open("/dev/null", O_RDWR | O_NOCTTY);
if (fd == -1)
return -1; /* errno set by open() */
if (fd == to_descriptor)
return 0; /* We got lucky! */
if (dup2(fd, to_descriptor) == -1) {
const int saved_errno = errno;
close(fd);
errno = saved_errno;
return -1;
}
close(fd);
return 0;
}
/* Execute a binary with the specified command-line arguments
* (args[] array terminated by a NULL pointer),
* and redirecting standard input, output, and error to the specified descriptors
* (which may, and should be, close-on-exec), or -1 to redirect to /dev/null.
* pgid can be -2 for a new session, -1 for no change, 0 for new process group, or >0 for a specific process group.
* Returns the PID of the new child process, or -1 with errno set if error.
*/
pid_t run(const char *pathname, const char *args[], pid_t pgid, int infd, int outfd, int errfd)
{
if (!pathname || !*pathname || !args || !args[0] || !args[0][0]) {
/* NULL or empty pathname (path or name) to the executable. */
errno = EINVAL;
return -1;
}
/* Create the control pipe we use between parent and child to monitor exec() success/failure. */
int ctrlfd[2];
if (safe_pipe(ctrlfd) == -1) {
/* errno set by safe_pipe(). */
return -1;
}
pid_t child = fork();
if (child == -1) {
/* Cannot fork a child process. */
const int saved_errno = errno;
close(ctrlfd[0]);
close(ctrlfd[1]);
errno = saved_errno;
return -1;
} else
if (!child) {
/* Child process. */
unsigned int close_mask = 0;
int err = 0;
do {
/* Close parent (read) end of the control pipe. */
close(ctrlfd[0]);
/* Adjust process group. */
if (pgid == NEW_SESSION) {
if (setsid() == -1) {
err = errno;
break;
}
} else
if (pgid == NEW_PGROUP) {
if (setpgid(0, 0) == -1) {
err = errno;
break;
}
} else
if (pgid > 0) {
if (setpgid(0, pgid) == -1) {
err = errno;
break;
}
} else
if (pgid != NO_CHANGE) {
err = EINVAL;
break;
}
/* Make sure infd does not shadow standard output or standard error. */
while (infd == 1 || infd == 2) {
close_mask |= 1 << infd;
infd = fcntl(infd, F_DUPFD_CLOEXEC, 3);
if (infd == -1) {
err = errno;
break;
}
}
if (err)
break;
/* Make sure outfd does not shadow standard input or standard error. */
while (outfd == 0 || outfd == 2) {
close_mask |= 1 << outfd;
outfd = fcntl(outfd, F_DUPFD_CLOEXEC, 3);
if (outfd == -1) {
err = errno;
break;
}
}
if (err)
break;
/* Make sure errfd does not shadow standard input or standard output. */
while (errfd == 0 || errfd == 1) {
close_mask |= 1 << errfd;
errfd = fcntl(errfd, F_DUPFD_CLOEXEC, 3);
if (errfd == -1) {
err = errno;
break;
}
}
if (err)
break;
/* Close unneeded descriptors. */
if ((close_mask & (1<<0)) || infd == -1)
close(0);
if ((close_mask & (1<<1)) || outfd == -1)
close(1);
if ((close_mask & (1<<2)) || errfd == -1)
close(2);
/* Redirect standard input. */
if (infd == DEV_NULL) {
if (dev_null(0) == -1) {
err = errno;
break;
}
infd = 0;
} else
if (infd != 0) {
if (dup2(infd, 0) == -1) {
err = errno;
break;
}
close(infd);
infd = 0;
}
/* Redirect standard output. */
if (outfd == DEV_NULL) {
if (dev_null(1) == -1) {
err = errno;
break;
}
outfd = 1;
} else
if (outfd != 1) {
if (dup2(outfd, 1) == -1) {
err = errno;
break;
}
close(outfd);
outfd = 1;
}
/* Redirect standard error. */
if (errfd == DEV_NULL) {
if (dev_null(2) == -1) {
err = errno;
break;
}
errfd = 2;
} else
if (errfd != 2) {
if (dup2(errfd, 2) == -1) {
err = errno;
break;
}
close(errfd);
errfd = 2;
}
/* Make sure the standard descriptors are not close-on-exec. */
if (fcntl(0, F_SETFD, 0) == -1 ||
fcntl(1, F_SETFD, 0) == -1 ||
fcntl(2, F_SETFD, 0) == -1) {
err = errno;
break;
}
/* Close the unneeded temporary descriptors. */
if (close_mask & (1<<0))
close(0);
if (close_mask & (1<<1))
close(1);
if (close_mask & (1<<2))
close(2);
close_mask = 0;
/* Execute. */
if (strchr(pathname, '/'))
execv(pathname, (char *const *)args); /* pathname has a slash, so it is a path to a binary */
else
execvp(pathname, (char *const *)args); /* pathname has no slash, so it specifies the binary name only */
/* Failed. */
err = errno;
} while (0);
/* Send err to parent via the control pipe. */
{
const char *ptr = (const char *)(&err);
const char *const end = (const char *)(&err) + sizeof err;
while (ptr < end) {
ssize_t n = write(ctrlfd[1], ptr, (size_t)(end - ptr));
if (n > 0) {
ptr += n;
} else
if (n != -1) {
/* Should never occur */
break;
} else
if (errno != EINTR) {
/* I/O error writing to the pipe too! */
break;
}
}
}
/* The kernel will close all open descriptors in the process. */
exit(127);
}
/* Parent process. */
/* Close read end of control pipe, so we detect if the child process exec() succeeded. */
close(ctrlfd[1]);
/* Read from the control pipe, to determine if child process exec succeeds or not. */
{
int err = 0;
char *ptr = (char *)(&err);
char *const end = (char *)(&err) + sizeof err;
while (ptr < end) {
ssize_t n = read(ctrlfd[0], ptr, (size_t)(end - ptr));
if (n > 0) {
ptr += n;
} else
if (n == 0) {
break;
} else
if (n != -1) {
err = EIO;
ptr = end;
break;
} else
if (errno != EINTR) {
err = errno;
ptr = end;
break;
}
}
close(ctrlfd[0]);
/* Treat partially received errors and received zero as EIO. */
if (ptr > (char *)(&err)) {
if (ptr != end || !err)
err = EIO;
}
if (err) {
/* Child failed to exec; reap it. */
/* Reap child process. Ignore errors, and retry if interrupted. */
pid_t p;
do {
p = waitpid(child, NULL, 0);
} while (p == -1 && errno == EINTR);
errno = err;
return -1;
}
}
/* Success. */
errno = 0;
return child;
}
int wait_all_children(int (*reaped)(pid_t, int))
{
int count = 0;
while (1) {
int status = 0;
pid_t pid;
pid = wait(&status);
if (pid == -1) {
/* errno set by wait() */
return count;
} else
if (pid < 1) {
/* C library or Linux kernel bug! */
errno = EIO;
return count;
}
count++;
if (reaped) {
int retval = reaped(pid, status);
if (retval < 0) {
/* errno set by reaped() */
return retval;
} else
if (retval > 0) {
errno = retval;
return count;
}
}
}
}
和 Makefile 从 example.c:
构建示例程序
CC := gcc
CFLAGS := -Wall -Wextra -O2
LDFLAGS :=
TARGETS := example
.PHONY: all clean
all: $(TARGETS)
clean:
rm -f $(TARGETS) *.o
%.o: %.c
$(CC) $(CFLAGS) -c $^
example: example.o run.o
$(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@
请注意,本论坛将Tabs 转换为空格,Makefile 缩进必须使用Tabs 而不是空格。幸运的是,运行ning sed -e 's|^ *|\t|' -i Makefile
通过将初始空格转换为制表符修复了上述问题以及大多数其他 Makefile。
上面的想法是为了帮助你在正确的道路上开始:
safe_pipe()
创建一个管道,当您执行任何 exec() 调用时,两端会自动关闭。即使您关闭了一些标准描述符(标准输入、输出或错误),它也会确保管道末端不会干扰标准描述符。
wait_all_children()
等待当前进程的所有立即 children(即该进程已分叉的那些),直到没有更多 children ,等待被信号传递或可选的报告功能(它作为参数)中断returns非零。
run()
派生一个 child 进程,根据需要重定向标准描述符,甚至处理进程组。它使用控制管道来检测 exec() 错误,并且不等待 child 进程退出,只等待 child 进程启动。
进程组很有用,因为如果每个逻辑任务都在它们自己的进程组中,您可以使用取反的进程组 ID 向特定组中的每个进程发送信号(如 KILL 或 TERM)。这样就保证了对于复杂的任务,当整个任务需要被kill掉的时候,不管它已经fork了多少个进程,都可以很好的清理掉。不过,对于简单的命令,我们不用担心进程组。
最好在我们做一个简单的示例程序后检查上面的内容,比如 运行s ls -laF | tr a-z A-Z | cat
,它列出了当前目录中的所有文件,但将小写字母 a 转换为 z大写,使用三个 child 进程:
// SPDX-License-Identifier: CC0-1.0
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include "run.h"
int report(pid_t pid, int code)
{
if (WIFEXITED(code)) {
if (WEXITSTATUS(code))
printf("Process %d exited with status %d.\n", (int)pid, WEXITSTATUS(code));
else
printf("Process %d exited with success (status 0).\n", (int)pid);
} else
if (WIFSIGNALED(code)) {
printf("Process %d died from signal %d.\n", (int)pid, WTERMSIG(code));
} else {
printf("Process %d died from unknown causes.\n", (int)pid);
}
return 0;
}
int main(void)
{
const char *cmd_a[] = { "ls", "-laF", NULL };
const char *cmd_b[] = { "tr", "a-z", "A-Z", NULL };
const char *cmd_c[] = { "cat", NULL };
int a_to_b[2], b_to_c[2];
pid_t a, b, c;
if (safe_pipe(a_to_b) == -1 || safe_pipe(b_to_c) == -1) {
fprintf(stderr, "Cannot create pipes: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
c = run(cmd_c[0], cmd_c, NO_CHANGE, b_to_c[0], STANDARD_OUTPUT, STANDARD_ERROR);
if (c == -1) {
fprintf(stderr, "%s: %s.\n", cmd_c[0], strerror(errno));
return EXIT_FAILURE;
}
b = run(cmd_b[0], cmd_b, NO_CHANGE, a_to_b[0], b_to_c[1], STANDARD_ERROR);
if (b == -1) {
fprintf(stderr, "%s: %s.\n", cmd_b[0], strerror(errno));
kill(c, SIGKILL);
wait_all_children(NULL);
return EXIT_FAILURE;
}
a = run(cmd_a[0], cmd_a, NO_CHANGE, DEV_NULL, a_to_b[1], STANDARD_ERROR);
if (a == -1) {
fprintf(stderr, "%s: %s.\n", cmd_a[0], strerror(errno));
kill(b, SIGKILL);
kill(c, SIGKILL);
wait_all_children(NULL);
return EXIT_FAILURE;
}
close(a_to_b[0]);
close(a_to_b[1]);
close(b_to_c[0]);
close(b_to_c[1]);
wait_all_children(report);
return EXIT_SUCCESS;
}
report()
函数只报告哪个 child 进程已经退出以及如何退出。您可以省略它,使用 wait_all_children(NULL);
而不是 wait_all_children(report);
,但是报告功能对于调试和检查您在不同情况下可以观察到什么样的退出状态很有用。
我们必须通过管道传输:a_to_b
和 b_to_c
。该对的第一个描述符(a_to_b[0]
和b_to_c[0]
)始终是读端,第二个(a_to_b[1]
和b_to_c[1]
)是写端。我们的三个child进程是a
、b
和c
,所以我们希望a
的标准输出是a_to_b[1]
、b
的标准输入为a_to_b[0]
,b
的标准输出为b_to_c[1]
,c
的标准输入为b_to_c[1]
。
我们需要先创建管道。
然后,我们需要 运行(fork 并执行)child 进程。因为 children 会阻塞直到他们得到输入(或者他们看到输入结束),所以我们在管道中从最后到第一创建 child 进程:首先是“消费者”,然后是“生产者”。这样,如果我们失败直到整个链都正常,生产者应该仍在等待输入。
如果我们无法创建某些child进程,我们确实需要杀死已经启动的进程,最好等待它们退出(通常称为,reap them).
当 child 进程启动后,parent 进程需要关闭它的管道描述符副本,以便当“生产者”(管道列表中的第一个进程)退出时并关闭管道的写入端,读取端报告输入结束。
如果 parent 进程不关闭其管道(写端)描述符的副本,“消费者”(从管道读取的 child 进程)将永远不会检测到 end-of-input – 因为理论上,parent 进程仍然可以写入管道! – 一切看起来都像“挂起”。
此时,管道中的进程开始工作。 parent 进程此时可以做其他事情。 (一个有趣的场景是当人们希望使用 filters 时:一系列进程,通常是一个脚本,将数据从一种格式转换为另一种格式。很可能是过滤输出管道(或管道链)和输入管道的写入端为parent可访问,使得parent在写入端写入要转换的数据,并读取转换后的数据来自读取端的数据。这有点棘手,因为我们不能假设我们可以在读取任何内容之前写入所有内容,所以通常 nonblocking I/O with使用 select()
或 poll()
。没有什么难本身,只需要正确地完成。)
由于 parent 进程除了等待管道进程完成它们的工作之外没有其他事情可做,因此 parent 只是等待它们退出。
因为有了Makefile,你只需要运行make all && ./example
编译和运行示例程序
因为整个问题被分解为 sub-problems(以对标准流造成最少问题的方式创建管道,并且不会意外泄漏到错误的 child 进程;以及分叉并启动一个 child 进程以稳健可靠的方式重定向其标准流),示例程序简短且在概念层面上易于理解。
玩过那个例子之后,是时候探索和解释 run.c 如何实现这些功能,以及为什么做出这些选择,但我已经因为没有回答规定的问题而被否决为负分,所以我将在这里停下来。尽管如此,我仍然相信以这种方式逐步解决问题,在经过测试和理解的部分之上构建解决方案,而不是试图“修复”OP 的当前代码,才是正确的“答案”。 (话又说回来,这就是为什么我 post 只是作为客人,从不注册的原因。)
我的目标是模仿 linux 管道命令,例如 ls |种类。而不是简单地排序。但不是输入 |用户类型:例如 "./program ls : sort" == "ls | sort"
我需要使用 fork() 和 pipe() 来完成此任务。我有一个 MRE 设置,一次只允许我 运行 一个命令,但我不知道如何将其设置为标准输出是第二个命令的标准输入。每当我尝试在父级中 dup() close() and exec()
时,似乎出了什么问题?我有一个设置,其中解析用户给出的输入,我为参数 A 获取 argA,其中包含 ls 或排序之类的命令,为参数 A 获取 ArgAP,以防用户想要指定 -lh 或 -r 等。同样的事情对于 argB.
我目前将此程序设置为硬编码以执行 bc 命令,但可以通过分配一些参数轻松修复。请帮助我,因为我坚持这个!
//################ #-for include
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>
#include <dirent.h>
//################
int main(int b, char ** locations) {
int ok = 0;
int dots = 0;
char argA[1000];
char argB[1000];
char argAP[1000];
char argBP[1000];
while (locations[ok] != NULL) {
//printf("%s \n", locations[ok]);
if (strcmp(locations[ok], ":") == 0) {
dots = 1;
}
ok++;
} //printf("%s %d \n", locations[2], b);
strcpy(argA, "");
strcpy(argB, "");
strcpy(argAP, "");
strcpy(argBP, "");
if (dots == 0) {
int x = 1;
strcat(argA, locations[x]);
strcat(argA, " ");
x++;
while (locations[x] != NULL) {
strcat(argAP, locations[x]);
strcat(argAP, " ");
x++;
}
printf("%s%s \n", argA, argAP);
}
if (dots == 1) {
int x = 1;
int compare = strcmp(locations[x], ":");
if (strcmp(locations[1], ":") == 0) {
printf("one arg\n");
strcat(argA, locations[x]);
strcat(argA, " ");
x++;
while (locations[x] != NULL) {
strcat(argAP, locations[x]);
strcat(argAP, " ");
x++;
}
printf("%s%s \n", argA, argAP);
} else {
printf("two args\n");
strcat(argA, locations[x]);
strcat(argA, " ");
compare = strcmp(locations[x], ":");
x++;
compare = strcmp(locations[x], ":");
while (compare != 0) {
printf("%d \n", x);
strcat(argAP, locations[x]);
strcat(argAP, " ");
compare = strcmp(locations[x], ":");
x++;
}
printf("argA: %s%s \n", argA, argAP);
x++;
strcat(argB, locations[x]);
strcat(argB, " ");
x++;
while (locations[x] != NULL) {
strcat(argBP, locations[x]);
strcat(argBP, " ");
x++;
}
printf("argB: %s%s \n", argB, argBP);
}
}
// fork/piping
int i, n;
int fd[2];
pipe(fd);
int rd = fd[0]; // rd points to 0 (read) in pipe
int wt = fd[1]; // wt points to 1 (write) in pipe
if (fork()) {
close(rd);
write(wt, "2*1*9*1", strlen("2*1*9*1"));
write(wt, "\n", 1);
close(wt);
exit(0);
} else {
close(wt);
close(0); // close zero
dup(rd); // dup rd into lowest possible orbit
close(rd);
execlp("bc", "bc", 0, NULL); // reading from zero means reading from rd!
exit(1);
}
return 0;
}
我已将需要帮助的部分注释掉。我如何使用一个命令的管道将结果通过 fork 提供给第二个命令?我认为这几乎是不可能的,因为通过我的尝试,我只能通过我在这里编写的这个设置获得一个命令。
原创
一旦要替换子进程的stdin
,需要使用dup2()
函数
这是解释为什么 dup()
函数永远无法满足您的目的的手册部分:
The dup() system call creates a copy of the file descriptor oldfd, using the lowest-numbered unused file descriptor for the new descriptor.
这里是解释为什么 dup2()
函数可以解决您的问题的手册部分:
The dup2() system call performs the same task as dup(), but instead of using the lowest-numbered unused file descriptor, it uses the file descriptor number specified in newfd.
要解决您的问题,请将 dup(rd)
调用替换为 dup2(rd, STDIN_FILENO)
。您也可以删除 close(0)
调用,一旦 dup2()
函数关闭 newfd
(如果它已在使用中)。
If the file descriptor newfd was previously open, it is silently closed before being reused.
编辑#1
我之前写的并没有解决问题,一旦close(0); dup(rd);
就会和dup2(rd, 0)
一样的效果,下面说到this user。所以,我按原样编译了你的代码,在 运行 之后,我
有这样的结果:
$ gcc -std=c99 -o program program.c
$ ./program ls : sort
two args
argA: ls
argB: sort
18
$
如你所见,最后一行显示18
,2*1*9*1
的结果。
现在,请注意父进程在写入描述为 wt
的文件后立即退出 - bc
命令的新 stdin
在子进程中执行。这意味着父进程可能会在子进程完成之前退出。我强烈建议您在父进程退出之前使用 wait()
或 waitpid()
调用来测试您的代码。例如:
// (...)
if (fork()) {
close(rd);
write(wt, "2*1*9*1", strlen("2*1*9*1"));
write(wt, "\n", 1);
close(wt);
wait(NULL);
exit(0);
} else {
close(wt);
close(0); // close zero
dup(rd); // dup rd into lowest possible orbit
close(rd);
execlp("bc", "bc", NULL);
exit(1);
}
我还用 execlp("bc", "bc", NULL);
行替换了 execlp("bc", "bc", 0, NULL);
行。我删除的零等同于 NULL
,表示使用 execlp()
.
编辑#2(实施)
阅读整个代码,我们可以将您的实现分为两部分:
- 正在解析程序的参数以适应
execlp()
函数的语法; - 将第一个命令的结果作为输入来执行第二个命令的进程。
如果您阅读 exec()
函数系列的手册页,您会注意到函数 execvp()
在此程序中更有用,因为 [=37= 的第二个参数] 函数与程序的参数类型相同:一个以 NULL
结尾的字符串数组。
按照这些步骤,您可以轻松地解析程序的参数以适应 execvp()
:
- 遍历程序的参数;
- 找到管道符号的位置;
- 在该位置,放置
NULL
以表示第一个命令的参数结束; - 将下一个位置的地址保存为第二个命令参数的开始。
解析程序的参数后,是时候创建管道并分叉进程了。在子进程中,在执行第一个命令之前,将 stdout
替换为管道的写入端。在父进程中,在执行第二个命令之前,将 stdin
替换为管道的读取端。
这是我编写的完整代码,运行 并经过测试:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#define PIPE_SYMBOL ":"
int main ( int argc , char **argv ) {
/* Validates the usage. At least is needed the program's name, two commands and the pipe symbol */
if ( argc < 4 ) {
fprintf(stderr, "usage: command-1 [args-1...] : command-2 [args-2...]\n");
return EXIT_FAILURE;
}
/* The start of the first comment is allways the start of the program arguments array */
char **command1 = &argv[1];
/* The start of the second command is undefined, once it depends where the pipe symbol is located */
char **command2 = NULL;
/* Finds the position of the pipe symbol */
for ( int i = 0 ; argv[i] != NULL ; i++ ) {
/* When found, ... */
if ( strcmp(PIPE_SYMBOL, argv[i]) == 0 ) {
/* ... replaces it for NULL, so the first command array is NULL terminated and... */
argv[i] = NULL;
/* ... the next position is the start of the second command */
command2 = &argv[i+1];
break;
}
}
/* If the pipe symbol is missing or if there is no command after the pipe symbol, bad usage */
if ( command2 == NULL || command2[0] == NULL ) {
fprintf(stderr, "usage: command-1 [args-1...] : command-2 [args-2...]\n");
return EXIT_FAILURE;
}
pid_t pid;
int pipefd[2];
if ( pipe(pipefd) == -1 ) {
perror("creating pipe");
return EXIT_FAILURE;
}
if ( (pid = fork()) == -1 ) {
perror("creating child process");
return EXIT_FAILURE;
}
/* Child process executes the first command */
if ( pid == 0 ) {
close(pipefd[0]);
close(STDOUT_FILENO);
dup(pipefd[1]);
close(pipefd[1]);
execvp(command1[0], command1);
perror("executing first command");
return EXIT_FAILURE;
}
/* Parent process executes the second command */
close(pipefd[1]);
close(STDIN_FILENO);
dup(pipefd[0]);
close(pipefd[0]);
execvp(command2[0], command2);
perror("executing second command");
return EXIT_FAILURE;
}
您应该始终尝试将问题拆分成更小的问题 sub-problems,您可以先解决并验证。
例如,考虑是否向您提供了以下 run.h:
#ifndef RUN_H
#define RUN_H
#include <sys/types.h>
/** Create a close-on-exec pipe with descriptors not standard streams
*
* Both read and write end of the pipe will be close-on-exec,
* and neither of them will be 0 (stdin), 1 (stdout), or 2 (stderr).
*
* @fd Read ([0]) and write ([1]) ends of the pipe
* @return 0 if success, -1 with errno set if error
*/
int safe_pipe(int fd[2]);
/* pgid: */
enum {
NEW_SESSION = -2,
NO_CHANGE = -1,
NEW_PGROUP = 0,
};
/* Descriptors: */
enum {
DEV_NULL = -1,
STANDARD_INPUT = 0,
STANDARD_OUTPUT = 1,
STANDARD_ERROR = 2,
};
/** Execute a binary in a child process
*
* This function uses a control pipe to check if the specified binary could be executed
* (started; not completed!), and to provide the errno number if not.
* It is careful to ensure even oddball descriptor configurations work.
* It is up to the parent to close any pipe descriptors specified after the call.
*
* @pathname Name or path to the binary to be executed.
* @args NULL-terminated array of command-line arguments.
* Note that args[0] is the command name itself.
* @pgid Process group to use: NEW_SESSION, NO_CHANGE, NEW_PGROUP,
* or a process group ID. NO_CHANGE runs the child process
* in the same session and process group as the current process.
* @infd Standard input descriptor. DEV_NULL, 0, or a pipe descriptor.
* @outfd Standard output descriptor. DEV_NULL, 1, or a pipe descriptor.
* @errfd Standard error descriptor. DEV_NULL, 2, or a pipe descriptor.
* @return PID of the child process, or -1 with errno set if an error occurs.
*/
pid_t run(const char *pathname, const char *args[], pid_t pgid, int infd, int outfd, int errfd);
/** Wait for all child processes to finish, and reap them
*
* errno is always set when this function returns:
* ECHILD if there are no more child processes
* EINTR if wait was interrupted by signal delivery
* or any other positive value returned by reaped().
*
* @reaped NULL, or a function called for each reaped child process.
* First parameter is the PID of the child process that exited,
* the second parameter is the "packed" exit status code; see man 2 wait.
* If reaped() returns a negative value, wait_all_children() will
* immediately return with that value, keeping the same errno value.
* If reaped() returns a positive value, wait_all_children() will
* immediately return with the reaped process count, and that value in errno.
* @return Number of processes reaped, or negative if an error occurs.
*/
int wait_all_children(int (*reaped)(pid_t, int));
#endif /* RUN_H */
及其知识共享零许可(随意使用!)实施,run.c:
// SPDX-License-Identifier: CC0-1.0
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include "run.h"
/* Create a close-on-exec pipe whose read and write ends do not
* overlap with standard descriptors (stdin, stdout, stderr).
* Returns 0 if success, -1 with errno set if error.
*/
int safe_pipe(int fd[2])
{
unsigned int close_mask = 0;
int err = 0;
int pfd[2];
fd[0] = -1;
fd[1] = -1;
if (pipe2(pfd, O_CLOEXEC) == -1) {
/* errno set by pipe2() */
return -1;
}
do {
/* Make sure read end does not shadow standard descriptors */
while (pfd[0] >= 0 && pfd[0] <= 2) {
close_mask |= 1 << pfd[0];
pfd[0] = fcntl(pfd[0], F_DUPFD_CLOEXEC, 3);
if (pfd[0] == -1) {
err = errno;
break;
}
}
if (err)
break;
/* Make sure write end does not shadow standard descriptors */
while (pfd[1] >= 0 && pfd[1] <= 2) {
close_mask |= 1 << pfd[1];
pfd[1] = fcntl(pfd[1], F_DUPFD_CLOEXEC, 3);
if (pfd[1] == -1) {
err = errno;
break;
}
}
if (err)
break;
/* Close any temporarily used descriptors. */
if (close_mask & (1<<0))
close(0);
if (close_mask & (1<<1))
close(1);
if (close_mask & (1<<2))
close(2);
/* Success! */
fd[0] = pfd[0];
fd[1] = pfd[1];
return 0;
} while (0);
/* Failed. Close all related descriptors. */
if (pfd[0] != -1)
close(pfd[0]);
if (pfd[1] != -1)
close(pfd[1]);
if (close_mask & (1<<0))
close(0);
if (close_mask & (1<<1))
close(1);
if (close_mask & (1<<2))
close(2);
errno = err;
return -1;
}
/* Open the null device to a specific descriptor.
*/
static int dev_null(int to_descriptor)
{
int fd;
if (to_descriptor == -1) {
errno = EBADF;
return -1;
}
close(to_descriptor);
fd = open("/dev/null", O_RDWR | O_NOCTTY);
if (fd == -1)
return -1; /* errno set by open() */
if (fd == to_descriptor)
return 0; /* We got lucky! */
if (dup2(fd, to_descriptor) == -1) {
const int saved_errno = errno;
close(fd);
errno = saved_errno;
return -1;
}
close(fd);
return 0;
}
/* Execute a binary with the specified command-line arguments
* (args[] array terminated by a NULL pointer),
* and redirecting standard input, output, and error to the specified descriptors
* (which may, and should be, close-on-exec), or -1 to redirect to /dev/null.
* pgid can be -2 for a new session, -1 for no change, 0 for new process group, or >0 for a specific process group.
* Returns the PID of the new child process, or -1 with errno set if error.
*/
pid_t run(const char *pathname, const char *args[], pid_t pgid, int infd, int outfd, int errfd)
{
if (!pathname || !*pathname || !args || !args[0] || !args[0][0]) {
/* NULL or empty pathname (path or name) to the executable. */
errno = EINVAL;
return -1;
}
/* Create the control pipe we use between parent and child to monitor exec() success/failure. */
int ctrlfd[2];
if (safe_pipe(ctrlfd) == -1) {
/* errno set by safe_pipe(). */
return -1;
}
pid_t child = fork();
if (child == -1) {
/* Cannot fork a child process. */
const int saved_errno = errno;
close(ctrlfd[0]);
close(ctrlfd[1]);
errno = saved_errno;
return -1;
} else
if (!child) {
/* Child process. */
unsigned int close_mask = 0;
int err = 0;
do {
/* Close parent (read) end of the control pipe. */
close(ctrlfd[0]);
/* Adjust process group. */
if (pgid == NEW_SESSION) {
if (setsid() == -1) {
err = errno;
break;
}
} else
if (pgid == NEW_PGROUP) {
if (setpgid(0, 0) == -1) {
err = errno;
break;
}
} else
if (pgid > 0) {
if (setpgid(0, pgid) == -1) {
err = errno;
break;
}
} else
if (pgid != NO_CHANGE) {
err = EINVAL;
break;
}
/* Make sure infd does not shadow standard output or standard error. */
while (infd == 1 || infd == 2) {
close_mask |= 1 << infd;
infd = fcntl(infd, F_DUPFD_CLOEXEC, 3);
if (infd == -1) {
err = errno;
break;
}
}
if (err)
break;
/* Make sure outfd does not shadow standard input or standard error. */
while (outfd == 0 || outfd == 2) {
close_mask |= 1 << outfd;
outfd = fcntl(outfd, F_DUPFD_CLOEXEC, 3);
if (outfd == -1) {
err = errno;
break;
}
}
if (err)
break;
/* Make sure errfd does not shadow standard input or standard output. */
while (errfd == 0 || errfd == 1) {
close_mask |= 1 << errfd;
errfd = fcntl(errfd, F_DUPFD_CLOEXEC, 3);
if (errfd == -1) {
err = errno;
break;
}
}
if (err)
break;
/* Close unneeded descriptors. */
if ((close_mask & (1<<0)) || infd == -1)
close(0);
if ((close_mask & (1<<1)) || outfd == -1)
close(1);
if ((close_mask & (1<<2)) || errfd == -1)
close(2);
/* Redirect standard input. */
if (infd == DEV_NULL) {
if (dev_null(0) == -1) {
err = errno;
break;
}
infd = 0;
} else
if (infd != 0) {
if (dup2(infd, 0) == -1) {
err = errno;
break;
}
close(infd);
infd = 0;
}
/* Redirect standard output. */
if (outfd == DEV_NULL) {
if (dev_null(1) == -1) {
err = errno;
break;
}
outfd = 1;
} else
if (outfd != 1) {
if (dup2(outfd, 1) == -1) {
err = errno;
break;
}
close(outfd);
outfd = 1;
}
/* Redirect standard error. */
if (errfd == DEV_NULL) {
if (dev_null(2) == -1) {
err = errno;
break;
}
errfd = 2;
} else
if (errfd != 2) {
if (dup2(errfd, 2) == -1) {
err = errno;
break;
}
close(errfd);
errfd = 2;
}
/* Make sure the standard descriptors are not close-on-exec. */
if (fcntl(0, F_SETFD, 0) == -1 ||
fcntl(1, F_SETFD, 0) == -1 ||
fcntl(2, F_SETFD, 0) == -1) {
err = errno;
break;
}
/* Close the unneeded temporary descriptors. */
if (close_mask & (1<<0))
close(0);
if (close_mask & (1<<1))
close(1);
if (close_mask & (1<<2))
close(2);
close_mask = 0;
/* Execute. */
if (strchr(pathname, '/'))
execv(pathname, (char *const *)args); /* pathname has a slash, so it is a path to a binary */
else
execvp(pathname, (char *const *)args); /* pathname has no slash, so it specifies the binary name only */
/* Failed. */
err = errno;
} while (0);
/* Send err to parent via the control pipe. */
{
const char *ptr = (const char *)(&err);
const char *const end = (const char *)(&err) + sizeof err;
while (ptr < end) {
ssize_t n = write(ctrlfd[1], ptr, (size_t)(end - ptr));
if (n > 0) {
ptr += n;
} else
if (n != -1) {
/* Should never occur */
break;
} else
if (errno != EINTR) {
/* I/O error writing to the pipe too! */
break;
}
}
}
/* The kernel will close all open descriptors in the process. */
exit(127);
}
/* Parent process. */
/* Close read end of control pipe, so we detect if the child process exec() succeeded. */
close(ctrlfd[1]);
/* Read from the control pipe, to determine if child process exec succeeds or not. */
{
int err = 0;
char *ptr = (char *)(&err);
char *const end = (char *)(&err) + sizeof err;
while (ptr < end) {
ssize_t n = read(ctrlfd[0], ptr, (size_t)(end - ptr));
if (n > 0) {
ptr += n;
} else
if (n == 0) {
break;
} else
if (n != -1) {
err = EIO;
ptr = end;
break;
} else
if (errno != EINTR) {
err = errno;
ptr = end;
break;
}
}
close(ctrlfd[0]);
/* Treat partially received errors and received zero as EIO. */
if (ptr > (char *)(&err)) {
if (ptr != end || !err)
err = EIO;
}
if (err) {
/* Child failed to exec; reap it. */
/* Reap child process. Ignore errors, and retry if interrupted. */
pid_t p;
do {
p = waitpid(child, NULL, 0);
} while (p == -1 && errno == EINTR);
errno = err;
return -1;
}
}
/* Success. */
errno = 0;
return child;
}
int wait_all_children(int (*reaped)(pid_t, int))
{
int count = 0;
while (1) {
int status = 0;
pid_t pid;
pid = wait(&status);
if (pid == -1) {
/* errno set by wait() */
return count;
} else
if (pid < 1) {
/* C library or Linux kernel bug! */
errno = EIO;
return count;
}
count++;
if (reaped) {
int retval = reaped(pid, status);
if (retval < 0) {
/* errno set by reaped() */
return retval;
} else
if (retval > 0) {
errno = retval;
return count;
}
}
}
}
和 Makefile 从 example.c:
构建示例程序CC := gcc
CFLAGS := -Wall -Wextra -O2
LDFLAGS :=
TARGETS := example
.PHONY: all clean
all: $(TARGETS)
clean:
rm -f $(TARGETS) *.o
%.o: %.c
$(CC) $(CFLAGS) -c $^
example: example.o run.o
$(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@
请注意,本论坛将Tabs 转换为空格,Makefile 缩进必须使用Tabs 而不是空格。幸运的是,运行ning sed -e 's|^ *|\t|' -i Makefile
通过将初始空格转换为制表符修复了上述问题以及大多数其他 Makefile。
上面的想法是为了帮助你在正确的道路上开始:
safe_pipe()
创建一个管道,当您执行任何 exec() 调用时,两端会自动关闭。即使您关闭了一些标准描述符(标准输入、输出或错误),它也会确保管道末端不会干扰标准描述符。wait_all_children()
等待当前进程的所有立即 children(即该进程已分叉的那些),直到没有更多 children ,等待被信号传递或可选的报告功能(它作为参数)中断returns非零。run()
派生一个 child 进程,根据需要重定向标准描述符,甚至处理进程组。它使用控制管道来检测 exec() 错误,并且不等待 child 进程退出,只等待 child 进程启动。进程组很有用,因为如果每个逻辑任务都在它们自己的进程组中,您可以使用取反的进程组 ID 向特定组中的每个进程发送信号(如 KILL 或 TERM)。这样就保证了对于复杂的任务,当整个任务需要被kill掉的时候,不管它已经fork了多少个进程,都可以很好的清理掉。不过,对于简单的命令,我们不用担心进程组。
最好在我们做一个简单的示例程序后检查上面的内容,比如 运行s ls -laF | tr a-z A-Z | cat
,它列出了当前目录中的所有文件,但将小写字母 a 转换为 z大写,使用三个 child 进程:
// SPDX-License-Identifier: CC0-1.0
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include "run.h"
int report(pid_t pid, int code)
{
if (WIFEXITED(code)) {
if (WEXITSTATUS(code))
printf("Process %d exited with status %d.\n", (int)pid, WEXITSTATUS(code));
else
printf("Process %d exited with success (status 0).\n", (int)pid);
} else
if (WIFSIGNALED(code)) {
printf("Process %d died from signal %d.\n", (int)pid, WTERMSIG(code));
} else {
printf("Process %d died from unknown causes.\n", (int)pid);
}
return 0;
}
int main(void)
{
const char *cmd_a[] = { "ls", "-laF", NULL };
const char *cmd_b[] = { "tr", "a-z", "A-Z", NULL };
const char *cmd_c[] = { "cat", NULL };
int a_to_b[2], b_to_c[2];
pid_t a, b, c;
if (safe_pipe(a_to_b) == -1 || safe_pipe(b_to_c) == -1) {
fprintf(stderr, "Cannot create pipes: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
c = run(cmd_c[0], cmd_c, NO_CHANGE, b_to_c[0], STANDARD_OUTPUT, STANDARD_ERROR);
if (c == -1) {
fprintf(stderr, "%s: %s.\n", cmd_c[0], strerror(errno));
return EXIT_FAILURE;
}
b = run(cmd_b[0], cmd_b, NO_CHANGE, a_to_b[0], b_to_c[1], STANDARD_ERROR);
if (b == -1) {
fprintf(stderr, "%s: %s.\n", cmd_b[0], strerror(errno));
kill(c, SIGKILL);
wait_all_children(NULL);
return EXIT_FAILURE;
}
a = run(cmd_a[0], cmd_a, NO_CHANGE, DEV_NULL, a_to_b[1], STANDARD_ERROR);
if (a == -1) {
fprintf(stderr, "%s: %s.\n", cmd_a[0], strerror(errno));
kill(b, SIGKILL);
kill(c, SIGKILL);
wait_all_children(NULL);
return EXIT_FAILURE;
}
close(a_to_b[0]);
close(a_to_b[1]);
close(b_to_c[0]);
close(b_to_c[1]);
wait_all_children(report);
return EXIT_SUCCESS;
}
report()
函数只报告哪个 child 进程已经退出以及如何退出。您可以省略它,使用 wait_all_children(NULL);
而不是 wait_all_children(report);
,但是报告功能对于调试和检查您在不同情况下可以观察到什么样的退出状态很有用。
我们必须通过管道传输:a_to_b
和 b_to_c
。该对的第一个描述符(a_to_b[0]
和b_to_c[0]
)始终是读端,第二个(a_to_b[1]
和b_to_c[1]
)是写端。我们的三个child进程是a
、b
和c
,所以我们希望a
的标准输出是a_to_b[1]
、b
的标准输入为a_to_b[0]
,b
的标准输出为b_to_c[1]
,c
的标准输入为b_to_c[1]
。
我们需要先创建管道。
然后,我们需要 运行(fork 并执行)child 进程。因为 children 会阻塞直到他们得到输入(或者他们看到输入结束),所以我们在管道中从最后到第一创建 child 进程:首先是“消费者”,然后是“生产者”。这样,如果我们失败直到整个链都正常,生产者应该仍在等待输入。
如果我们无法创建某些child进程,我们确实需要杀死已经启动的进程,最好等待它们退出(通常称为,reap them).
当 child 进程启动后,parent 进程需要关闭它的管道描述符副本,以便当“生产者”(管道列表中的第一个进程)退出时并关闭管道的写入端,读取端报告输入结束。
如果 parent 进程不关闭其管道(写端)描述符的副本,“消费者”(从管道读取的 child 进程)将永远不会检测到 end-of-input – 因为理论上,parent 进程仍然可以写入管道! – 一切看起来都像“挂起”。
此时,管道中的进程开始工作。 parent 进程此时可以做其他事情。 (一个有趣的场景是当人们希望使用 filters 时:一系列进程,通常是一个脚本,将数据从一种格式转换为另一种格式。很可能是过滤输出管道(或管道链)和输入管道的写入端为parent可访问,使得parent在写入端写入要转换的数据,并读取转换后的数据来自读取端的数据。这有点棘手,因为我们不能假设我们可以在读取任何内容之前写入所有内容,所以通常 nonblocking I/O with使用 select()
或 poll()
。没有什么难本身,只需要正确地完成。)
由于 parent 进程除了等待管道进程完成它们的工作之外没有其他事情可做,因此 parent 只是等待它们退出。
因为有了Makefile,你只需要运行make all && ./example
编译和运行示例程序
因为整个问题被分解为 sub-problems(以对标准流造成最少问题的方式创建管道,并且不会意外泄漏到错误的 child 进程;以及分叉并启动一个 child 进程以稳健可靠的方式重定向其标准流),示例程序简短且在概念层面上易于理解。
玩过那个例子之后,是时候探索和解释 run.c 如何实现这些功能,以及为什么做出这些选择,但我已经因为没有回答规定的问题而被否决为负分,所以我将在这里停下来。尽管如此,我仍然相信以这种方式逐步解决问题,在经过测试和理解的部分之上构建解决方案,而不是试图“修复”OP 的当前代码,才是正确的“答案”。 (话又说回来,这就是为什么我 post 只是作为客人,从不注册的原因。)