具体来讲,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; }
关于管道,其实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的实现源码,从那里可以看到背后的一切。