官术网_书友最值得收藏!

  • 編程之美
  • 《編程之美》小組
  • 2038字
  • 2019-01-10 16:03:56

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”了。

主站蜘蛛池模板: 临洮县| 依兰县| 博罗县| 洪湖市| 泾阳县| 循化| 团风县| 宜宾市| 临潭县| 乐都县| 兰州市| 吉隆县| 合水县| 晋宁县| 平果县| 南平市| 阜南县| 平定县| 谢通门县| 丰都县| 桓台县| 鄂伦春自治旗| 延吉市| 长白| 神农架林区| 明光市| 宁南县| 通榆县| 隆尧县| 阆中市| 定兴县| 鹰潭市| 株洲县| 长宁县| 龙里县| 象州县| 中超| 六安市| 宁津县| 重庆市| 体育|