因为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你在干什么(扶额)
上网一搜,果然出来了这个
因为我写的这个类没有复制构造函数,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