编程之美
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.1 让CPU占用率曲线听你指挥

写一个程序,让用户来决定Windows任务管理器(Task Manager)的CPU占用率。程序越精简越好,计算机语言不限。例如,可以实现下面三种情况当面试的同学听到这个问题的时候,很多人都有点意外。我把我的笔记本电脑交给他们说,这是开卷考试,你可以上网查资料,干什么都可以。大部分面试者在电脑上的第一个动作就是上网搜索“CPU控制50%”这样的关键字,当然没有找到什么直接的结果。不过这本书出版以后,情况可能就不一样了。

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

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

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

分析与解法

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

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

然后她就陷入了苦苦思索:else干什么呢?怎么才能让电脑不做事情呢?CPU使用率为0的时候,到底是什么东西在用CPU?另一名学生花了很多时间构想如何“深入内核,以控制CPU占用率”——可是事情真的有这么复杂吗?

MSRA IEG(Microsoft Research Asia, Innovation Engineering Group)的一些实习生写了各种解法,他们写的简单程序可以达到如图1-1所示的效果。

图1-1 编程控制CPU占用率呈现正弦曲线形态

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

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

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

解法一:简单的解法

要操纵CPU的使用率曲线,就需要使CPU在一段时间内(根据Task Manager的采样率)跑busy和idle两个不同的循环(loop),从而通过不同的时间比例,来调节CPU使用率。

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

问题的关键在于如何控制两个loop的时间,我们先试验一下Sleep一段时间,然后循环n次,估算n的值。

那么对于一个空循环for(i=0; i<n; i++);又该如何来估算这个最合适的n值呢?我们都知道CPU执行的是机器指令,而最接近于机器指令的语言是汇编语言,所以我们可以先把这个空循环简单地写成如下汇编代码(此代码为示意性的伪代码)后再进行分析:

    next:
    mov  eax, dword ptr [i] ;将i放入寄存器
    add  eax,1               ;寄存器加1
    mov  dword ptr [i], eax ;寄存器赋回i
    cmp  eax, dword ptr [n] ;比较i和n
    jl   next                 ; i小于n时重复循环

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

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

代码清单1-1

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

在不断调整9600000的参数后,我们就可以在一台指定的机器上获得一条大致稳定的50% CPU占用率直线。

使用这种方法要注意两点影响:

1.尽量减少sleep/awake的频率,减少操作系统内核调度程序的干扰;

2.尽量不要调用system call(比如I/O这些privilege instruction),因为它也会导致很多不可控的内核运行时间。

该方法的缺点也很明显:不能适应机器差异性。一旦换了一个CPU,我们又得重新估算n值。有没有办法动态地了解CPU的运算能力,然后自动调节忙/闲的时间比呢?请看下一个解法。

解法二:使用GetTickCount()和Sleep()

我们知道GetTickCount()可以得到“系统启动到现在”所经历时间的毫秒值,最多能够统计到49.7天。我们可以利用GetTickCount()来判断busy loop要循环多久,伪代码如清单1-2所示。

代码清单1-2

    const DWORD busyTime=10;            // 10 ms
    const DWORD int idleTime=busyTime; //same ratio will lead to 50% cpu usage
    Int64 startTime=0;
    while(true)
    {
        DWORD startTime=GetTickCount();
        // busy loop
        while((GetTickCount() - startTime) <=busyTime)
            ;
        // idle loop
        Sleep(idleTime);
    }

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

怎么做呢?这就要用到另一个工具来帮忙——Perfmon.exe。

Perfmon是从Windows NT开始就包含在Windows管理工具组中的专业检测工具之一(如图1-2所示)。Perfmon可获取有关操作系统、应用程序和硬件的各种效能计数器(perf counter)。Perfmon的用法相当直接,只要选择你所要检测的对象(比如:处理器、RAM或硬盘),然后选择效能计数器(比如监视物理磁盘的平均队列长度)即可。

图1-2 系统监视器(Perfmon)

我们可以写程序来查询Perfmon的值,Microsoft .Net Framework提供了PerformanceCounter这一对象,可以方便地得到当前各种性能数据,包括CPU的使用率。例如下面这个程序(见代码清单1-3)。

解法三:能动态适应的解法

代码清单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。

解法四:正弦曲线

代码清单1-4

    // C++code to make task manager generate sine graph
    #include "Windows.h"
    #include "stdlib.h"
    #include "math.h"
    //把一条正弦曲线0~2π之间的弧度等分成200份进行抽样,计算每个抽样点的振幅
    //然后每隔300ms的时间取下一个抽样点,并让CPU工作对应振幅的时间
    const int SAMPLING_COUNT=200;  //抽样点数量
    const double PI=3.14159265;    //pi值
    const int TOTAL_AMPLITUDE=300; //每个抽样点对应的时间片
    int _tmain(int argc, _TCHAR* argv[])
    {
        DWORD busySpan[SAMPLING_COUNT];
        int amplitude=TOTAL_AMPLITUDE / 2;
        double radian=0.0;
        double radianIncrement=2.0 / (double)SAMPLING_COUNT; //抽样弧度的增量
        for(int i=0; i<SAMPLING_COUNT; i++)
        {
            busySpan[i]=(DWORD)(amplitude+(sin(PI * radian) * amplitude));
            radian+=radianIncrement;
            // printf("%d\t%d\n", busySpan[i], TOTAL_AMPLITUDE- busySpan[i]);
        }
        DWORD startTime=0;
        for (int j=0; ; j=(j+1) % SAMPLING_COUNT
        {
            startTime=GetTickCount();
            while((GetTickCount() - startTime) <=busySpan[j])
                  ;
            Sleep(TOTAL_AMPLITUDE - busySpan[j]);
        }
        return 0;
    }

讨论

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

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

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

在x86平台上定义函数:

    inline unsigned__int64 GetCPUTickCount()
    {
        __asm
        {
            rdtsc;
        }
    }

在x64平台上定义:

      #define GetCPUTickCount() __rdtsc()

使用CallNtPowerInformation API得到CPU频率,从而将周期数转化为毫秒数,例如代码清单1-5所示。

代码清单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
    unsigned__int64 t_begin=GetCPUTickCount();
    // do something
    unsigned __int64 t_end=GetCPUTickCount();
    double millisec=(double)(t_end-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”了。