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