- 編程之美
- 《編程之美》小組
- 2038字
- 2019-01-10 16:03:56
1.1 讓CPU占用率曲線聽你指揮
寫一個程序,讓用戶來決定Windows任務管理器(Task Manager)的CPU占用率。程序越精簡越好,計算機語言不限。例如,可以實現下面三種情況:
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%。我們也可以打開任務管理器,實際觀測一下它是怎樣變動的。憑肉眼觀察,它大約是1秒鐘更新一次。一般情況下,CPU使用率會很低。但是,當用戶運行一個程序,執行一些復雜操作的時候,CPU的使用率會急劇升高。當用戶晃動鼠標時,CPU的使用率也有小幅度的變化。
那當任務管理器報告CPU使用率為0的時候,是誰在使用CPU呢?通過任務管理器的“進程(Process)”一欄可以看到,System Idle Process占用了CPU空閑的時間——這時候大家該回憶起在“操作系統原理”這門課上學到的一些知識了吧。系統中有那么多進程,它們什么時候能“閑下來”呢?答案很簡單,這些程序或在等待用戶的輸入,或者在等待某些事件的發生,或者主動進入休眠狀態
。
在任務管理器的一個刷新周期內,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”了。
- iOS面試一戰到底
- 華為HMS生態與應用開發實戰
- Python Deep Learning
- Mastering C# Concurrency
- Mastering Ubuntu Server
- Mastering macOS Programming
- Learning Apache Mahout Classification
- Android驅動開發權威指南
- Scratch趣味編程:陪孩子像搭積木一樣學編程
- Unity 5.X從入門到精通
- Python Projects for Kids
- Ext JS 4 Plugin and Extension Development
- XML程序設計(第二版)
- 體驗之道:從需求到實踐的用戶體驗實戰
- Processing開發實戰