问题

 

写一个程序,让用户来决定 Windows 任务管理器(Task Manager)的 CPU 占用率。程

序越精简越好,计算机语言不限。例如,可以实现下面三种情况:

1. CPU的占用率固定在50%,为一条直线;

2. CPU的占用率为一条直线,但是具体占用率由命令行参数决定(参数范围1~ 100

3. CPU的占用率状态是一个正弦曲线。

MSRA TTGMicrosoft Research Asia, Technology Transfer Group)的一些实习生写了各

种解法,他们写的简单程序可以达到如图所示的效果。

 

看来这并不是不可能完成的任务。让我们仔细地回想一下写程序时曾经碰到的问题,如

果我们不小心写了一个死循环,CPU 占用率就会跳到最高,并且一直保持 100%。我们也可

以打开任务管理器 ,实际观测一下它是怎样变动的。凭肉眼观察,它大约是 1 秒钟更新一

次。一般情况下,CPU 使用率会很低。但是,当用户运行一个程序,执行一些复杂操作的

时候,CPU 的使用率会急剧升高。当用户晃动鼠标时,CPU 的使用率也有小幅度的变化。

那当任务管理器报告 CPU 使用率为 0 的时候,谁在使用 CPU 呢?通过任务管理器的

程(Process一栏可以看到,System Idle Process 占用了 CPU 空闲的时间——这时候大家该

回忆起在操作系统原理这门课上学到的一些知识了吧。系统中有那么多进程,它们什么时

候能闲下来呢?答案很简单,这些程序或者在等待用户的输入,或者在等待某些事件的发生,

或者进入休眠状态(通过 Sleep()来实现)。生(WaitForSingleObject()

在任务管理器的一个刷新周期内,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 < n; i++);又该如何来估算这个最合适的 n 值呢?

我们都知道 CPU 执行的是机器指令,而最接近于机器指令的语言是汇编语言,所以我们可

以先把这个空循环简单地写成如下汇编代码后再进行分析:

 

假设这段代码要运行的 CPU P4 2.4Ghz2.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 服务器时,

可以选择监视特定的处理器。

 

Perfmon Microsoft .Net Framework

PerformanceCounter()这一类型,从而可以方便地拿到当前各种计算机性能数据,包括 CPU

的使用率。例如下面这个程序——