C 为什么在写端关闭之前读取会阻塞管道?
我试图通过编写以下popen类型函数来加深对fork、exec、dup和重定向stdin/stdout/stderr相关内容的理解:C 为什么在写端关闭之前读取会阻塞管道?,c,linux,pipe,fork,stdout,C,Linux,Pipe,Fork,Stdout,我试图通过编写以下popen类型函数来加深对fork、exec、dup和重定向stdin/stdout/stderr相关内容的理解: // main.c #include <pthread.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <
// main.c
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define INVALID_FD (-1)
typedef enum PipeEnd {
READ_END = 0,
WRITE_END = 1
} PipeEnd;
typedef int Pipe[2];
/** Encapsulates information about a created child process. */
typedef struct popen2_t {
bool success; ///< true if the child process was spawned.
Pipe stdin; ///< parent -> stdin[WRITE_END] -> child's stdin
Pipe stdout; ///< child -> stdout[WRITE_END] -> parent reads stdout[READ_END]
Pipe stderr; ///< child -> stderr[WRITE_END] -> parent reads stderr[READ_END]
pid_t pid; ///< child process' pid
} popen2_t;
/** dup2( p[pe] ) then close and invalidate both ends of p */
static void dupFd( Pipe p, const PipeEnd pe, const int fd ) {
dup2( p[pe], fd);
close( p[READ_END] );
close( p[WRITE_END] );
p[READ_END] = INVALID_FD;
p[WRITE_END] = INVALID_FD;
}
popen2_t popen2( const char* cmd ) {
popen2_t r = { false, { INVALID_FD, INVALID_FD } };
if ( -1 == pipe( r.stdin ) ) { goto end; }
if ( -1 == pipe( r.stdout ) ) { goto end; }
if ( -1 == pipe( r.stderr ) ) { goto end; }
switch ( (r.pid = fork()) ) {
case -1: // Error
goto end;
case 0: // Child process
dupFd( r.stdin, READ_END, STDIN_FILENO );
dupFd( r.stdout, WRITE_END, STDOUT_FILENO );
dupFd( r.stderr, WRITE_END, STDERR_FILENO );
{
char* argv[] = { "sh", "-c", (char*)cmd, NULL };
if ( -1 == execvp( argv[0], argv ) ) { exit(0); }
}
}
// Parent process
close( r.stdin[READ_END] );
r.stdin[READ_END] = INVALID_FD;
close( r.stdout[WRITE_END] );
r.stdout[WRITE_END] = INVALID_FD;
close( r.stderr[WRITE_END] );
r.stderr[WRITE_END] = INVALID_FD;
r.success = true;
end:
if ( ! r.success ) {
if ( INVALID_FD != r.stdin[READ_END] ) { close( r.stdin[READ_END] ); }
if ( INVALID_FD != r.stdin[WRITE_END] ) { close( r.stdin[WRITE_END] ); }
if ( INVALID_FD != r.stdout[READ_END] ) { close( r.stdout[READ_END] ); }
if ( INVALID_FD != r.stdout[WRITE_END] ) { close( r.stdout[WRITE_END] ); }
if ( INVALID_FD != r.stderr[READ_END] ) { close( r.stderr[READ_END] ); }
if ( INVALID_FD != r.stderr[WRITE_END] ) { close( r.stderr[WRITE_END] ); }
r.stdin[READ_END] = r.stdin[WRITE_END] =
r.stdout[READ_END] = r.stdout[WRITE_END] =
r.stderr[READ_END] = r.stderr[WRITE_END] = INVALID_FD;
}
return r;
}
int main( int argc, char* argv[] ) {
popen2_t p = popen2( "./child.out" );
{
int status = 0;
sleep( 2 );
{
char buf[1024] = { '\0' };
read( p.stdout[READ_END], buf, sizeof buf );
printf( "%s", buf );
}
//pid_t wpid = waitpid( p.pid, &status, 0 );
//return wpid == p.pid && WIFEXITED( status ) ? WEXITSTATUS( status ) : -1;
}
}
我的问题是关于读取的——我不太明白为什么在子进程完成从而关闭其管道末端之前,读取看起来是阻塞的
这是巧合吗?你可以看到,我试图让主进程在子进程的中间用睡眠2语句执行它的读取。
子进程总共向其重定向的标准输出转储50个字符。主进程是否可能在孩子的执行过程中读取它,只读取其中50个字符中的n个,因此,主进程“PrtTF”不能全部打印子进程中的所有四行?
就功能而言,一切都很好-我的问题是更好地理解read默认情况下,stdout在不向终端写入时会被完全缓冲。因此,在刷新缓冲区之前,子对象中的printf调用不会向管道写入任何内容。当缓冲区可能填满1K或4K字节或进程退出时,就会发生这种情况
您可以立即使用fflushstdout;刷新缓冲区;。在每次printf调用之后添加,您就可以在父进程中读取它们,而无需等待进程退出。默认情况下,stdout在不向终端写入数据时会被完全缓冲。因此,在刷新缓冲区之前,子对象中的printf调用不会向管道写入任何内容。当缓冲区可能填满1K或4K字节或进程退出时,就会发生这种情况
您可以立即使用fflushstdout;刷新缓冲区;。在每次printf调用之后,您都可以在父进程中读取它们,而无需等待进程退出。您认为它还能合理地做什么?@NateEldredge-我不能说这是合理的,但我想象管道可能像TCP套接字,如果管道中有任何东西,你可以阅读其中的一些部分,而不知道另一端是否完成了。@StoneThrow:事实就是这样。但是子级正在使用printf进行打印,当标准输出不是终端时,printf会被完全缓冲。因此,尽管printf已完成,但直到刷新缓冲区时才真正调用write,而刷新缓冲区是在子项退出时发生的。@StoneThrow:确实如此。在这种情况下,无论有多少字节可用,您都会看到read立即返回。您必须检查返回值以查看有多少字节。如果在此之后需要更多字节,请再次调用read(通常在循环中),它将阻塞,直到有一些字节可用或管道关闭。在所有正常情况下,您都希望以这种方式继续读取。就你的程序而言,家长会在孩子写东西之前关闭管道,这会导致孩子在写更多东西时被SIGPIPE杀死。你认为它还能合理地做什么?@NateEldredge-我不能说这是合理的,但我想象管道可能像TCP套接字,如果管道中有任何东西,你可以阅读其中的某个子集,而不管另一端是否完成了。@StoneThrow:这正是发生的事情。但是子级正在使用printf进行打印,当标准输出不是终端时,printf会被完全缓冲。因此,尽管printf已完成,但直到刷新缓冲区时才真正调用write,而刷新缓冲区是在子项退出时发生的。@StoneThrow:确实如此。在这种情况下,无论有多少字节可用,您都会看到read立即返回。您必须检查返回值以查看有多少字节。如果在此之后需要更多字节,请再次调用read(通常在循环中),它将阻塞,直到有一些字节可用或管道关闭。在所有正常情况下,您都希望以这种方式继续读取。按照您的程序,家长将在孩子完成写作之前关闭管道,这将导致孩子在写更多内容时被SIGPIPE杀死。同样感谢您-与NateEldredge的回答相同-我将根据您的解释进行实验。我们能够通过实验进行确认-感谢您的解释!也谢谢你-和NateEldredge的回答一样-我将根据你的解释去做实验。我们能够通过实验来确认-谢谢你的解释!
// child.c
#include <stdio.h>
#include <unistd.h>
int main( int argc, char* argv[] ) {
printf( "%s:%d\n", __FILE__, __LINE__ );
sleep( 1 );
printf( "%s:%d\n", __FILE__, __LINE__ );
sleep( 1 );
printf( "%s:%d\n", __FILE__, __LINE__ );
sleep( 1 );
printf( "%s:%d\n", __FILE__, __LINE__ );
sleep( 1 );
return 0;
}
$ gcc --version && gcc -g ./child.c -o ./child.out && gcc -g ./main.c && ./a.out
gcc (Debian 6.3.0-18+deb9u1) 6.3.0 20170516
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
./child.c:6
./child.c:8
./child.c:10
./child.c:12
$