CPU利用率编程

让多核CPU占用率曲线听你指挥——《编程之美》1.1学习笔记
 Problem:
写一个程序,让用户来决定Windows任务管理器(Task Manager)的CPU占用率。有以下几种情况: 1.CPU占用率固定在50%,为一条直线; 2.CPU的占用率为一条直线,具体占用率由命令行参数决定(范围1~100); 3.CPU的占用率状态为一条正弦曲线。 
分析与解法:
 (1)通过观察任务管理器,它大约1s更新一次。当CPU使用率为0时,System Idle Process占用了CPU的空闲时间。 
System Idle Process在CPU空闲的的时候,发出一个IDLE命令,使CPU挂起(暂时停止工作),可有效的降低CPU内核的温度,无法终止。在这个进程里出现的CPU占用数值并不是真正的占用而是体现的CPU的空闲率,也就说这个数值越大CPU的空闲率就越高,反之就是CPU的占用率越高。Linux中对应的进程为init,PID为1。
 当系统中的进程或者在等待用户输入,或者在等待某些事件的发生(发出I/O请求等待I/O响应),或者主动进入休眠状态(比如Sleep())。 
在任务管理器中的一个刷新周期内,CPU忙(执行应用程序)的时间和刷新周期总时间的比率就是CPU的占用率。其显示的是每个刷新周期内CPU占用率的统计平均值。我们可以写一个程序让它在任务管理器的刷新时间内一会儿忙,一会儿闲,通过调节忙/闲的比例,来控制任务管理器中显示的CPU占用率。
 书上的代码以单核CPU为前提,但对于多核CPU来说,同一个进程可能被CPU的任务分配器分配到不同的核心上执行,所以造成无法让任务管理器达到预想的效果。其实打开任务管理器,可以看到多个CPU使用记录。本人电脑CPU是Core i5 450M,双核4线程。在OS看来就如同有四个CPU工作一样。我的任务管理器中就有四个CPU使用记录。 
所谓超线程技术就是利用特殊的硬件指令,把多线程处理器内部的两个逻辑内核模拟成两个物理芯片,从而使单个处理器就能“享用”线程级的并行计算的处理器技术。多线程技术可以在支持多线程的操作系统和软件上,有效的增强处理器在多任务、多线程处理上的处理能力。
 可以使用SetProcessAffinityMask()函数可以使特定的处理器运行指定进程。 
BOOL SetProcessAffinityMask(HANDLE hProcess, DWORD_PTR dwProcessAffinityMask);
 第一个参数用来指定指定哪个进程,传入它的句柄。第二个进程用来指定哪个CPU核心来执行此进程。
 DWORD_PTR,其实就是unsigned long*.Unsigned long type for pointer precision.Use when casting a pointer to a long type to perform pointer arithmetic.(Also commonly used for general 32-bit parameters that have been extended to 64 bits in 64-bit windows.)
 DWORD 其实就是unsigned long。Windows下常用来保存地址或存放指针。
 比如这样调用函数:
  ::SetProcessAffinityMask(::GetCurrentProcess(),0x00000001);可以指定当前执行的进程在第一个CPU上运行。对于双核CPU,
  ::SetProcessAffinityMask(::GetCurrentProcess(),0x00000002);可以指定在第二个CPU上运行。
  ::SetProcessAffinityMask(::GetCurrentProcess(),0x00000003);可以允许在两个CPU上任意运行。
 HANDLE GetCurrentProcess(void);
 可以获得当前进程的句柄。注意,这个句柄为一个伪句柄。只能在我们的进程中才能代表当前进程的句柄,事实上这个函数目前只是简单的返回-1这个值。也就是说在我们的程序中-1便能表示本进程的句柄。
 (2)那么对于绘制50%直线,程序代码为:
 #include <Windows.h>
  1. #include<stdlib.h>
  2. #include<tchar.h>
  3. int _tmain(int argc,_TCHAR* argv[])
  4. {
  5. int busyTime = 10;
  6. int idleTime = busyTime*5;
  7. __int64startTime = 0;
  8. ::SetThreadAffinityMask(::GetCurrentProcess(),0x00000001);
  9. while(true)
  10. {
  11. startTime = GetTickCount();
  12. //busy loop
  13. while((GetTickCount() - startTime) <= busyTime);
  14. //idle loop
  15. Sleep(idleTime);
  16. }
  17. return 0;
  18. }
 GetTickCount()可以得到系统从启动到运行到现在所经历时间的毫秒值。最多能统计到49.7天。我们利用它判断busy loop要持续多久。
 其中idleTime为busyTime的五倍,可以修改其值使其更逼近50%。不同机子的情况不同。
 __int64是VC++的64位扩展。范围为[-2^63,2^63)。当64位与32位混合运算时,32位整数会隐式转换成64位整数。输入输出它时使用cin、cout会造成错误。需要使用scanf("%I64d",&a);和printf("%I64d",a);
 还有unsigned __int64,其范围为[0,2^64)。
 对应g++中的64位扩展为long long和unsigned long long。范围与运算与上相仿。输入输出使用scanf("%lld",&a);和printf("%lld",a);
 int _tmain(int argc, _TCHAR* argv[])。
 _tmain这个符号多见于VC++创建的控制台工程中,这个是为了保证移植unicode而加入的(一般_t、_T、T()这些东西都和unicode有关系)。定义在头文件tchar.h中。
 (3)对于绘制正弦曲线:
 #include <Windows.h>
  1. #include<stdlib.h>
  2. #include<math.h>
  3. #include<tchar.h>
  4. const double SPLIT = 0.01;
  5. const int COUNT = 200;
  6. const double PI = 3.14159265;
  7. const int INTERVAL = 300;
  8. int _tmain(int argc, _TCHAR* argv[] )
  9. {
  10. DWORDbusySpan[COUNT]; //array of busy times
  11. DWORDidleSpan[COUNT]; //array of idle times
  12. int half = INTERVAL/2;
  13. double radian = 0.0;
  14. //如何近似趋近一条正弦曲线?这样!
  15. for(int i = 0; i < COUNT; ++i)
  16. {
  17. busySpan[i] = (DWORD)(half + (sin(PI * radian) * half));
  18. idleSpan[i] = INTERVAL - busySpan[i];
  19. radian += SPLIT;
  20. }
  21. DWORDstartTime = 0;
  22. int j = 0;
  23. ::SetProcessAffinityMask(::GetCurrentProcess(),0x00000002);
  24. while(true)
  25. {
  26. j = j % COUNT;
  27. startTime = GetTickCount();
  28. while((GetTickCount() - startTime) <= busySpan[j]);
  29. Sleep(idleSpan[j]);
  30. j++;
  31. }
  32. return 0;
  33. }
 
通过在一个周期2*PI中等分200份,将每一个间隔点的half + (sin( PI * radian) * half))的值存入busySpan[i],将其补植存入idleSpan[i]。half是整个值域INTERVAL的一半。这样可以近似趋近一条正弦曲线。
 
运行效果为:
 
(4)可以通过RDTSC指令获得当前CPU核心运行周期数。
 
在x86平台上定义函数:
 
  1. inline __int64GetCPUTickCount()
  2. {
  3. __asm
  4. {
  5. rdtsc;
  6. }
  7. }
 
在x64平台上定义:
 
#define GetCPUTickCount() __rdtsc()
 
使用CallNtPowerInformation API得到CPU频率,从而将周期数转化为毫秒数,例如如下:
 
  1. _PROCESSOR_POWER_INFORMATIONinfo;
  2. CallNTPowerInformation(11, //query processor power information
  3. NULL, //no input buffer
  4. 0, //input buffer size is zero
  5. &info, //output buffer
  6. sizeof(info) //outbuf size
  7. );
  8. __int64t_begin = GetCPUTickCount();
  9. //do something
  10. __int64t_end = GetCPUTickCount();
  11. millisec = ((double)t_end - (double)t_begin)/(double)info.CurrentMhz;
 
RDTSC指令读取当前CPU的周期数,在多CPU系统中这个周期数在不同的CPU间基数不同,频率也不同。用从两个不同的CPU得到的周期数来计算会得出没有意义的值。所以需要用SetProcessAffinityMask避免进程迁移。另外,CPU的频率也会随系统供电及负荷情况有所调整。

猜你喜欢

转载自clebers.iteye.com/blog/1848245