Windows中的管道解析

来源:互联网 发布:linux 查看文件夹列表 编辑:程序博客网 时间:2024/05/19 20:57

       具体来讲,Pipe是一种POSIX规范,在不同系统上都有实现。msvcrt提供了_pipe这个函数。但是,它的实现是基于CreatePipe,这是无庸置疑的。这种非标准(带下划线)的C函数,在CRT中的很多。比如_open返回的文件指针FIFL*,很多时候我们都没有注意到,它几乎等同于CreateFile传回来的HANDLE。在Windows核心编程中,我们知道,每个进程有一个句柄表。创建子进程时,可以指定子进程是否继承父进程句柄表。如果子进程继承了父进程,且句柄有有继承属性,就可以很方便地共享句柄,如果这人句柄是管道,则可以用于进程间通讯。

       

       言归正传,现在正式介绍管道。管道其实比较容易理解,它就像一个管子一样,但是要注意它是有方向性。即,一个管道只允许在同一时间,以某一方向操作。换而言之,同一时间,其中一个进程在写管道,而另只能读管道。先看看Win32中的管道创建方法。

BOOL CreatePipe(

 PHANDLE hReadPipe,

 PHANDLE hWritePipe,

 LPSECURITY_ATTRIBUTES lpPipeAttributes,

 DWORD nSize

);

       34个参数用于属性,只使用一次。最为重要的是hReadPipehWritePipe,它分别代表管道的读端与写端。这里有几点要说明:

       1.确切地说,HANDLE只是一个有特殊意义的整数。比如,我们在CreatePipe后又调用CreateProcess创建子进程,并都设置了继承属性,那么这个整数在两个线程中都有效。而且,我们倾向于用命令行参数的方式传给子线程。

       2.假设父进程创建了一个管道,读端和写端分别是fhR, fhW,它把它两个值传给子进程(假设就是用命令行的方式),分别为shW, shR,注意到,这里把RW的标识反着写了,这是通俗写法。例如,我在子进程使用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有一个参数,可以指定创建的子进程所使用的管道,这一机制非常方便。例如,在没有STDINSTDOUTGUI程序中,就可以创建两个管道,然后调用控制台程序,可以很容易捕获到输出。

关于管道,其实CRT中也有提供。事实上,管道是POSIX标准之一,很多系统上都提供其实现。下面看看两个重要的管道函数。

       int_pipe(int*phandles,unsignedintpsize,inttextmode);

       FILE*_popen(constchar*command,constchar*mode);

 

       第一个函数非常类似于CreatePipe函数,phandles是一个int[2]数组;_popen函数创建一个进程,mode如果指定了“r",即读管道,那么返回的FILE是一个用于读的管道,你可以用fgetsStream 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);

       两个函数都是打开文件,区别在于后者有缓冲的概念。例如,stdinstdout就是属于流对象。


       
Stream I/OLow-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的实现源码,从那里可以看到背后的一切。