Win200 0的Job对象 介绍job对象
在Windows平台上编程时,你的程序一不小心就会"吃掉"所有可用的CPU资源和内存资源。某些时候这不成其为问题,但如果你创建服务器程序来相应多个客户请求的话,就要小心不要因为耗费太多系统资源而导致当机。假如你用Windows2000作为平台,可以利用几个API调用来解决这个问题。
许多Delphi程序员已经转到Windows 2000平台。Borland对在此平台上使用Delphi 5没有做出任何官方支持保证。
尽管如此,除了由一些本地化版本的操作系统或显示驱动程序引起的小问题之外,Delphi 5看来在Windows 2000平台上工作得很好。Delphi 6对Windows 2000应当会有很好的支持。
既然Delphi 5(甚至更早的版本)支持Windows 2000,那何不在Delphi 6之前就好好利用新操作系统的新特性呢?本文将论及在Delphi 5中如何利用Windows 2000的job对象。不保证示例程序可以在Delphi 6中编译通过,不过应该问题不大。
介绍job对象
当应用程序在Windows 2000中运行时,可以自由地消耗需要的资源以完成工作。例如,为了实现繁重的计算工作,应用程序可能会创建新的线程来更有效地利用CPU。如果应用程序需要和其它程序共享信息,也可能会简单地利用剪贴板,假使需要修改系统设置的话,它也会毫不犹豫地去做的。另外,如果应用程序需要分配内存,系统也会尽量不拒绝它。在现在的系统中,分配50MB甚至100MB的内存根本不成问题。
Windows 2000提供给应用程序一个更加不受限制的运行环境。系统管理员固然可以指定普通用户可以在系统上进行何种操作,而他却无法单独控制应用程序。比如,系统管理员可以限制某一用户(以及这个用户运行的程序)不可存取特定的文件夹或网络资源,但特定的应用程序允许消耗的内存却无法指定。
这是就该Windows 2000的job对象上场了。Job对象是Windows 2000的新特性,利用它可以限制很多东西,比如内存消耗量、CPU时间、剪贴板存取、进程数等等。Job对象是Windows 2000的核心对象,这意味着操作系统将在基本子系统层面支持它。
"job(工作)"听起来像是大型机时代的批处理应用程序,但Windows 2000里的job对象并不周期性崩溃--正好相反。而且,也不应把job对象和print job(打印工作)混淆起来,两者完全不同。
要利用job对象,首先要创建它。后文将述及一些细节,但现在你应该把job创建对象当做和创建其它系统对象(文件、管道、线程等)一样。一旦创建了job对象,它就被初始化为空值。为了使用它,必须首先改变它的设置。
最重要的是把新创建的job对象和一个或多个进程结合起来。进程默认为不与任何job对象相结合,但你可以随自己的心愿去这么干。这种结合是永久性的,所以不能被中止。
为job对象指定了一个或多个进程之后,可以开始为对象设置限制。比如,限制结合到对象的单一进程可以消耗的内存数,可以是5MB、30MB等等。限制可用于单一应用程序、或者整个job对象
准备消耗
你可能已经可以想见job对象的众多用途了。如果你的应用程序要响应客户端请求,就可以把它连接到限制性质的job对象。假如觉得某个应用程序耗费太多资源,也可以把它结合到合适的job对象。如此等等,不一而足。
需要找到有关的C语言文件头的Delphi版本。Windows Platform SDK中包括了原始头文件和API文档。http://msdn.microsoft.com/downloads有下载。WINBASE.H和WINNT.H就是需要注意的头文件
翻译C语言头文件耗时甚多。我已经简单翻译了一下,命名为JOBS.PAS。你可以在项目中自由使用。也许D6会包括所有需要的翻译头文件。
在我看来,了解操作系统新特性的最好方法就是去用它。Windows 2000 的job 对象是用来限制应用程序的,我写了两个简单的消耗系统资源的例子来做演示。
这两个例子程序,一个叫做CPUHog,另一个叫做MemoryHog。在图1中可以看到:
图一
顾名思义,这两个程序都是CPU时间和内存消耗大户。现实中自然不可能存在这样的程序,但许多其它程序在消耗方面干得也毫不逊色。
从代码段一和代码端二中可以看出它们是如何工作的。最好不要把这些代码直接拷贝到你的项目中--一般而言,Delphi程序员都会教你"应该这样写代码",而我将会演示"不要这样写代码"。
代码段一在While循环中浪费CPU时间
procedure TCPUHogForm.StartClick(Sender: TObject);
begin
Hogging := True;
Start.Enabled := False;
Stop.Enabled := True;
While Hogging do Begin
Application.ProcessMessages;
End;
end;
procedure TCPUHogForm.StopClick(Sender: TObject);
begin
Hogging := False;
Start.Enabled := True;
Stop.Enabled := False;
end;
代码段二使用GetMem分配以兆字节计算的内存
procedure TMemoryHogForm.AllocateClick(
Sender: TObject);
Var P : PChar;
begin
GetMem(P,StrToInt(Edit1.Text)*1024*1024);
{ just to store something there }
StrCopy(P,PChar(DateTimeToStr(Now)));
end;
示例程序
现在你应该已经明白这两个程序是如何工作的了。图二是Job对象示例程序。
图二
记住,因为Windows 2000第一次引入job对象,所以必须在Windows 2000上运行这个程序。
如果一个进程被分配给Job对象,Windows 2000就会收集关于进程的信息。如果达到某一极限(如内存量消耗等等),系统将中止它。看起来有点粗暴,但如果应用程序本来就像避免这种情况时,它会工作得很好。你也可以设定为让Windows 2000记录到日志,然后继续运行进程。
示例程序在启动时创建一个核心job对象(在主窗体的OnCreate中),然后用timer组件不断地监视状态。你可以选择用十进制或者十六进制来显示统计数字。
示例程序有三个按钮:Run CPUHog(运行CPUHog)、RUN MemoryHog(运行MemoryHog),Set Restrictions(设置限制)。前两个按钮用来执行上文谈及的程序,第三个实现预定义的限制。下面的部分,我将演示这一切是如何实现的。
交给Windows去办!
如前所述,示例程序在启动时创建核心job对象。代码段三展示了如何调用CreateJobObject这个Win32 API函数。这个函数有两个参数:lpJobAttributes和lpName,均可被设置为nil,以使用缺省设置。
代码段三调用CreateJobObject创建job对象
procedure TJobMainForm.FormCreate(Sender: TObject);
begin
JobObj := CreateJobObject(nil,nil);
If (JobObj = 0) Then
ShowMessage(SysErrorMessage(GetLastError));
end;
...
Function CreateJobObject(lpJobAttributes :
PSecurityAttributes;
lpName : PAnsiChar) : THandle; StdCall;
External Kernel32 Name 'CreateJobObjectA';
如果调用成功,返回值是被创建对象的句柄。反之则返回0,我在程序中用一个错误提示框表示。注意代码是如何运用在SYSUTILS.PAS单元中声明的SysErrorMessage函数构造错误信息的。
Job对象被创建之后,跟着就到了用来跟踪显示job对象状态的Ttimer组件的事件句柄了。在代码段四中可以看到。用来查询job对象信息的API函数是QueryInformationJobObject。注意SysUtils.Win32Check函数是如何在API函数调用失败时引发异常的。I2S32和I2S64这两个转换函数用于进行基于10位或16位数的整数到字符串转换。
代码段四 调用QueryInformationJobObject获取job对象信息
procedure TJobMainForm.Timer1Timer(Sender: TObject);
Var
Info : TJobObjectBasicAndIOAccountingInformation;
Len : Cardinal;
begin
{ retrieve basic and IO information
about the job object }
Win32Check(QueryInformationJobObject(JobObj,
JobObjectBasicAndIoAccountingInformation,@Info,
SizeOf(Info),@Len));
{ show the results on screen }
With Info do Begin
Edit1.Text := I2S64(BasicInfo.TotalUserTime);
Edit2.Text := I2S64(BasicInfo.TotalKernelTime);
Edit3.Text :=
I2S64(BasicInfo.ThisPeriodTotalUserTime);
Edit4.Text :=
I2S64(BasicInfo.ThisPeriodTotalKernelTime);
Edit5.Text :=
I2S32(BasicInfo.TotalPageFaultCount);
Edit6.Text := I2S32(BasicInfo.ActiveProcesses);
Edit7.Text :=
I2S32(BasicInfo.TotalTerminatedProcesses);
Edit8.Text := I2S64(IoInfo.ReadOperationCount);
Edit9.Text := I2S64(IoInfo.WriteOperationCount);
Edit10.Text := I2S64(IoInfo.ReadTransferCount);
Edit11.Text := I2S64(IoInfo.WriteTransferCount);
End;
end;
Job对象本身不做任何事。你需要把进程和它相连。前面谈到的两个按钮启动应用程序。代码段五显示OnClick事件句柄,代码段六显示ExecuteProcess助手函数。
代码段五 执行进程,选择性地连接到job对象
procedure TJobMainForm.RunCPUHogClick(
Sender: TObject);
Var Proc : THandle;
begin
Proc := ExecuteProcess('cpuhog.exe');
If AssociateWithJob.Checked Then
Win32Check(AssignProcessToJobObject(JobObj,Proc));
CloseHandle(Proc);
end;
procedure TJobMainForm.RunMemoryHogClick(
Sender: TObject);
Var Proc : THandle;
begin
Proc := ExecuteProcess('memoryhog.exe');
If AssociateWithJob.Checked Then
Win32Check(AssignProcessToJobObject(JobObj,Proc));
CloseHandle(Proc);
end;
代码段六 使用API函数CreateProcess执行进程
Function TJobMainForm.ExecuteProcess(
EXE : String) : THandle;
Var
SI : TStartupInfo;
PI : TProcessInformation;
Begin
Result := INVALID_HANDLE_VALUE;
FillChar(SI,SizeOf(SI),0);
SI.cb := SizeOf(SI);
If CreateProcess(nil,PChar('.\'+EXE),nil,nil,
False,0,nil,nil,SI,PI) Then Begin
{ close thread handle }
CloseHandle(PI.hThread);
Result := PI.hProcess;
End
Else ShowMessage('CreateProcess call failed: '+
SysErrorMessage(GetLastError));
End;
如你所见,假如选项框"Associate process with the job object(连接进程到job对象)"被选择,就把进程连接到job对象。如果在ExecuteProcess中没有指定路径,就要确保CPUHOG.EXE和MEMORYHOG.EXE和示例程序JOBOBJECTS.EXE在同一目录。API函数AssignProcessToJobObject将进程连接到一个job。
Job对象默认为不对进程设置任何限制。Windows 2000仅仅是收集关于进程的统计数据供你使用。设置限制才是比较有趣的部分。用API函数SetInformationJobObject 可以做到这一点。
实现限制
在示例程序中,我打算把限制设定为:每个进程10秒CPU时间,整个job 20秒时间。活动(正在运行)进程限制为5个,所有属于这个job的进程可以消耗20MB的内存。
点击Set Restrictions(设置限制)按钮时,代码段七中的代码被执行,显示图三所示消息框。
图三
代码段七 使用API函数SetInformationJobObject实现限制
procedure TJobMainForm.SetRestrictionsClick(
Sender: TObject);
Const MegaByte = 1024*1024;
Var Limits : TJobObjectExtendedLimitInformation;
begin
{ 填充结构 }
FillChar(Limits,SizeOf(Limits),0);
With Limits,BasicLimitInformation do Begin
PerProcessUserTimeLimit :=
SecondsTo100NSTicks(10);
PerJobUserTimeLimit := SecondsTo100NSTicks(20);
ActiveProcessLimit := 5;
ProcessMemoryLimit := 20*MegaByte;
{ 设置标志 }
LimitFlags := JOB_OBJECT_LIMIT_PROCESS_TIME Or
JOB_OBJECT_LIMIT_JOB_TIME Or
JOB_OBJECT_LIMIT_ACTIVE_PROCESS Or
JOB_OBJECT_LIMIT_PROCESS_MEMORY;
End;
{ 执行限制 }
Win32Check(SetInformationJobObject(JobObj,
JobObjectExtendedLimitInformation,
@Limits,SizeOf(Limits)));
ShowMessage('Limits set!'#13#13+
'User time per process: 10 seconds'#13+ //每个进程时间
'User time per job: 20 seconds'#13+ //每个job时间
'Active process limit: 5 processes'#13+ //活动进程限制
'Per process memory allocation limit: 20 MB'); //每个进程分配的内存
SetRestrictions.Enabled := False;
end;
执行限制之后,运行MemoryHog。程序正常开始,点击Allocate(分配)按钮,可以分配10MB的内存。第二次点击这个按钮,尽管系统可能还有很多可用内存(按Ctrl-Shift-Esc,在任务管理器中查看),也不能分配更多内存。如果还没有看到错误信息(很少见),请看图四。
图四
接着我们来看看CPUHog。缺省的限制为每个进程10秒CPU用户时间,整个job对象为20秒。为了测试前一个限制,点击Run CPUHog按钮,注意屏幕上的数值变化。然后,点击CPUHog程序的Start按钮,然后等上顶多10-12秒钟。这时,CPUHog程序应当直接从屏幕上消失。Job对象再一次成功了。
现在,我们已经测试了对每个进程的限制。但就像你在代码段七中见到的,我还准备限制整个job对象CPU时间。重新启动JobObjects,启动一个或两个MemoryHog,启动两个CPUHog。
不用去管MemoryHog程序,点击CPUHog上的Start(开始)按钮。同样等上10-12分钟,CPUHog的一个实例将会被终止。
点击剩下那个CPUHog的Start(开始)按钮。这个CPUHog也同样消失,而且MemoryHog的实例也不见了!看起来很不公平,但这就是job对象的默认工作方式。当达到job对象的CPU时间限制,这个job对象就不能继续运行任何其它进程,因为每个进程都要消耗一定量的CPU时间。
结论
本文展示了如何在程序中使用job对象。如你在代码中所见,这并不复杂。和其它API层次技术相比,实在是太简单了。
使用job对象是如此之简单,我建议你在合适的地方多多利用它。比如,假如某个应用程序运行起来没有想象中那幺满意,就可以写一个简单的"启动"程序:
C:\>limitmem 25000 "C:\Program Files\SomeApp\App.exe"
除了本文述及的限制CPU和内存消耗,job对象还可用来限制许多其它方面。要讲清楚的话,恐怕需要两三篇文章。篇幅所限,不能一一尽述。可以下载Platform SDK或者在线阅览:http://msdn.microsoft.com/library。
最后一个诀窍:用它来测试程序。你经常在内存超载的情况下测试程序吗?在发现Windows 2000 job对象之前,我一直觉得:"嗯,太难啦……"
补充知识:理解CPU消耗
考虑使用job对象,它真的可以做到使你的进程只耗费80秒的CPU时间。假如你在Windows 2000系统开发Web程序,肯定见过Web站点属性对话框中的"Enable process throttling(允许进程中止)"选择框和相应的"Maximum CPU Use %"设置值。
IIS(Internet信息服务器)是如何以百分比为度量单位限制CPU使用的呢?job对象并不支持这种方法。
要知道迷底,先要理解Windows 2000的CPU消耗(其它多任务操作系统也一样)。系统不断地运行不同的程序(实际上是进程中的线程),并在它们之间切换。在单任务操作系统中,同一时间只能有一个程序运行。这个程序将会尽可能多地使用CPU,而不是50%或者75%。
由于多任务的实现,Windows 2000在某一时点只运行一个程序,这个时段大约是20毫秒,然后切换到另一个程序。"同时"执行多个程序成为可能。所以,所谓CPU消耗就是一个时间函数。
具体来看看例子:某个应用程序以10秒为周期使用了5秒钟的CPU时间,那么,平均CPU使用就是50%。以24小时为周期,就是12小时,如此等等。
所以你可以以百分比为度量单位限制CPU使用。技巧就在于选择合适的周期!