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