编程之美-CPU曲线

本文核心问题就是如何控制CPU运行曲线

基本是基于编程之美的,能扩展多少就扩展多少吧

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

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

先说单核,再说双核
注:现在的计算机基本都是多核的,笔者的计算机就是双核的,为了做实验,先关闭了一个核,win7方法如下:
运行->msconfig 打开系统配置->引导->高级选项->处理器数->1
XP类似,就不说了

1、最简单的方法,就是让CPU歇十毫秒,工作十毫秒(接近CPU系统调度时间)(假设计算机上就我们这一个程序运行)

书上的第一种方法是写一个让CPU循环10ms再sleep 10ms的程序, 该方法要估算循环多少次才能 CPU循环10ms,我的电脑Intel(R) Core(TM)2 Duo CPU T6570,2.10GHz,按每个时钟周期两条代码计。

(为什么是10ms呢,可见解释如下:所谓cpu使用率就是任务管理器检测cpu使用情况的显示,其值为 (CPU忙碌时间/总时间),其刷新周期为1s,所以就是要让cpu在这1s内闲忙时间参半,此时如果时间设置太长,如1s,显然,会出现锯齿形的cpu使用率,而时间设置太短则会造成线层的频繁唤醒和挂起,增加内核时间的不确定性)

一个循环的汇编代码如下

loop
mov dx i     
inc dx          
mov dx i       
cmp i n       
jl loop  

可求得n=(1/100)*(2100,000,000*2/5)=8400000
故循环8400000次(…我电脑好搓啊),再睡10ms,代码如下

//C++
#include "stdafx.h"
#include "stdio.h"

int _tmain(int argc, _TCHAR* argv[])
{
 int i;
 while(true)
 {
  for(i=0;i<840000;++i)
   ;
  Sleep(10);
 }
 return 0;
}

在尽可能的关闭了电脑运行的进程的情况下,运行上述程序,可得到如下效果
170%

这个CPU的使用率是70%,虽然不是想象中的50%,但也算的上是接近直线了,
所以我们离目标又进了一步。至于为什么是70%呢,大概是因为系统中还有一些没有关掉甚至无法关掉的进程在执行占用了一定的cpu使用率吧,在做实验的时候,笔者不小心把循环次数少写了一个0,此时获得的 cpu使用率如下:
220%

这是一个20%左右的使用曲线,由于循环降了一个数量级,我们的程序的cpu使用率也就从原来的10ms/(10ms+10ms)=50%降到了1ms/(1ms+10ms)=9.09%
综合这两个使用率来看,系统中其他进程所占用的CPU大概在15%~20%左右,然后我们小心的不断调整循环次数,最终,可达到下图所示的效果
3
这基本就是50%的使用率了

缺陷:这个方法由于太简单了(把操作系统也想的太简单了),所以限制很多,比如只能有这一个进程在运行,不同的计算机需要配置不同的循环数,而且随便一个进程都会影响到该程序效果(而根本就做不到就让这一个程序运行的效果嘛,首先,很多系统进程是很重要的,其次,大多数程序的编写原则都是尽量让cpu发挥最大能力运作,而不是让他歇着,所以很多程序也是这样写的,即在cpu不忙的时候,进程就不运行,给其他进程让道,但cpu一闲下来,他就加紧干活,很多系统进程是这样的话,我们的程序就很不好使了)

2、第二种方法也很简单,还是让CPU歇十毫秒,工作十毫秒(假设计算机上就我们这一个程序运行),但用到了两个api获取时间,就不需要考虑到底多少次循环才能占够10ms的时间了,这解决了第一个方法中的一个缺陷

这里用到了GetTickCount()函数和Sleep()函数

GetTickCount()函数是获得系统启动到现在的时间,返回单位为毫秒,类型为uint,精度为1ms,但看网上说实际误差为15ms左右,笔者没有验证,但应该是对的,因为可以查到,windows时间片是15ms~20ms 缺点:由于返回值是uint,最大值是2的32次方,因此如果服务器连续开机大约49天以后,该方法取得的返回值会归零

Sleep()函数(该函数需要”windows.h”头文件)
使用:sleep(1000),在Windows和Linux下1000代表的含义并不相同,Windows下的表示1000毫秒,也就是1秒钟;Linux下表示1000秒,Linux下使用毫秒级别的函数可以使用usleep。
原理:sleep函数是使调用sleep函数的线程休眠,线程主动放弃时间片。当经过指定的时间间隔后,再启动线程,继续执行代码。Sleep函数并不能起到定时的作用,主要作用是延时。在一些多线程中可能会看到sleep(0);其主要目的是让出时间片。
精度:sleep函数的精度非常低,当系统越忙它精度也就越低,有时候我们休眠1秒,可能3秒后才能继续执行。它的精度取决于线程自身优先级、其他线程的优先级,以及线程的数量等因素。

代码:

//C++
#include "stdafx.h"
#include "stdio.h"
#include "windows.h"

const DWORD busy =10;

int _tmain(int argc, _TCHAR* argv[])
{
 while(true)
 {
  DWORD start=GetTickCount();
  while((GetTickCount()-start)<=busy)
   ;
  Sleep(busy);
 }
 return 0;
}

4
和第一个方法基本一样,接近直线,略带小锯齿卖萌,不过物理内存使用率真是出奇的直啊。。

缺陷:虽然解决了第一个方法中的第二个问题,但是没有解决计算机上有其他进程运行的问题

3、第三个方法,用到了perfmon.exe工具,一个从windowsNT开始就包含在windows系统中的工具,用法么,命令行敲perfmon就出来了

这个方法的意思就是,既然系统中有其他程序运行,那么我们就监测其他程序对cpu的使用率,如果超过50%,就歇一会,低于50%就忙一会,其中用于监测的工具就是perfmon.exe工具,书中是在.net环境下调用他的,我也就用.net了(后来查了一下,performancecounter这类的api好像在c++中是不能使用的,至少低版本的c++中是不能使用的,网上有说修改windows.h头文件的,笔者试过,未成功,敬请高手赐教)

代码如下:

//C #
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
namespace CPU_Line_C_sharp
{
    class Program
    {
        static void Main(string[] args)
        {
            PerformanceCounter p = new PerformanceCounter("Processor", "% Processor Time", "_Total");
            while (true)
            {
                float i = p.NextValue();
                if (i > 50)
                    System.Threading.Thread.Sleep(10);
                else if (i < 50)
                {
                    int start = System.Environment.TickCount & Int32.MaxValue;
                    while ((System.Environment.TickCount - start) <= 10)
                        ;
                }   

            }
        }
    }
}

原本是看书学习么,原本也是类书上的那个代码,不过运行之后,cpu使用率一直低于50%,后来感觉书上的代码只解决了系统进程多,cpu使用率基本高于50%的情况,但是当cpu使用率本来就低于50%时是不作处理的,这样并不能实现cpu使用率50%的效果,于是增加了一段cpu使用率小于50%的处理,用了一个c#下的系统属性System.Environment.TickCount,相当于C++下面的GetTickCount()

运行以上代码可以获得以下效果
5
如果和前两个方法的图放在一起比较的话,可以看出这个图还是好了很多的,浮动误差基本是1%左右,更重要的是,具有适应性,开几个程序无所谓,cpu使用率岿然不动!

但是,可以看到,仍然有卖萌的小锯齿存在!虽然说可能这是不可避免的,但是毕竟其和我们初期幻想的cpu一条笔直的线还是有差距的,必须继续努力,于是有了下面一段代码

主要拿两个函数开刀,一个是Sleep(),一个是System.Environment.TickCount,因为因为这两个函数在精度方面的名声不是很好,所以调整他们的精度(感谢网络上无所不会的大神们!!!)

下边的代码调整了精度,并且设置成了根据读入数据确定百分比的情况,也就是题目中第二个问题

//C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace CPU_Line_C_sharp
{
    class Program
    {
        [DllImport("winmm")]
        static extern void timeBeginPeriod(int t);

        [DllImport("winmm")]
        static extern void timeEndPeriod(int t);

        static void Main(string[] args)
        {
            PerformanceCounter p = new PerformanceCounter("Processor", "% Processor Time", "_Total");
            float n;
            String s = System.Console.ReadLine();
            n =(float) System.Convert.ToDouble(s);
            int k = (int) (100 - n);
            //System.Console.WriteLine(n.ToString());
            while (true)
            {
                timeBeginPeriod(1);
                float i = p.NextValue();
                if (i > n)
                    System.Threading.Thread.Sleep(k);
                else if (i < n)
                {
                    int start = System.Environment.TickCount & Int32.MaxValue;
                    while ((System.Environment.TickCount - start) <= n)
                        ;
                }
                timeEndPeriod(1);
            }
        }
    }
}

这个代码又是一个和书上代码有较大差异的,因为书上的代码其实只是一个满足让cpu使用率50%的代码,当输入其他数值时,基本是实现不了的,都会趋近于50%,哪怕你输入的是10%。笔者感觉主要问题是出在了该代码不论是输入多少,大于该值时一概随眠10ms,小于该值时一概工作10ms。(感觉该代码其实不论其他进程是什么样子的,睡眠和工作出现的概率相差并不大,所以睡眠和工作的耗时设置还是很重要的) 所以修改代码就成了这个样子,使随眠时间和工作时间与输入值相关联 效果如下

输入为80
6
输入为20
7
输入为10
8

本来以为输入越小偏差越大呢(n是后输入的,存在内存中,程序涉及不断地寻址访存操作影响了cpu使用率,那么输入越小,显然,影响效果就应该更明显啊),结果看起来输入为10的情况要好于输入为20的情况,笔者猜是由于输入为10时,保证了绝大部分时候是调用的Sleep()分支,提高了一定的程序稳定性

(输入为80的时候,cpu使用率无比稳定地呆在了85的位置,很囧啊)

4、忙活了大半天,直线终于搞定了,泪奔啊,下面进攻正弦曲线吧

有了前面的积累,其实正弦曲线已经不在话下了,既然我们想让CPU使用率维持在什么值,就可以维持在什么值,那么此时,CPU曲线呈什么型,已经完全有我们决定了

既然cpu使用率是1s刷新一次,那么我们只要在某一范围内,使cpu在某一特定点的使用频率恰为该点的sin对应值就可以了,把一个循环分为200份(书上也是这么分的,应该是因为sin函数的max-min值为2的缘故吧,同时可得到在一分钟内,分为200段,1*60*1000/200=300ms),每一份对应于1s钟,输出对应的sin函数值

(书上,包括网上有很多相关代码,都是C++实现的,笔者试过,是可以实现的,但是书上的代码并没有应用到perfmon什么的,也就是没有监测其他进程,笔者想,既然第三种方法代码已经写好了,何不借来用用,就写了一个C#下的,语言不同,原理一致)

代码如下:

//C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace CPU_Line_Sin_C_sharp
{
    class Program
    {
        [DllImport("winmm")]
        static extern void timeBeginPeriod(int t);

        [DllImport("winmm")]
        static extern void timeEndPeriod(int t);
        const double PI = 3.14159265;
        const int INTERVAL = 300;
        const double WIDTH = 200;
        const double HIGH = 1;
        static void Main(string[] args)
        {
            PerformanceCounter p = new PerformanceCounter("Processor", "% Processor Time", "_Total");
            double split = 2 / WIDTH;
            int []busySpan=new int[(int)WIDTH];
             int []idleSpan=new int[(int)WIDTH];
             int half = INTERVAL/2;
             double radian = 0.0;
             double rate, k;
             for(int i=0;i rate)
                    System.Threading.Thread.Sleep(idleSpan[j]);
                else if (k < rate)
                {
                    int start = System.Environment.TickCount & Int32.MaxValue;
                    while ((System.Environment.TickCount - start) <= busySpan[j])
                        ;
                }
                timeEndPeriod(1);
            }
        }
    }
}

如上代码可实现如下效果:
9
通过修改WIDTH和HIGH宏,可以对曲线拉伸如下:
10 11

5、终于,终于,闯过了单核的岗哨,我们可以向多核的堡垒进攻了,首先,把cpu双核打开,关机,重启。。

首先还是画直线(直线会了,曲线就不是事了),很多人说,双核CPU只要写个死循环就可以让cpu使用率达到50%,使CPU曲线呈直线状,于是笔者以迅雷不及掩耳盗铃之势写了一个死循环,运行效果如下:
12
问题很显然了,cpu使用率确实达到了50%的水平,但是cpu曲线却没有停留在50%,显然两个核的分配率很不一样,这种单纯的死循环,只会累死一个cpu的。

所以写一个是不行的,那就写两个吧,用双线程试一下 代码如下:

//C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading;

namespace CPU_Line_Sin_Multi
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread one, two;
            one = new Thread(new ThreadStart(Run));
            two = new Thread(new ThreadStart(Run));
            one.Start();
            two.Start();
        }
        static void Run()
        {
            PerformanceCounter p = new PerformanceCounter("Processor", "% Processor Time", "_Total");
            while (true)
            {
                float i = p.NextValue();
                if (i > 50)
                    System.Threading.Thread.Sleep(10);
                else if (i < 50)
                {
                    int start = System.Environment.TickCount & Int32.MaxValue;
                    while ((System.Environment.TickCount - start) <= 10)
                        ;
                }

            }

        }
    }
}

效果如下:
13

这说明两个cpu确实不是平均分配任务的,我们必须对每一个CPU单独处理了
那么就需要学习和运用书上给出的还有书上没给出的一些函数了,如下:

GetSystemInfo()
声明:
void WINAPI GetSystemInfo(
__out LPSYSTEM_INFO lpSystemInfo
);
用法:
就是你创建一个SYSTEM_INFO对象,通过调用 ::GetSystemInfo(&sysInfo);,就可以返回你想要的很多系统信息了
主要包括:
CPU个数: sysInfo.dwNumberOfProcessors
内存分页大小: sysInfo.dwPageSize
CPU类型: sysInfo.dwProcessorType
CPU架构: sysInfo.wProcessorArchitecture
CPU的级别: sysInfo.wProcessorLevel
CPU的版本: sysInfo.wProcessorRevision
GetProcessorInfo()
SetThreadAffinityMask()
WaitForSingleObject()
WaitForMultipleObjects()
RDTSC
GetCurrentThread()

学习了如上这些函数,可以习得一下能力

  1. 获取系统信息(包括CPU)
  2. 指定线程在哪一个CPU上运行
  3. 获得当前线程句柄
  4. 等待线程执行
  5. 等等

应用这些能力,已经可以较好的控制cpu了,可获得代码如下

//C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading;
using System.Runtime.InteropServices;
using System.Management;

namespace CPU_Line_Sin_Multi
{
    //Struct to retrive system info
    [StructLayout(LayoutKind.Sequential)]
    public struct SYSTEM_INFO
    {
        public uint dwOemId;
        public uint dwPageSize;
        public uint lpMinimumApplicationAddress;
        public uint lpMaximumApplicationAddress;
        public uint dwActiveProcessorMask;
        public uint dwNumberOfProcessors;
        public uint dwProcessorType;
        public uint dwAllocationGranularity;
        public uint dwProcessorLevel;
        public uint dwProcessorRevision;
    }
    class Indirect
    {
        [DllImport("kernel32.dll")]
        static extern IntPtr GetCurrentThread();
        [DllImport("kernel32.dll")]
        static extern UIntPtr SetThreadAffinityMask(IntPtr hThread, UIntPtr dwThreadAffinityMask);
        int x;
        public Indirect(int xx) { x = xx; }
        public void IndirectRun() { Run(x); }
        public void Run(int a)
        {
            SetThreadAffinityMask(GetCurrentThread(), (UIntPtr)(a + 1));
            PerformanceCounter p = new PerformanceCounter("Processor", "% Processor Time", "_Total");
            while (true)
            {
                float i = p.NextValue();
                if (i > 50)
                    System.Threading.Thread.Sleep(10);
                else if (i < 50)
                {
                    int start = System.Environment.TickCount & Int32.MaxValue;
                    while ((System.Environment.TickCount - start) <= 10)
                        ;
                }

            }
        }

    } 
    class Program
    {
        [DllImport("kernel32")]
        static extern void GetSystemInfo(ref SYSTEM_INFO pSI);
        [DllImport("kernel32.dll")]
        static extern uint WaitForMultipleObjects(uint nCount, IntPtr [] lpHandles,bool bWaitAll,uint dwMilliseconds);
        static void Main(string[] args)
        {

            SYSTEM_INFO info = new SYSTEM_INFO();
            GetSystemInfo(ref info);
            System.Console.WriteLine("处理器数目:"+info.dwNumberOfProcessors.ToString());
            int num = (int)info.dwNumberOfProcessors;
            Thread[] mythread= new Thread[num];
            for (int i = 0; i < num;++i)
            {
                Indirect temp = new Indirect(i);
                mythread[i] = new Thread(new ThreadStart(temp.IndirectRun));
                mythread[i].Start();
            }
        }
    }
}

其中有两个问题需要注意

  1. System_Info需要自己写一个数据结构
  2. ThreadStart函数不能调用带参函数,MSDN的建议是间接调用

如上代码可获得如下效果
14

简单修改函数,将直线函数改成正弦曲线,代码如下:

//C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading;
using System.Runtime.InteropServices;
using System.Management;

namespace CPU_Line_Sin_Multi
{
    //Struct to retrive system info
    [StructLayout(LayoutKind.Sequential)]
    public struct SYSTEM_INFO
    {
        public uint dwOemId;
        public uint dwPageSize;
        public uint lpMinimumApplicationAddress;
        public uint lpMaximumApplicationAddress;
        public uint dwActiveProcessorMask;
        public uint dwNumberOfProcessors;
        public uint dwProcessorType;
        public uint dwAllocationGranularity;
        public uint dwProcessorLevel;
        public uint dwProcessorRevision;
    }
    class Indirect
    {
        [DllImport("kernel32.dll")]
        static extern IntPtr GetCurrentThread();
        [DllImport("kernel32.dll")]
        static extern UIntPtr SetThreadAffinityMask(IntPtr hThread, UIntPtr dwThreadAffinityMask);
        [DllImport("winmm")]
        static extern void timeBeginPeriod(int t);
        [DllImport("winmm")]
        static extern void timeEndPeriod(int t);

        int x;
        public Indirect(int xx) { x = xx; }
        public void IndirectRun() { Run(x); }
        public void Run(int a)
        {
            SetThreadAffinityMask(GetCurrentThread(), (UIntPtr)(a + 1));
            PerformanceCounter p = new PerformanceCounter("Processor", "% Processor Time", a.ToString());
            const double PI = 3.14159265;
            const int INTERVAL = 300;
            const double WIDTH = 200;
            const double HIGH = 1;
            double split = 2 / WIDTH;
            int[] busySpan = new int[(int)WIDTH];
            int[] idleSpan = new int[(int)WIDTH];
            int half = INTERVAL / 2;
            double radian = 0.0;
            double rate, k;
            for (int i = 0; i < WIDTH; i++)
            {
                busySpan[i] = (int)(half + (System.Math.Sin(PI * radian) * half * HIGH));
                idleSpan[i] = INTERVAL - busySpan[i];
                radian += split;
            }
            //System.Console.WriteLine(n.ToString());
            for (int j = 0; ; j = (j + 1) % (int)WIDTH)
            {
                timeBeginPeriod(1);
                rate = busySpan[j] / 3;
                k = p.NextValue();
                if (k > rate)
                    System.Threading.Thread.Sleep(idleSpan[j]);
                else if (k < rate)
                {
                    int start = System.Environment.TickCount & Int32.MaxValue;
                    while ((System.Environment.TickCount - start) <= busySpan[j])
                        ;
                }
                timeEndPeriod(1);
            }

        }

    } 
    class Program
    {
        [DllImport("kernel32")]
        static extern void GetSystemInfo(ref SYSTEM_INFO pSI);
        [DllImport("kernel32.dll")]
        static extern uint WaitForMultipleObjects(uint nCount, IntPtr [] lpHandles,bool bWaitAll,uint dwMilliseconds);
        static void Main(string[] args)
        {

            SYSTEM_INFO info = new SYSTEM_INFO();
            GetSystemInfo(ref info);
            System.Console.WriteLine("处理器数目:"+info.dwNumberOfProcessors.ToString());
            int num = (int)info.dwNumberOfProcessors;
            Thread[] mythread= new Thread[num];
            for (int i = 0; i < num;++i)
            {
                Indirect temp = new Indirect(i);
                mythread[i] = new Thread(new ThreadStart(temp.IndirectRun));
                mythread[i].Start();
            }
        }
    }
}

其中分别监视了每一个核的CPU,如上代码可获得如下效果
15
来张大的
16
很明显,双核下面正弦曲线已经没有那么完了
通过改变参数,也可以让两个核不一样,比如,只让其中一个核画正弦曲线如下:
17

到这里,控制cpu曲线的学习就算告一段落了,花了一些时间,学习和写了一些东西,截图留念
18
19

参考

期间参考无数大牛博客,记录如下(可从以下网址获得更多相关知识)(注意一下,其中最后一篇最好)
http://blog.zjol.com.cn/173915/viewspace-308680
http://www.cnblogs.com/tewuapple/archive/2012/01/04/2312579.html
http://hi.baidu.com/robinlxzh/blog/item/ba4fb57ec15c5d310dd7da25.html
http://www.cnblogs.com/xd125/archive/2007/12/12/992406.html
http://www.cnblogs.com/freebird92/articles/846520.html
http://www.cnblogs.com/gisoracle/archive/2010/06/11/1756040.html
http://www.cnblogs.com/cnmaizi/archive/2011/01/17/1937772.html
http://blog.csdn.net/solstice/article/details/5196544
http://nanhaochen.blog.51cto.com/228629/60043
http://blog.csdn.net/caimouse/article/details/1887370
http://blog.csdn.net/wesweeky/article/details/6402564
http://hi.baidu.com/wyxustcsa09/blog/item/07ab0e5307042214367abeba.html
http://noulizang2004.blog.163.com/blog/static/98195252009731545490/
http://www.cnblogs.com/xumingming/archive/2008/10/10/1308248.html
http://chris.blog.51cto.com/112473/29285
http://blog.csdn.net/lanzhengpeng2/article/details/2401554
http://www.shenjk.com/detail/624
http://xiaoqq.blog.51cto.com/3484427/729132
http://blog.csdn.net/ulark/article/details/5131014
http://www.cnblogs.com/SSatyr/
http://www.cnblogs.com/jintianhu/archive/2010/09/01/1815031.html
http://www.cnblogs.com/flyinghearts/archive/2011/03/22/1991965.html