一种解决启动进程传递参数过长的方法
工作中,QA同学在测试我们程序的时候,发现在XP下,我们的A进程无法启动我们的B进程。而在Win7 64bit系统下功能正常。RD同学调试后,发现我们A进程中使用ShellExcute去启动了B进程(转载请指明出于breaksoftware的csdn博客)
HINSTANCE ShellExecute( _In_opt_ HWND hwnd, _In_opt_ LPCTSTR lpOperation, _In_ LPCTSTR lpFile, _In_opt_ LPCTSTR lpParameters, _In_opt_ LPCTSTR lpDirectory, _In_ INT nShowCmd );
其中不成功的场景是:我们给lpParameters传递了大概32K字节长度的参数。
我当时就觉得这个是因为ShellExcute中参数长度限制问题。我决定将这个逻辑使用CreateProcess去实现,这样我将会有更多的控制权力。但是最后我们发现问题还是依旧的,因为我们查看MSDN关于CreateProcess的lpCommandLine说明:
lpCommandLine [in, out, optional] The command line to be executed. The maximum length of this string is 32,768 characters, including the Unicode terminating null character.
它最长只可以穿32768个字符(而我之后测试结果却是32766)。看来简单的使用CreateProcess还是不能解决我们的问题。
为了解决这个问题,我们首先分析问题出现的场景:
- A进程去启动B进程
- A进程启动B进程时要传递一个很长的数据
- A进程不关心B进程执行结果和生命周期
- B进程不关心A进程的生命周期
遇到这类问题,首先肯定先想到,使用管道(Pipe)或者Socket这类进程间通信手段。这个方法可以解决上述特点中的1、2两个问题。但是管道和Socket给人最直观的映像就是:双方交互式通信。即A要关心B的存在与否,B也要关心A的存在与否。任何一方断了,都会影响另一方的流程。这个和我们上述特点中的3、4是相背的。那么怎么解决呢?我想到了另一个进程间通信的方法——内存映射文件。
内存映射文件分为两种,一种是“命名”文件,一种是“匿名”内存映射文件。“命名”文件一般用于安全性要求不高的进程间通信,而“匿名”内存映射文件一般是用于安全性较高的进程间通信。我们肯定优先考虑安全性更高的“匿名”内存映射文件。我举一个之前我写得工程的例子解释如何使用“匿名”内存映射文件进行进程间通信的:
- A和B进程建立管道连接
- A创建一个“匿名”内存映射文件
- A打开B进程句柄
- A将“匿名”内存映射文件Handle Duplicate给B进程,生成B进程可以使用的HandleB
- A将HandleB通过管道传递给进程B
- 进程B使用HandleB访问数据
这个流程给出了一个使用匿名管道进行进程间通信的一个必要的条件:B进程的已经存在,并且可以通知B进程去使用Duplicate后的HandleB。
在我们的场景中,就是不希望使用除了文件映射之外的通信方式。而且,我们要在B进程创建时,就将文件映射传给B进程,所以无法使用“匿名”内存映射文件。
目前只剩下“命名”内存映射文件一条路可以走了。虽然这种方式存在种种不安全性,但是它是目前场景下唯一可以选择的方向。
为了不存在“名称”的冲突问题。我选择了随机生成“名称”的方案
VOID CTransmitParam::GenerateFileMappingName() { time_t t; srand((unsigned)time(&t)); WCHAR wchName[MAX_PATH] = {0}; wsprintf( wchName, L"%d", rand()); m_wstrFileMappingName.clear(); m_wstrFileMappingName.append(wchName); }
虽然每次都是随机的,但是我还是不放心这个“随机”碰撞的概率。于是我在创建内存映射文件时判断了下当前创建的“名字”是否在系统中已经存在。如果存在,我会重新随机生成名字并创建该名字的内存映射文件。
BOOL CTransmitParam::CreateFileMappingEx(DWORD dwNewBufferSize) { BOOL bSuc = FALSE; DWORD dwMaxLoopCount = 32; do { m_hFileMapping = CreateFileMapping( INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, dwNewBufferSize, m_wstrFileMappingName.c_str()); if ( NULL == m_hFileMapping ) { std::cerr<<"CreateFileMapping Failed.The error code is"<<::GetLastError()<<std::endl; break; } if ( ERROR_ALREADY_EXISTS == ::GetLastError() ) { ::CloseHandle(m_hFileMapping); m_hFileMapping = NULL; dwMaxLoopCount--; GenerateFileMappingName(); continue; } else if ( 0 == dwMaxLoopCount ) { ::CloseHandle(m_hFileMapping); m_hFileMapping = NULL; break; } else { bSuc = TRUE; break; } }while (TRUE); return TRUE; }
待内存映射文件创建成功后,我们往该“文件”中写入数据,其数据格式是:前sizeof(DWORD)保存的是要传递给子进程的数据长度,其后跟着数据内容。
struct StData { DWORD dwBufferSize; // 从BufferFirst开始的数据长度 BYTE BufferFirst; };
具体的数据填充代码是
BOOL CTransmitParam::PackData( LPVOID lpMem, DWORD dwNewBufferSize, LPCBYTE lpBuffer, DWORD dwBufferSize ) { BOOL bSuc = FALSE; do { LPBYTE lpFilePointer = (LPBYTE)lpMem; OVERLAPPED op; memset(&op, 0, sizeof(op)); DWORD dwRead = 0; DWORD dwBufferSizeSize = sizeof(dwNewBufferSize); errno_t e = memcpy_s( lpFilePointer, dwNewBufferSize, &dwNewBufferSize, dwBufferSizeSize); if ( 0 != e ) { std::cerr<<"Memcpy_s Failed.The error code is"<<e<<std::endl; break; } lpFilePointer += sizeof(dwNewBufferSize); e = memcpy_s( lpFilePointer, dwNewBufferSize - dwBufferSizeSize, lpBuffer, dwBufferSize ); if ( 0 != e ) { std::cerr<<"Memcpy_s Failed.The error code is"<<e<<std::endl; break; } bSuc = TRUE; } while (0); return bSuc; }
下一步就是我们要使用挂起的方式创建子进程B。之所以要使用挂起方式创建,是因为我们要获取其进程的句柄,并且使用该进程句柄去Duplicate出内存映射文件句柄HandleB。之所以要这么做,所因为,我们要在此时让该内存映射文件和子进程B的生命周期相关联。因为从父进程角度来说,我们CreateFileMapping后,要进行对应的CloseHandle,从而不会造成资源泄露。如果我们不让父进程创建的内存映射文件和子进程B相关联,在父进程CloseHandle后,内存映射文件的引用计数将降为0,从而被释放掉。此时,子进程可能还没有时机去读取到内存映射文件。
BOOL CTransmitParam::CreateProcess_TransmitParam( LPCWSTR lpChildProcssPath ) { BOOL bSuc = FALSE; do { STARTUPINFO st; memset(&st, 0, sizeof(st)); st.cb = sizeof(st); PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi)); std::wstring wstrCmd = GenerateCommandLine(); BOOL bCreateSuc = CreateProcess( lpChildProcssPath, (LPWSTR) wstrCmd.c_str(), NULL, NULL, FALSE, /*CREATE_NO_WINDOW |*/ CREATE_SUSPENDED, NULL, NULL, &st, &pi ); if ( FALSE == bCreateSuc ) { std::cerr<<"CreateProcess Error.The error code is"<<::GetLastError()<<std::endl; break; } HANDLE hTargetHandle = NULL; if ( FALSE == DuplicateHandle( GetCurrentProcess(), m_hFileMapping, pi.hProcess, &hTargetHandle, DUPLICATE_SAME_ACCESS, FALSE, DUPLICATE_SAME_ACCESS ) ) { std::cerr<<"DuplicateHandle Failed.The error code is"<<::GetLastError()<<std::endl; break; } if ( NULL != pi.hThread ) { ::ResumeThread( pi.hThread ); } CloseHandle(pi.hThread); CloseHandle(pi.hProcess); bSuc = TRUE; } while (0); return bSuc; }
在父进程CloseHandle后,父进程的逻辑就此走完。我们再看下子进程的数据接收过程。
子进程接收一个以“FM”为Key的参数,该参数中保存了“命名”内存映射文件的名字,通过该名字,我们可以获取父进程传送过来的数据内容。
BOOL CTransmitParam::UnPackData(LPVOID lpMem) { BOOL bSuc = FALSE; do { m_dwRecvBufferLength = 0; errno_t e = memcpy_s( &m_dwRecvBufferLength, sizeof(m_dwRecvBufferLength), lpMem, sizeof(DWORD)); if ( 0 != e ) { std::cerr<<"Memcpy_s Failed.The error code is"<<e<<std::endl; break; } if ( 0 == m_dwRecvBufferLength ) { std::cerr<<"FileMapping's size is 0.\n"<<::GetLastError()<<std::endl; break; } m_lpRecvBuffer = new BYTE[m_dwRecvBufferLength]; memset( m_lpRecvBuffer, 0, sizeof(m_lpRecvBuffer)); e = memcpy_s( m_lpRecvBuffer, m_dwRecvBufferLength, (LPBYTE)lpMem + sizeof(DWORD), m_dwRecvBufferLength ); if ( 0 != e ) { std::cerr<<"Memcpy_s Failed.The error code is"<<e<<std::endl; break; } bSuc = TRUE; } while (0); return bSuc; } BOOL CTransmitParam::GetRecvBuffer(const std::wstring& wstrFileMappingName) { if ( NULL != m_lpRecvBuffer ) { return TRUE; } BOOL bSuc = FALSE; do { HANDLE hFileMapping = OpenFileMapping( FILE_MAP_READ, FALSE, wstrFileMappingName.c_str()); if ( NULL == hFileMapping ) { std::cerr<<"OpenFileMapping Failed.The error code is"<<::GetLastError()<<std::endl; break; } LPVOID lpMem = MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0); if ( NULL == lpMem ) { std::cerr<<"MapViewOfFile Failed.The error code is"<<::GetLastError()<<std::endl; break; } if ( FALSE == UnPackData(lpMem) ) { break; } UnmapViewOfFile(lpMem); CloseHandle(hFileMapping); bSuc = TRUE; } while (0); return bSuc; }