Windows Server 2008无法使用arp命令添加静态MAC绑定
这两天办公室不时有人机器中毒,结果整个内部局域网经常被ARP Poison充斥,导致网络瞬断,于是不得不想办法来实现静态IP-Mac地址绑定。然而在我用的Windows Server 2008 beta3的机器上,却碰到了一个很奇怪的问题。
这两天办公室不时有人机器中毒,结果整个内部局域网经常被ARP Poison充斥,导致网络瞬断,于是不得不想办法来实现静态IP-Mac地址绑定。然而在我用的Windows Server 2008 beta3的机器上,却碰到了一个很奇怪的问题。
环境光的使用比较简单,Direct3D把它作为一个渲染状态,通过调用IDirect3DDevice9::SetRenderState进行设置,对应的状态常数为D3DRS_AMBIENT。
按光源划分,直射光可分为三种:
1)点光源
点光源(Point Light)从一个点向周围均匀地发射光线。点光源有颜色、位置、作用范围,光强随距离而衰减,没有方向。
屏幕剪辑的捕获时间: 2007-6-27, 18:29
2)平行光
平行光(Directional Light)由相互平行的光线组成。平行光只有颜色和方向,没有位置,也没有作用范围和衰减,因此不论实体位于场景的何处,所受到的光照都相同。
屏幕剪辑的捕获时间: 2007-6-27, 18:31
3)聚光灯(Spotlight)是三种直射光中最复杂的一种。它的光束是一个圆锥,分内、外核两部分:内核最亮,且亮度保持不变;外核较暗,沿径向有一个衰减。
屏幕剪辑的捕获时间: 2007-6-27, 18:34
如下图,其中夹角Theta和Phi定义了内、外核的大小。
屏幕剪辑的捕获时间: 2007-6-27, 18:35
聚光灯有颜色、位置、方向(即光束中心所指方向)、作用范围、衰减(沿光线方向)。
在Direct3D中,用结构D3DLIGHT9来描述直射光,它的定义如下:
typedef struct _D3DLIGHT9{
D3DLIGHTTYPE Type;
//类型:只能是点光源、平行光或聚光灯
D3DCOLORVALUE Diffuse;
//
在Direct3D中,实体模型中的一个点可能被 多个三角形面所共用,如下图,虽然只有4个顶点,却由4个三角形面组成.
屏幕剪辑的捕获时间: 2007-6-27, 9:46
如果把顶点数据按对应图元的格式,直接放进顶点缓存区,该棱锥使用三角形列,4个锥面其需要4*3=12个顶点,也就是有8个顶点是重复的.如果实体比较复杂,重复的顶点会更多,造成资源浪费.
为些Direct3D引入了索引缓存的概念,把顶点的具体数据和代表图元格式的顶点顺序分开存储:顶点数据仍然放到顶点缓存区中,索引缓存区则按照图元格式,顺序存放顶点的索引.
以上图为例:头等在顶点缓存中保存A、B、C、D这4个顶点的FVF数据项,相应的索引为0、1、2、3;然后按照三角形列的组成顺序,把顶点索引值存入索引缓存区,4个三角形分别为△ACB、△ADC、△ADB、△BCD(注意顶点排列顺序和可视面的关系),则索引序列为0 2 1 0 3 2 0 1 3 1 2 3.这样原本要用12个顶点数据构建一个三棱锥,索引缓存后只需要4个。
CUSTOMVERTEX vertices[]={ // FVF顶点数据 // 四方体
{ 1.0f, 0.25f, 0.0f, D3DCOLOR_XRGB(0,255,255)}, // 蓝白
{ 1.0f, 0.75f, 0.0f, D3DCOLOR_XRGB(0,255,255)},
{0.25f, 0.75f, 0.0f, D3DCOLOR_XRGB(255,0,255)}, // 粉红
{0.25f, 0.25f, 0.0f, D3DCOLOR_XRGB(255,0,255)},
{ 1.0f, 0.25f, 1.0f, D3DCOLOR_XRGB(127,127,255)}, // 蓝
{ 1.0f, 0.75f, 1.0f, D3DCOLOR_XRGB(127,127,255)},
{0.25f, 0.75f, 1.0f, D3DCOLOR_XRGB(255,255,0)}, // 黄
{0.25f, 0.25f, 1.0f, D3DCOLOR_XRGB(255,255,0)}
};
//WORD indices[] = {0,1,2,2,1,3, 0,3,4,4,3,7, 1,2,5,5,2,6, 0,1,4,4,1,5, 3,2,7,7,2,6, 4,5,6,6,5,7};
WORD indices[] = {0,1,3,1,3,2, 0,3,4,3,4,7, 5,1,6,6,1,2, 1,0,5,5,0,4, 3,2,7,2,7,6, 4,5,7,5,7,6};
//创建顶点缓存区, 并获取接口IDirect3DVertexBuffer9的指针
m_pDevice->CreateVertexBuffer(
sizeof(vertices), // 缓存区尺寸
0,D3DFVF_CUSTOMVERTEX,
D3DPOOL_DEFAULT, &m_pVB,NULL);
//把顶点数据填入顶点缓存区
void* pVertices;
m_pVB->Lock(0, sizeof(vertices), (void**)&pVertices, 0);
memcpy(pVertices, vertices, sizeof(vertices));
m_pVB->Unlock();
// 创建索引缓存区, 并获取接口 LPDIRECT3DINDEXBUFFR9 的指针
m_pDevice->CreateIndexBuffer(sizeof(indices),
0,D3DFMT_INDEX16,
D3DPOOL_DEFAULT, &m_pIB,NULL);
// 把索引值填入索引缓存区
void* pIndices;
m_pIB->Lock(0,sizeof(indices),(void**)&pIndices,0);
memcpy(pIndices,indices,sizeof(indices));
m_pIB->Unlock();
渲染:
// 设置自定义的FVF
m_pDevice->SetFVF(D3DFVF_CUSTOMVERTEX);
// 绑定顶点缓冲区至设备数据源
m_pDevice->SetStreamSource(0, m_pVB, 0, sizeof(CUSTOMVERTEX));
// 绑定索引缓存区
m_pDevice->SetIndices(m_pIB);
// 从索引缓存区绘制图元,参数1为图元格式,参数4为顶点数,参数6为三角形数
//m_pDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 4, 0, 4); // 三角形
m_pDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 8, 0, 12);
// 绘制图元,其中参数1为图元格式,参数3为三角形数目
//m_pDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);
在Direct3D中,三角形是构成实体的基本单位,因为一个三角形正好是一个平面,以三角形面为单位进行渲染效率最高。
一个三角形由三个点构成,习惯上把这些点称为顶点(Vertex)。三角形平面有正反面之分,由顶点的排序决定:顶点按顺时针排列的表面是正面,如图。
屏幕剪辑的捕获时间: 2007/6/22, 14:59
其中与三角形平面垂直、且指向正面的矢量称为该平面的法线(Normal)。
在Direct3D中,为提高渲染效率,缺省条件下只有正面可见,不过可以通过IDirect3DDevice9::SetRenderState来改变设置,其对应的渲染状态常数为D3DRS_CULLMODE,具体用法请参阅SDK文档。
顶点法线(Vertex Normal)是过顶点的一个矢量,用于在高洛德着色(Gouraud Shading)中的计算光照和纹理效果。在生成曲面时,通常令顶点法线和相邻平面的法线保持等角,如图1,这样进行渲染时,会在平面接缝处产生一种平滑过渡的效果。如果是多边形,则令顶点法线等于该点所属平面(三角形)的法线,如图2,以便在接缝处产生突出的边缘。
屏幕剪辑的捕获时间: 2007/6/22, 15:17
屏幕剪辑的捕获时间: 2007/6/22, 15:16
按坐标轴之间的相互关系划分,三维坐标系可分为左手体系和右手体系,如下图所示。在左手体系中,坐标轴的定义符合法则:左手四个手指的旋转方向从X轴到Y轴,大拇指的指向就是Z轴。右手体系依次类推。Direct3D使用左手坐标系,其中X轴表示左右,Y轴表示上下,Z轴表示远近(深度)。
屏幕剪辑的捕获时间: 2007/6/22, 14:07
取定坐标系后,空间中的任意一点可以用一组坐标值(X,Y,Z)来表示。矢量是空间中的一条有向线段,Direct3D用它来标识空间方向。适量的表示方法与点坐标类似,也是用{X,Y,Z}不过它表示的是从原点指向点(X,Y,Z)的有向线段。适量与起点无关,只要两个矢量同向(平行)且等长,就认为它们相等。在Direct3D中,点和矢量通常使用同一个结构D3DXVECTOR3保存。
矢量的计算公式很简单:假设矢量的起点为M(X1,Y,Z1),终点为N(X2,Y2,Z2),则矢量→MN={X2-X1,Y2-Y1,Z2-Z1}。
使用D3DXVec3Normalize把它变换成单位矢量(长度为1)。
Lua的语法非常灵活, 使用他的metatable及metamethod可以模拟出很多语言的特性.
C#中我们这样使用事件:
xxx.Click += new System.EventHandler(xxx_Click);
private void xxx_Click(object sender, EventArgs e)
{
/**/
}
在Lua中要达到同样的效果, 并且支持事件多播机制, 其关键在于重写metamethod __call, 从而使得不光function才能被调用, table也能够被调用.
主要思想就是, 通过一个table来保存注册事件的若干响应函数, 然后拿table当function一样来调用, 重写__call后, 实现调用table时遍历执行table中的注册方法.
需要在lua5.0 或 lua.net上执行, lua 5.1略有改动.
1 --test.lua
2 do
写作目的:(此段可跳过)
同步Internet时间,即通过Internet的校时网站传来的数据校准本机时间。但是现在网络上查到的相关编程资料并不多,且其中多是VB和Delphi的代码,VC的代码我还没找到过。是这个东西太难了?应该不是;是太简单了?那也总该有人写吧。
我认为,自己懂和让别人懂压根不是一回事,我写这篇文章,目的当然是后者。当然,理工科出身的河蚌不大可能像文科出身的河蚌那样修出光彩夺目的珍珠来,所以,行文有不妥之处,欢迎指正。
校时原理:
互联网上有很多时间服务器能够提供准确的时间,我们通过连接到这样的服务器来获取时间值。这里向大家介绍一下服务器传来的数据格式先。数据一共四个字节(4 Byte),我们可以在接收数据后对它进行“重新组装”,把组装所得的值放在一个32位的整数里,这个值的意义是:自1900年1月1日0时0分0秒 至 服务器发送这个时间数据时 所经历的秒数。显然,任何一个时刻到1900年所经历的秒数是唯一的,因此,由服务器传来的时间数据即可推出现在的时间,然后用API函数调整系统的时间即可。
流程图如下:
设计目标:
好了,我们的目标是:(没有蛀牙~)
-_-!!
常言说一图千言,我们还是看图吧:
程序的实现:
从技术角度来看,解决三个问题即可:
1. 通过网络通信从服务器获取时间数据。
2. 处理基于1900年的时间数据,转化为我们常见的时间形式。
3. 解决网络造成的延时问题。
下面分条讲述:
1. 通过网络通信从服务器获取时间数据。
至于接收数据,没什么可说的,这里用CSocket就可以了。
代码片断:
CSocket sockClient;
sockClient.Create(); //创建socket
//for debug
m_info += "Connect server: " + strServer + "
";
UpdateData(FALSE);
//for debug
sockClient.Connect((LPCTSTR)strServer, 37); // strServer:时间服务器网址; 37:端口号
DWORD dwTime = 0; //用来存放服务器传来的标准时间数据
unsigned char nTime[8]; //临时接收数据
memset(nTime, 0, sizeof(nTime));
sockClient.Receive(nTime, sizeof(nTime)); //接收服务器发送来得4个字节的数据
sockClient.Close(); //关闭socket
//for debug
m_info += "Connect shut down. ";
UpdateData(FALSE);
//for debug
dwTime += nTime[0] << 24; //整合数据
dwTime += nTime[1] << 16;
dwTime += nTime[2] << 8;
dwTime += nTime[3]; 
if(0 == dwTime) return FALSE;
到此为止,服务器传来的时间数据经过“重新组装”已经正确放置到DWORD类型的变量 dwTime 里面了。下面我们接着对其进行必要的处理。
2. 处理基于1900年的时间数据,转化为我们常见的时间形式。
在前面我们提到,时间数据已经正确放置到变量 dwTime 里面了。那么,怎样由它得到现在的时间呢?
微软已经给我们提供了一个很好用的时间类:CTime。不过,MFC的CTime类的时间起点是基于1970年的,而dwTime 里面的秒数是从1900年计时的。
用CTime?无法由 dwTime 中的数据直接构造CTime类的对象。
用C的函数库?我尝试了多次,N次碰壁。
大家都知道,一个3D 场景中,我们见到的任何光辉灿烂的物体,

都是由一个一个面片组成的。而装载面片位置信息的就是其各个定点的三维坐标。这是用来在模型中存储的,而要把物体显示在屏幕上,还需要将它们转换成显示器上的二维坐标。这就需要对每个点实施一套 3 to 2 的转换公式,在Direct3D中叫做“几何流水线”(Geometry Pipeline)。
每渲染一桢,我们都要用到这条流水线把所有定点的坐标转化成当前要显示的位置。不过放心,D3D不会改变你原有的顶点坐标,变换出的顶点数据会存放在新的地方用来渲染。想一想物体,也就是面片,也就是顶点要显示在屏幕上,其位置取决于什么呢?首先它一定取决于该点在场景中的位置,然后还在于你从什么角度看,更详细一点就是我的眼睛在哪儿,我注视着哪儿,以及我的视野宽窄等等。
对于每个独立被引入程序的mesh物体,它们的坐标系、坐标原点理论上都应该是不同的,其顶点也都是用局部坐标表示的。那么要做统一的变换,首先应将它们引入到同一个坐标系下,也就是我们称之为“世界坐标系”的坐标。这个变换也因此得名世界变换(World Transform)。对物体所需要做的移动、旋转等工作也是要在此时完成的(这些本质上不就是坐标的更改么)。

经过了以上一些操作后,每个顶点(也就是每个物体)在整个场景中的位置就如你所愿确定下来了。要把它们映射到屏幕上,还要确定观察者(你可以叫他玩家、摄影机都无所谓)的位置和视角。我们是要把所有的点变换到新建立的以观察者为基准的坐标系下。这个步骤就是“视图变换”(View Transform)。实际上和后面要说的射影变换相比,这两种变换并没有什么本质区别。有时候为了效率,可以把世界变换与视图变换合并为一个世界——视图变换。这不就是说你一开始就选择观察者的位置为世界坐标系的原点,并按照视角来确定坐标轴么?
后面一步是“射影变换”(Projection Transform),有必要重点说一下。很多教材(包括MSDN)上都是假装读者已经知道为什么要有射影变换而给读者讲它的。实际上,我们要做的所有坐标转换归根结蒂是要把三维的点投影到二维的屏幕上,如图所示

经过上述两次坐标转换后,我们已经让屏幕平行于坐标轴平面了,也就是说,经过一些比例范围的调整,理论上我们能从点的三维坐标中的某两个直接得到期待已久的屏幕坐标。但是别急,此时得到的坐标绘出的图就像我们小时候画的那些画一样——没有立体感。比如上图那个矩形,因为近大远小,在我们的视野中应该看起来像个梯形。但是如果我们不做任何处理就直接把它的顶点(已经过前两重变换)投影到显示器上(假设平行于图中的XY平面)这样还是一个方方正正的矩形。
想象一下,投影实际上就是把空间中的所有点都压扁,扁到某一个平面上。这样出来的图形自然不会有透视效果。(之所以有近大远小是因为人眼的凸透镜成像,其像高是物距的减函数。这里不多说了)你可能想到让每个点像这样斜着投影,但是仔细想想,如何斜着投影呢?等你想明白了再回答这样做真的方便么?于是另一种办法就是把整个空间范围变成一个棱台(里面的点随之进行放缩)。

相对来说把较远端缩小会造成数据的不准确,因此采用放大较近端。对每个点,我们进行最后一步变换就是根据其远近程度进行一下放缩。
D3D把剪切也纳入此流水线中,尽管它没对顶点作任何变换,只是剔出那些不用的点。
以上就是D3D中的几何流水线。幸运的是,我们并不需要自己去写代码来完成这些转换。实际上我们只需要设计好参数,调用相应的D3D函数设置上面提到的各种决定因素,它会在渲染画面的时候把每个顶点自动转化成所需的屏幕坐标的。正因为这一套流水线操作的通用性和规范性,各种3D渲染引擎都将它封装了,而当代很多先进的显卡都将其固化到硬件线路上,这样大大提高了渲染速度。
下面我们来看看一些具体的实施。在计算机图形学中,坐标的变换通常是通过与一个矩阵(Matrix)相乘来实现的。基本变换包括平移、缩放、旋转都用此方法完成,其他任何的变换,包括不同坐标系之间的互化,也都是通过这三种基本转换完成的。因此说,Matrix无处不在 , 在我们的周围,就在这间屋子里。你能在窗户往外看到它,在电视里看到它。当你上班,去教堂或者缴税你可以感觉到它。你眼前的世界让你看不到真实……(和我们说的Matrix不大一样,不过多少有点这个意思吧)。具体到三维坐标系中,定义某点的坐标为(X,Y,Z)则用(X,Y,Z,W)乘以一个相应的4X4矩阵就可以得到新的坐标(X',Y',Z',W'),这里的W自有用处,一般是1。还有一点很重要,一个矩阵就代表着一重变换,而几个矩阵的乘积就代表着多重变换的合变换。这点用处很大,读者会慢慢体会到。
那么在这条流水线中,按规范我们至少需要三个矩阵来实现以上三步变换,也就是世界矩阵(World Matrix)、视矩阵(View Matrix)以及射影矩阵(Projection Matirx)。
世界矩阵有时候需要我们自己填写,根据我们的各种变换需要来填写一个D3DXMATRIX结构体(其成员就是各行各列的数值),具体方法MSDN上有详细讲解,这里不多做赘述了。之后通过调用IDirect3DDevice9::SetTransform( D3DTRANSFORMSTATETYPE State,CONST D3DMATRIX *pMatrix )设置世界矩阵为你填好的那个。参数意义如下:
D3DTRANSFORMSTATETYPE State
代表你要设置的变换类型。D3DTS_WORLD,D3DTS_VIEW,D3DTS_PROJECTION分别表示要射知识界、视图、射影三种变换
CONST D3DMATRIX *pMatrix
指向一个矩阵结构的指针,就是你所要用到的矩阵。
后面的两个矩阵也要通过此函数设置。D3D中,三个变换矩阵是要存放在固定位置的,每次执行流水线,D3D就依次从这三个位置读取矩阵信息,并乘以所有的点,得到新的点的坐标,这个过程是不用我们操心的。我们调用SetTransform()就是要把填充好的矩阵放进这三个位置中的某一个,第一个参数表示了哪一个。
在设置视矩阵时,我们先要很清楚地(在脑子里或纸上)建立好“视坐标系”。这个坐标系以观察着为原点,沿着视线方向(观察着——注视点方向)为纵深方向(也就是Z轴方向)。仅有两个点还不足以确定一个三维坐标系,我们还需要一个参考点,能与另两个点构成某一个坐标平面。这样的坐标系构件起来后,就可以根据两个坐标系的变换填充视矩阵了。D3D提供了函数
D3DXMATRIX *D3DXMatrixLookAtLH(
D3DXMATRIX *pOut,
CONST D3DXVECTOR3 *pEye,
CONST D3DXVECTOR3 *pAt,
CONST D3DXVECTOR3 *pUp
);
或 D3DXMATRIX *D3DXMatrixLookAtLH( 参数同 ),区别仅在于前者用于左手系而后者用于右手系。该函数自动填充一个矩阵,参数依次是将要填充的矩阵以及上面说到的三个点,这里三个点构成视坐标系的YoZ平面。别忘了调用SetTransform()把这个矩阵交给D3D。经过上一步被统一了坐标的各个顶点将被这个矩阵转到视坐标中。
第三步要将点乘上一个射影矩阵,这个矩阵将越近的点放得越大。填充这个矩阵我们用函数
D3DXMATRIX *D3DXMatrixPerspectiveFovLH(
D3DXMATRIX *pOut,
FLOAT fovY,
FLOAT Aspect,
FLOAT zn,
FLOAT zf
);
或 D3DXMATRIX *D3DXMatrixPerspectiveFovLH( 参数同 ),区别同上面一样。第一个参数仍然是输出矩阵。第二个描述了在Y轴上的视角,弧度制表示,可以想象,视角越大,近端被抻拉的比例就越大。下一个参数是视图区的长宽比。后面两个参数就是最近视平面和最远视平面的位置,用它们的Z坐标(Z坐标的值在射影变换前后是不变的)表示。这两个平面的意义将在下一步说到。
最后说一下这条流水线的倒数第一步——剪切。剪切就是把理论上根本不该看到的点从渲染元中剔除掉(这里不包括因遮挡关系产生的图形的剪切以及隐面消除),用过DirectDraw的朋友很容易想到屏幕范围以外的就是这样的点。在3D世界里,还存在一个最近视平面和一个最远视平面,它们共同组成了一个视图截锥(Viewing Frustum)。对于这个东西,微软有个很好的说法:就好像你在一间黑屋子里向外看,窗户的四个边圈定了视图范围,并且窗户所在平面之前的物体是看不见的(黑屋子里的东西是看不见的),窗户所在的平面就是最近视平面;而且我们并不能看到无限远,总要有个最远视平面。这六个平面视可以根据需要设定的,它们组成了视截锥——下图中的蓝色范围。

可以想象,刚才进行的射影变换也可以说是把视图截锥这个棱台挤压成长方体的过程。读者还能发现,上述D3DXMatrixPerspectiveFovLH( )的参数实际上是描述视截锥的。你会觉得这个蓝色的东西很有用,它与射影变换以及剪切都有着异常紧密的联系。

以上,如图所示,就是一个顶点要被真正用于渲染所经历的四重门。笔者没有介绍多少算法,以及如何推导这几个矩阵。关于这些,网上有大量的文章可供参考,MSDN讲得更加详细,那些才是深入了解的工具,不过笔者相信读者朋友都有这个能力自己推导。本篇旨在阐述一些笔者认为比较重要的概念性问题,希望能给读者一个清晰的思路。欢迎大家来信与我讨论。
(四)一些问题的讨论
前面几章的内容都是服务的一些通用的编写原理,但里面隐含着一些问题,编写简单的服务时看不出来,但遇到复杂的应用就会出现一些问题,所以本章就是用来分析、解决这些问题的,适用于高级应用的开发人员。我这一章的内容都是经过实验得到的,很有实际意义。
我在第一章里面就说过,是由一个服务的主线程执行CtrlHandler函数,它将收到各种控制命令,但是真正处理命令,执行操作的是ServiceMain的线程。现在,当一个SERVICE_CONTROL_STOP到达之后,你作为一个开发者,要怎样停止这个服务?在我看过的一些源代码里,大部分只是简单的调用TerminateThread函数去强行杀掉服务进程。但应该稍稍有点线程编程的常识就应该知道TerminateThread函数是可用的调用中最为糟糕的一个,服务线程将得不到任何机会去做应该的清理工作,诸如清除内存、释放核心对象,Dlls也得不到任何线程已经被毁的通知。
所以停止服务的适当方法是以某种方式激活服务线程,让它停止继续提供服务功能,然后执行完当前操作和清除工作后返回。这就表示你必须在CtrlHandler线程和ServiceMain线程之间执行适当的线程通信。现在已知的最好的内部线程通信机制是I/O Completion Port(I/O 完成端口),假如你编写的是一个大型的服务,需要同时处理为数众多的请求,并且运行在多处理器系统上面,这个模型就可以提供最佳的系统性能。但也正因为它的复杂性较高,在小规模的应用上面不值得花费很多的时间和精力,这时作为开发者可以适当的选取其它的通信方式,诸如异步过程调用队列、套接字和窗口消息,以适应实际情况。
开发服务时的另外一个重要问题就是调用SetServiceStatus函数时的所有状态报告问题。很多的服务开发者为了在什么时候调用SetServiceStatus的问题而常常产生争论,一般推荐的方法就是:先调用SetServiceStatus函数,报告SERVICE_STOP_PENDING状态,然后将控制代码传给服务线程或者再建立一个新的线程,让它去继续执行操作,当该线程即将执行完操作之前,再由它将服务的状态设置成SERVICE_STOPPED,然后服务正好停止。
上面的主意从两个方面来讲还是很不错的。首先服务可以立即确认收到了控制代码,并将在它认为适当的时候进行处理;然后就是因为前面说过的,执行CtrlHandler函数的是主线程,如果按照这种工作方法,CtrlHandler函数可以迅速的返回,不会影响到其它服务可能收到的控制请求,对含有多个服务的程序来说,响应各个服务的控制代码的速度会大大的提高。可是,随之而来的是问题—— race condition 即“竞争条件”的产生。
摆在下面的就是一个竞争条件的例子,我花了一点时间来修改我的基本服务的代码,意图故意引发“竞争条件”的发生。我添加了一个线程,CtrlHandler函数的线程在收到请求后立刻作出反应,将当前的服务状态设置成“请求正在被处理”即..._PENDING,然后由我添加的线程在睡眠了5秒之后再将服务状态设置成“请求已完成”状态——以模拟服务正在处理一些不可中止的事件,只有处理完成后才会更改服务的状态。一切就绪之后,我尝试在短时间内连续发送两个“暂停”请求,如果“竞争条件”不存在的话应该只有先发送的那个请求能够到达SCM,而另一个则应该返回请求发送失败的信息,天下太平。
事实上很不幸的,我成功了。当我在两个不同的“命令提示符”窗口分别同样的输入下面的命令:
net pause kservice
之后在“事件查看器”里面,我找到了我的服务在“应用程序日志”里添加的事件记录,结果是我得到了这样的事件列表:
SERVICE_PAUSE_PENDING
SERVICE_PAUSE_PENDING
SERVICE_PAUSED
SERVICE_PAUSED
看上去很奇怪是不是?因为服务处于正在暂停状态的时候,它不应该被再次暂停的。但事实摆在眼前,很多服务都曾明确的报告过上面的顺序状态。我曾经认为这时SCM应该说些什么或做些什么,以阻止“竞争状态”的出现,但实验结果告诉我SCM似乎对此无能为力,因为它不能控制状态代码在什么时候被发送。当用户使用“管理工具”里面的“服务”工具来管理服务的状态的时候,在一个“暂停”请求已经发出之后不能再次用这个工具向它发出“暂停”请求,如果正在暂停服务,会有一个对话框出现,阻止你按下它后面的“服务”工具的工具栏上的任何按钮,如果已经暂停,“暂停“按钮将变成灰色。但是这时用命令行工具 net.exe 就可以很顺利地将暂停请求再次送到服务。证据就是我添加的其他事件记录里面记下了SetServiceStatus的调用全都成功了,这更进一步的说明了我提交的两个暂停请求都经过SCM,然后到达了我的服务。
接下来我又进行了其它的测试,例如先发送“暂停”请求,后发送“停止”请求,和先发送“停止”请求,再发送“暂停”或“停止”请求。前一种情况更加糟糕,先发送的“暂停”请求和后发送的“停止”请求都没有得到什么好下场,虽然SCM老老实实的先暂停了服务,后停止了服务,但 net.exe 的两个实例的调用均告失败。不过在测试先发送停止“请求”的时候,所有的现象都表示这两个请求只有先发送的“停止”到达了SCM,这还算是个好消息...
为了解决这个问题,当服务得到一个“停止”“暂停”或“继续”请求的时候,应该首先检查服务是否已经在处理另外的一个请求,如果是,就依情况而定:是不调用SetServiceStatus直接返回还是暂时忍耐直到前一个请求动作完成再调用SetServiceStatus,这是你作为一个开发者要自己决定的。
如果说前面的问题已经足够麻烦了,下面的问题会令你觉得更加怪异。它其实是一种可以解决上面的问题的方法:当CtrlHandler函数的线程收到SERVICE_PAUSE_PENDING请求之后,它调用SetServiceStatus报告服务正在暂停,然后由它自己调用SuspendThread来暂停服务的线程,然后再由它自己调用SetServiceStatus报告服务已经被暂停。这样做的确避免了“竞争条件”的出现,因为所有的工作都是由一个函数来做的。现在需要注意的不是“竞争条件”而是服务本身,挂起服务的线程会不会暂停服务呢?答案是会的。但是暂停服务意味着什么呢?
假如我的服务是用来处理网络客户的请求,那么暂停对于我的服务来说应该是停止接受新的请求。如果我现在正处在处理请求的过程中,那么我应该怎么办?也许我应该结束它,使客户不至于无限期悬挂。但如果我只是简单的调用SuspendThread,那么不排除服务线程正处于孤立的中间状态的可能,或者正在调用malloc函数去尝试分配内存,如果运行在同一个进程中的另一个服务也调内存分配函数,那么它也会被挂起,这肯定不是我期望的结果。
还有一个问题:用户认为自己可以被允许去停止一个已经被暂停了的服务吗?我认为是这样的,而且很明显的,微软也这么认为。因为当我们在“服务”管理工具里面选中一个已暂停的服务之后,“停止”按钮是可以被按下的。但我要怎样停止一个由于线程被挂起才处于暂停状态的服务呢?不,不要TerminateThread,请别跟我提起它。
解决这所有的混乱的最好方法,就是有一个能够把所有事做好的线程,而且它应该是服务线程,而不是CtrlHandler线程。当CtrlHandler函数得到控制代码之后,它要迅速的将控制代码通过线程内部通讯手段送到服务线程中排队,然后CtrlHandler函数就应该返回,它决不应该调SetServiceStatus。这样,服务可以随心所欲的控制每件事情,因为没有什么比它更有发言权的了,没有“竞争条件”。服务决定暂停意味着什么,服务能够允许自己在已经暂停的情况下停止,服务决定什么内部通讯机制是最好的——并且CtrlHandler函数必须简单的与这种机制相一致。
事情没有完美的,上面的方法也不例外,它仅有一个小缺陷:就是假定当服务收到控制代码后,在较短的时间内就能做出应有的响应。如果服务线程正在忙于处理一个客户的请求,控制代码可能进入等待队列,而且SetServiceStatus可能也无法迅速的被调用。如果真是这样的话,负责发送通知的SCP可能会认为你的服务已经失败,并向用户报告一个消息框。事实上服务并没有失败,而且也不会被终止。
这种情况够糟糕了,没有用户会去责怪SCP——虽然SCP将他们引导到了错误的状态,他们只会责怪服务的作者——就是我或你...因此,在服务中怎么做才能防止这种问题发生呢?很简单,使服务快速有效的运行,并且总保持一个活动线程等待去处理控制代码。
说起来好像很容易,但实际做起来就被那么简单了,这也不是我能够向各位解释的了,只有认真的调试自己的服务,才能找出最为适合处理方法。所以我的文章也真的到了该结束的时候了,感谢各位的浏览。如果我有什么地方说的不对,请不吝赐教,谢谢。
下面是我写的一个服务的源代码,没什么功能,只能启动、停止和安装。
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <tchar.h>
#define SZAPPNAME "basicservice"
#define SZSERVICENAME "KService"
#define SZSERVICEDISPLAYNAME "KService"
#define SZDEPENDENCIES ""
void WINAPI KServiceMain(DWORD argc, LPTSTR * argv);
void InstallService(const char * szServiceName);
void LogEvent(LPCTSTR pFormat, ...);
void Start();
void Stop();
SERVICE_STATUS ssStatus;
SERVICE_STATUS_HANDLE sshStatusHandle;
int main(int argc, char * argv[])
{
if ((argc==2) && (::strcmp(argv[1]+1, "install")==0))
{
InstallService("KService");
return 0;
}
SERVICE_TABLE_ENTRY service_table_entry[] =
{
{ "KService", KServiceMain },
{ NULL, NULL }
};
::StartServiceCtrlDispatcher(service_table_entry);
return 0;
}
void InstallService(const char * szServiceName)
{
SC_HANDLE handle = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
char szFilename[256];
::GetModuleFileName(NULL, szFilename, 255);
SC_HANDLE hService = ::CreateService(handle, szServiceName,
szServiceName, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, szFilename, NULL,
NULL, NULL, NULL, NULL);
::CloseServiceHandle(hService);
::CloseServiceHandle(handle);
}
SERVICE_STATUS servicestatus;
SERVICE_STATUS_HANDLE servicestatushandle;
void WINAPI ServiceCtrlHandler(DWORD dwControl)
{
switch (dwControl)
{
//下面虽然添加了暂停、继续等请求的处理代码,但没有实际作用
//这是为什么呢?到了下面的KServiceMain函数里面就明白了...
case SERVICE_CONTROL_PAUSE:
servicestatus.dwCurrentState = SERVICE_PAUSE_PENDING;
// TODO: add code to set dwCheckPoint & dwWaitHint
// This value need to try a lot to confirm
// ...
::SetServiceStatus(servicestatushandle, &servicestatus);
// TODO: add code to pause the service
// not called in this service
// ...
servicestatus.dwCurrentState = SERVICE_PAUSED;
// TODO: add code to set dwCheckPoint & dwWaitHint to 0
break;
case SERVICE_CONTROL_CONTINUE:
servicestatus.dwCurrentState = SERVICE_CONTINUE_PENDING;
// TODO: add code to set dwCheckPoint & dwWaitHint
::SetServiceStatus(servicestatushandle, &servicestatus);
// TODO: add code to unpause the service
// not called in this service
// ...
servicestatus.dwCurrentState = SERVICE_RUNNING;
// TODO: add code to set dwCheckPoint & dwWaitHint to 0
break;
case SERVICE_CONTROL_STOP:
servicestatus.dwCurrentState = SERVICE_STOP_PENDING;
// TODO: add code to set dwCheckPoint & dwWaitHint
::SetServiceStatus(servicestatushandle, &servicestatus);
// TODO: add code to stop the service
Stop();
servicestatus.dwCurrentState = SERVICE_STOPPED;
// TODO: add code to set dwCheckPoint & dwWaitHint to 0
break;
case SERVICE_CONTROL_SHUTDOWN:
// TODO: add code for system shutdown
// as quick as possible
break;
case SERVICE_CONTROL_INTERROGATE:
// TODO: add code to set the service status
// ...
servicestatus.dwCurrentState = SERVICE_RUNNING;
break;
}
::SetServiceStatus(servicestatushandle, &servicestatus);
}
void WINAPI KServiceMain(DWORD argc, LPTSTR * argv)
{
servicestatus.dwServiceType = SERVICE_WIN32;
servicestatus.dwCurrentState = SERVICE_START_PENDING;
servicestatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;//上面的问题的答案就在这里
servicestatus.dwWin32ExitCode = 0;
servicestatus.dwServiceSpecificExitCode = 0;
servicestatus.dwCheckPoint = 0;
servicestatus.dwWaitHint = 0;
servicestatushandle =
::RegisterServiceCtrlHandler("KService", ServiceCtrlHandler);
if (servicestatushandle == (SERVICE_STATUS_HANDLE)0)
{
return;
}
bool bInitialized = false;
// Initialize the service
// ...
Start();
bInitialized = true;
servicestatus.dwCheckPoint = 0;
servicestatus.dwWaitHint = 0;
if (!bInitialized)
{
servicestatus.dwCurrentState = SERVICE_STOPPED;
servicestatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
servicestatus.dwServiceSpecificExitCode = 1;
}
else
{
servicestatus.dwCurrentState = SERVICE_RUNNING;
}
::SetServiceStatus(servicestatushandle, &servicestatus);
return;
}
void Start()
{
LogEvent("Service Starting...");
}
void LogEvent(LPCTSTR pFormat, ...)
{
TCHAR chMsg[256];
HANDLE hEventSource;
LPTSTR lpszStrings[1];
va_list pArg;
va_start(pArg, pFormat);
_vstprintf(chMsg, pFormat, pArg);
va_end(pArg);
lpszStrings[0] = chMsg;
if (1)
{
// Get a handle to use with ReportEvent().
hEventSource = RegisterEventSource(NULL, "KService");
if (hEventSource != NULL)
{
// Write to event log.
ReportEvent(hEventSource, EVENTLOG_INFORMATION_TYPE, 0, 0, NULL, 1, 0, (LPCTSTR*) &lpszStrings[0], NULL);
DeregisterEventSource(hEventSource);
}
}
else
{
// As we are not running as a service, just write the error to the console.
_putts(chMsg);
}
}
void Stop()
{
LogEvent("Service Stoped.");
}