|
寫一個程序,讓用戶來決定Windows任務管理器(Task Manager)的CPU占用率。程序越精簡越好,計算機語言不限。例如,可以實現(xiàn)下面三種情況: 1. CPU的占用率固定在50%,為一條直線; 2. CPU的占用率為一條直線,但是具體占用率由命令行參數(shù)決定(參數(shù)范圍1~ 100); 3. CPU的占用率狀態(tài)是一個正弦曲線。 分析與解法 有一名學生寫了如下的代碼: while (true)
{ if (busy) i++;
else
}
然后她就陷入了苦苦思索:else干什么呢?怎么才能讓電腦不做事情呢?CPU使用率為0的時候,到底是什么東西在用CPU?另一名學生花了很多時間構(gòu)想如何“深入內(nèi)核,以控制CPU占用率”——可是事情真的有這么復雜么? MSRA TTG(Microsoft Research Asia, Technology Transfer Group)的一些實習生寫了各種解法,他們寫的簡單程序可以達到如圖1-1所示的效果。
圖1-1 編碼控制CPU占用率呈現(xiàn)正弦曲線形態(tài) 看來這并不是不可能完成的任務。讓我們仔細地回想一下寫程序時曾經(jīng)碰到的問題,如果我們不小心寫了一個死循環(huán),CPU占用率就會跳到最高,并且一直保持100%。我們也可以打開任務管理器,實際觀測一下它是怎樣變動的。憑肉眼觀察,它大約是1秒鐘更新一次。一般情況下,CPU使用率會很低。但是,當用戶運行一個程序,執(zhí)行一些復雜操作的時候,CPU的使用率會急劇升高。當用戶晃動鼠標時,CPU的使用率也有小幅度的變化。 那當任務管理器報告CPU使用率為0的時候,誰在使用CPU呢?通過任務管理器的“進程(Process)”一欄可以看到,System Idle Process占用了CPU空閑的時間——這時候大家該回憶起在“操作系統(tǒng)原理”這門課上學到的一些知識了吧。系統(tǒng)中有那么多進程,它們什么時候能“閑下來”呢?答案很簡單,這些程序或者在等待用戶的輸入,或者在等待某些事件的發(fā)生(WaitForSingleObject()),或者進入休眠狀態(tài)(通過Sleep()來實現(xiàn))。 在任務管理器的一個刷新周期內(nèi),CPU忙(執(zhí)行應用程序)的時間和刷新周期總時間的比率,就是CPU的占用率,也就是說,任務管理器中顯示的是每個刷新周期內(nèi)CPU占用率的統(tǒng)計平均值。因此,我們寫一個程序,讓它在任務管理器的刷新期間內(nèi)一會兒忙,一會兒閑,然后通過調(diào)節(jié)忙/閑的比例,就可以控制任務管理器中顯示的CPU占用率。 【解法一】簡單的解法 步驟1 要操縱CPU的usage曲線,就需要使CPU在一段時間內(nèi)(根據(jù)Task Manager的采樣率)跑busy和idle兩個不同的loop,從而通過不同的時間 比例,來獲得調(diào)節(jié)CPU Usage的效果。 步驟2 Busy loop可以通過執(zhí)行空循環(huán)來實現(xiàn),idle可以通過Sleep()來實現(xiàn)。 問題的關(guān)鍵在于如何控制兩個loop的時間,方法有二: Sleep一段時間,然后以for循環(huán)n次,估算n的值。 那么對于一個空循環(huán)for(i = 0; i < n; i++);又該如何來估算這個最合適的n值呢?我們都知道CPU執(zhí)行的是機器指令,而最接近于機器指令的語言是匯編語言,所以我們可以先把這個空循環(huán)簡單地寫成如下匯編代碼后再進行分析: loop: mov dx i ;將i置入dx寄存器 inc dx ;將dx寄存器加1 mov i dx ;將dx中的值賦回i cmp i n ;比較i和n jl loop ;i小于n時則重復循環(huán) 假設這段代碼要運行的CPU是P4 2.4Ghz(2.4 * 10的9次方個時鐘周期每秒)。現(xiàn)代CPU每個時鐘周期可以執(zhí)行兩條以上的代碼,那么我們就取平均值兩條,于是讓(2 400 000 000 * 2)/5=960 000 000(循環(huán)/秒),也就是說CPU 1秒鐘可以運行這個空循環(huán)960 000 000次。不過我們還是不能簡單地將n = 60 000 000,然后Sleep(1000)了事。如果我們讓CPU工作1秒鐘,然后休息1秒鐘,波形很有可能就是鋸齒狀的——先達到一個峰值(大于>50%),然后跌到一個很低的占用率。 我們嘗試著降低兩個數(shù)量級,令n = 9 600 000,而睡眠時間相應改為10毫秒(Sleep(10))。用10毫秒是因為它不大也不小,比較接近Windows的調(diào)度時間片。如果選得太?。ū热?毫秒),則會造成線程頻繁地被喚醒和掛起,無形中又增加了內(nèi)核時間的不確定性影響。最后我們可以得到如下代碼: 代碼清單1-1 int main() { for(;;) { for(int i = 0; i < 9600000; i++); Sleep(10); } return 0; } 在不斷調(diào)整9 600 000的參數(shù)后,我們就可以在一臺指定的機器上獲得一條大致穩(wěn)定的50% CPU占用率直線。 使用這種方法要注意兩點影響: 1. 盡量減少sleep/awake的頻率,如果頻繁發(fā)生,影響則會很大,因為此時優(yōu)先級更高的操作系統(tǒng)內(nèi)核調(diào)度程序會占用很多CPU運算時間。 2. 盡量不要調(diào)用system call(比如I/O這些privilege instruction),因為它也會導致很多不可控的內(nèi)核運行時間。 該方法的缺點也很明顯:不能適應機器差異性。一旦換了一個CPU,我們又得重新估算n值。有沒有辦法動態(tài)地了解CPU的運算能力,然后自動調(diào)節(jié)忙/閑的時間比呢?請看下一個解法。 【解法二】使用GetTickCount()和Sleep() 我們知道GetTickCount()可以得到“系統(tǒng)啟動到現(xiàn)在”的毫秒值,最多能夠統(tǒng)計到49.7天。另外,利用Sleep()函數(shù),最多也只能精確到1毫秒。因此,可以在“毫秒”這個量級做操作和比較。具體如下: 利用GetTickCount()來實現(xiàn)busy loop的循環(huán),用Sleep()實現(xiàn)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的循環(huán) while ((GetTickCount() - startTime) <= busyTime) ; //idle loop Sleep(idleTime); } 這兩種解法都是假設目前系統(tǒng)上只有當前程序在運行,但實際上,操作系統(tǒng)中有很多程序都會在不同時間執(zhí)行各種各樣的任務,如果此刻其他進程使用了10% 的CPU,那我們的程序應該只能使用40%的CPU(而不是機械地占用50%),這樣可達到50%的效果。 怎么做呢? 我們得知道“當前CPU占用率是多少”,這就要用到另一個工具來幫忙——Perfmon.exe。 Perfmon是從Windows NT開始就包含在Windows服務器和臺式機操作系統(tǒng)的管理工具組中的專業(yè)監(jiān)視工具之一(如圖1-2所示)。Perfmon可監(jiān)視各類系統(tǒng)計數(shù)器,獲取有關(guān)操作系統(tǒng)、應用程序和硬件的統(tǒng)計數(shù)字。Perfmon的用法相當直接,只要選擇您所要監(jiān)視的對象(比如:處理器、RAM或硬盤),然后選擇所要監(jiān)視的計數(shù)器(比如監(jiān)視物理磁盤對象時的平均隊列長度)即可。還可以選擇所要監(jiān)視的實例,比如面對一臺多CPU服務器時,可以選擇監(jiān)視特定的處理器。
圖1-2 系統(tǒng)監(jiān)視器(Perfmon) 我們可以寫程序來查詢Perfmon的值,Microsoft .Net Framework提供了PerformanceCounter()這一類型,從而可以方便地拿到當前各種計算機性能數(shù)據(jù),包括CPU的使用率。例如下面這個程序—— 【解法三】能動態(tài)適應的解法 代碼清單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使用率參數(shù)。這個程序可以解答前面提到的問題2。 有了前面的積累,我們應該可以讓任務管理器畫出優(yōu)美的正弦曲線了,見下面的代碼。 【解法四】正弦曲線 代碼清單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,上面的程序會出現(xiàn)什么結(jié)果?如何在多個CPU時顯示同樣的狀態(tài)?例如,在雙核的機器上,如果讓一個單線程的程序死循環(huán),能讓兩個CPU的使用率達到50%的水平么?為什么? 多CPU的問題首先需要獲得系統(tǒng)的CPU信息??梢允褂肎etProcessorInfo()獲得多處理器的信息,然后指定進程在哪一個處理器上運行。其中指定運行使用的是SetThreadAffinityMask()函數(shù)。 另外,還可以使用RDTSC指令獲取當前CPU核心運行周期數(shù)。 在x86平臺上定義函數(shù): inline __int64 GetCPUTickCount() { __asm { rdtsc; } } 在x64平臺上定義: #define GetCPUTickCount() __rdtsc() 使用CallNtPowerInformation API得到CPU頻率,從而將周期數(shù)轉(zhuǎn)化為毫秒數(shù),例如: 代碼清單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的周期數(shù),在多CPU系統(tǒng)中,這個周期數(shù)在不同的CPU之間基數(shù)不同,頻率也有可能不同。用從兩個不同的CPU得到的周期數(shù)作計算會得出沒有意義的值。如果線程在運行中被調(diào)度到了不同的CPU,就會出現(xiàn)上述情況??捎肧etThreadAffinityMask避免線程遷移。另外,CPU的頻率會隨系統(tǒng)供電及負荷情況有所調(diào)整。 總結(jié)能幫助你了解當前線程/進程/系統(tǒng)效能的API大致有以下這些: 1. Sleep()——這個方法能讓當前線程“停”下來。 2. WaitForSingleObject()——自己停下來,等待某個事件發(fā)生 3. GetTickCount()——有人把Tick翻譯成“嘀嗒”,很形象。 4. QueryPerformanceFrequency()、QueryPerformanceCounter()——讓你訪問到精度更高的CPU數(shù)據(jù)。 5. timeGetSystemTime()——是另一個得到高精度時間的方法。 6. PerformanceCounter——效能計數(shù)器。 7. GetProcessorInfo()/SetThreadAffinityMask()。遇到多核的問題怎么辦呢?這兩個方法能夠幫你更好地控制CPU。 8. GetCPUTickCount()。想拿到CPU核心運行周期數(shù)嗎?用用這個方法吧。 |
|
|