A!die Software Studio Welcome to A!Die Software Studio

Windows管道(Pipe)重定向stdout,stderr,stdin

by other
2012-03-25 13:50:37

stdin是标准输入,stdout是标准输出,stderr是标准错误输出。
大多数的命令行程序从stdin输入,输出到stdout或stderr,有时我们需要重定向stdout,stderr,stdin。比如:将输出写入文件,又或者我们要将命令行程序输出结果显示到Windows对话框中。

在Windows编程中,重定向需要用到管道(Pipe)的概念。管道是一种用于在进程间共享数据的机制。一个管道类似于一个管子的两端,一端是写入的,一端是读出的。由一个进程从写入端写入、另一个进程从读出端读出,从而实现通信,就向一个“管道”一样。

重定向的原理是:

首先声明两个概念:主程序(重定向的操纵者)、子进程(被重定向的子进程)

如果要重定位stdout的话,先生成一个管道, 管道的写入端交给子进程去写,主程序从管道的读出端读数据,然后可以把数据写成文件、显示等等。重定向stderr和stdout是相同的。

同理,要重定向stdin的话,生成一个管道, 管道的写入端由主程序写,子进程从管道的读出端读数据。

其中需要用到几个Windows API :  CreatePipe, DuplicateHandle, CreateProcess, ReadFile, WriteFile 等,函数详解可参见MSDN.


一、编程实现原理 ( C语言)

  1. #include <windows.h>  
  2.   
  3.   //定义句柄: 构成stdin管道的两端句柄  
  4.   HANDLE  hStdInRead;         //子进程用的stdin的读入端  
  5.   HANDLE  hStdInWrite;        //主程序用的stdin的读入端  
  6.   
  7.   //定义句柄: 构成stdout管道的两端句柄  
  8.   HANDLE  hStdOutRead;     ///主程序用的stdout的读入端  
  9.   HANDLE  hStdOutWrite;    ///子进程用的stdout的写入端  
  10.   
  11.   //定义句柄: 构成stderr管道的句柄,由于stderr一般等于stdout,我们没有定义hStdErrRead,直接用hStdOutRead即可  
  12.   HANDLE  hStdErrWrite;       ///子进程用的stderr的写入端  
  13.   
  14.   //定义一个用于产生子进程的STARTUPINFO结构体 (定义见CreateProcess,函数说明)  
  15.   STARTUPINFO siStartInfo;  
  16.   //定义一个用于产生子进程的PROCESS_INFORMATION结构体 (定义见CreateProcess,函数说明)  
  17.   PROCESS_INFORMATION piProcInfo;  
  18.   
  19.   
  20.   //产生一个用于stdin的管道,得到两个HANDLE:  hStdInRead用于子进程读出数据,hStdInWrite用于主程序写入数据  
  21.   //其中saAttr是一个STARTUPINFO结构体,定义见CreatePipe函数说明  
  22.   if   (!CreatePipe(&hStdInRead, &hStdInWrite,&saAttr, 0))  
  23.           return;  
  24.   
  25.   //产生一个用于stdout的管道,得到两个HANDLE:  hStdInRead用于主程序读出数据,hStdInWrite用于子程序写入数据  
  26.   if  (!CreatePipe(&hStdOutRead, &hStdOutWrite,&saAttr, 0))  
  27.         return;  
  28.   //由于stderr一般就是stdout, 直接复制句柄hStdOutWrite,得到 hStdErrWrite  
  29.    if (!DuplicateHandle(GetCurrentProcess(), hStdOutWrite, GetCurrentProcess(), &hStdErrWrite, 0, TRUE, DUPLICATE_SAME_ACCESS))  
  30.       return;  
  31.   
  32.   //对STARTUPINFO结构体赋值,对stdin,stdout,stderr的Handle设置为刚才得到的管道HANDLE  
  33.   ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) );  
  34.   siStartInfo.cb = sizeof(STARTUPINFO);  
  35.   siStartInfo.dwFlags  |= STARTF_USESHOWWINDOW;  
  36.   siStartInfo.dwFlags  |= STARTF_USESTDHANDLES;  
  37.   siStartInfo.hStdOutput = hStdOutWrite;     //意思是:子进程的stdout输出到hStdOutWrite  
  38.   siStartInfo.hStdError  =  hStdErrWrite;        //意思是:子进程的stderr输出到hStdErrWrite  
  39.   siStartInfo.hStdInput  = hStdInRead;  
  40.   
  41.   // 产生子进程,具体参数说明见CreateProcess函数  
  42.   bSuccess = CreateProcess(NULL,  
  43.       CommandLine,    // 子进程的命令行  
  44.       NULL,                   // process security attributes  
  45.       NULL,                   // primary thread security attributes  
  46.       TRUE,                   // handles are inherited  
  47.       0,                          // creation flags  
  48.       NULL,                  // use parent's environment  
  49.       NULL,                  // use parent's current directory  
  50.       &siStartInfo,      // STARTUPINFO pointer  
  51.       &piProcInfo);     // receives PROCESS_INFORMATION  
  52.   
  53.    //如果失败,退出  
  54.    if (!bSuccess ) return;  
  55.   
  56.     //然后,就可以读写管道了  
  57.     //写入stdin,具体代码在一个WriteToPipe函数中  
  58.     WriteToPipe();  
  59.   
  60.    //不断子检测进程有否结束  
  61.     while (GetExitCodeProcess(piProcInfo.hProcess,&process_exit_code))  
  62.         {  
  63.             //读stdout,stderr  
  64.            ReadFromPipe();  
  65.            //如果子进程结束,退出循环  
  66.            if (process_exit_code!=STILL_ACTIVE) break;  
  67.         }  


 
具体看一下WriteToPipe(), ReadFromPipe函数:

  1. //写入stdin  
  2. BOOL WriteToPipe()  
  3. {  
  4.   DWORD dwWritten;  
  5.   BOOL bSuccess = FALSE;  
  6.     
  7.  //用WriteFile,从hStdInWrite写入数据,数据在in_buffer中,长度为dwSize  
  8.   bSuccess = WriteFile( hStdInWrite, in_buffer, dwSize, &dwWritten, NULL);  
  9.   return bSuccess;  
  10. }  

 

  1. // 读出stdout  
  2. BOOL ReadFromPipe()  
  3. {  
  4.   char out_buffer[4096];  
  5.   DWORD dwRead;    
  6.   BOOL bSuccess = FALSE;  
  7.   
  8.    //用WriteFile,从hStdOutRead读出子进程stdout输出的数据,数据结果在out_buffer中,长度为dwRead  
  9.    bSuccess = ReadFile( hStdOutRead, out_buffer, BUFSIZE, &dwRead, NULL);  
  10.    if ((bSuccess) && (dwRead!=0))  //如果成功了,且长度>0  
  11.              {  
  12.                    // 此处加入你自己的代码  
  13.                    // 比如:将数据写入文件或显示到窗口中  
  14.              }  
  15.   return bSuccess;  
  16. }  


OK,到此原理写完了。为简化说明原理,上述代码省略了出错处理、结束处理(如:CloseHandle等),具体可以参见我的源码。


二、封装、实用的代码

上述过程有些麻烦,实际使用中,我封装成几个函数:

首先定义三个回调函数 (就是函数指针类型)

//当stdin输入时,调用此函数。将要写的数据写入buf中,*p_size写为数据长度即可。
typedef void FuncIn(char *buf,int *p_size);

//当stdout,stderr输出时,调用此函数。可读取的数据在buf中,数据长度为size。
typedef void FuncOut(char *buf,int size);

//当子进程持续过程中,周期性调用此函数,设置p_abort可中断子进程。
typedef void FuncProcessing(int *p_abort);

然后定义了四个函数

//执行一个命令行,重定向stdin, stdout,stderr。
//OnStdOut是回调函数指针,当有输出时,OnStdOut被调用。
//OnStdIn是回调函数指针,当输入时,OnStdIn被调用。
int ExecCommandEx(char *szCommandLine,char *CurrentDirectory,char *Environment,unsigned short ShowWindow,
              FuncOut *OnStdOut,FuncProcessing *OnProcessing,FuncIn *OnStdIn);

//执行一个命令行,重定向stdout,stderr。
//OnLineOut是回调函数指针,当有一行输出时,OnLineOut被调用。
int ExecCommandOutput(char *szCommandLine,char *Environment,unsigned short ShowWindow,
                       FuncOut *OnLineOut,FuncProcessing *OnProcessing);

//执行一个命令行,等待子进程结束,返回子进程的程序退出代码。不处理重定向。
int ExecCommandWait(char *szCommandLine,unsigned short ShowWindow,FuncProcessing *OnProcessing);

//执行一个命令行,不等待子进程结束,即返回。不处理重定向。功能相当于 Windows API  WinExec.
int ExecCommandNoWait(char *szCommandLine,unsigned short ShowWindow);


还定义了一个存储数据的EXEC_INFO结构体及操作它的函数。

全部代码为C语言,在JExecution.c, JExecution.h两个文件中。只用到了Windows API,没有用MFC及其他库。


三、使用方法

有了JExecution.c,使用就很方便了。比如:

  1. #include <stdio.h>  
  2. #include <windows.h>  
  3. #include "JExecution.h"  
  4.   
  5. //定义一个处理输出的回调函数  
  6. void OnLineOut(char *buf,int size)  
  7. {  
  8.    printf("%s\n", buf);  
  9. }  
  10.   
  11.   
  12. int main(int argc, char* argv[])  
  13. {  
  14.   int n;  
  15.   char *command;  
  16.   
  17.   command = "cmd.exe  /r dir/w ";   //这个命令的意思就是调用DOS,执行dir命令,显示当前目前下的文件  
  18.   n=ExecCommandOutput(command,NULL,SW_HIDE,OnLineOut,NULL);  
  19.   printf("<Return:>%d",n);  
  20. }  


 

我用C++ Builder 6写了一个示范程序。示范将stdout重定向,输出到窗口中。

 

 

四、小结

Windows管道虽然有些麻烦,却可以产生好的效果。

比如:我们常用的IDE就是把调用命令行的编译程序,将其stdout重定向,将结果信息显示在窗口中。

▲评论

› 网友 匿名 () 于 2017-05-04 10:38:55 发表评论说:

求封装好的函数

› 网友 小虾米 (sex:男; qq:565784399; ) 于 2017-05-04 10:39:46 发表评论说:

求您封装好的函数,最上边的函数感觉无法使用

› 网友 匿名 (qq:565784399; ) 于 2017-05-04 10:42:35 发表评论说:

求JExecution.c

› 网友 rainfeng@163.com () 于 2018-04-29 15:19:29 发表评论说:

求 JExecution
自己测试了很多,感觉怎么都不是太好!

X 正在回复:
姓 名: 留下更多信息
性 别:
邮 件:
主 页:
Q Q:
来 自:
职 业:
评 论:
验 证:


Valid HTML 4.01 Strict Valid CSS!
Copyleft.A!die Software Studio.ADSS
Power by webmaster@adintr.com