相信用 Windows API 写过重定向子进程的标准输入输出的人都弄过这东西。大概过程是不难,不过对初学者坑很多。几年前我写了个文大概说这个的 http://blog.sorayuki.net/?p=85 ,现在要说的也是这个话题,不过是另外的事。
之前的经验是这样的,比如我有一个进程,这个进程要开个子进程、并且发数据给子进程。那么做法是创建一对匿名管道,把管道读的那一端拿去给子进程,写的这一端自己留着。容易出错的地方在这个“自己拿着”和“拿去给子进程”。拿去给子进程可以理解为,这个句柄要设置成可继承的,子进程开启以后就带着这个句柄了,那么自己进程拿着的就可以关掉(CloseHandle)了;自己拿着,那么字面意思就是不继承给子进程了。API函数CreatePipe有个 SECURITY_ATTRIBUTES 参数,这个结构体里是可以设置创建的管道句柄能不能继承的,要么两个都能继承要么两个都不能继承。但是现在要的是写的那个不继承、读的那个继承。做法不止一种,不过思路是这么两个,一个是创建的时候两个都不能继承,然后把其中一个改成可以;另一个是创建的时候两个都能继承,然后把其中一个改成不行。改继承属性可以用 SetHandleInformation 也可以用 DuplicateHandle 复制一个能/不能继承的新句柄出来、随后把原来的关掉。我现在是用 SetHandleInformation 的方法,DuplicateHandle 的方法已经忘记是在哪里看的了,现在也不知道怎么比较好坏和必要性。
复杂一点的场景,是我要起两个进程,第一个进程的输出给第二个进程的输入。麻烦一点的办法可以两个都和自己通信、自己在中间做转发;简单的办法可以直接创建一对管道,一个子进程分一个、自己不留。现在这里要说的是后者,复杂性一个表现在管道是自己创建的,但是自己在起了子进程之后一个都不留;另一个表现在要刚好一个进程继承一个,如果有一个进程继承了两个,比如写的那一端一不小心两个进程都继承了,那么很不幸这一对进程永远跑不完了:写入数据的那个子进程写完了关掉了这个写入端,但是由于读取数据的那个子进程也持有这个写端,自己有自己都不知道,也就是说这个写端没法全部被关闭,那么读的那一端就永远在等待了。
虽然上面那个例子只要搞清楚关系、小心操作的话,也没有那么大问题。不过麻烦的事情在,假设我现在是一个有好几个线程在同时执行的程序,在其中一个线程A里,要调用 CreateProcess 的时候,它不知道其他线程比如B有没有创建什么会被继承的句柄,假设这个时候线程B也在调用 CreateProcess,那么线程B创建的这个子进程会把线程A创建的那些句柄也一起继承走。然后问题呢就会出在刚才说的那个场景上,比如一对管道,读取端永远阻塞因为写入端没有全部被关闭。也许会觉得这样的场景只要在要创建子进程的时候锁个整个进程范围的锁就好了,但如果这里面调用了什么第三方库,或者是工程上好多人同时开发一个项目,有些事情就不一定有那么可控了。而且这种Bug还可能在极偶然的情况下才会遇到那么一两次,调试的时候难以重现十分痛苦……啊别说了,害怕。
那么在 CreateProcess 的时候有没有什么更可靠的方法,比如不光是给每个句柄设置什么标志能不能继承,还能在调用 CreateProcess 的时候通过什么参数来显式指定我要这个、这个、这个和那个句柄?其实是有的,我以前没用过,最近看了一个博客之后才知道有这样的玩法。博客在这里:
https://blogs.msdn.microsoft.com/oldnewthing/20111216-00/?p=8873/
其中很关键的一个示例函数的代码是
BOOL CreateProcessWithExplicitHandles( __in_opt LPCTSTR lpApplicationName, __inout_opt LPTSTR lpCommandLine, __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes, __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, __in BOOL bInheritHandles, __in DWORD dwCreationFlags, __in_opt LPVOID lpEnvironment, __in_opt LPCTSTR lpCurrentDirectory, __in LPSTARTUPINFO lpStartupInfo, __out LPPROCESS_INFORMATION lpProcessInformation, // here is the new stuff __in DWORD cHandlesToInherit, __in_ecount(cHandlesToInherit) HANDLE *rgHandlesToInherit) { BOOL fSuccess; BOOL fInitialized = FALSE; SIZE_T size = 0; LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL; fSuccess = cHandlesToInherit < 0xFFFFFFFF / sizeof(HANDLE) && lpStartupInfo->cb == sizeof(*lpStartupInfo); if (!fSuccess) { SetLastError(ERROR_INVALID_PARAMETER); } if (fSuccess) { fSuccess = InitializeProcThreadAttributeList(NULL, 1, 0, &size) || GetLastError() == ERROR_INSUFFICIENT_BUFFER; } if (fSuccess) { lpAttributeList = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST> (HeapAlloc(GetProcessHeap(), 0, size)); fSuccess = lpAttributeList != NULL; } if (fSuccess) { fSuccess = InitializeProcThreadAttributeList(lpAttributeList, 1, 0, &size); } if (fSuccess) { fInitialized = TRUE; fSuccess = UpdateProcThreadAttribute(lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, rgHandlesToInherit, cHandlesToInherit * sizeof(HANDLE), NULL, NULL); } if (fSuccess) { STARTUPINFOEX info; ZeroMemory(&info, sizeof(info)); info.StartupInfo = *lpStartupInfo; info.StartupInfo.cb = sizeof(info); info.lpAttributeList = lpAttributeList; fSuccess = CreateProcess(lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags | EXTENDED_STARTUPINFO_PRESENT, lpEnvironment, lpCurrentDirectory, &info.StartupInfo, lpProcessInformation); } if (fInitialized) DeleteProcThreadAttributeList(lpAttributeList); if (lpAttributeList) HeapFree(GetProcessHeap(), 0, lpAttributeList); return fSuccess; }
我自己在用这个方法的时候倒是没有直接抄代码,里面有些地方被我换成更简单的代码。不过核心思想是,用 STARTUPINFOEX 来代替 STARTUPINFO,这样在那个带 EX 后缀的结构体里有个 lpAttributeList 可以用,里面可以塞一些比如我要继承那些句柄这样的信息。那么在你给的列表之外的句柄,就不会被子进程继承。
这样回到刚才的例子,我要开两个进程比如A和B,把A的标准输出连到B的标准输入上,就可以创建一对管道,反正都设成继承、也不用一个继承一个不继承了。然后,创建子进程的时候一人塞一个,用这种方式指定只继承其中一个、不继承另外一个,两个进程都开好了就把自己手上的全关了。剩下的就是等待子进程运行完成了。
在实验过程中我还发现有这样一个坑(?),Windows的那几个标准输入输出标准错误的句柄,就是用 GetStdHandles 获得的那几个,虽然可以用 DuplicateHandle 来复制,但是这复制出来的却不能继承。如果尝试在继承句柄列表里塞上这样复制出来的 Stdin、Stdout 或者 Stderr 句柄,CreateProcess 会调用失败,然后给你一个 1450(ERROR_NO_SYSTEM_RESOURCES) 错误。还有就是,虽然指定了要继承的句柄的列表,CreateProcess 的那个 bInheritHandles 函数还是要设置成 TRUE 的,不然你列表里指定的那些句柄它继承不下去。以及在要继承的句柄列表里指定的那些句柄,也必须是可继承的,如果有不可继承的在里面,CreateProcess 会报 87(ERROR_INVALID_PARAMETER) 错误。
我这边就拿一个程序就以用lame重编码MP3文件为例的sample吧。代码不一定就没问题,只演示一下用法
#include <windows.h> #include <exception> #include <string> #include <vector> #include <functional> #include <atlbase.h> class OnExit { std::function<void()> onExit_; public: template<class T> OnExit(const T& lambda) : onExit_(lambda) {} ~OnExit() { onExit_(); } }; class ReEncoderErr : public std::exception { std::string msg_; public: ReEncoderErr(const char* msg) : msg_(msg) {} const char* what() const { return msg_.c_str(); } }; class ReEncoder { CHandle hProcDec_, hProcEnc_; CHandle hPipeRead_, hPipeWrite_; void ClosePipePair() { hPipeRead_.Close(); hPipeWrite_.Close(); } void CreatePipePair() { ClosePipePair(); HANDLE hPipeRead, hPipeWrite; SECURITY_ATTRIBUTES sa{}; sa.bInheritHandle = TRUE; if (!CreatePipe(&hPipeRead, &hPipeWrite, &sa, 0)) throw ReEncoderErr("Fail to create pipe"); hPipeRead_.Attach(hPipeRead); hPipeWrite_.Attach(hPipeWrite); } void RunLame(bool isEncoder) { wchar_t decCmdLine[] = L"lame.exe --decode input.mp3 -"; wchar_t encCmdLine[] = L"lame.exe --preset medium - output.mp3"; wchar_t* cmdLine; STARTUPINFOEXW si{}; PROCESS_INFORMATION pi{}; std::vector<HANDLE> handlesToInherit; si.StartupInfo.cb = sizeof(si); si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; HANDLE hStdErr = GetStdHandle(STD_ERROR_HANDLE); si.StartupInfo.hStdError = hStdErr; if (isEncoder) { si.StartupInfo.hStdInput = hPipeRead_; handlesToInherit.push_back(hPipeRead_); cmdLine = encCmdLine; } else { si.StartupInfo.hStdOutput = hPipeWrite_; handlesToInherit.push_back(hPipeWrite_); cmdLine = decCmdLine; } SIZE_T nProcThreadAttrSize = 0; InitializeProcThreadAttributeList(0, 1, 0, &nProcThreadAttrSize); if (nProcThreadAttrSize == 0) throw ReEncoderErr("Fail to get size of procthreadattr"); std::vector<char> buf(nProcThreadAttrSize); si.lpAttributeList = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(buf.data()); if (InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &nProcThreadAttrSize) == FALSE) throw ReEncoderErr("Fail to init procthreadattr"); OnExit clean([&]() { DeleteProcThreadAttributeList(si.lpAttributeList); }); if (UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handlesToInherit.data(), handlesToInherit.size() * sizeof(HANDLE), NULL, NULL) == FALSE) throw ReEncoderErr("Fail to update procthreadattr"); if (!CreateProcessW(NULL, cmdLine, NULL, NULL, TRUE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi)) throw ReEncoderErr("Fail to create process"); CloseHandle(pi.hThread); if (isEncoder) hProcEnc_.Attach(pi.hProcess); else hProcDec_.Attach(pi.hProcess); } public: void Run() { CreatePipePair(); RunLame(false); RunLame(true); ClosePipePair(); HANDLE handles[2] = { hProcEnc_.m_h, hProcDec_.m_h }; WaitForMultipleObjects(2, handles, TRUE, INFINITE); } }; #include <iostream> int main(int argc, char* argv[]) { try { ReEncoder reenc; reenc.Run(); return 0; } catch (const std::exception& exception) { std::cerr << exception.what() << std::endl; return 1; } }