探讨多进程参数传递技术
所谓多进程参数传递过程,实际上是在程序的多次重复运行时,为保证内存中的进程唯一性又不丢失后来启动时传递的命令行参数,并把此命令行参数传递给已经运行进程的过程。而这次可能会激活已运行进程中某个处理事件或线程。
这主要涉及到程序的重复运行检测、进程间通信、远程进程激活以及可能的多线程技术。
最初需要这种技术的场合就是像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消息即可。这种方法简洁方便,缺点除了系统建立映射文件以及拷贝数据时需要一点代价外似乎没有什么可以挑剔的。但是,仔细想想就会发现会有如下的问题:
1因为是发送消息,要求接受进程的接受线程必须有消息队列,即必须有消息循环和窗口过程。
2必须在使用前得到接受进程的窗口句柄,以便发送消息。
这里的应用场合仅仅是传递命令行参数给目标进程,不要求目标进程必须有窗口。这便是问题的所在。
下面介绍采用的非常规方法,很好实现了仅是传递命令行参数给目标进程而不要求目标进程必须有窗口的任务。
由于两个通信的进程实际是同一个程序的两个运行进程,所以它们的地址空间分布是完全一样的。从某种角度上说,传递给另一个进程的某个数据指针就是另一个进程的相应数据的指针。我们所需要做的只是把数据复制到另一个进程的同一地址。注意这里的数据应该是全部的,以便保证地址的恒定性和存在性。这可以通过API函数WriteProcessMemory来实现。代码大概如下:
//GlobalBuffer