Windows中的管道解析

       具体来讲,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 有一个参数,可以指定创建的子进程所使用的管道,这一机制非常方便。例如,在没有 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是一个用于读的管道,你可以用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的实现源码,从那里可以看到背后的一切。


你可能感兴趣的:(Windows中的管道解析)