具体来讲,Pipe是一种POSIX规范,在不同系统上都有实现。msvcrt提供了_pipe这个函数。但是,它的实现是基于CreatePipe,这是无庸置疑的。这种非标准(带下划线)的C函数,在CRT中的很多。比如_open返回的文件指针FIFL*,很多时候我们都没有注意到,它几乎等同于CreateFile传回来的HANDLE。在Windows核心编程中,我们知道,每个进程有一个句柄表。创建子进程时,可以指定子进程是否继承父进程句柄表。如果子进程继承了父进程,且句柄有有继承属性,就可以很方便地共享句柄,如果这人句柄是管道,则可以用于进程间通讯。
言归正传,现在正式介绍管道。管道其实比较容易理解,它就像一个管子一样,但是要注意它是有方向性。即,一个管道只允许在同一时间,以某一方向操作。换而言之,同一时间,其中一个进程在写管道,而另只能读管道。先看看Win32中的管道创建方法。
BOOL CreatePipe(
PHANDLE hReadPipe,
PHANDLE hWritePipe,
LPSECURITY_ATTRIBUTES lpPipeAttributes,
DWORD nSize
);
第3、4个参数用于属性,只使用一次。最为重要的是hReadPipe和hWritePipe,它分别代表管道的读端与写端。这里有几点要说明:
1.确切地说,HANDLE只是一个有特殊意义的整数。比如,我们在CreatePipe后又调用CreateProcess创建子进程,并都设置了继承属性,那么这个整数在两个线程中都有效。而且,我们倾向于用命令行参数的方式传给子线程。
2.假设父进程创建了一个管道,读端和写端分别是fhR, fhW,它把它两个值传给子进程(假设就是用命令行的方式),分别为shW, shR,注意到,这里把R和W的标识反着写了,这是通俗写法。例如,我在子进程使用shW来写数据,它在父进程中刚好对应fhR;反之父进程用fdW写,子进程用shR读。
来看一例子
#include <stdio.h>
#include <windows.h>
#define BUFSIZE 4096 HANDLE hfInRd, hfInWr, hfoutWrDup, hfOutRd, hfOutWr, hChildStdoutRdDup, hStdout;
DWORD main(int argc, char *argv[])
{
SECURITY_ATTRIBUTES saAttr;
BOOL fSuccess; // 设置一个有继承属性的安全属性,用于创建管道.
saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
saAttr.bInheritHandle = TRUE;
saAttr.lpSecurityDescriptor = NULL; // 创建一个有继承属性的管道
CreatePipe(&hfOutRd, &hSInWr, &saAttr, 0); //给父进程读的// 将管道的读句柄拷贝一份到hfRdDup
DuplicateHandle(GetCurrentProcess(), hfOutRd,
GetCurrentProcess(), &hfOutRdDup , 0,
FALSE, // 非继承
DUPLICATE_SAME_ACCESS); //关闭读管道,注意,虽然它关闭了,但是还有一个可读管道保存在hfRdDup中
CloseHandle(hfRd);
CreatePipe(&hSOutRd, &hfInWr, &saAttr, 0)); //给父进程写的
DuplicateHandle(GetCurrentProcess(), hfInWr,
GetCurrentProcess(), &hfInWrDup, 0,
FALSE, // 非继承
DUPLICATE_SAME_ACCESS);
CloseHandle(hfInWr); // 创建进程
PROCESS_INFORMATION piProcInfo;
STARTUPINFO siStartInfo;
BOOL bFuncRetn = FALSE;
ZeroMemory( &piProcInfo, sizeof(PROCESS_INFORMATION) );
ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) );
siStartInfo.cb = sizeof(STARTUPINFO);
siStartInfo.hStdError = hSInWr;
siStartInfo.hStdOutput = hSInWr;
siStartInfo.hStdInput = hSOutRd;
siStartInfo.dwFlags |= STARTF_USESTDHANDLES; //子进程的hStdOutput被赋予了hSInWr, 而这个写端对应的读端是hfOutRd,所以父进程可以 //从hfOutRdDup上读到子进程的标准输出; //而子程序的hStdInput被赋予了hfInRd, 这个端对应的写端是hfInWr //这表示,父进程可以通过hfInWrUp把数据写到子进程的标准输入上 //这里主要以父进程为目标来说明的,因为子进程通常是别人写的程序。所以创建了两个管道,分别用于输入到输出(相对于子进程),否则,完全可以用一个管道,由两个进程协商IO的顺序
CreateProcess(NULL, "child", // command line
NULL, // process security attributes
NULL, // primary thread security attributes
TRUE, // handles are inherited
0, // creation flags
NULL, // use parent's environment
NULL, // use parent's current directory
&siStartInfo, // STARTUPINFO pointer
&piProcInfo); // receives PROCESS_INFORMATION
CloseHandle(piProcInfo.hProcess);
CloseHandle(piProcInfo.hThread);
WriteToPipe(hfInWrDup); // 。。。
ReadFromPipe(hfOutRdDup); //。。。
return 0;
}
CreateProcess 有一个参数,可以指定创建的子进程所使用的管道,这一机制非常方便。例如,在没有 STDIN 和 STDOUT 的 GUI 程序中,就可以创建两个管道,然后调用控制台程序,可以很容易捕获到输出。
关于管道,其实CRT中也有提供。事实上,管道是POSIX标准之一,很多系统上都提供其实现。下面看看两个重要的管道函数。
int_pipe(int*phandles,unsignedintpsize,inttextmode);
FILE*_popen(constchar*command,constchar*mode);
第一个函数非常类似于CreatePipe函数,phandles是一个int[2]数组;_popen函数创建一个进程,mode如果指定了“r",即读管道,那么返回的FILE是一个用于读的管道,你可以用fgets等Stream I/O函数读,而且父进程的stdin自动转发到子进程的stdin;如果mode指定的"w",那么是一个写管道,用fputs可以写到子进程的stdin,而子进程的stdout是在创建时就连接到父进程的stdou上了。
phandles[0]与phandles[1]与CreatePipe创建的管道一样,注意到,它是一个整数,或者专业一点:文件描述符,它其实对应的是一个句柄(经过一系列转换)。文件描述符可以用_read, _write等操作,通常称之为Low-Level I/O。例如,打开文件有两种方式:
int_open(constchar*filename,intoflag [,intpmode] );
FILE*fopen(constchar*filename,constchar*mode);
两个函数都是打开文件,区别在于后者有缓冲的概念。例如,stdin和stdout就是属于流对象。
Stream I/O是Low-Level I/O的子类(我是这样理解的),_fileno函数可以得到流对应的文件描述符。我以前很少使用fopen这种C语言流,因为对于流,我更倾向于用iostream。不过,C++没有提供Low-Level I/O,所以很多时候很有必要使用它。这里有两个函数非常有用,_dup和_dup2。它类似于DuplicateHandle函数,可以用于子进程与父进程通信。
正如前面所述,子进程可以继承父进程的句柄表。当用dup复制一个文件描述符后,就可以用于通信了(比如管道或共享文件)。
下面这个简单程序,展示的是如何通过管道来读子进程的输出。
#include <stdio.h>
#include <string.h>
int main()
{
int i;
for(i=0;i<100;++i)
{
printf("\nThis is speaker beep number %d... \n\7", i+1);
}
return 0;
}
// BeepFilter.Cpp
/* Compile options needed: none
Execute as:BeepFilter.exe <path>Beeper.exe
*/
#include <windows.h>
#include <process.h>
#include <memory.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <io.h>
#define OUT_BUFF_SIZE 512
#define READ_HANDLE 0
#define WRITE_HANDLE1
#define BEEP_CHAR7
char szBuffer[OUT_BUFF_SIZE];
int Filter(char* szBuff, ULONG nSize, int nChar)
{
char* szPos =szBuff + nSize -1;
char* szEnd =szPos;
int nRet =nSize;
while (szPos> szBuff)
{
if (*szPos ==nChar)
{
memmove(szPos, szPos+1, szEnd - szPos);
--nRet;
}
--szPos;
}
return nRet;
}
int main(int argc, char** argv)
{
int nExitCode =STILL_ACTIVE;
if (argc >=2)
{
HANDLEhProcess;
int hStdOut;
inthStdOutPipe[2];
// Create thepipe
if(_pipe(hStdOutPipe, 512, O_BINARY | O_NOINHERIT) == -1)
return 1;
// Duplicatestdout handle (next line will close original)
hStdOut =_dup(_fileno(stdout));
// Duplicate write end of pipe to stdouthandle
if(_dup2(hStdOutPipe[WRITE_HANDLE], _fileno(stdout)) != 0)
return 2;
// Closeoriginal write end of pipe
close(hStdOutPipe[WRITE_HANDLE]);
// Spawnprocess
hProcess =(HANDLE)spawnvp(P_NOWAIT, argv[1],
(const char*const*)&argv[1]);
// Duplicatecopy of original stdout back into stdout
if(_dup2(hStdOut, _fileno(stdout)) != 0)
return 3;
// Closeduplicate copy of original stdout
close(hStdOut);
if(hProcess)
{
intnOutRead;
while (nExitCode == STILL_ACTIVE)
{
nOutRead = read(hStdOutPipe[READ_HANDLE],
szBuffer, OUT_BUFF_SIZE);
if(nOutRead)
{
nOutRead = Filter(szBuffer, nOutRead, BEEP_CHAR);
fwrite(szBuffer, 1, nOutRead, stdout);
}
if(!GetExitCodeProcess(hProcess,(unsigned long*)&nExitCode))
return 4;
}
}
}
printf("\nPress \'ENTER\' key to continue... ");
getchar();
returnnExitCode;
}
下面这个程序是popen的示例,它展示的是子进程与父进程共享句柄表的的方式。
#include <stdio.h>
#include <stdlib.h>
void main( void )
{
char psBuffer[128];
FILE *chkdsk;
/* Run DIRso that it writes its output to a pipe. Open this
* pipe withread text attribute so that we can read it
* like atext file.
*/
if( (chkdsk =_popen( "dir *.c /on /p", "rt" )) == NULL )
exit( 1 );
/* Read pipeuntil end of file. End of file indicates that
* CHKDSK closedits standard out (probably meaning it
*terminated).
*/
while( !feof(chkdsk ) )
{
if( fgets(psBuffer, 128, chkdsk ) != NULL )
printf(psBuffer );
}
/* Close pipeand print return value of CHKDSK. */
printf("\nProcess returned %d\n", _pclose( chkdsk ) );
}
关于管道,还有一个非常好的资料,即popen的实现源码,从那里可以看到背后的一切。