尽管管道关闭,读取仍继续阻塞
read continues to block despite pipe closure
首先,准备好看到一些魔法。
您好,在过去的几个小时里,我一直在为这个问题而苦恼和挣扎,我不明白为什么 child 进程没有死掉。我基本上只有一个 parent 进程和许多 children 进程。所有 children 都需要与 parent 通信,而 parent 需要与所有 children 通信。对于初学者,我只是让 children 不断尝试 read
,但我的 parent 什么也没发送,只是关闭了管道的 write
端,从而导致他们的 reads
停止阻塞。这是我的过程(我的宏定义是 5
children processes):
首先创建一个指向 2
管道的 5 int*
数组。 parent 使用第一个与 child 对话,child 使用第二个
叉 5
children 并关闭管道的适当端
每个 child 不断尝试读取
Parent 关闭管道的所有写入端,因此 children 中的 read
循环应该终止
Parent 等待 child 死亡
Parent 死亡
这是我的代码:
#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;
}
我知道是children中的read
循环阻止了children死亡,因为当它被删除,它工作正常。但是,我不明白为什么会这样。在底部的循环中,我清楚地关闭了所有管道,甚至检查了错误。为什么是这样?!为什么 read
仍然阻碍我实现我的 return;
目标??!?
一些事情。
(1) 对于每个 child,您只创建了一个管道 [parent-to-child],但您需要第二个 [child-to-parent](即管道是 不是双向像套接字)。
(2) 当你 pre-create 所有 管道时,在 child 中,你必须关闭那些 not 当前 child,不仅仅是它的两个管道的管道边。
如果你不这样做,那么childN会hold开parent[和 child] 侧 each child 的两个管道 all children 不是 N.
在一个[给定]分叉之后,如果parent完全关闭一个打开的管道,child将仍然 继承分叉时在 parent 中打开的任何文件描述符的 [副本]。所以,关闭 parent 没有效果,因为 child 仍然保持打开状态——对于 all child仁
这就是你原来的程序所做的。
在我的 [下面] 版本中,它没有那么严重。没有预关闭(通过 childclose
),child 0 只保持它自己的管道打开。但是,Child 1 将保持打开 child 0 的管道。 Child 2 将为 child 0 和 child 1 保持打开管道。并且,依此类推...
因此,许多 children 都在互相保持管道描述符打开。因此,当 parent 进程关闭管道时,它们仍然被其他 children 保持打开状态,因此 no child 将永远看到 EOF
如果您想将其可视化,请使用您的原始代码,并将其作为 fork
之后 child 的第一个可执行部分(例如紧跟在 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)个打开的描述符,您会在 each[=59= 中看到 (2 * PROCESSES)
(即 10)个描述符] child.
在 parent 中完成最终关闭后,您可以 [在 parent] 中重复这样的序列,并且您将 仍然 看到相同的结果事情 [减去每个 child 将关闭的两个]。
使用结构可以更轻松地组织起来。为了证明它确实有效,我添加了一些带有回显的实际数据传输。我还添加了一些调试选项以显示差异。
下面是更正后的代码[请原谅不必要的样式清理]:
#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;
}
首先,我将回顾一些您显然知道的信息。我写这篇文章是因为其他人也可能会读到这个答案,因为为答案提供一些上下文总是好的。
之后我会展示为什么你的代码在:
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
ed 副本,每个 pipe
由两个文件描述符组成,一个用于输入(读取)和另一个用于输出(写入)。
每次您 fork 进程时,这两个端点(文件描述符)- 对于 每个 打开的管道 - 都是重复的。
read
将阻塞,而传入数据仍有可能最终到达 - 这意味着,read
将阻塞,而至少有一个 "output"(写入)文件描述符是仍然开放。
在下面的示例中,我将打开一个管道并分叉进程。分叉进程将关闭它的 "input"(写入)端点并调用 read
。 read
会阻塞,因为在父进程中仍然有一个打开的输入 fd(记住,fd
是重复的)。 parent关闭后是"input"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
的调用失败(而不是阻塞),我们需要关闭所有写入 endpoints/channels.
在您的代码中,每个进程都有一个管道,但您允许每个进程保持其他进程的 "input"(写入)端点打开 - 因此 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.
很少为每个进程打开管道,除非每个进程都有单独的角色。
共享同一功能的所有进程共享同一管道的情况更为常见。
例如,如果使用一系列进程来执行共享的任务系列,那么哪个进程执行哪个任务可能并不重要 - 因此如果将任务提交到共享管道并且第一个读取数据的进程是执行任务的进程。
当一个进程忙于执行任务时,它不会从管道读取,如果另一个进程可用(在 "read" 上阻塞),它会立即投入工作(而不是等待繁忙的进程)。
这种单管道设计最大限度地减少了 "waiting" 周期并消除了任何调度问题(管道缓冲区的限制除外)。
首先,准备好看到一些魔法。
您好,在过去的几个小时里,我一直在为这个问题而苦恼和挣扎,我不明白为什么 child 进程没有死掉。我基本上只有一个 parent 进程和许多 children 进程。所有 children 都需要与 parent 通信,而 parent 需要与所有 children 通信。对于初学者,我只是让 children 不断尝试 read
,但我的 parent 什么也没发送,只是关闭了管道的 write
端,从而导致他们的 reads
停止阻塞。这是我的过程(我的宏定义是 5
children processes):
2
管道的 5 int*
数组。 parent 使用第一个与 child 对话,child 使用第二个
5
children 并关闭管道的适当端
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;
}
我知道是children中的read
循环阻止了children死亡,因为当它被删除,它工作正常。但是,我不明白为什么会这样。在底部的循环中,我清楚地关闭了所有管道,甚至检查了错误。为什么是这样?!为什么 read
仍然阻碍我实现我的 return;
目标??!?
一些事情。
(1) 对于每个 child,您只创建了一个管道 [parent-to-child],但您需要第二个 [child-to-parent](即管道是 不是双向像套接字)。
(2) 当你 pre-create 所有 管道时,在 child 中,你必须关闭那些 not 当前 child,不仅仅是它的两个管道的管道边。
如果你不这样做,那么childN会hold开parent[和 child] 侧 each child 的两个管道 all children 不是 N.
在一个[给定]分叉之后,如果parent完全关闭一个打开的管道,child将仍然 继承分叉时在 parent 中打开的任何文件描述符的 [副本]。所以,关闭 parent 没有效果,因为 child 仍然保持打开状态——对于 all child仁
这就是你原来的程序所做的。
在我的 [下面] 版本中,它没有那么严重。没有预关闭(通过 childclose
),child 0 只保持它自己的管道打开。但是,Child 1 将保持打开 child 0 的管道。 Child 2 将为 child 0 和 child 1 保持打开管道。并且,依此类推...
因此,许多 children 都在互相保持管道描述符打开。因此,当 parent 进程关闭管道时,它们仍然被其他 children 保持打开状态,因此 no child 将永远看到 EOF
如果您想将其可视化,请使用您的原始代码,并将其作为 fork
之后 child 的第一个可执行部分(例如紧跟在 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)个打开的描述符,您会在 each[=59= 中看到 (2 * PROCESSES)
(即 10)个描述符] child.
在 parent 中完成最终关闭后,您可以 [在 parent] 中重复这样的序列,并且您将 仍然 看到相同的结果事情 [减去每个 child 将关闭的两个]。
使用结构可以更轻松地组织起来。为了证明它确实有效,我添加了一些带有回显的实际数据传输。我还添加了一些调试选项以显示差异。
下面是更正后的代码[请原谅不必要的样式清理]:
#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;
}
首先,我将回顾一些您显然知道的信息。我写这篇文章是因为其他人也可能会读到这个答案,因为为答案提供一些上下文总是好的。
之后我会展示为什么你的代码在:
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
ed 副本,每个 pipe
由两个文件描述符组成,一个用于输入(读取)和另一个用于输出(写入)。
每次您 fork 进程时,这两个端点(文件描述符)- 对于 每个 打开的管道 - 都是重复的。
read
将阻塞,而传入数据仍有可能最终到达 - 这意味着,read
将阻塞,而至少有一个 "output"(写入)文件描述符是仍然开放。
在下面的示例中,我将打开一个管道并分叉进程。分叉进程将关闭它的 "input"(写入)端点并调用 read
。 read
会阻塞,因为在父进程中仍然有一个打开的输入 fd(记住,fd
是重复的)。 parent关闭后是"input"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
的调用失败(而不是阻塞),我们需要关闭所有写入 endpoints/channels.
在您的代码中,每个进程都有一个管道,但您允许每个进程保持其他进程的 "input"(写入)端点打开 - 因此 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.
很少为每个进程打开管道,除非每个进程都有单独的角色。
共享同一功能的所有进程共享同一管道的情况更为常见。
例如,如果使用一系列进程来执行共享的任务系列,那么哪个进程执行哪个任务可能并不重要 - 因此如果将任务提交到共享管道并且第一个读取数据的进程是执行任务的进程。
当一个进程忙于执行任务时,它不会从管道读取,如果另一个进程可用(在 "read" 上阻塞),它会立即投入工作(而不是等待繁忙的进程)。
这种单管道设计最大限度地减少了 "waiting" 周期并消除了任何调度问题(管道缓冲区的限制除外)。