使用Windows的匿名管道和Boost.Iostreams

因为Boost.Iostreams提供了封装Windows的Handle的支持,利用这个可以简化匿名管道的操作。我想要达到的目标,先实现这样一个简单的命令行:

ffmpeg -i c:\1.mp3 -acodec pcm_s16le -ac 2 -ar 44100 -f s16le - | oggenc2 -o c:\1.ogg -q 1 -B 16 -C 2 -R 44100 -

功能很简单,就是把一个mp3文件转换成ogg文件而已。然而要达到这个目的,耗了不少精力……

首先是管道。Windows提供了CreatePipe函数,于是简单易懂我来

CreatePipe(&_hInputPipeForRead, &_hInputPipeForWrite, 0, 0);

然后创建进程的时候,STARTUPINFO里的dwFlags加上STARTF_USESTDHANDLES,把管道赋值过去,调用

CreateProcess(0, &tmpbuf[0], 0, 0, FALSE, 0, 0, 0, &si, &pi)

结果是ffmpeg秒退。看出错信息,无法打开输出。明显是管道不行。去Windows SDK的文档看了半天例子,琢磨了两下,是HANDLE没有被继承给子进程的原因。那好,把CreateProcess里的那个FALSE改成TRUE。运行,秒退(死目

再看那个例子,它创建管道的时候专门设置了一下安全属性。那我也来

 SECURITY_ATTRIBUTES saAttr;
 saAttr.nLength = sizeof(saAttr);
 saAttr.bInheritHandle = TRUE;
 saAttr.lpSecurityDescriptor = NULL;

 CreatePipe(&_hInputPipeForRead, &_hInputPipeForWrite, &saAttr, 0);

好,这下ffmpeg运行起来在进程列表里可以看到了, 没有秒退了。接下来自己写一个类来封装这个进程的输入输出,我的话是这么搞的

class process_io {
 public:
   typedef char char_type;
   typedef bio::bidirectional_device_tag category;

   enum {SOURCE_NONE = 0, SOURCE_STDIN, SOURCE_STDOUT, SOURCE_STDERR};

   process_io(const char* strCmdLine, int inputSource = SOURCE_NONE, int outputSource = SOURCE_NONE);
   ~process_io();
   bool start();
   bool close();
   void terminate();
   bool wait(int milliseseconds);

   std::streamsize read(char* s, std::streamsize n);
   std::streamsize write(const char* s, std::streamsize n);
 private:
   ...
};

析构函数里面终止进程。然后照着教学,来

bio::stream<process_io> p1("d:\\tools\\ffmpeg.exe -i c:\\1.mp3 -acodec pcm_s16le -ar 44100 -ac 2 -f s16le -", process_io::SOURCE_NONE, process_io::SOURCE_STDOUT);

……(真心死的快有木有啊)。这下连ffmpeg的进程都看不到了。在terminate里面下一个断点,运行,果断发现析构函数被调用了。这尼玛……我明明才创建对象你就给我析构。一想,什么时候一边构造一边还有析构,大概是它给我process_io复制一份自己留着了然后我的就被丢掉了吧。卧槽,“复制一份”?!boost你在干什么(扶额)

上网一搜,果然出来了这个

http://lists.boost.org/Archives/boost/2005/10/95913.php

因为我写的这个类没有复制构造函数,C++给我自己生成了一个,还是浅复制的。HANDLE不能给你这样搞啊(锤地)。那我就先把自己这个类的复制功能给弄残,声明一个复制构造函数但不实现。然后按照这邮件列表里面给出的回复,用boost::reference_wrapper给搞一下:

 bio::stream<boost::reference_wrapper<process_io>> p1;
 process_io p1_impl("d:\\tools\\ffmpeg.exe -i c:\\1.mp3 -acodec pcm_s16le -ar 44100 -ac 2 -f s16le -", process_io::SOURCE_NONE, process_io::SOURCE_STDOUT);
 p1.open(boost::ref(p1_impl));

编译,通过。因为复制构造函数已经残了,所以确定它没有给我复制。

然后搞出了如下main函数用于测试代码能不能用:

int main() {
  bio::stream<boost::reference_wrapper<process_io>> p1;
  bio::stream<boost::reference_wrapper<process_io>> p2;
  process_io p1_impl("d:\\tools\\ffmpeg.exe -i c:\\1.mp3 -acodec pcm_s16le -ar 44100 -ac 2 -f s16le -", process_io::SOURCE_NONE, process_io::SOURCE_STDOUT);
  process_io p2_impl("c:\\oggenc2 -o c:\\1.ogg -B 16 -R 44100 -C 2 -q 2 -", process_io::SOURCE_STDIN);
  p1.open(boost::ref(p1_impl));
  p2.open(boost::ref(p2_impl));
  while(!p1.eof()) {
    char buf[1024];
    p1.read(buf, 1024);
    int len = p1.gcount();
    p2.write(buf, len);
  }
  p1.close();
  p2.close();
  return 0;
}

能转换,但是转换完进程就卡得死死的了。在p1.read那边不动了。

既然管道的另一边都已经被关闭了,为什么这边ReadFile不直接返回而是卡死?又仔细想想Windows SDK里面那个示例代码,自己创建了一个管道,一个用于读一个用于写,读的自己留着写的拿去给子进程写,写完以后子进程把写的关掉……等下,只有子进程关没用啊,自己这边也要关啊,这个HANDLE才算是完全释放了。哦我把HANDLE给子进程之后自己还留着。OK,那我就在CreateProcess之后把不用的HANDLE给关掉。再试,OK可以正常结束了。

补充:

多个进程互相交互的时候,又出了点问题。明明关掉了hStdInput的HANDLE,但是进程却没有认为读到了end,还在继续等待数据。又是多余的HANDLE的问题,但是怎么找却找不到哪里多出来:该关的关掉了。最后用processexporer查看,发现子进程也持有一个和我关掉的HANDLE的value一样的HANDLE。于是又想起Windows SDK上那个例子,果然还是因为没有完全按照例子做的缘故:自己留下来用来读写的HANDLE给继承到子进程里面去了,所以自己关掉管道的输入HANDLE以后子进程还持有一个输入HANDLE。方法就是用SetHandleInformation把自己留着用的那个HANDLE的继承属性去掉,才得以正常。

小总结一下:

1、要把管道的某个HANDLE传给子进程,要注意支持继承

2、复制构造函数如果没写的话,不确定人家会怎么用你的类,那就声明一个复制构造函数但是不实现

3、没用的HANDLE趁早关掉,如果允许继承的,因为管道一套是两个,所以如果设了继承那么就是两个都给你继承。

完整的代码下载: pipe_test

此代码只是试着玩的时候用的。代码稳定性、强度等都还…… -_,- 随便乱调用里面的函数程序还是会崩的(一堆东西都没检查嗯……

补充说明一下,boost.iostreams里面搞出来这类read和write不给你同时调用的,里面拿锁锁着。同时调用会有一个阻塞。不知道是不是可以设置成允许,我现在最新代码(没传上来)暂时是把input和output搞成两个类。

 

pre

发表评论