你在滥用管道。
管道是单向通信通道。您可以使用它将数据从父进程发送到子进程,或者将数据从子进程发送到父进程。你不能两者都做——即使你在两个进程上保持管道的读写通道打开,每个进程也永远不会知道轮到它从管道读取的时间(例如,你最终可能会在子进程中读取某些内容应该由父母阅读)。
将字符从父级发送到子级的代码似乎大部分是正确的(下面有更多详细信息),但是您需要重新设计子级到父级的通信。现在,您有两个选项可以将结果从子级发送给父级:
- 使用另一个管道。您在分叉之前设置了一个额外的管道以进行子父通信。这使设计和代码变得复杂,因为现在您有 4 个文件描述符要从 2 个不同的管道进行管理,并且您需要小心关闭每个文件描述符的位置以确保进程不会挂起。这也可能有点矫枉过正,因为孩子只是向父母发送一个数字。
- 从子节点返回结果作为退出值。这就是你现在正在做的事情,这是一个不错的选择。但是,您无法在父级中检索该信息:子级的终止状态告诉您处理的字符数,您可以使用
waitpid(2) 获取此值,您已经这样做了,但是您永远不会查看 status(其中包含您要查找的结果)。
请记住,子进程有自己的地址空间。尝试在父级中读取charCounter 是没有意义的,因为父级从未修改过它。子进程拥有自己的charCounter 副本,因此只有子进程才能看到任何修改。您的代码似乎假设并非如此。
为了使这一点更明显,我建议将变量声明移到相应的流程代码中。只有fd 和pid 需要在两个进程中复制,其他变量特定于每个进程的任务。因此,您可以将status 和nChar 的声明移动到父进程特定代码,您可以将charCounter、readbuffer 和readIn 移动到子进程。这将非常明显地表明变量完全独立于每个进程。
现在,一些更具体的评论:
-
pipe(2) 可以返回错误。你忽略了返回值,你不应该。至少,如果pipe(2) 由于某种原因失败,您应该打印一条错误消息并终止。我还注意到您在fork(2) 和printf("fork error %d\n", pid); 中报告了错误。这不是正确的方法:fork(2) 和其他系统调用(和库调用)总是在错误时返回-1 并设置errno 全局变量来指示原因。这样printf() 将始终打印 fork error -1 无论错误原因是什么。这没有帮助。此外,它将错误消息打印到stdout,并且由于多种原因,错误消息应该打印到stderr。所以我建议改用perror(3),或者用fprintf(3)手动将错误打印到stderr。 perror(3) 具有将错误消息描述附加到您提供的文本的额外好处,因此它通常是一个不错的选择。
例子:
if (pipe(fd) < 0) {
perror("pipe(2) error");
exit(EXIT_FAILURE);
}
您在整个代码中使用的其他函数也可能会失败,并且您再次忽略了(可能的)错误返回。 close(2) 和 read(2) 可能会失败。处理错误,它们的存在是有原因的。
-
你使用readIn的方式是错误的。 readIn 是read(2) 的结果,它返回读取的字符数(应该是int)。该代码使用readIn,就好像它是读取的下一个字符一样。读取的字符存储在readbuffer 中,readIn 将告诉您该缓冲区中有多少字符。因此,您使用readIn 循环浏览缓冲区内容并计算字符数。像这样的:
readIn = read(fd[0], readbuffer, sizeof(readbuffer));
while (readIn > 0) {
int i;
for (i = 0; i < readIn; i++) {
if (readbuffer[i] != ' ') {
charCounter++;
}
}
readIn = read(fd[0], readbuffer, sizeof(readbuffer));
}
现在,关于父进程:
您没有将字符写入管道。这是没有意义的:
write(fd[1], &argv, sizeof(argv));
&argv 是 char *** 类型,sizeof(argv) 与 sizeof(char **) 相同,因为 argv 是 char **。传入函数时不保留数组维度。
您需要手动循环遍历argv 并将每个条目写入管道,如下所示:
int i;
for (i = 1; i < argv; i++) {
size_t to_write = strlen(argv[i]);
ssize_t written = write(fd[1], argv[i], to_write);
if (written != to_write) {
if (written < 0)
perror("write(2) error");
else
fprintf(stderr, "Short write detected on argv[%d]: %zd/zd\n", i, written, to_write);
}
}
注意argv[0]是程序的名称,这就是为什么i从1开始。如果你也想计算argv[0],把它改成从0开始。
最后,正如我之前所说,您需要使用waitpid(2) 获取的终止状态来获取孩子返回的实际计数。因此,您只能在 waitpid(2) 返回并确保子进程正常终止后打印结果。此外,要获取实际的退出代码,您需要使用 WEXITSTATUS 宏(只有在 WIFEXITED 返回 true 时才能安全使用)。
以下是解决所有这些问题的完整计划:
// Characters from command line arguments are sent to child process
// from parent process one at a time through pipe.
//
// Child process counts number of characters sent through pipe.
//
// Child process returns number of characters counted to parent process.
//
// Parent process prints number of characters counted by child process.
#include <stdlib.h>
#include <stdio.h>
#include <string.h> // for strlen()
#include <unistd.h> // for fork()
#include <sys/types.h> // for pid_t
#include <sys/wait.h> // for waitpid()
int main(int argc, char **argv)
{
int fd[2];
pid_t pid;
if (pipe(fd) < 0) {
perror("pipe(2) error");
exit(EXIT_FAILURE);
}
pid = fork();
if (pid < 0) {
perror("fork(2) error");
exit(EXIT_FAILURE);
}
if (pid == 0) {
int readIn;
int charCounter = 0;
char readbuffer[80];
if (close(fd[1]) < 0) {
perror("close(2) failed on pipe's write channel");
/* We use abort() here so that the child terminates with SIGABRT
* and the parent knows that the exit code is not meaningful
*/
abort();
}
readIn = read(fd[0], readbuffer, sizeof(readbuffer));
while (readIn > 0) {
int i;
for (i = 0; i < readIn; i++) {
if (readbuffer[i] != ' ') {
charCounter++;
}
}
readIn = read(fd[0], readbuffer, sizeof(readbuffer));
}
if (readIn < 0) {
perror("read(2) error");
}
printf("The value of charCounter is %d\n", charCounter);
return charCounter;
} else {
int status;
if (close(fd[0]) < 0) {
perror("close(2) failed on pipe's read channel");
exit(EXIT_FAILURE);
}
int i;
for (i = 1; i < argc; i++) {
size_t to_write = strlen(argv[i]);
ssize_t written = write(fd[1], argv[i], to_write);
if (written != to_write) {
if (written < 0) {
perror("write(2) error");
} else {
fprintf(stderr, "Short write detected on argv[%d]: %zd/%zd\n", i, written, to_write);
}
}
}
if (close(fd[1]) < 0) {
perror("close(2) failed on pipe's write channel on parent");
exit(EXIT_FAILURE);
}
if (waitpid(pid, &status, 0) < 0) {
perror("waitpid(2) error");
exit(EXIT_FAILURE);
}
if (WIFEXITED(status)) {
printf("CS201 - Assignment 3 - Andy Grill\n");
printf("The child processed %d characters\n\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
fprintf(stderr, "Child terminated abnormally with signal %d\n", WTERMSIG(status));
} else {
fprintf(stderr, "Unknown child termination status\n");
}
return 0;
}
}
一些最后的笔记:
- shell 用空格分割参数,所以如果你以
./a.out this is a test 启动程序,代码将看不到一个空格。这无关紧要,因为无论如何都应该忽略空格,但是如果您想测试代码是否真的忽略了空格,则需要引用参数,以便 shell 不会处理它们,如./a.out "this is a test" "hello world" "lalala"。
- 仅使用程序退出代码的最右边(最低有效)8 位,因此
WEXITSTATUS 将永远不会返回超过 255。如果孩子读取超过 255 个字符,则该值将回绕,因此您有效地拥有一个以 256 为模的字符计数器。如果这是一个问题,那么您需要采用另一种方法并设置第二个管道用于孩子与父母的通信并将结果写入那里(并让父母阅读它)。您可以在man 2 waitpid 上确认这一点:
WEXITSTATUS(状态)
返回孩子的退出状态。这包括最少的
显着的 8 位状态参数的孩子
在对 exit(3) 或 _exit(2) 的调用中指定或作为返回的参数
main() 中的语句。仅在以下情况下才应使用此宏
WIFEXITED 返回 true。