NtQuerySystemInformation 是所谓 Undocuments 函数,主要用来获取系统各类信息。Windows 2000的任务管理器 TaskMgr 主要就是使用该函数来获取各类信息,如CPU使用率,内核使用率,句柄总数,线程总数,进程总数...等等在任务管理器中的几乎所有信息都是来自该函数。(当然也可以用别的函数完成类似的工作,如PDH,ToolHelp,或读取注册表等方法,各种方法中,应该以使用NtQuerySystemInformation 最好,这就是 TaskMgr 为什么使用该函数的原因。) 在MSDN知识库中是这样描写该函数的: [NtQuerySystemInformation is available for use in Windows 2000 and Windows XP. It may be altered or unavailable in subsequent versions. Applications should use the alternate functions listed in this topic.] 但幸运的是,至少在Windows 2003 的任务管理器中仍然是使用该函数来获取系统各类信息的。 下面将具体讲述该函数在Delphi中的使用 1. 函数NtQuerySystemInformation NtQuerySystemInformation函数隶属Ntdll.dll,函数的调用非常复杂,有许多入口参数,MSDN知识库中基本都是一带而过,没有具体的说明,这里所写的都是自己具体使用的感受,和网站上一些少的可怜的资料,而这些可怜的资料也都是别人自己的体会,所以难免有错误,因此在具体使用中,如有任何问题,概不负责。
1.1函数的调用格式: function NtQuerySystemInformation( SystemInformationClass: TSystemInformationClass; { SystemInformationClass [in] One of the values enumerated in SYSTEM_INFORMATION_CLASS, indicating the kind of system information to be retrieved.} pSystemInformation: PVOID; { SystemInformation [in, out] Points to a buffer where the requested information is/ to be returned. The size and structure of this information varies depending on the value of the SystemInformationClass parameter:} uSystemInformationLength: ULONG; { SystemInformationLength [in] Size of the buffer pointed to by the SystemInformation parameter, in bytes.} puReturnLength: PULONG { ReturnLength [out, optional] Optional pointer to a location where the function writes the actual size of the information requested. If that size is less than or equal to the SystemInformationLength parameter, the function copies the information into the SystemInformation buffer; otherwise, it returns an NTSTATUS error code and returns in ReturnLength the size of buffer required to receive the requested information. } ): NTSTATUS; stdcall; {Return Values Returns an NTSTATUS success or error code. The forms and significance of NTSTATUS error codes are listed in the Ntstatus.h header file available in the Windows Device Driver Kit (DDK), and are described in the DDK documentation under Kernel-Mode Driver Architecture / Design Guide / Driver Programming Techniques / Logging Errors.}
{uses a NtQuerySystemInformation call to obtain information about the Cache's settings and NtSetSystemInformation to set new sizing information. The working-set information for a process serves as guidelines for NT's Memory Manager egarding how many pages of/ physical memory should be assigned to the application. Because they are guidelines, conditions can result such that the Memory Manager grows a working-set to a size greater than the maximum, or shrinks it to less than the minimum. However, the settings are factors that will affect the overall allocation, and hence responsiveness, of an application. In the case of CacheSet the application is the file system Cache.}
下面是具体程序实现,这个程序完全是模仿 TaskMgr 写的,版权属于微软,这里只是从技术角度来研究,请不要用于商业用途。当然你硬要用,我也没有办法,但我声明了,我就推卸责任了,这也是跟微软学的。这里只列出了获取CPU 部分,其余部分将在后面阐明。 CalcCPUTime的命名也是微软的版权; TaskMgr 在初始化过程时根据 CPU的个数,为每个 CPU 使用率和内核使用率各自固定分配了8000个字节,用于存放CPU和内核使用率的历史记录。每个新产生的使用率总是放在首位,然后利用memmove函数将历史记录向后移。在这里我采用了略微不同的方法,我建立了一个记录,并在记录中设立了记录长度,位置,和记录数三个参数。根据CPU历史记录绘图需要的记录长度动态的分配内存,并且也避免了每次使用memmove来移动记录,而只是移动记录的指针。 type UsageRecord = packed record uLength: Word; //记录的长度 Position: Word; //当前指针位置 ReNo: Word; //现在已经有的记录数 Usages: array of Word; // 使用率 动态数组根据绘图需要分配长度。 end;
var SysBaseInfo: TSystemBasicInformation; SysProcPerfInfo: array[0..31] of TSystemProcessorPerformanceInformation; SysPerfInfo: TSystemPerformanceInformation; PreviousCPUIdleTime: array[0..31] of LARGE_INTEGER; PreviousCPUTotalTime: array[0..31] of LARGE_INTEGER; PreviousCPUKernelTime: array[0..31] of LARGE_INTEGER; CPUUsage: DWORD; KernelUsage: DWORD; MEMUsage: DWORD; CPUHistory: array of UsageRecord; KernelHistory: array of UsageRecord;
procedure TFormMainWin.CalcCPUTime; var status: NTSTATUS; SumCPUIdleTime: LARGE_INTEGER; SumCPUTotalTime: LARGE_INTEGER; SumCPUKernelTime: LARGE_INTEGER; tmpCPUIdleTime: LARGE_INTEGER; tmpCPUKernelTime: LARGE_INTEGER; tmpCPUTotalTime: LARGE_INTEGER; I: DWORD; begin //一次读取32颗CPU的数据,不管你实际安装了几颗CPU。 Status := NtQuerySystemInformation(SystemProcessorPerformanceInformation, @sysprocperfInfo, SizeOf(TSystemProcessorPerformanceInformation) * 32, nil); //如果 Status不等于零,发生错误,退出,这里非常奇怪,为什么不显示错误信息?例如相这样加一句: //if status <> 0 then // if MessageDlg('NtQuerySystemInformation ' + GetNTSTATUS(Status) + ' Exit now?', // mtConfirmation, [mbYes, mbNo], 0) = mrYes then // Exit; // 其中 GetNtstatus(Status) 是将返回的错误号转换成错误解释 // 既然原程序不做处理,我们也不做处理,忠于原作。 if status <> 0 then Exit; I := 0; SumCPUIdleTime.QuadPart := 0; SumCPUKernelTime.QuadPart := 0; SumCPUTotalTime.QuadPart := 0; // 循环体,遍历每一个 CPU,如果有这么多 repeat with SysProcPerfInfo[I] do begin //计算每一颗CPU 空闲时间 tmpCPUIdleTime.QuadPart := (IdleTime.QuadPart - PreviousCPUIdleTime[I].QuadPart); //计算每一颗CPU 总使用时间 (空闲+内核+使用) tmpCPUTotalTime.QuadPart := (UserTime.QuadPart + KernelTime.QuadPart) - PreviousCPUTotalTime[I].QuadPart; //计算每一颗 CPU 内核使用时间 tmpCPUKernelTime.QuadPart := (KernelTime.QuadPart - IdleTime.QuadPart) - PreviousCPUKernelTime[I].QuadPart; //所有CPU 空闲时间 Sum(0..CPU个数)(空闲时间) SumCPUIdleTime.QuadPart := SumCPUIdleTime.QuadPart + tmpCPUIdleTime.QuadPart; //所有CPU 总使用时间 Sum(0..CPU个数)(空闲+内核+使用) SumCPUTotalTime.QuadPart := SumCPUTotalTime.QuadPart + tmpCPUTotalTime.QuadPart; //所有 CPU 内核使用时间 Sum(0..CPU个数)(内核使用时间) SumCPUKernelTime.QuadPart := SumCPUKernelTime.QuadPart + tmpCPUKernelTime.QuadPart; //保存 CPU 空闲时间 PreviousCPUIdleTime[I].QuadPart := IdleTime.QuadPart; //保存 CPU 总使用时间 PreviousCPUTotalTime[I].QuadPart := (UserTime.QuadPart + KernelTime.QuadPart); //保存 CPU 内核使用时间 PreviousCPUKernelTime[I].QuadPart := KernelTime.QuadPart - IdleTime.QuadPart; end; //这段程序如果用Delphi实现编译后代码太长,因为任务管理器是实时采集CPU数据,程序应尽可能简练, //所以保留了原TaskMgr的处理方法,这里采用了汇编语言来实现;
asm MOV EAX, tmpCPUTotalTime.LowPart MOV EDX, tmpCPUTotalTime.HighPart OR EAX, EDX JE @@exit //计算CPU使用百分比 //入口参数 PUSH 0 //乘数的高位 PUSH $00000064 //乘数的低位 $64=100 PUSH tmpCPUIdleTime.HighPart //被乘数高位 PUSH tmpCPUIdleTime.LowPart //被乘数低位 CALL _allmul //_allmul 用于处理LARGE_INTEGER 的乘法函数,在ntdll.dll中 PUSH tmpCPUTotalTime.HighPart //除数高位 PUSH tmpCPUTotalTime.LowPart //除数低位 PUSH EDX //被除数高位 _allmul的返回结果高位 PUSH EAX //被除数低位 _allmul的返回结果低位 CALL _alldiv //_alldiv 用于处理 LARGE_INTEGER 的除法函数,在ntdll.dll中 XOR EDX, EDX MOV DL, $64 SUB DL, AL //CpuUsage = 100 - AL (_alldiv返回的结果) PUSH EDX POP CpuUsage //CpuUsage 百分数 //计算内核使用百分比和上面的类同 PUSH $00000000 PUSH $00000064 PUSH tmpCPUKernelTime.HighPart PUSH tmpCPUKernelTime.LowPart CALL _allmul PUSH tmpCPUTotalTime.HighPart PUSH tmpCPUTotalTime.LowPart PUSH EDX PUSH EAX CALL _alldiv XOR EDX, EDX MOV DL, $64 SUB DL, AL PUSH EDX POP KernelUsage @@exit: end; //保存CPU使用率,并移记录指针 if CPUHistory[i].Position = CPUHistory[i].uLength then CPUHistory[i].Position := 0 else inc(CPUHistory[i].Position); CPUHistory[i].Usages[CPUHistory[i].Position] := CPUUsage; if CPUHistory[i].ReNo < CPUHistory[i].uLength then inc(CPUHistory[i].ReNo); //保存内核使用率,并移动记录指针 if KernelHistory[i].Position = KernelHistory[i].uLength then KernelHistory[i].Position := 0 else inc(KernelHistory[i].Position); KernelHistory[i].Usages[KernelHistory[i].Position] := KernelUsage; if KernelHistory[i].ReNo < KernelHistory[i].uLength then inc(KernelHistory[i].ReNo); //CPU的个数加一 inc(I); //Processors 是系统CPU的个数,在前面初始化单元中通过调用NtQuerySystemInformation的 //SYSTEM_BASES_INFORMATION功能获得 //遍历每一颗CPU until I >= Processors; //下面是计算所有CPU的平均(总)使用率,使用方法和上面相同。 I := Processors - 1; asm MOV EAX, SumCPUTotalTime.LowPart MOV EDX, SumCPUTotalTime.HighPart OR EAX, EDX JE @@exit PUSH I PUSH $00000064 PUSH SumCPUIdleTime.HighPart PUSH SumCPUIdleTime.LowPart CALL _allmul PUSH SumCPUTotalTime.HighPart PUSH SumCPUTotalTime.LowPart PUSH EDX PUSH EAX CALL _alldiv XOR EDX, EDX MOV DL, $64 SUB DL, AL PUSH EDX POP CpuUsage
PUSH $00000000 PUSH $00000064 PUSH SumCPUKernelTime.HighPart PUSH SumCPUKernelTime.LowPart CALL _allmul PUSH SumCPUTotalTime.HighPart PUSH SumCPUTotalTime.LowPart PUSH EDX PUSH EAX CALL _alldiv XOR EDX, EDX MOV DL, $64 SUB DL, AL PUSH EDX POP KernelUsage @@exit: end; //这里开始计算内存使用率 status := NtQuerySystemInformation(SystemPerformanceInformation, @SysPerfInfo, SizeOf(TSystemPerformanceInformation), nil); if status <> 0 then Exit; // PageSize 页面大小是在前面初始化单元中通过调用NtQuerySystemInformation的 //SYSTEM_BASES_INFORMATION功能获得, //内存使用率 = (页面大小 / 1024) * CommittedPages MEMUsage := (PageSize shr 10) * SysPerfInfo.CommittedPages; //保存内存使用率,并移动记录指针 if MEMHistory.Position = MEMHistory.uLength then MEMHistory.Position := 0 else inc(MEMHistory.Position); MEMHistory.Usages[MEMHistory.Position] := tmpMem; if MEMHistory.ReNo < MEMHistory.uLength then inc(MEMHistory.ReNo); end; 注:1、这些程序在后面出现时也许会有不同,因为这些程序都是在写这篇东西时根据需要而写的,并不是完整程序中的一部分,随着讨论问题的深入,或许会修正一些地方,后面就不在特别说明了。 2、因为是一边写一边帖(包括程序),所以会很慢,大家给点耐性。 下一段将讨论如何取得性能中的其他数据: 句柄数、线程数、进程数、物理内存总数、可用数系统缓存、认可用量总数、限制、峰值、核心内存总数、分页数、未分页。 另外,这些程序在我的文档中排列是非常整齐的,不知为什么一贴上来就乱七八糟了,这可不是我的错。 待续....
----------------------------------------------
ask not what your country can do for you--ask what you can do for your country.