摘自:http://dev.csdn.net/article/63/63391.shtm
Machine Producer Operating system C-Compiler recognized through |
- 作者: jhfgrh 2005年03月9日, 星期三 18:05 回复(0) | 引用(0) 加入博采
Windows多线程间同步事件的控制方法
关键词:Windows95 线程 同步事件 event 对象 Win32
一. 引言
Windows 95是一个多任务、多线程的操作系统,其中的每一个应用程序都是一个进程(process)。进程可以创建多个并发的线程(thread),同时进程也以主线程(primarythread)的形式被系统调度。所谓的线程是系统调度的一个基本单位,在程序中线程是以函数的形式出现的,它的代码是进程代码的一部分,并与进程及其派生的其它线程共享进程的全局变量和文件打开表等公用信息。主线程类似于UNIX系统中的父进程,线程则类似于子进程。主线程也是一个线程,称作主线程仅仅是为了和它创建的线程区别开来。每个线程都相对于主线程而独立运行,为了使得线程能对用户的控制作出响应,必须控制线程的运行,比如用户可暂停、终止一个线程的运行或改变线程运行的条件等。而且在用户控制与线程运行之间有时应该有一定的同步控制关系,以保证用户对线程的有效控制。线程可以根据不同的条件对用户的控制作出不同的响应。为了实现上述目的必须使用系统提供的同步对象(Synchronization Object),如event对象。编写多线程应用程序必须使用Win32 API。
二. 线程的创建方法
调用Win32 API中的CreateThread函数创建线程。hThread=CreateThread(NULL,0,&TEventWindow::ThreadFunc,this,0,&hThreadId);第一个参数设定线程的安全属性,因其仅用于Windows NT,故不设定。第二个参数为0指定线程使用缺省的堆栈大小。第三个参数指定线程函数,线程即从该函数的入口处开始运行,函数返回时就意味着线程终止运行。第四个参数为线程函数的参数,可以是指向任意数据类型的指针。第五个参数设定线程的生成标志。hThreadId存放线程的标识号。线程函数如下定义,上述的 this参数是指向线程所属窗口的句柄指针,通过thrdWin参数传送过来,利用这个指针再调用相应的LoopFunc函数,线程的具体事务都在这个函数中执行。
DWORD _stdcall TEventWindow::ThreadFunc(void *thrdWin){
return STATIC_CAST(TEventWindow*,thrdWin)->LoopFunc( );
}
三. 线程的同步事件控制方法
Windows 95提供两种基本类型的系统对象,一种是彼此互斥的对象,用来协调访问数据,如 mutex对象;一种是事件同步对象,用来发送命令或触发事件,安排事件执行的先后次序,如 event对象。系统对象在系统范围内有效,它们都具有自己的安全属性、访问权限和以下两种状态中的一种:Signaled和nonSignaled。对于event对象调用SetEvent函数可将其状态设为Signaled,调用ResetEvent函数则可将其状态设为nonSignaled。演示程序中的线程在一个大循环中不断地将运行结果显示出来,当用户要关闭窗口时线程才终止运行。不过必须在窗口关闭之前先终止线程的运行,否则线程运行的结果将会显示在屏幕的其他地方,所以有必要在线程结束与关闭窗口这两个事件之间建立起同步关系。为此在TEventWindow类的构造函数中创建两个event对象,用来实现事件同步。
hCloseEvent=CreateEvent(0,FALSE,FALSE,0); hNoCloseEvent=CreateEvent(0,FALSE,FALSE,0);
第二个参数为FALSE 表示创建的是一个自动event对象,第三个参数为FALSE表示对象的初始状态为nonSignaled,第四个参数为0表示该对象没有名字。
在TEventWindow类的构造函数中还同样创建hWatchEvent和hNtyEvent对象,初始状态都为nonSignaled。用户要关闭窗口时,程序首先调用CanClose 函数,在该函数中设置hCloseEvent对象的状态为Signaled,利用这个方法来通知线程,要求线程终止运行。然后主线程调用函数WaitForMultipleObjects(该函数以下简称wait函数 ),wait函数先判断对象hThread和hNoCloseEvent中任意一个的状态是否为Signaled, 如果都不是就堵塞主线程的运行,直到上述条件满足;如果有一个对象的状态为Signaled,wait函数就返回,不再堵塞主线程。如果对象是自动event对象,wait函数在返回之前还会将对象的状态设为nonSignaled。wait函数中的参数FALSE表示不要求两个对象的状态同时为Signaled,参数-1表示要无限期地等待下去直到条件满足,参数2表示SignalsC数组中有两个对象。
在Windows 95中线程也被看作是一种系统对象,同样具有两种状态。线程运行时其状态为nonSignaled,如果线程终止运行,则其状态被系统自动设为Signaled( 可以通过线程的句柄hThread得到线程状态),此时wait函数返回0,表示第一个对象满足条件,于是CanClose返回TRUE表示窗口可以关闭;如果线程不能满足终止运行的条件,就设置hNoCloseEvent 对象的状态为Signaled,此时wait函数返回1,表示第二个对象满足条件,于是CanClose返回FALSE表示窗口暂时还不能关闭。 BOOL TEventWindow::CanClose(){
HANDLE SignalsC[2]={hThread,hNoCloseEvent};
SetEvent(hCloseEvent);
if(WaitForMultipleObjects(2,SignalsC,FALSE,-1)==0) return TRUE;
else return FALSE;
}
另一个用户控制的例子是,用户使主线程暂停运行直到线程满足某种条件为止。比如用户选择"Watch"菜单后,主线程调用如下函数开始对线程的运算数据进行监测。首先设置hWatchEvent对象的状态为Signaled,以此来通知线程,主线程此时已进入等待状态并开始对数据进行监测,然后主线程调用wait函数等待线程的回应。线程在满足某个条件后就设置hNtyEvent对象的状态为Signaled,使主线程结束等待状态,继续运行。
void TEventWindow::CmWatch(){
SetEvent(hWatchEvent);
WaitForSingleObject(hNtyEvent,-1);
::MessageBox(GetFocus(),"线程已符合条件,主线程继续运行!","",MB_OK);
}
线程函数所调用的LoopFunc是一个大循环,它不断地判断同步对象的状态,并根据这些对象的状态执行相应的操作,这些对象在数组SignalsL中列出。在这个数组中各元素的排列顺序是很重要的,前两个对象分别对应两种不同的用户控制事件,通过判断对象的状态可以知道发生的是哪一种用户控制。只有当前面两个对象的状态都不是Signaled时才会判断第三个对象的状态,这样一方面保证线程能检测到所有的用户控制事件,另一方面又保证了在不发生用户控制事件时线程也能继续运行。为此特地在TEventWindow类的构造函数中创建的对象hNoBlockEvent的状态始终为Signaled。
hNoBlockEvent=CreateEvent(0,TRUE,TRUE,"MyEvent");
第二个参数为TRUE表示创建的是一个手工event对象, 其状态是不会被wait函数所改变的,除非显式地调用ResetEvent函数。第三个参数为TRUE表示对象初始状态为Signaled,第四个参数定义了该对象的名字为"MyEvent"。LoopFunc函数调用wait函数,如果检测到hCloseEvent的状态为Signaled, 此时wait函数返回0,线程知道用户要关闭窗口了,就判断线程是否可以终止,条件是iCount>100,如果满足终止条件LoopFunc函数就返回,实际上就终止了线程的运行;如果不满足条件线程就设置 hNoCloseEvent对象的状态为Signaled,让主线程知道线程暂时还不能终止。由于hCloseEvent是自动event对象,所以wait函数返回0时还会将对象hCloseEvent的状态设置为nonSignaled,这样在第二次循环时,wait函数就不会判断出hCloseEvent对象的状态为Signaled,避免了线程错误地再次去判断是否会满足终止条件。如果wait函数检测到对象hWatchEvent的状态为Signaled,此时wait函数返回1,线程知道主线程已进入等待状态并在对数据进行监测,就设置变量bWatch的值为TRUE。如果前面的两个事件都未发生,则前面两个对象的状态都为nonSignaled,于是wait函数就检测第三个对象的状态, 由于第三个对象hNoBlockEvent 的状态始终为Signaled,所以线程就无阻碍地继续运行下去,将变量iCount不断加一,当变量大于200时,如果bWatch为TRUE,就设置hNtyEvent的状态为
Signaled,从而使主线程停止等待,继续运行。
DWORD TEventWindow::LoopFunc(){
HANDLE SignalsL[3]={hCloseEvent,hWatchEvent,hNoBlockEvent};
static BOOL bWatch=false;int dwEvent;
while(1){
dwEvent=WaitForMultipleObjects(3,SignalsL,FALSE,-1);
switch(dwEvent){
case 0: if(iCount>100) return 0;
else SetEvent(hNoCloseEvent);
break;
case 1: bWatch=TRUE;break;
case 2: ++iCount;
if(bWatch && iCount>200) SetEvent(hNtyEvent);
break;
}
}
}
四. 进程间的多线程同步事件控制方法
由于event对象是系统范围内有效的,所以另一个进程(即一个应用程序,本身也是一个线程)可调用OpenEvent函数,通过对象的名字获得对象的句柄, 但对象必须是已经创建的,然后可将这个句柄用于ResetEvent、SetEvent和WaitForMultipleObjects等函数中。这样可以实现一个进程的线程控制另一进程生成的线程的运行。如下面的语句就是通过对象名字"MyEvent"获得了上面进程生成的hNoBlockEvent对象的句柄,再使用这个句柄将对象状态设为nonSignaled。在上述的 LoopFunc函数中由于该对象的状态已经改变,使得上面的线程暂停运行。
HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent");
ResetEvent(hEvent);
OpenEvent函数的第一个参数表示函数的调用线程对event对象的访问权限,比如让线程拥有对象所有的访问权限,就选参数EVENT_ALL_ACCESS,这样线程就能用ResetEvent函数改变对象的状态;参数true表示由这个进程派生的子进程可以继承该句柄;最后一个参数指出了event对象的名字。用下面的语句设置对象hNoBlockEvent的状态为Signaled,就可以使线程继续运行,如SetEvent(hEvent)。
进程不再使用该句柄时尽可以用CloseHandle函数关闭对象句柄,但对于同一个event对象而言,因为它可能还在别的线程中被使用,所以只有在它的所有被引用的句柄都关闭后对象才会被系统释放,文中提到的所有 event对象在主线程和线程之间以及在不同的进程之间所起的控制作用如图1所示:
| ① ┌───────┐ ①:关闭窗口 ┌──→─┤ hCloseEvent ├───┐ ②:对上面事件的反应 │ └───────┘ │ | │ ┌───────┐ ↓ | 暂停/恢复线程的运行 │ │ hThread 或 │②┌─┴─┐ ┌───────┐ ┌───┐ ┌─┴─┐ ┌┤hNoCloseEvent ├←┤ 线程 ├←┤hNoBlockEvent ├←┤进程 2│ │主线程├←┘└───────┘ └┬─┬┘ └───────┘ └───┘ │/进程1├→┐┌───────┐ ↑ │ |不同进程之间 └─┬─┘⑴└┤ hWatchEvent ├──┘ │ |的地址界限 ↑ └───────┘ │ │ ┌───────┐ │ ⑴:监测数据 └────┤ hNtyEvent ├←───┘ ⑵:线程满足监测条件 └───────┘⑵ |
图1 event对象在多线程间同步事件控制中的作用
五. 结束语
多线程编程技术在多媒体、网络通讯、数学计算和实时控制方面有着很广阔的应用前景。当然在实际编程中情况往往是很复杂的,这时应注意的是如何将任务准确地划分成可并发的线程以及象文中提到的SignalsL数组中元素的排列顺序等问题。本文所讲内容对于在Windows NT或在某些支持多线程的UNIX系统中设计多线程应用程序也是有所帮助的。
VC常用数据类型使用转换详解
刚接触VC编程的朋友往往对许多数据类型的转换感到迷惑不解,本文将介绍一些常用数据类型的使用.
参考资料:
http://www.yesky.com/20021205/1642979_1.shtml
我们先定义一些常见类型变量借以说明int i = 100;
long l = 2001;
float f=300.2;
double d=12345.119;
char username[]="程佩君";
char temp[200];
char *buf;
CString str;
_variant_t v1;
_bstr_t v2;
一、其它数据类型转换为字符串
短整型(int)
itoa(i,temp,10);///将i转换为字符串放入temp中,最后一个数字表示十进制
itoa(i,temp,2); ///按二进制方式转换
长整型(long)
ltoa(l,temp,10);
浮点数(float,double)
用fcvt可以完成转换,这是MSDN中的例子:
int decimal, sign;
char *buffer;
double source = 3.1415926535;
buffer = _fcvt( source, 7, &decimal, &sign );
运行结果:source: 3.1415926535 buffer: '31415927' decimal: 1 sign: 0
decimal表示小数点的位置,sign表示符号:0为正数,1为负数
CString变量
str = "2008北京奥运";
buf = (LPSTR)(LPCTSTR)str;
BSTR变量
BSTR bstrValue = ::SysAllocString(L"程序员");
char * buf = _com_util::ConvertBSTRToString(bstrValue);
SysFreeString(bstrValue);
AfxMessageBox(buf);
delete(buf);
CComBSTR变量
CComBSTR bstrVar("test");
char *buf = _com_util::ConvertBSTRToString(bstrVar.m_str);
AfxMessageBox(buf);
delete(buf); _bstr_t变量
_bstr_t类型是对BSTR的封装,因为已经重载了=操作符,所以很容易使用
_bstr_t bstrVar("test");
const char *buf = bstrVar;///不要修改buf中的内容
AfxMessageBox(buf);
通用方法(针对非COM数据类型)
用sprintf完成转换
char buffer[200];
char c = '1';
int i = 35;
long j = 1000;
float f = 1.7320534f;
sprintf( buffer, "%c",c);
sprintf( buffer, "%d",i);
sprintf( buffer, "%d",j);
sprintf( buffer, "%f",f);
二、字符串转换为其它数据类型
strcpy(temp,"123");
短整型(int)
i = atoi(temp);
长整型(long)
l = atol(temp);
浮点(double)
d = atof(temp);
CString变量
CString name = temp;
BSTR变量
方法一,使用SysAllocString等API函数。例如:
| BSTR bstrText = ::SysAllocString(L"Test"); BSTR bstrText = ::SysAllocStringLen(L"Test",4); BSTR bstrText = ::SysAllocStringByteLen("Test",4); |
方法二,使用COleVariant或_variant_t。例如:
| //COleVariant strVar("This is a test"); _variant_t strVar("This is a test"); BSTR bstrText = strVar.bstrVal; |
方法三,使用_bstr_t,这是一种最简单的方法。例如:
| BSTR bstrText = _bstr_t("This is a test"); |
方法四,使用CComBSTR。例如:
| BSTR bstrText = CComBSTR("This is a test"); |
| CComBSTR bstr("This is a test"); BSTR bstrText = bstr.m_str; |
方法五,使用ConvertStringToBSTR。例如:
| char* lpszText = "Test"; BSTR bstrText = _com_util::ConvertStringToBSTR(lpszText); |
CComBSTR变量
CComBSTR类型变量可以直接赋值
CComBSTR bstrVar1("test");
CComBSTR bstrVar2(temp);
_bstr_t变量
_bstr_t类型的变量可以直接赋值
_bstr_t bstrVar1("test");
_bstr_t bstrVar2(temp);
三、其它数据类型转换到CString
使用CString的成员函数Format来转换,例如:
整数(int)
str.Format("%d",i);
浮点数(float)
str.Format("%f",i);
字符串指针(char *)等已经被CString构造函数支持的数据类型可以直接赋值
str = username;
对于Format所不支持的数据类型,可以通过上面所说的关于其它数据类型转化到char *的方法先转到char *,然后赋值给CString变量。
BSTR转换成CString
BSTR bstrText = ::SysAllocString(L"Test");
CStringA str;
str.Empty();
str = bstrText; 或 CStringA str(bstrText);
四、BSTR、_bstr_t与CComBSTR
CComBSTR 是ATL对BSTR的封装,_bstr_t是C++对BSTR的封装,BSTR是32位指针,但并不直接指向字串的缓冲区。
char *转换到BSTR可以这样:
BSTR b=_com_util::ConvertStringToBSTR("数据");///使用前需要加上comutil.h和comsupp.lib
SysFreeString(bstrValue);
反之可以使用
char *p=_com_util::ConvertBSTRToString(b);
delete p;
具体可以参考一,二段落里的具体说明。
CComBSTR与_bstr_t对大量的操作符进行了重载,可以直接进行=,!=,==等操作,所以使用非常方便。
特别是_bstr_t,建议大家使用它。
五、VARIANT 、_variant_t 与 COleVariant
VARIANT的结构可以参考头文件VC98\Include\OAIDL.H中关于结构体tagVARIANT的定义。
对于VARIANT变量的赋值:首先给vt成员赋值,指明数据类型,再对联合结构中相同数据类型的变量赋值,举个例子:
VARIANT va;
i nt a=2001;
va.vt=VT_I4;///指明整型数据
va.lVal=a; ///赋值
对于不马上赋值的VARIANT,最好先用Void VariantInit(VARIANTARG FAR* pvarg);进行初始化,其本质是将vt设置为VT_EMPTY,下表我们列举vt与常用数据的对应关系:
Byte bVal; // VT_UI1.
Short iVal; // VT_I2.
long lVal; // VT_I4.
float fltVal; // VT_R4.
double dblVal; // VT_R8.
VARIANT_BOOL boolVal; // VT_BOOL.
SCODE scode; // VT_ERROR.
CY cyVal; // VT_CY.
DATE date; // VT_DATE.
BSTR bstrVal; // VT_BSTR.
DECIMAL FAR* pdecVal // VT_BYREF|VT_DECIMAL.
IUnknown FAR* punkVal; // VT_UNKNOWN.
IDispatch FAR* pdispVal; // VT_DISPATCH.
SAFEARRAY FAR* parray; // VT_ARRAY|*.
Byte FAR* pbVal; // VT_BYREF|VT_UI1.
short FAR* piVal; // VT_BYREF|VT_I2.
long FAR* plVal; // VT_BYREF|VT_I4.
float FAR* pfltVal; // VT_BYREF|VT_R4.
double FAR* pdblVal; // VT_BYREF|VT_R8.
VARIANT_BOOL FAR* pboolVal; // VT_BYREF|VT_BOOL.
SCODE FAR* pscode; // VT_BYREF|VT_ERROR.
CY FAR* pcyVal; // VT_BYREF|VT_CY.
DATE FAR* pdate; // VT_BYREF|VT_DATE.
BSTR FAR* pbstrVal; // VT_BYREF|VT_BSTR.
IUnknown FAR* FAR* ppunkVal; // VT_BYREF|VT_UNKNOWN.
IDispatch FAR* FAR* ppdispVal; // VT_BYREF|VT_DISPATCH.
SAFEARRAY FAR* FAR* pparray; // VT_ARRAY|*.
VARIANT FAR* pvarVal; // VT_BYREF|VT_VARIANT.
void FAR* byref; // Generic ByRef.
char cVal; // VT_I1.
unsigned short uiVal; // VT_UI2.
unsigned long ulVal; // VT_UI4.
int intVal; // VT_INT.
unsigned int uintVal; // VT_UINT.
char FAR * pcVal; // VT_BYREF|VT_I1.
unsigned short FAR * puiVal; // VT_BYREF|VT_UI2.
unsigned long FAR * pulVal; // VT_BYREF|VT_UI4.
int FAR * pintVal; // VT_BYREF|VT_INT.
unsigned int FAR * puintVal; //VT_BYREF|VT_UINT.
_variant_t是VARIANT的封装类,其赋值可以使用强制类型转换,其构造函数会自动处理这些数据类型。
使用时需加上#include <comdef.h>
例如:
long l=222;
ing i=100;
_variant_t lVal(l);
lVal = (long)i;
COleVariant的使用与_variant_t的方法基本一样,请参考如下例子:
COleVariant v3 = "字符串", v4 = (long)1999;
CString str =(BSTR)v3.pbstrVal;
long i = v4.lVal;
六、其它一些COM数据类型
根据ProgID得到CLSID
HRESULT CLSIDFromProgID( LPCOLESTR lpszProgID,LPCLSID pclsid);
CLSID clsid;
CLSIDFromProgID( L"MAPI.Folder",&clsid);
根据CLSID得到ProgID
WINOLEAPI ProgIDFromCLSID( REFCLSID clsid,LPOLESTR * lplpszProgID);
例如我们已经定义了 CLSID_IApplication,下面的代码得到ProgID
LPOLESTR pProgID = 0;
ProgIDFromCLSID( CLSID_IApplication,&pProgID);
...///可以使用pProgID
CoTaskMemFree(pProgID);//不要忘记释放
七、ANSI与Unicode
Unicode称为宽字符型字串,COM里使用的都是Unicode字符串。
将ANSI转换到Unicode
(1)通过L这个宏来实现,例如: CLSIDFromProgID( L"MAPI.Folder",&clsid);
(2)通过MultiByteToWideChar函数实现转换,例如:
char *szProgID = "MAPI.Folder";
WCHAR szWideProgID[128];
CLSID clsid;
long lLen = MultiByteToWideChar(CP_ACP,0,szProgID,strlen(szProgID),szWideProgID,sizeof(szWideProgID));
szWideProgID[lLen] = '\0';
(3)通过A2W宏来实现,例如:
USES_CONVERSION;
CLSIDFromProgID( A2W(szProgID),&clsid);
将Unicode转换到ANSI
(1)使用WideCharToMultiByte,例如:
// 假设已经有了一个Unicode 串 wszSomeString...
char szANSIString [MAX_PATH];
WideCharToMultiByte ( CP_ACP, WC_COMPOSITECHECK, wszSomeString, -1, szANSIString, sizeof(szANSIString), NULL, NULL );
(2)使用W2A宏来实现,例如:
USES_CONVERSION;
pTemp=W2A(wszSomeString);
八、其它
对消息的处理中我们经常需要将WPARAM或LPARAM等32位数据(DWORD)分解成两个16位数据(WORD),例如:
LPARAM lParam;
WORD loValue = LOWORD(lParam);///取低16位
WORD hiValue = HIWORD(lParam);///取高16位
对于16位的数据(WORD)我们可以用同样的方法分解成高低两个8位数据(BYTE),例如:
WORD wValue;
BYTE loValue = LOBYTE(wValue);///取低8位
BYTE hiValue = HIBYTE(wValue);///取高8位
两个16位数据(WORD)合成32位数据(DWORD,LRESULT,LPARAM,或WPARAM)
LONG MAKELONG( WORD wLow, WORD wHigh );
WPARAM MAKEWPARAM( WORD wLow, WORD wHigh );
LPARAM MAKELPARAM( WORD wLow, WORD wHigh );
LRESULT MAKELRESULT( WORD wLow, WORD wHigh );
两个8位的数据(BYTE)合成16位的数据(WORD)
WORD MAKEWORD( BYTE bLow, BYTE bHigh );
从R(red),G(green),B(blue)三色得到COLORREF类型的颜色值
COLORREF RGB( BYTE byRed,BYTE byGreen,BYTE byBlue );
例如COLORREF bkcolor = RGB(0x22,0x98,0x34);
从COLORREF类型的颜色值得到RGB三个颜色值
BYTE Red = GetRValue(bkcolor); ///得到红颜色
BYTE Green = GetGValue(bkcolor); ///得到绿颜色
BYTE Blue = GetBValue(bkcolor); ///得到兰颜色
九、注意事项
假如需要使用到ConvertBSTRToString此类函数,需要加上头文件comutil.h,并在setting中加入comsupp.lib或者直接加上#pragma comment( lib, "comsupp.lib" )
用VC进行COM编程,必须要掌握哪些COM理论知识
(1) COM组件实际上是一个C++类,而接口都是纯虚类。组件从接口派生而来。我们可以简单的用纯粹的C++的语法形式来描述COM是个什么东西:
| class IObject { public: virtual Function1(...) = 0; virtual Function2(...) = 0; .... }; class MyObject : public IObject { public: virtual Function1(...){...} virtual Function2(...){...} .... }; |
看清楚了吗?IObject就是我们常说的接口,MyObject就是所谓的COM组件。切记切记接口都是纯虚类,它所包含的函数都是纯虚函数,而且它没有成员变量。而COM组件就是从这些纯虚类继承下来的派生类,它实现了这些虚函数,仅此而已。从上面也可以看出,COM组件是以 C++为基础的,特别重要的是虚函数和多态性的概念,COM中所有函数都是虚函数,都必须通过虚函数表VTable来调用,这一点是无比重要的,必需时刻牢记在心。为了让大家确切了解一下虚函数表是什么样子,从《COM+技术内幕》中COPY了下面这个示例图:
(2) COM组件有三个最基本的接口类,分别是IUnknown、IClassFactory、IDispatch。
COM规范规定任何组件、任何接口都必须从IUnknown继承,IUnknown包含三个函数,分别是 QueryInterface、AddRef、Release。这三个函数是无比重要的,而且它们的排列顺序也是不可改变的。QueryInterface用于查询组件实现的其它接口,说白了也就是看看这个组件的父类中还有哪些接口类,AddRef用于增加引用计数,Release用于减少引用计数。引用计数也是COM中的一个非常重要的概念。大体上简单的说来可以这么理解,COM组件是个DLL,当客户程序要用它时就要把它装到内存里。另一方面,一个组件也不是只给你一个人用的,可能会有很多个程序同时都要用到它。但实际上DLL只装载了一次,即内存中只有一个COM组件,那COM组件由谁来释放?由客户程序吗?不可能,因为如果你释放了组件,那别人怎么用,所以只能由COM组件自己来负责。所以出现了引用计数的概念,COM维持一个计数,记录当前有多少人在用它,每多一次调用计数就加一,少一个客户用它就减一,当最后一个客户释放它的时侯,COM知道已经没有人用它了,它的使用已经结束了,那它就把它自己给释放了。引用计数是COM编程里非常容易出错的一个地方,但所幸VC的各种各样的类库里已经基本上把AddRef的调用给隐含了,在我的印象里,我编程的时侯还从来没有调用过AddRef,我们只需在适当的时侯调用Release。至少有两个时侯要记住调用Release,第一个是调用了 QueryInterface以后,第二个是调用了任何得到一个接口的指针的函数以后,记住多查MSDN 以确定某个函数内部是否调用了AddRef,如果是的话那调用Release的责任就要归你了。 IUnknown的这三个函数的实现非常规范但也非常烦琐,容易出错,所幸的事我们可能永远也不需要自己来实现它们。
IClassFactory的作用是创建COM组件。我们已经知道COM组件实际上就是一个类,那我们平常是怎么实例化一个类对象的?是用‘new'命令!很简单吧,COM组件也一样如此。但是谁来new它呢?不可能是客户程序,因为客户程序不可能知道组件的类名字,如果客户知道组件的类名字那组件的可重用性就要打个大大的折扣了,事实上客户程序只不过知道一个代表着组件的128位的数字串而已,这个等会再介绍。所以客户无法自己创建组件,而且考虑一下,如果组件是在远程的机器上,你还能new出一个对象吗?所以创建组件的责任交给了一个单独的对象,这个对象就是类厂。每个组件都必须有一个与之相关的类厂,这个类厂知道怎么样创建组件,当客户请求一个组件对象的实例时,实际上这个请求交给了类厂,由类厂创建组件实例,然后把实例指针交给客户程序。这个过程在跨进程及远程创建组件时特别有用,因为这时就不是一个简单的new操作就可以的了,它必须要经过调度,而这些复杂的操作都交给类厂对象去做了。IClassFactory最重要的一个函数就是CreateInstance,顾名思议就是创建组件实例,一般情况下我们不会直接调用它,API函数都为我们封装好它了,只有某些特殊情况下才会由我们自己来调用它,这也是VC编写COM组件的好处,使我们有了更多的控制机会,而VB给我们这样的机会则是太少太少了。
IDispatch叫做调度接口。它的作用何在呢?这个世上除了C++还有很多别的语言,比如VB、 VJ、VBScript、JavaScript等等。可以这么说,如果这世上没有这么多乱七八糟的语言,那就不会有IDispatch。:-) 我们知道COM组件是C++类,是靠虚函数表来调用函数的,对于VC来说毫无问题,这本来就是针对C++而设计的,以前VB不行,现在VB也可以用指针了,也可以通过VTable来调用函数了,VJ也可以,但还是有些语言不行,那就是脚本语言,典型的如 VBScript、JavaScript。不行的原因在于它们并不支持指针,连指针都不能用还怎么用多态性啊,还怎么调这些虚函数啊。唉,没办法,也不能置这些脚本语言于不顾吧,现在网页上用的都是这些脚本语言,而分布式应用也是COM组件的一个主要市场,它不得不被这些脚本语言所调用,既然虚函数表的方式行不通,我们只能另寻他法了。时势造英雄,IDispatch应运而生。:-) 调度接口把每一个函数每一个属性都编上号,客户程序要调用这些函数属性的时侯就把这些编号传给IDispatch接口就行了,IDispatch再根据这些编号调用相应的函数,仅此而已。当然实际的过程远比这复杂,仅给一个编号就能让别人知道怎么调用一个函数那不是天方夜潭吗,你总得让别人知道你要调用的函数要带什么参数,参数类型什么以及返回什么东西吧,而要以一种统一的方式来处理这些问题是件很头疼的事。IDispatch接口的主要函数是Invoke,客户程序都调用它,然后Invoke再调用相应的函数,如果看一看MS的类库里实现 Invoke的代码就会惊叹它实现的复杂了,因为你必须考虑各种参数类型的情况,所幸我们不需要自己来做这件事,而且可能永远也没这样的机会。:-)
(3) dispinterface接口、Dual接口以及Custom接口
这一小节放在这里似乎不太合适,因为这是在ATL编程时用到的术语。我在这里主要是想谈一下自动化接口的好处及缺点,用这三个术语来解释可能会更好一些,而且以后迟早会遇上它们,我将以一种通俗的方式来解释它们,可能并非那么精确,就好象用伪代码来描述算法一样。-:)
所谓的自动化接口就是用IDispatch实现的接口。我们已经讲解过IDispatch的作用了,它的好处就是脚本语言象VBScript、 JavaScript也能用COM组件了,从而基本上做到了与语言无关它的缺点主要有两个,第一个就是速度慢效率低。这是显而易见的,通过虚函数表一下子就可以调用函数了,而通过Invoke则等于中间转了道手续,尤其是需要把函数参数转换成一种规范的格式才去调用函数,耽误了很多时间。所以一般若非是迫不得已我们都想用VTable的方式调用函数以获得高效率。第二个缺点就是只能使用规定好的所谓的自动化数据类型。如果不用IDispatch我们可以想用什么数据类型就用什么类型,VC会自动给我们生成相应的调度代码。而用自动化接口就不行了,因为Invoke的实现代码是VC事先写好的,而它不能事先预料到我们要用到的所有类型,它只能根据一些常用的数据类型来写它的处理代码,而且它也要考虑不同语言之间的数据类型转换问题。所以VC自动化接口生成的调度代码只适用于它所规定好的那些数据类型,当然这些数据类型已经足够丰富了,但不能满足自定义数据结构的要求。你也可以自己写调度代码来处理你的自定义数据结构,但这并不是一件容易的事。考虑到IDispatch的种种缺点(它还有一个缺点,就是使用麻烦,:-) )现在一般都推荐写双接口组件,称为dual接口,实际上就是从IDispatch继承的接口。我们知道任何接口都必须从 IUnknown继承,IDispatch接口也不例外。那从IDispatch继承的接口实际上就等于有两个基类,一个是IUnknown,一个是IDispatch,所以它可以以两种方式来调用组件,可以通过 IUnknown用虚函数表的方式调用接口方法,也可以通过IDispatch::Invoke自动化调度来调用。这就有了很大的灵活性,这个组件既可以用于C++的环境也可以用于脚本语言中,同时满足了各方面的需要。
相对比的,dispinterface是一种纯粹的自动化接口,可以简单的就把它看作是IDispatch接口 (虽然它实际上不是的),这种接口就只能通过自动化的方式来调用,COM组件的事件一般都用的是这种形式的接口。
Custom接口就是从IUnknown接口派生的类,显然它就只能用虚函数表的方式来调用接口了
(4) COM组件有三种,进程内、本地、远程。对于后两者情况必须调度接口指针及函数参数。
COM是一个DLL,它有三种运行模式。它可以是进程内的,即和调用者在同一个进程内,也可以和调用者在同一个机器上但在不同的进程内,还可以根本就和调用者在两台机器上。这里有一个根本点需要牢记,就是COM组件它只是一个DLL,它自己是运行不起来的,必须有一个进程象父亲般照顾它才行,即COM组件必须在一个进程内.那谁充当看护人的责任呢?先说说调度的问题。调度是个复杂的问题,以我的知识还讲不清楚这个问题,我只是一般性的谈谈几个最基本的概念。我们知道对于WIN32程序,每个进程都拥有4GB的虚拟地址空间,每个进程都有其各自的编址,同一个数据块在不同的进程里的编址很可能就是不一样的,所以存在着进程间的地址转换问题。这就是调度问题。对于本地和远程进程来说,DLL 和客户程序在不同的编址空间,所以要传递接口指针到客户程序必须要经过调度。Windows 已经提供了现成的调度函数,就不需要我们自己来做这个复杂的事情了。对远程组件来说函数的参数传递是另外一种调度。DCOM是以RPC为基础的,要在网络间传递数据必须遵守标准的网上数据传输协议,数据传递前要先打包,传递到目的地后要解包,这个过程就是调度,这个过程很复杂,不过Windows已经把一切都给我们做好了,一般情况下我们不需要自己来编写调度DLL。
我们刚说过一个COM组件必须在一个进程内。对于本地模式的组件一般是以EXE的形式出现,所以它本身就已经是一个进程。对于远程DLL,我们必须找一个进程,这个进程必须包含了调度代码以实现基本的调度。这个进程就是dllhost.exe。这是COM默认的DLL代理。实际上在分布式应用中,我们应该用MTS来作为DLL代理,因为MTS有着很强大的功能,是专门的用于管理分布式DLL组件的工具。
调度离我们很近又似乎很远,我们编程时很少关注到它,这也是COM的一个优点之一,既平台无关性,无论你是远程的、本地的还是进程内的,编程是一样的,一切细节都由COM自己处理好了,所以我们也不用深究这个问题,只要有个概念就可以了,当然如果你对调度有自己特殊的要求就需要深入了解调度的整个过程了,这里推荐一本《COM+技术内幕》,这绝对是一本讲调度的好书。
(5) COM组件的核心是IDL。
我们希望软件是一块块拼装出来的,但不可能是没有规定的胡乱拼接,总是要遵守一定的标准,各个模块之间如何才能亲密无间的合作,必须要事先共同制订好它们之间交互的规范,这个规范就是接口。我们知道接口实际上都是纯虚类,它里面定义好了很多的纯虚函数,等着某个组件去实现它,这个接口就是两个完全不相关的模块能够组合在一起的关键试想一下如果我们是一个应用软件厂商,我们的软件中需要用到某个模块,我们没有时间自己开发,所以我们想到市场上找一找看有没有这样的模块,我们怎么去找呢?也许我们需要的这个模块在业界已经有了标准,已经有人制订好了标准的接口,有很多组件工具厂商已经在自己的组件中实现了这个接口,那我们寻找的目标就是这些已经实现了接口的组件,我们不关心组件从哪来,它有什么其它的功能,我们只关心它是否很好的实现了我们制订好的接口。这种接口可能是业界的标准,也可能只是你和几个厂商之间内部制订的协议,但总之它是一个标准,是你的软件和别人的模块能够组合在一起的基础,是COM组件通信的标准。
COM具有语言无关性,它可以用任何语言编写,也可以在任何语言平台上被调用。但至今为止我们一直是以C++的环境中谈COM,那它的语言无关性是怎么体现出来的呢?或者换句话说,我们怎样才能以语言无关的方式来定义接口呢?前面我们是直接用纯虚类的方式定义的,但显然是不行的,除了C++谁还认它呢?正是出于这种考虑,微软决定采用IDL来定义接口。说白了,IDL实际上就是一种大家都认识的语言,用它来定义接口,不论放到哪个语言平台上都认识它。我们可以想象一下理想的标准的组件模式,我们总是从IDL开始,先用IDL制订好各个接口,然后把实现接口的任务分配不同的人,有的人可能善长用VC,有的人可能善长用VB,这没关系,作为项目负责人我不关心这些,我只关心你把最终的DLL 拿给我。这是一种多么好的开发模式,可以用任何语言来开发,也可以用任何语言来欣赏你的开发成果。
(6) COM组件的运行机制,即COM是怎么跑起来的。
这部分我们将构造一个创建COM组件的最小框架结构,然后看一看其内部处理流程是怎样的
| IUnknown *pUnk=NULL; IObject *pObject=NULL; CoInitialize(NULL); CoCreateInstance(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IUnknown, (void**)&pUnk); pUnk->QueryInterface(IID_IOjbect, (void**)&pObject); pUnk->Release(); pObject->Func(); pObject->Release(); CoUninitialize(); |
这就是一个典型的创建COM组件的框架,不过我的兴趣在CoCreateInstance身上,让我们来看看它内部做了一些什么事情。以下是它内部实现的一个伪代码:
| CoCreateInstance(....) { ....... IClassFactory *pClassFactory=NULL; CoGetClassObject(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void **)&pClassFactory); pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnk); pClassFactory->Release(); ........ } |
这段话的意思就是先得到类厂对象,再通过类厂创建组件从而得到IUnknown指针。继续深入一步,看看CoGetClassObject的内部伪码:
| CoGetClassObject(.....) { //通过查注册表CLSID_Object,得知组件DLL的位置、文件名 //装入DLL库 //使用函数GetProcAddress(...)得到DLL库中函数DllGetClassObject的函数指针。 //调用DllGetClassObject } DllGetClassObject是干什么的,它是用来获得类厂对象的。只有先得到类厂才能去创建组件. 下面是DllGetClassObject的伪码: DllGetClassObject(...) { ...... CFactory* pFactory= new CFactory; //类厂对象 pFactory->QueryInterface(IID_IClassFactory, (void**)&pClassFactory); //查询IClassFactory指针 pFactory->Release(); ...... } CoGetClassObject的流程已经到此为止,现在返回CoCreateInstance,看看CreateInstance的伪码: CFactory::CreateInstance(.....) { ........... CObject *pObject = new CObject; //组件对象 pObject->QueryInterface(IID_IUnknown, (void**)&pUnk); pObject->Release(); ........... } |
下图是从COM+技术内幕中COPY来的一个例图,从图中可以清楚的看到CoCreateInstance的整个流程。
(7) 一个典型的自注册的COM DLL所必有的四个函数
DllGetClassObject:用于获得类厂指针
DllRegisterServer:注册一些必要的信息到注册表中
DllUnregisterServer:卸载注册信息
DllCanUnloadNow:系统空闲时会调用这个函数,以确定是否可以卸载DLL
DLL还有一个函数是DllMain,这个函数在COM中并不要求一定要实现它,但是在VC生成的组件中自动都包含了它,它的作用主要是得到一个全局的实例对象。
(8) 注册表在COM中的重要作用
首先要知道GUID的概念,COM中所有的类、接口、类型库都用GUID来唯一标识,GUID是一个128位的字串,根据特制算法生成的GUID可以保证是全世界唯一的。 COM组件的创建,查询接口都是通过注册表进行的。有了注册表,应用程序就不需要知道组件的DLL文件名、位置,只需要根据CLSID查就可以了。当版本升级的时侯,只要改一下注册表信息就可以神不知鬼不觉的转到新版本的DLL。
本文是本人一时兴起的涂鸭之作,讲得并不是很全面,还有很多有用的体会没写出来,以后如果有时间有兴趣再写出来。希望这篇文章能给大家带来一点用处,那我一晚上的辛苦就没有白费了。-:)
配合代理脚本 实现访问内外网站的自动转换
本人所在单位启用了内部OA(办公自动化)系统,由于有许多部门并不在公司总部,不处于同一个局域网内,外围单位无法直接访问内网的OA服务器,影响了OA系统在总公司的推行。为了解决这一问题曾试图通过VPN来从互联网接入,但由于外围单位分别通过不同的ISP(Internet Service Provider)接入互联网的,导致VPN服务器配置的不一致,不能全面畅通接入,最终选择了使用代理服务器的接入方案。外围单位可以从互联网通过代理服务器访问内网的OA服务器。
外网用户在访问公司OA服务器时,就需要在IE浏览器中配置代理服务器,配置方法见图1。
图1 设置代理服务器
配置好IE后就可以访问内网OA服务器了。但这样一来,就存在一个问题,就是外网用户在访问OA时需要在IE中设置代理服务器,而在访问互联网时又需要取消这一配置,不使用代理服务器而直接访问。每次都需要手动配置,十分不便,对于"菜鸟"而言就更显得繁琐了。
如何让IE浏览器自动实现配置的转换呢?这就需要用到代理自动配置脚本(PAC-file:Proxy Auto-Config file) 了。自动配置脚本也就是PAC脚本,这是一种以.PAC为扩展名的JavaScript脚本,PAC脚本其实就是定义一个名为"FindProxyForURL"的Java Script函数,该函数会被浏览器自动调用,从而实现代理服务器的自动转换。
由于我们的代理是反向(由外向内)的,脚本的具体内容如下:
function findproxyforurl(host,url)
{
if (shexpmatch(host,"*.jigang.com.cn"))
return "proxy 218.XXX.XXX.30:8080"
else if (shexpmatch(host,"172.16.*.*"))
return "proxy 218.XXX.XXX.30:8080"
else
return "direct"
}
这段脚本的含义就是:如果是访问内网OA的请求(域名为jigang.com.cn),则使用代理,如果访问内网的某些网址(IP地址为:172.16.*.*),则使用代理,除此之外的所有请求则不使用代理而直接连接。
将上述脚本内容保存为一个扩展名为PAC的脚本文件,如C:\proxy.pac。
然后我们设置IE浏览器把"自动配置脚本选项"指向它,完成集中设置代理配置的工作。我们只需一次性配置完毕,让IE自动识别是否使用代理服务器,而无需手动转换,从而实现访问内、外网站的自动转换。
IE的代理设置里面有一个"使用自动配置脚本"的选项,这里的具体设置如下:
自动配置脚本设置:打开浏览器,选择"工具/Internet选项/连接/局域网配置",随后勾选"使用自动脚本配置"项,最后输入自动配置脚本所存在地址即可(比如file://C:\proxy.pac,如图2)。
图2 设置本机自动配置脚本
当然我们也可以将这个脚本文件放在Wrb服务器上,这样不用为每台客户机都写一个PAC文件了,只需要在"使用自动配置脚本"的地址处填入相关的IP地址就行了。比如:http://218.XXX.XXX.30/proxy.pac。在IE中的设置见图3。
图3 设置网络自动配置脚本
这样做的好处是,我们可以随意修改代理脚本从而改变代理服务器的地址或端口等而不用去修改每台客户机的PAC文件
这种情况够糟糕了,没有用户会去责怪SCP——虽然SCP将他们引导到了错误的状态,他们只会责怪服务的作者——就是我或你...因此,在服务中怎么做才能防止这种问题发生呢?很简单,使服务快速有效的运行,并且总保持一个活动线程等待去处理控制代码。
说起来好像很容易,但实际做起来就被那么简单了,这也不是我能够向各位解释的了,只有认真的调试自己的服务,才能找出最为适合处理方法。所以我的文章也真的到了该结束的时候了,感谢各位的浏览。如果我有什么地方说的不对,请不吝赐教,谢谢。
例子:
附件 example.txt:http://blog.blogchina.com/upload/2005-02-22/2005022215124613207.txt
| 有那么一类应用程序,是能够为各种用户(包括本地用户和远程用户)所用的,拥有用户授权级进行管理的能力,并且不论用户是否物理的与正在运行该应用程序的计算机相连都能正常执行,这就是所谓的服务了。 (一)服务的基础知识 Question 1. 什么是服务?它的特征是什么? 在NT/2000中,服务是一类受到操作系统优待的程序。一个服务首先是一个Win32可执行程序,如果要写一个功能完备且强大的服务,需要熟悉动态连接库(Dlls)、结构异常处理、内存映射文件、虚拟内存、设备I/O、线程及其同步、Unicode以及其他的由WinAPI函数提供的应用接口。当然本文讨论的只是建立一个可以安装、运行、启动、停止的没有任何其他功能的服务,所以无需上述知识仍可以继续看下去,我会在过程中将理解本文所需要的知识逐一讲解。 第二要知道的是一个服务决不需要用户界面。大多数的服务将运行在那些被锁在某些黑暗的,冬暖夏凉的小屋子里的强大的服务器上面,即使有用户界面一般也没有人可以看到。如果服务提供任何用户界面如消息框,那么用户错过这些消息的可能性就极高了,所以服务程序通常以控制台程序的形式被编写,进入点函数是main()而不是WinMain()。 也许有人有疑问:没有用户界面的话,要怎样设置、管理一个服务?怎样开始、停止它?服务如何发出警告或错误信息、如何报告关于它的执行情况的统计数据?这些问题的答案就是服务能够被远程管理,Windows NT/2000提供了大量的管理工具,这些工具允许通过网络上的其它计算机对某台机器上面的服务进行管理。比如Windows 2000里面的"控制台"程序(mmc.exe),用它添加"管理单元"就可以管理本机或其他机器上的服务。 Question 2. 服务的安全性... 想要写一个服务,就必须熟悉Win NT/2000的安全机制,在上述操作系统之中,所有安全都是基于用户的。换句话说——进程、线程、文件、注册表键、信号、事件等等等等都属于一个用户。当一个进程被产生的时候,它都是执行在一个用户的上下文(context),这个用户帐号可能在本机,也可能在网络中的其他机器上,或者是在一个特殊的账号:System Account——即系统帐号的上下文 如果一个进程正在一个用户帐号下执行,那么这个进程就同时拥有这个用户所能拥有的一切访问权限,不论是在本机还是网络。系统帐号则是一个特殊的账号,它用来标识系统本身,而且运行在这个帐号下的任何进程都拥有系统上的所有访问权限,但是系统帐号不能在域上使用,无法访问网络资源... 服务也是Win32可执行程序,它也需要执行在一个context,通常服务都是在系统账号下运行,但是也可以根据情况选择让它运行在一个用户账号下,也就会因此获得相应的访问资源的权限。 Question 3. 服务的三个组成部分 一个服务由三部分组成,第一部分是Service Control Manager(SCM)。每个Windows NT/2000系统都有一个SCM,SCM存在于Service.exe中,在Windows启动的时候会自动运行,伴随着操作系统的启动和关闭而产生和终止。这个进程以系统特权运行,并且提供一个统一的、安全的手段去控制服务。它其实是一个RPC Server,因此我们可以远程安装和管理服务,不过这不在本文讨论的范围之内。SCM包含一个储存着已安装的服务和驱动程序的信息的数据库,通过SCM可以统一的、安全的管理这些信息,因此一个服务程序的安装过程就是将自身的信息写入这个数据库。 第二部分就是服务本身。一个服务拥有能从SCM收到信号和命令所必需的的特殊代码,并且能够在处理后将它的状态回传给SCM。 第三部分也就是最后一部分,是一个Service Control Dispatcher(SCP)。它是一个拥有用户界面,允许用户开始、停止、暂停、继续,并且控制一个或多个安装在计算机上服务的Win32应用程序。SCP的作用是与SCM通讯,Windows 2000管理工具中的"服务"就是一个典型的SCP。 在这三个组成部分中,用户最可能去写服务本身,同时也可能不得不写一个与其伴随的客户端程序作为一个SCP去和SCM通讯,本文只讨论去设计和实现一个服务,关于如何去实现一个SCP则在以后的其它文章中介绍。 Question 4. 怎样开始设计服务 还记得前面我提到服务程序的入口点函数一般都是main()吗?一个服务拥有很重要的三个函数,第一个就是入口点函数,其实用WinMain()作为入口点函数也不是不可以,虽然说服务不应该有用户界面,但是其实存在很少的几个例外,这就是下面图中的选项存在的原因。 由于要和用户桌面进行信息交互,服务程序有时会以WinMain()作为入口点函数。 入口函数负责初始化整个进程,由这个进程中的主线程来执行。这意味着它应用于这个可执行文件中的所有服务。要知道,一个可执行文件中能够包含多个服务以使得执行更加有效。主进程通知SCM在可执行文件中含有几个服务,并且给出每一个服务的ServiceMain回调(Call Back)函数的地址。一旦在可执行文件内的所有服务都已经停止运行,主线程就在进程终止前对整个进程进行清除。 第二个很重要的函数就是ServiceMain,我看过一些例子程序里面对自己的服务的进入点函数都固定命名为ServiceMain,其实并没有规定过一定要那样命名,任何的函数只要符合下列的形式都可以作为服务的进入点函数。 VOID WINAPI ServiceMain( DWORD dwArgc, // 参数个数 LPTSTR *lpszArgv // 参数串 ); 这个函数由操作系统调用,并执行能完成服务的代码。一个专用的线程执行每一个服务的ServiceMain函数,注意是服务而不是服务程序,这是因为每个服务也都拥有与自己唯一对应的ServiceMain函数,关于这一点可以用"管理工具"里的"服务"去察看Win2000里面自带的服务,就会发现其实很多服务都是由service.exe单独提供的。当主线程调用Win32函数StartServiceCtrlDispatcher的时候,SCM为这个进程中的每一个服务产生一个线程。这些线程中的每一个都和它的相应的服务的ServiceMain函数一起执行,这就是服务总是多线程的原因——一个仅有一个服务的可执行文件将有一个主线程,其它的线程执行服务本身。 第三个也就是最后的一个重要函数是CtrlHandler,它必须拥有下面的原型: VOID WINAPI CtrlHandler( DWORD fdwControl //控制命令 ) 像ServiceMain一样,CtrlHandler也是一个回调函数,用户必须为它的服务程序中每一个服务写一个单独的CtrlHandler函数,因此如果有一个程序含有两个服务,那么它至少要拥有5个不同的函数:作为入口点的main()或WinMain(),用于第一个服务的ServiceMain函数和CtrlHandler函数,以及用于第二个服务的ServiceMain函数和CtrlHandler函数。 SCM调用一个服务的CtrlHandler函数去改变这个服务的状态。例如,当某个管理员用管理工具里的"服务"尝试停止你的服务的时候,你的服务的CtrlHandler函数将收到一个SERVICE_CONTROL_STOP通知。CtrlHandler函数负责执行停止服务所需的一切代码。由于是进程的主线程执行所有的CtrlHandler函数,因而必须尽量优化你的CtrlHandler函数的代码,使它运行起来足够快,以便相同进程中的其它服务的CtrlHandler函数能在适当的时间内收到属于它们的通知。而且基于上述原因,你的CtrlHandler函数必须要能够将想要传达的状态送到服务线程,这个传递过程没有固定的方法,完全取决于你的服务的用途。 |