首先,要准备好看一些魔法
嗨,过去几个小时我一直对这个问题感到沮丧和挣扎,我无法理解为什么孩子的过程不会消亡。我基本上只有一个父进程和许多子进程。所有孩子都需要与父母沟通,家长需要与所有孩子沟通。对于初学者,我只是让孩子们不断尝试read
,但我的父母什么也没有发送,只是关闭了管道的write
端,从而导致他们的reads
停止阻止。这是我的过程(我的宏def是5
子进程):
5 int*
管道的2
数组。父母使用第一个与孩子交谈,孩子使用第二个
5
个孩子并关闭管道的适当末端
read
循环应终止
这是我的代码:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/types.h>
#define PROCESSES 5
int main(int argc, char ** argv) {
int * pipes[PROCESSES];
for (int i = 0; i < PROCESSES; i++) {
pipes[i] = malloc(sizeof(int) * 2);
if (pipe(pipes[i]) == -1) {
perror("Error piping");
exit(1);
}
}
//PIDS we will wait on
int children_pids[PROCESSES];
for (int i = 0; i < PROCESSES; i++) {
int status = fork();
switch(status) {
case -1:
perror("Error forking a child");
exit(1);
case 0:
//Close the pipes we don't need
close(pipes[i][1]);
//Inside the child process, die immediately
char buffer[128] = "";
while (read(pipes[i][0], buffer, 127) > 0) {
//Keep reading and doing nothing
}
printf("Dying\n");
exit(1);
default:
//Parent process, close the pipes we don't need
close(pipes[i][0]);
break;
}
//Parent continue spawning children
children_pids[i] = status;
}
//CLOSE ALL PIPES FROM PARENT TO CHILDREN------------
for (int i = 0; i < PROCESSES; i++) {
if (close(pipes[i][1]) == -1) {
perror("Error closing a pipe");
exit(1);
}
}
//AWAIT CHILDREN DEATHS
for (int i = 0; i < PROCESSES; i++) {
wait(&children_pids[i]);
}
printf("All children have died");
return 0;
}
我知道这是儿童中的read
循环阻止儿童死亡,因为当它被移除时,它可以正常工作。但是,我无法弄清楚为什么会这样。在我的底部循环中,我清楚地关闭所有管道甚至检查错误。为什么是这样?! read
如何阻止我完成return;
目标?!
答案 0 :(得分:4)
一些事情。
(1)对于每个孩子,你只创建了一个管道[parent-to-child],但你需要第二个[child-to-parent](即管道不双向像插座一样。)
(2)当您预先创建所有管道时,您必须关闭当前孩子不的管道,而不仅仅是两个管道的管道侧面。
如果不执行此操作,则子N将为每个打开父 [和子]边 N
一个[给定]分支之后,如果父代完全关闭了一个打开的管道,那么孩子仍然继承[副本]任何文件描述符在叉子时的父母。因此,在父级中关闭没有任何效果,因为孩子仍然将它们打开 - 对于孩子的所有
这就是你原来的程序所做的。
在我的[下面]版本中,它不那么严重。如果没有预关闭(通过childclose
),子0只保持自己的管道打开。但是,Child 1将保持开放式0的管道。孩子2将为孩子0和孩子1打开管道。等等......
因此,许多孩子正在互相抱着管道描述符打开。因此,当父进程关闭管道时,它们仍被其他子进行打开,因此 no 子进程将看到EOF
如果您想要想象这一点,请在fork
之后(例如,case 0
之后),将原始代码作为孩子的第一个可执行部分:
{
pid_t pid = getpid();
char buf[100];
printf("DEBUG: %d\n",pid);
sprintf(buf,"ls -l /proc/%d/fd",pid);
system(buf);
}
忽略stdin / stdout / stderr而不是预期的2(应该是4个)打开描述符,你会在每个子项中看到(2 * PROCESSES)
(即10个)描述符。
在父母的最后关闭之后你可以重复[在父母中]这样的序列,你仍然看到同样的事情[减去每个孩子将关闭的两个]。
使用结构可以更轻松地组织它。为了证明它确实有效,我添加了一些回显的实际数据传输。我还添加了一些调试选项来显示差异。
下面是更正后的代码[请原谅无偿风格的清理]:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/wait.h>
#define PROCESSES 5
int opt_n; // do _not_ close other children
int opt_p; // original semantics
int opt_v; // show list
// child control
struct child {
int cld_idx; // child index
pid_t cld_pid; // child's pid
int cld_status; // child's exit status
int cld_topar[2]; // pipe: child-to-parent
int cld_tocld[2]; // pipe: parent-to-child
};
#define CLOSEME(_fd) \
do { \
if (_fd >= 0) \
close(_fd); \
_fd = -1; \
} while (0)
struct child children[PROCESSES];
// fdlist -- output list of open descriptors
void
fdlist(struct child *cld,const char *reason)
{
struct child cld2;
char cmd[100];
if (cld == NULL) {
cld = &cld2;
cld->cld_pid = getpid();
cld->cld_idx = -1;
}
printf("\n");
printf("fdlist: idx=%d pid=%d (from %s)\n",
cld->cld_idx,cld->cld_pid,reason);
sprintf(cmd,"ls -l /proc/%d/fd",cld->cld_pid);
system(cmd);
}
// childclose -- close any pipe units from other children
void
childclose(int i)
{
struct child *cld;
for (int j = 0; j < PROCESSES; ++j) {
if (j == i)
continue;
cld = &children[j];
CLOSEME(cld->cld_topar[0]);
CLOSEME(cld->cld_topar[1]);
CLOSEME(cld->cld_tocld[0]);
CLOSEME(cld->cld_tocld[1]);
}
}
// childopen -- create pipes for child
void
childopen(int i)
{
struct child *cld;
cld = &children[i];
// to cut down on the clutter, only create the pipes as we need them
pipe(cld->cld_topar);
pipe(cld->cld_tocld);
}
// childstart -- start up child
void
childstart(int i)
{
struct child *cld;
pid_t pid;
cld = &children[i];
// to cut down on the clutter, only create the pipes as we need them
if (! opt_p)
childopen(i);
pid = fork();
if (pid < 0) {
perror("Error forking a child");
exit(1);
}
switch (pid) {
case 0: // child
// close any pipe that doesn't belong to us
if (! opt_n)
childclose(i);
pid = getpid();
cld->cld_pid = pid;
if (opt_v)
fdlist(cld,"childstart");
// Close the pipe sides we don't need
CLOSEME(cld->cld_topar[0]);
CLOSEME(cld->cld_tocld[1]);
// Inside the child process, die immediately
int len;
char buffer[128];
while (1) {
len = read(cld->cld_tocld[0], buffer, sizeof(buffer) - 1);
if (len <= 0)
break;
// Keep reading and echoing
write(cld->cld_topar[1],buffer,len);
}
printf("child %d: Dying\n",i);
exit(1);
break;
default: // parent
// give child time to print message
if (opt_v)
sleep(1);
cld->cld_pid = pid;
// Parent process, close the pipe sides we don't need
CLOSEME(cld->cld_topar[1]);
CLOSEME(cld->cld_tocld[0]);
break;
}
}
int
main(int argc, char **argv)
{
char *cp;
struct child *cld;
int len;
char buf[128];
--argc;
++argv;
for (; argc > 0; --argc, ++argv) {
cp = *argv;
if (*cp != '-')
break;
switch (cp[1]) {
case 'n': // do _not_ close other descriptors
opt_n = 1;
break;
case 'p': // preopen all pipes
opt_p = 1;
break;
case 'v': // show verbose messages
opt_v = 1;
break;
}
}
setlinebuf(stdout);
printf("main: pipes will be created %s\n",
opt_p ? "all at once" : "as needed");
printf("main: other child descriptors %s be closed\n",
opt_n ? "will not" : "will");
for (int i = 0; i < PROCESSES; i++) {
cld = &children[i];
cld->cld_idx = i;
cld->cld_topar[0] = -1;
cld->cld_topar[1] = -1;
cld->cld_tocld[0] = -1;
cld->cld_tocld[1] = -1;
}
// create pipes for _all_ children ahead of time
if (opt_p) {
for (int i = 0; i < PROCESSES; i++)
childopen(i);
if (opt_v)
fdlist(NULL,"master/OPEN");
}
// start up all children
for (int i = 0; i < PROCESSES; i++)
childstart(i);
// show final list
if (opt_v) {
sleep(1);
for (int i = 0; i < PROCESSES; i++) {
cld = &children[i];
fdlist(cld,"master/POSTSTART");
}
}
// send to child
for (int i = 0; i < PROCESSES; i++) {
cld = &children[i];
len = sprintf(buf,"child %d, you are pid %d\n",i,cld->cld_pid);
write(cld->cld_tocld[1],buf,len);
}
// receive from child
printf("\n");
for (int i = 0; i < PROCESSES; i++) {
cld = &children[i];
len = read(cld->cld_topar[0],buf,sizeof(buf));
printf("RECV(%d): %s",i,buf);
}
// show final list
if (opt_v) {
sleep(1);
for (int i = 0; i < PROCESSES; i++) {
cld = &children[i];
fdlist(cld,"master/FINAL");
}
}
// CLOSE ALL PIPES FROM PARENT TO CHILDREN------------
for (int i = 0; i < PROCESSES; i++) {
cld = &children[i];
CLOSEME(cld->cld_topar[0]);
CLOSEME(cld->cld_tocld[1]);
}
// AWAIT CHILDREN DEATHS
for (int i = 0; i < PROCESSES; i++) {
cld = &children[i];
waitpid(cld->cld_pid,&cld->cld_status,0);
}
printf("All children have died\n");
return 0;
}
答案 1 :(得分:4)
首先,我会回顾一些您明显知道的信息。我写这篇文章是因为其他人也可能会读到这个答案,因为总有一些答案是有好处的。
之后我将在以下位置显示您的代码:
case 0: // the child process
close(pipes[i][1]); // <- if (i == 2) pipes[1][1] is open.
可能意味着执行了以下任务:
case 0: // the child process
// close all input endpoints (input only performed by root process)
// also close all irrelevant output endpoints:
for (int j = 0; j < PROCESSES; j++){
close(pipes[j][1]);
if(j != i)
close(pipes[j][0]);
}
众所周知,每个子进程都会收到文件描述符(fd)的dup
副本,每个pipe
由两个文件描述符组成,一个用于输入(读取),另一个用于输入(读取)输出(写作)。
每次分叉流程时,这两个端点(文件描述符) - 用于每个打开管道 - 都是重复的。
read
将阻止,但传入的数据仍有可能最终到达 - 这意味着read
将阻塞,而至少一个“输出”(写入)文件描述符仍然打开。 / p>
在下面的示例中,我将打开一个管道并分叉该过程。分叉进程将关闭它的“输入”(写入)端点并调用read
。 read
将阻止,因为父进程中仍有一个打开的输入fd(请记住,fd
是重复的)。在父关闭它的“输入”fd
之后,没有更多的写入端点,读取将失败(停止阻塞)。
注意,我没有在管道上写任何东西。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
typedef struct {
int in; // the input fd
int out; // the output fd
} pipe_io;
int main() {
// the container;
pipe_io io;
// make the pipe
pipe((int*)&io);
// forking will duplicate the open files
pid_t child;
if (!(child = fork())) { // fork==0 => we're in the child process
close(io.out); // closing one reading access point.
char buff[4];
// read will block because there's still an open writing access point.
printf("Child waiting (read will block)\n");
read(io.in, buff, 1);
// cleanup and exit process.
close(io.in);
printf("Child exits (read stopped blocking once all inputs were closed)\n");
exit(0);
}
sleep(1); // wait...
printf("closing parent's writing (output) endpoint.\n");
close(io.out);
sleep(1); // wait...
printf("closing parent's reading (input) endpoint.\ndone.\n");
waitpid(child, NULL, 0);
}
输出是代码控制流程的明确指示:
Child waiting (read will block)
closing parent's writing (output) endpoint.
Child exits (read stopped blocking once all inputs were closed)
closing parent's reading (input) endpoint.
done.
因此,为了调用read
失败(不阻塞),我们需要关闭所有写入端点/通道。
在您的代码中,每个进程都有一个管道,但是您允许每个进程保持其他进程的“输入”(写入)端点打开 - 因此read
将始终阻塞。
case 0: // the child process
// This line only closes this process's input stream, but this stream is
// open for all other processes:
close(pipes[i][1]); // <- if (i == 2) pipes[1][1] is open.
//...
// `read` will ALWAYS block because other processes keep input endpoints.
while (read(pipes[i][0], buffer, 127) > 0) {
//Keep reading and doing nothing
}
printf("Dying\n");
exit(1);
你可能想写:
case 0: // the child process
// closing all input endpoints (input only performed by root process)
// also closing all irrelevant output endpoints:
for (int j = 0; j < PROCESSES; j++){
close(pipes[j][1]);
if(j != i)
close(pipes[j][0]);
}
//...
P.S。
除非每个流程都有一个单独的角色,否则为每个流程打开一个管道是不常见的。
共享相同功能的所有进程通常共享同一个管道。
例如,如果使用一系列进程来执行共享的任务系列,则哪个进程执行哪个任务可能并不重要 - 因此如果将任务提交到共享管道并且第一个任务被提交,则更有效读取数据的过程是执行任务的过程。
当进程忙于执行任务时,它不会从管道读取,如果有另一个进程可用(阻塞“读取”),它会立即投入使用(而不是等待繁忙的进程)。
这种单一管道设计最大限度地减少了“等待”时间段,并消除了任何调度问题(管道缓冲区的限制除外)。