Skip to main content

[转]让CPU占用率曲线听你指挥

题目《让CPU占用率曲线听你指挥》

问题

写一个程序,让用户来决定Windows任务管理器(Task Manager)的CPU占用率。程序越精简越好,计算机语言不限。例如,可以实现下面三种情况:

1. CPU的占用率固定在50%,为一条直线;
2. CPU的占用率为一条直线,但是具体占用率由命令行参数决定(参数范围1~ 100);
3. CPU的占用率状态是一个正弦曲线。

分析与解法

有一名学生写了如下的代码:

while (true)
{
if (busy)
i++;
else

}


然后她就陷入了苦苦思索:else干什么呢?怎么才能让电脑不做事情呢?CPU使用率为0的时候,到底是什么东西在用CPU?另一名学生花了很多时间构想如何“深入内核,以控制CPU占用率”——可是事情真的有这么复杂么?
MSRA TTG(Microsoft Research Asia, Technology Transfer Group)的一些实习生写了各种解法,他们写的简单程序可以达到如图1-1所示的效果。

498)this.style.width=498;" onmousewheel="javascript:return big(this)" alt="" src="http://new.51cto.com/files/uploadimg/20080306/103426364.jpg" border="0" height="367" width="324">
图1-1 编码控制CPU占用率呈现正弦曲线形态

看 来这并不是不可能完成的任务。让我们仔细地回想一下写程序时曾经碰到的问题,如果我们不小心写了一个死循环,CPU占用率就会跳到最高,并且一直保持 100%。我们也可以打开任务管理器 ,实际观测一下它是怎样变动的。凭肉眼观察,它大约是1秒钟更新一次。一般情况下,CPU使用率会很低。但是,当用户运行一个程序,执行一些复杂操作的时 候,CPU的使用率会急剧升高。当用户晃动鼠标时,CPU的使用率也有小幅度的变化。

那当任务管理器报告CPU使用率为0的时候,谁在使用CPU呢?通过任务管理器的“进程(Process)”一栏可以看到,System Idle Process占用了CPU空闲的时间——这时候大家该回忆起在“操作系统原理”这门课上学到的一些知识了吧。系统中有那么多进程,它们什么时候能“闲下 来”呢?答案很简单,这些程序或者在等待用户的输入,或者在等待某些事件的发生(WaitForSingleObject()),或者进入休眠状态(通过 Sleep()来实现)。

在任务管理器的一个刷新周期内,CPU忙(执行应用程序)的时间和刷新周期总时间的比率,就是CPU的占用率,也就是说,任务管理器中显示的是每个 刷新周期内CPU占用率的统计平均值。因此,我们写一个程序,让它在任务管理器的刷新期间内一会儿忙,一会儿闲,然后通过调节忙/闲的比例,就可以控制任 务管理器中显示的CPU占用率。

【解法一】简单的解法

步骤1 要操纵CPU的usage曲线,就需要使CPU在一段时间内(根据Task Manager的采样率)跑busy和idle两个不同的loop,从而通过不同的时间 比例,来获得调节CPU Usage的效果。

步骤2 Busy loop可以通过执行空循环来实现,idle可以通过Sleep()来实现。

问题的关键在于如何控制两个loop的时间,方法有二:

Sleep一段时间,然后以for循环n次,估算n的值。

那么对于一个空循环for(i = 0; i <>

loop:
mov dx i ;将i置入dx寄存器
inc dx ;将dx寄存器加1
mov i dx ;将dx中的值赋回i
cmp i n ;比较i和n
jl loop ;i小于n时则重复循环

假设这段代码要运行的CPU是P4 2.4Ghz(2.4 * 10的9次方个时钟周期每秒)。现代CPU每个时钟周期可以执行两条以上的代码,那么我们就取平均值两条,于是让(2 400 000 000 * 2)/5=960 000 000(循环/秒),也就是说CPU 1秒钟可以运行这个空循环960 000 000次。不过我们还是不能简单地将n = 60 000 000,然后Sleep(1000)了事。如果我们让CPU工作1秒钟,然后休息1秒钟,波形很有可能就是锯齿状的——先达到一个峰值(大 于>50%),然后跌到一个很低的占用率。

我们尝试着降低两个数量级,令n = 9 600 000,而睡眠时间相应改为10毫秒(Sleep(10))。用10毫秒是因为它不大也不小,比较接近Windows的调度时间片。如果选得太小(比如1 毫秒),则会造成线程频繁地被唤醒和挂起,无形中又增加了内核时间的不确定性影响。最后我们可以得到如下代码:

代码清单1-1

 

int main()
{
for(;;)
{
for(int i = 0; i < 9600000; i++);
Sleep(10);
}
return 0;
}

在不断调整9 600 000的参数后,我们就可以在一台指定的机器上获得一条大致稳定的50% CPU占用率直线。
使用这种方法要注意两点影响:

1. 尽量减少sleep/awake的频率,如果频繁发生,影响则会很大,因为此时优先级更高的操作系统内核调度程序会占用很多CPU运算时间。
2. 尽量不要调用system call(比如I/O这些privilege instruction),因为它也会导致很多不可控的内核运行时间。
该方法的缺点也很明显:不能适应机器差异性。一旦换了一个CPU,我们又得重新估算n值。有没有办法动态地了解CPU的运算能力,然后自动调节忙/闲的时间比呢?请看下一个解法。

【解法二】使用GetTickCount()和Sleep()
我们知道GetTickCount()可以得到“系统启动到现在”的毫秒值,最多能够统计到49.7天。另外,利用Sleep()函数,最多也只能精确到1毫秒。因此,可以在“毫秒”这个量级做操作和比较。具体如下:

利用GetTickCount()来实现busy loop的循环,用Sleep()实现idle loop。伪代码如下:

代码清单1-2

 int busyTime = 10;  //10 ms
int idleTime = busyTime; //same ratio will lead to 50% cpu usage

Int64 startTime = 0;
while (true)
{
startTime = GetTickCount();
// busy loop的循环
while ((GetTickCount() - startTime) <= busyTime) ;

//idle loop
Sleep(idleTime);
}


这两种解法都是假设目前系统上只有当前程序在运行,但实际上,操作系统中有很多程序都会在不同时间执行各种各样的任务,如果此刻其他进程使用了10% 的CPU,那我们的程序应该只能使用40%的CPU(而不是机械地占用50%),这样可达到50%的效果。

怎么做呢?
我们得知道“当前CPU占用率是多少”,这就要用到另一个工具来帮忙——Perfmon.exe。

Perfmon是从Windows NT开始就包含在Windows服务器和台式机操作系统的管理工具组中的专业监视工具之一(如图1-2所示)。Perfmon可监视各类系统计数器,获取 有关操作系统、应用程序和硬件的统计数字。Perfmon的用法相当直接,只要选择您所要监视的对象(比如:处理器、RAM或硬盘),然后选择所要监视的 计数器(比如监视物理磁盘对象时的平均队列长度)即可。还可以选择所要监视的实例,比如面对一台多CPU服务器时,可以选择监视特定的处理器。

498)this.style.width=498;" onmousewheel="javascript:return big(this)" alt="" src="http://new.51cto.com/files/uploadimg/20080306/103439305.jpg" border="0" height="284" width="347">
图1-2 系统监视器(Perfmon)

我们可以写程序来查询Perfmon的值,Microsoft .Net Framework提供了PerformanceCounter()这一类型,从而可以方便地拿到当前各种计算机性能数据,包括CPU的使用率。例如下面这个程序——

【解法三】能动态适应的解法
代码清单1-3

//C# code
static void MakeUsage(float level)
{
PerformanceCounter p = new PerformanceCounter("Processor", "% Processor Time", "_Total");

while (true)
{
if (p.NextValue() > level)
System.Threading.Thread.Sleep(10);
}
}


可以看到,上面的解法能方便地处理各种CPU使用率参数。这个程序可以解答前面提到的问题2。
有了前面的积累,我们应该可以让任务管理器画出优美的正弦曲线了,见下面的代码。

【解法四】正弦曲线
代码清单1-4

 //C++ code  to make task manager generate sine graph
#include "Windows.h"
#include "stdlib.h"
#include "math.h"

const double SPLIT = 0.01;
const int COUNT = 200;
const double PI = 3.14159265;
const int INTERVAL = 300;

int _tmain(int argc, _TCHAR* argv[])
{
DWORD busySpan[COUNT]; //array of busy times
DWORD idleSpan[COUNT]; //array of idle times
int half = INTERVAL / 2;
double radian = 0.0;
for(int i = 0; i < COUNT; i++)
{
busySpan[i] = (DWORD)(half + (sin(PI * radian) * half));
idleSpan[i] = INTERVAL - busySpan[i];
radian += SPLIT;
}

DWORD startTime = 0;
int j = 0;
while (true)
{
j = j % COUNT;
startTime = GetTickCount();
while ((GetTickCount() - startTime) <= busySpan[j]) ;
Sleep(idleSpan[j]);
j++;
}
return 0;
}

讨论
如果机器是多CPU,上面的程序会出现什么结果?如何在多个CPU时显示同样的状态?例如,在双核的机器上,如果让一个单线程的程序死循环,能让两个CPU的使用率达到50%的水平么?为什么?

多CPU的问题首先需要获得系统的CPU信息。可以使用GetProcessorInfo()获得多处理器的信息,然后指定进程在哪一个处理器上运行。其中指定运行使用的是SetThreadAffinityMask()函数。

另外,还可以使用RDTSC指令获取当前CPU核心运行周期数。

在x86平台上定义函数:

inline __int64 GetCPUTickCount()
{
__asm
{
rdtsc;
}
}
在x64平台上定义:

#define GetCPUTickCount() __rdtsc()

使用CallNtPowerInformation API得到CPU频率,从而将周期数转化为毫秒数,例如:

代码清单1-5

  _PROCESSOR_POWER_INFORMATION info;

CallNTPowerInformation(11, //query processor power information
NULL, //no input buffer
0, //input buffer size is zero
&info, //output buffer
Sizeof(info)); //outbuf size

__int64 t_begin = GetCPUTickCount();

//do something

__int64 t_end = GetCPUTickCount();
double millisec = ((double)t_end –
(double)t_begin)/(double)info.CurrentMhz;


RDTSC指令读取当前CPU的周期数,在多CPU系统中,这个周期 数在不同的CPU之间基数不同,频率也有可能不同。用从两个不同的CPU得到的周期数作计算会得出没有意义的值。如果线程在运行中被调度到了不同的 CPU,就会出现上述情况。可用SetThreadAffinityMask避免线程迁移。另外,CPU的频率会随系统供电及负荷情况有所调整。

总结

能帮助你了解当前线程/进程/系统效能的API大致有以下这些:

1. Sleep()——这个方法能让当前线程“停”下来。
2. WaitForSingleObject()——自己停下来,等待某个事件发生
3. GetTickCount()——有人把Tick翻译成“嘀嗒”,很形象。
4. QueryPerformanceFrequency()、QueryPerformanceCounter()——让你访问到精度更高的CPU数据。
5. timeGetSystemTime()——是另一个得到高精度时间的方法。
6. PerformanceCounter——效能计数器。
7. GetProcessorInfo()/SetThreadAffinityMask()。遇到多核的问题怎么办呢?这两个方法能够帮你更好地控制CPU。
8. GetCPUTickCount()。想拿到CPU核心运行周期数吗?用用这个方法吧。

了解并应用了上面的API,就可以考虑在简历中写上“精通Windows”了。

Comments

Popular posts from this blog

[转] UTF-8 and Unicode FAQ for Unix/Linux

这几天,这个东西把我搞得很头疼 而且这篇文章好像太大了,blogger自己的发布系统不能发 只好用mail了 //原文 http://www.cl.cam.ac.uk/~mgk25/unicode.html UTF-8 and Unicode FAQ for Unix/Linux by Markus Kuhn This text is a very comprehensive one-stop information resource on how you can use Unicode/UTF-8 on POSIX systems (Linux, Unix). You will find here both introductory information for every user, as well as detailed references for the experienced developer. Unicode has started to replace ASCII, ISO 8859 and EUC at all levels. It enables users to handle not only practically any script and language used on this planet, it also supports a comprehensive set of mathematical and technical symbols to simplify scientific information exchange. With the UTF-8 encoding, Unicode can be used in a convenient and backwards compatible way in environments that were designed entirely around ASCII, like Unix. UTF-8 is the way in which Unicode is used under Unix, Linux, and similar systems. It is now time to make sure that you are well familiar

Determine Perspective Lines With Off-page Vanishing Point

In perspective drawing, a vanishing point represents a group of parallel lines, in other words, a direction. For any point on the paper, if we want a line towards the same direction (in the 3d space), we simply draw a line through it and the vanishing point. But sometimes the vanishing point is too far away, such that it is outside the paper/canvas. In this example, we have a point P and two perspective lines L1 and L2. The vanishing point VP is naturally the intersection of L1 and L2. The task is to draw a line through P and VP, without having VP on the paper. I am aware of a few traditional solutions: 1. Use extra pieces of paper such that we can extend L1 and L2 until we see VP. 2. Draw everything in a smaller scale, such that we can see both P and VP on the paper. Draw the line and scale everything back. 3. Draw a perspective grid using the Brewer Method. #1 and #2 might be quite practical. #3 may not guarantee a solution, unless we can measure distances/p

Moving Items Along Bezier Curves with CSS Animation (Part 2: Time Warp)

This is a follow-up of my earlier article.  I realized that there is another way of achieving the same effect. This article has lots of nice examples and explanations, the basic idea is to make very simple @keyframe rules, usually just a linear movement, then use timing function to distort the time, such that the motion path becomes the desired curve. I'd like to call it the "time warp" hack. Demo See the Pen Interactive cubic Bezier curve + CSS animation by Lu Wang ( @coolwanglu ) on CodePen . How does it work? Recall that a cubic Bezier curve is defined by this formula : \[B(t) = (1-t)^3P_0+3(1-t)^2tP_1+3(1-t)t^2P_2+t^3P_3,\ 0 \le t \le 1.\] In the 2D case, \(B(t)\) has two coordinates, \(x(t)\) and \(y(t)\). Define \(x_i\) to the be x coordinate of \(P_i\), then we have: \[x(t) = (1-t)^3x_0+3(1-t)^2tx_1+3(1-t)t^2x_2+t^3x_3,\ 0 \le t \le 1.\] So, for our animated element, we want to make sure that the x coordiante (i.e. the "left" CSS property) is \(