所谓多进程参数传递过程,实际上是在程序的多次重复运行时,为保证内存中的进程唯一性又不丢失后来启动时传递的命令行参数,并把此命令行参数传递给已经运行进程的过程。而这次可能会激活已运行进程中某个处理事件或线程。
这主要涉及到程序的重复运行检测、进程间通信、远程进程激活以及可能的多线程技术。
最初需要这种技术的场合就是像NetAnts的IE右键菜单启动。使用这种技术可以在每一次的添加一个下载任务时,无需通过COM来和进程通信。因为这样显然会增加COM组件的复杂度以及和进程的耦合程度。只需要把下载的URL作为命令行参数再启动程序,然后由此次启动的进程负责传递URL给早先运行的进程,并激发目标进程中的添加下载事件。
下面分别论述涉及到的基本技术。
- 程序重复运行检测
能实现这种功能的技术不止一种,但最常用和健壮的方法还是用互斥内核对象mutex。Mutex能确保线程拥有对单个资源的互斥访问权,当这些线程分别位于不同的进程中时也就是确保进程能够拥有对某单个资源的互斥访问。这里可以把运行条件作为互斥资源,即只有单个进程可以获得运行条件而投入运行,后启动的进程因为运行条件的互斥而无法得到。从而也就确保了内存中关于某个进程运行的唯一性。
要使用互斥对象,必须有一个进程首先创建一个互斥对象,这自然是首次运行产生的进程需要作的工作。这可以通过调用API函数CreateMutex来创建一个互斥对象。
HANDLE CreateMutex (PSECURITY_ATTRIBUTES psa,BOOL fInitialOwner,PCTSTR pszName);
参数 psa :一个指向SECURITY_ATTRIBUTES结构的指针,表示安全描述符。安全描述符用于描述谁创建了该对象,谁能访问或使用该对象,谁无权访问它。安全描述符通常在编写服务器应用程序时使用,如果是写客户端的应用程序则可以忽略这个特性。Windows98系统,没有配备这个安全特性,但从Windows2000起都具备了这个特性。但大多数应用程序是要为该参数传递一个NULL,这样可以创建带有默认安全性的内核对象。
参数 fInitialOwner:用于控制互斥对象的初始状态。如果传递FALSE,那么意味着互斥对象没有被任何线程拥有,因此要发出它的同志信号。如果传递TRUE,那么该对象的被创建线程拥有,开始时不发出通知信号。
参数 pszName:一个以0结尾最大长度为MAX_PATH的字符串指针,表示此互斥对象的名称。由于系统中所有的内核对象都共享单个名字空间,所以不能重复命名为已经存在的名称。
释放一个互斥对象。注意只有成功等待到此对象的线程能够释放它。
BOOL ReleaseMutex(HANDLE hMutex);
参数hMutex:欲释放的对象句柄。
通过这种方法实现的判断函数看上去应该是这个样子:
BOOL IsOtherStarted() //判断是否有此程序的另一个进程备份已经运行
{
HANDLE hMutex=CreateMutex(NULL,TRUE,”MY_MUTEX_NAME”);
DWORD lastErr=GetLastError();
return (lastErr==ERROR_ALREADY_EXISTS)?TRUE:FALSE;
}
其中的MY_MUTEX_NAME是互斥对象的名称,尽量取的唯一一点。一般用程序名加版本号或日期是最好了。当然,如果你愿意,使用GUID似乎就更好了。
- 进程间通信
关于进程间通信的专题可以写一本书,所涉及的方面实在太广泛。这里仅仅提出几种可行的方案,并解释采用其中一种而不是另一种的理由。
众所周知,Windows是个多任务的操作系统。系统内运行的各个进程间不能相互影响,一个进程的崩溃不能影响其他进程。因而,为了实现这种安全机制,每个进程只可以访问它自己的私有地址空间,无法访问其他进程的地址空间。这无疑给进程间通信带来了困难。
Microsoft推荐的进程间通信是采用文件映射技术。但建立映射文件并非轻松,首先要两个进程指定同样一个磁盘文件打开它,然后是创建文件映射对象,接着是地址映射,使用完后在一个个释放、撤销和关闭资源。如果再加上这其间要应付的容错问题,足足可以让你的代码一下子大个百行。其实有一个变相使用文件映射技术的方法——WM_COPYDATA消息。其原理,WM_COPYDATA消息亦是通过文件映射技术,只不过建立和撤销文件映射的过程由系统帮我们代劳罢了。因为这里需要通信的仅仅是URL之类的命令行参数,短小而没有复杂的结构,所以WM_COPYDATA消息就完全可以胜任。
WM_COPYDATA消息的使用。
COPYDATASTRUCT cds;
SendMessage (hwndReceiver,WM_COPYDATA,(WPARAM)hwndSender,(LPARAM)&cds);
COPYDATASTRUCT 是一个结构,形式如下:
typedef struct tagCOPYDATASTRUCT {
ULONG_PTR dwData;
DWORD cbData;
PVOID lpData;
} COPYDATASTRUCT;
发送数据时,必须首先初始化此结构。dwData 是一个备用数据项,可以存放任意值。一般用来说明此次发送数据的相关信息,由你自己定义。CbData数据成员规定了向另外进程发送的字节数,lpData数据成员指向要发送的第一个字节。lpData所指的地址当然在发送消息的进程地址空间中。
这样,在接受进程中只要处理此WM_COPYDATA消息即可。这种方法简洁方便,缺点除了系统建立映射文件以及拷贝数据时需要一点代价外似乎没有什么可以挑剔的。但是,仔细想想就会发现会有如下的问题:
- 因为是发送消息,要求接受进程的接受线程必须有消息队列,即必须有消息循环和窗口过程。
- 必须在使用前得到接受进程的窗口句柄,以便发送消息。
这里的应用场合仅仅是传递命令行参数给目标进程,不要求目标进程必须有窗口。这便是问题的所在。
下面介绍采用的非常规方法,很好实现了仅是传递命令行参数给目标进程而不要求目标进程必须有窗口的任务。
由于两个通信的进程实际是同一个程序的两个运行进程,所以它们的地址空间分布是完全一样的。从某种角度上说,传递给另一个进程的某个数据指针就是另一个进程的相应数据的指针。我们所需要做的只是把数据复制到另一个进程的同一地址。注意这里的数据应该是全部的,以便保证地址的恒定性和存在性。这可以通过API函数WriteProcessMemory来实现。代码大概如下:
//GlobalBuffer 为全局变量充当数据缓冲区,大小为MAX_PATH。
lstrcpy(GlobalBuffer,lpCmdLine);
HANDLE ph=OpenProcess(PROCESS_ALL_ACCESS,FALSE,OtherID);
if (WriteProcessMemory(ph,(LPVOID)GlobalBuffer,(LPVOID)GlobalBuffer,MAX_PATH,NULL)==0)
CloseHandle(ph);
//以上代码写入参数lpCmdLine到另一个备份进程的全局缓冲区
- 远程进程激活
数据复制完毕就可以通知接受进程,以便接受进程开始进行处理。那么如何通知接受进程呢?消息自然不能使用,可以解决的办法是使用事件内核对象(event)来通知接受接受进程。也就是远程进程的激活。
事件内核对象能够通知一个操作已经完成。有两种不同类型的事件对象。一种是手动重置的事件,另一种是自动重置的事件。当自动重置的事件得到通知时,等待该事件的线程中只有一个变成可调度线程从而被激活。
下面是CreateEvent函数用来创建事件内核对象:
HANDLE CreateEvent(PSECUITY_ATTRIBUTES psa,BOOL fManualReset,BOOL fInitialState,PCTSTR pszName);
第一个和最后一个参数前面已经介绍过,这里说明中间两个参数的含义。
参数fManualReset:指出是创建一个手动重置(TRUE)事件还是创建一个自动重置事件(FALSE)。
参数fInitialState:用于指明该事件是要初始化为已通知状态(TRUR)还是未通知状态(FALSE)。
当然这也需要接受进程等待这个事件内核对象,方法是使用同样在pszName中传递相同的名字,用OpenEvent得到此事件内核对象的句柄。然后用API函数WaitForSingleObject来等待。
下面是OpenEvent和WaitForSingleObject函数的说明:
HANDLE OpenEvent(DWORD fdwAccess,BOOL fInherit,PCTSTR pszName);
参数fdwAccess:指定事件对象所需要的访问权限。对支持对象安全的系统而言,如果指定对象的安全描述符不允许以该指定的权限调用,那么该函数将失败。
可以下列值的任意组合:
EVENT_ALL_ACCESS:指定所有可能的访问标志。
EVENT_MODITY_STATE:允许修改事件状态(通过API函数SetEvent或ResetEvent)。
参数fInherit:指定返回的句柄是否可以被继承。这里不需要,传递NULL即可。
DWORD WaitForSingleObject(HANDLE hObject,DWORD dwMilliseconds);
参数hObject:等待对象的句柄。
参数dwMilliseconds:以毫秒为单位的超时值,超过这个事件间隔,无论等待成功否都将返回。可以指定为INFINITE表示从不超时,即永久等待。
到这里为止,等待进程可以写出像下面的代码了。
HANDLE hEvent=CreateEvent(NULL,FALSE,FALSE,EVENT_NAME);
WaitForSingleObject(hEvent, INFINITE);
发送进程如何通知事件呢?使用API函数SetEvent。
BOOL SetEvent(HANDLE hEvent);
参数hEvent:欲通知的事件句柄。
发送进程只要在数据复制完成后,调用SetEvent。代码如下:
HANDLE hEvent= OpenEvent (EVENT_ALL_ACCESS|EVENT_MODIFY_STATE, TRUE, EVENT_NAME );
SetEvent(hEvent);
CloseHandle(hEvent);
一旦接受进程成功等待说明数据已经有效,遂可以读出数据并进行操作。但值得注意的是因为接受进程在等待内核对象时是挂起状态,但显然接受进程还有别的事情做,不能仅仅为等待这个事件而挂起。怎么解决这个矛盾呢?办法之一就是专门用一个线程来等待事件。一旦事件等待成功,此等待线程将被激活,然后再通知主线程或干脆自己处理。
上面的技术很好的解决了问题,不过因为引入了多线程同时也引入了一些复杂性。由于等待线程十分简单,复杂性其实也并非很大。
多线程
多线程并非是什么新鲜的技术了,Windows就是一个支持多线程的操作系统。关于多线程的技术细节很多,不是这篇文章能够讲明的。可以参考很多专门讲解多线程编程的书,这里不再累述。实例代码
下面的实例代码采用C++ Builder6编写,并有详细的注释。此处不再另作说明。CSysProTab.h文件
#ifndef CSysProTabH
#define CSysProTabH
////////////////////////////////////////////////////////
//本类用来得到系统中所有的进程信息
//2003.4.10
//
////////////////////////////////////////////////////////
class CSysProcessTable
{
vector m_ProcessTable;
public:
CSysProcessTable();
int GetCount();//得到系统中的进程总数
//得到指定进程名的进程总数(即同一程序的多个运行备份)
int GetCount(const AnsiString& ProcName);
//从Offer开始查找指定进程名的进程ID
DWORD GetProcessID(const AnsiString& ProcName,int& iOffer);
int GetThreadCount(DWORD ProcID);//得到指定进程中的线程总数
void SaveToStrings(TStringList* sl)
{
for (vector ::iterator i=m_ProcessTable.begin(); i!=m_ProcessTable.end(); i++)
sl->Add(i->szExeFile);
}
};
#endif
- CSysProTab.cpp文件
//---------------------------------------------------------------------------
#pragma hdrstop
#include "CSysProTab.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
//---------------------------------------------------------------------------
CSysProcessTable::CSysProcessTable()
{
HANDLE hSnapShot=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
PROCESSENTRY32 t_ProcEnt;
if (hSnapShot!=(void*)-1)
{
if (Process32First(hSnapShot,&t_ProcEnt)==TRUE)
{do
{
m_ProcessTable.push_back(t_ProcEnt);
}
while (Process32Next(hSnapShot,&t_ProcEnt)==TRUE);
}
CloseHandle(hSnapShot);
}
else
{
ShowMessage("CreateToolhelp32Snapshot ERROR!");
}
}
//---------------------------------------------------------------------------
int CSysProcessTable::GetCount() //得到系统中的进程总数
{
return m_ProcessTable.size();
}
//---------------------------------------------------------------------------
int CSysProcessTable::GetCount(const AnsiString& ProcName)
//得到指定进程名的进程总数(即同一程序的多个运行备份)
{
int t_Ret=0;
for (vector ::iterator i=m_ProcessTable.begin();i!=m_ProcessTable.end();i++)
{
if (ProcName.LowerCase()==AnsiString(i->szExeFile).LowerCase()) t_Ret++;
}
return t_Ret;
}
//---------------------------------------------------------------------------
DWORD CSysProcessTable::GetProcessID(const AnsiString& ProcName,int& iOffer)
//从Offer开始查找指定进程名的进程ID
{
int j=iOffer;
for (vector ::iterator i=m_ProcessTable.begin()+iOffer;i!=m_ProcessTable.end();i++)
{
if (ProcName.LowerCase()==AnsiString(i->szExeFile).LowerCase())
{
iOffer=j;
return i->th32ProcessID;
}
j++;
}
return 0; //没有找到返回0
}
//---------------------------------------------------------------------------
int CSysProcessTable::GetThreadCount(DWORD ProcID) //得到指定进程中的线程总数
{
for (vector ::iterator i=m_ProcessTable.begin();i!=m_ProcessTable.end();i++)
{
if (i->th32ProcessID==ProcID) return i->cntThreads;
}
return 0;
}
//---------------------------------------------------------------------------
- project.cpp文件
//---------------------------------------------------------------------------
#include
#pragma hdrstop
//---------------------------------------------------------------------------
#include "CSysProTab.h"
#define EVENT_NAME "Goldroc_ NT_Event"
#define MUTEX_NAME "Goldroc_ NT_Mutex"
#define PROCESS_ALREADY_START 1
#define PROCESS_PARAM_START 2
#define PROCESS_NORMAL_START 0
#define ErrBackBox(errno) ShowMessage("应用程序出现错误!/n请把错误号、操作系统版本反馈给作者,以便改进!谢谢!/n/n错误号:"+AnsiString(errno))
//---------------------------------------------------------------------------
USEFORM("UFrmMain.cpp", FormMain);
//---------------------------------------------------------------------------
typedef int STARTTAG;
char GlobalBuffer[260]; //全局缓冲区,用来让其他进程备份写入URL和Title
HANDLE ThreadEvent;
bool IsOtherStarted() //判断是否有此程序的另一个进程备份已经运行
{
HANDLE hmutex=CreateMutex(NULL,TRUE,MUTEX_NAME);
DWORD lastErr=GetLastError();
if (lastErr==ERROR_ALREADY_EXISTS) return true;
if (hmutex==NULL)
{
ErrBackBox(lastErr);
Application->Terminate();
}
return false;
}
//---------------------------------------------------------------------------
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR lpCmdLine, int)
{
try
{
STARTTAG StartTag;
if (AnsiString(lpCmdLine).IsEmpty())
StartTag=PROCESS_NORMAL_START;
else
StartTag=PROCESS_PARAM_START;
if (IsOtherStarted()) StartTag=StartTag|PROCESS_ALREADY_START;
CSysProcessTable* t_PT=new CSysProcessTable;
int j=0;
DWORD OtherID;
AnsiString ExeFileName=ExtractFileName(Application->ExeName);
HANDLE ph;HANDLE hevent;
int OtherProcCount;
switch(StartTag)
{
case PROCESS_PARAM_START: //带参启动
lstrcpy(GlobalBuffer,lpCmdLine);
ThreadEvent=CreateEvent(NULL,FALSE,TRUE,EVENT_NAME);
case PROCESS_NORMAL_START: //正常启动
ThreadEvent=CreateEvent(NULL,FALSE,FALSE,EVENT_NAME);
break;
case PROCESS_PARAM_START|PROCESS_ALREADY_START://带参启动并已有备份运行
//任务:把带的参数送到另一个备份进程的全局缓冲区,并把一个事件对象置
//为信号,让另一个备份进程中等此事件的线程开始运行。
//另一个备份进程中等此事件的线程发送GM_ADDURL消息到主窗口.
//主窗口得到消息后从全局缓冲区中读出URL,Title ,作响应
//以此方法来实现进程间通信.Event 通知消息到达;GlobalBuffer 放消息内容
lstrcpy(GlobalBuffer,lpCmdLine);
OtherProcCount=t_PT->GetCount(ExeFileName) ;
for (int i=0;i
{
OtherID=t_PT->GetProcessID(ExeFileName,j);
if (OtherID!=GetCurrentProcessId())
break;
else
j++;
}
//以上代码得到另一个备份进程的ID:(OtherID)
ph=OpenProcess(PROCESS_ALL_ACCESS,FALSE,OtherID);
if (ph==NULL)
{
ErrBackBox("0X0001/t打开远程进程错误");
return 0;
}
if (WriteProcessMemory (ph,(LPVOID)GlobalBuffer, (LPVOID)GlobalBuffer, MAX_PATH,NULL) ==0)
{
ErrBackBox("0X1000/t写入远程进程错误");
return 0;
}
CloseHandle(ph);
//以上代码写入参数lpCmdLine到另一个备份进程的全局缓冲区
hevent=OpenEvent(EVENT_ALL_ACCESS|EVENT_MODIFY_STATE,TRUE,EVENT_NAME);
if (hevent==NULL)
{
ErrBackBox(GetLastError());
return 0;
}
SetEvent(hevent);
CloseHandle(hevent);
//以上代码把事件对象设置为通知状态,以便启动另一个备份进程中等此事件的线程
delete t_PT;
t_PT=NULL;
case PROCESS_NORMAL_START|PROCESS_ALREADY_START://无参启动并已有备份运行
return 0;
//因为已经有另一个进程备份,此进程退出
}
Application->Initialize();
Application->CreateForm(__classid(TFormMain), &FormMain);
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}
//---------------------------------------------------------------------------
- UThreadAdd.h文件
//---------------------------------------------------------------------------
#ifndef UThreadAddH
#define UThreadAddH
//---------------------------------------------------------------------------
#include
//---------------------------------------------------------------------------
class TThreadAddURL : public TThread
{
private:
protected:
void __fastcall Execute();
public:
__fastcall TThreadAddURL(bool CreateSuspended);
};
//---------------------------------------------------------------------------
#endif
//---------------------------------------------------------------------------
- UThreadAdd.cpp文件
//---------------------------------------------------------------------------
#include
#pragma hdrstop
#include "UThreadAdd.h"
#include "UFrmMain.h"
#pragma package(smart_init)
//---------------------------------------------------------------------------
extern HANDLE ThreadEvent;
__fastcall TThreadAddURL::TThreadAddURL(bool CreateSuspended)
: TThread(CreateSuspended)
{
}
//---------------------------------------------------------------------------
//等待从另一个进程备份中通知的事件对象,然后发送消息到主窗口
void __fastcall TThreadAddURL::Execute()
{
for (;;)
{
WaitForSingleObject(ThreadEvent,INFINITE);
if (Terminated) return;
//To do 成功等待,作一些处理。
}
}
//---------------------------------------------------------------------------