最近我要在公司的一个study group负责AWS的AutoScaling功能的介绍。AWS可以根据instance(虚拟机)的CPU使用量进行scaling。
为了做demo,于是就有这样一个需求:让instance上的CPU听我指挥,当然简单的方法就是写一个死循环,让CPU 100%。但如果make things more interesting,希望实现CPU在某个范围内变化又要怎么做哩?
之前看过,邹欣大神的书《编程之美》,其中第一个问题就是“让CPU占有率曲线听你指挥”,里面提到了一些解法,更有甚者,做到了能让CPU占有率曲线按正弦函数波动。我和同事大神Jason san中午吃饭时聊了这个需求,想不到他下午就分别用C++和Python实现了一种动态适应的解决方法。以下我们就来讨论下这个有趣问题的解法:
首先书上提到一个简单方法让CPU维持在50%:
让CPU在一段时间内(根据Task Manager的采样率)跑busy和idle两个不同的循环,从而通过不同的时间比例,来调节CPU使用率。
那么对于一个空循环
for(i = 0; i < n; i++);
又该如何来估算这个最合适的n的值呢?首先我们把这个空循环简单写成汇编代码(伪代码):
1 next: 2 mov eax, dword ptr[i]; i放入寄存器 3 add exa, 1; 寄存器加1 4 mov dword ptr [i], eax; 寄存器赋回i 5 cmp eax, dword ptr[n]; 比较i和n 6 jl next; i小于n时重复循环
假设这段代码要运行的CPU是P4,主频是2.4Ghz(2.4*10的9次方个时钟周期每秒)。现代CPU每个时钟周期可以执行两条以上的代码,我们取平均值两条,于是有
(2 400 000 000*2)/5 = 960 000 000 (循环/秒)(这边除以5是因为上面汇编代码有5条汇编语句,尼玛书上没有说清楚,让我这样的小白想了好久……),也就是说CPU每一秒钟可以运行这个空循环960 000 000次。不过我们还是不能简单的将n=960 000 000,然后Sleep(1000)了事。如果我们让CPU工作1秒钟,然后休息1秒钟,波形很有可能就是锯齿状的一样先到达峰值(>50%),然后跌倒一个很低的占用率。
我们尝试着降低两个数量级,令n = 9 600 000,而睡眠时间则相应的改为10毫秒。代码清单如下:
int main() { for(; ;) { for(int i = 0; i < 9600000; i++) ; sleep(10) } return 0; }
再不断调整参数后,就能得到一条大致稳定的CPU占有率直线。
但是这个方法最大的问题是,参数都是根据特定机器算出来的。在其他机器上要重新调整,所以需要有更加智能的方法。
之后书中给出了另外两个解法:
解法二:使用了系统API:GetTickCount()和Sleep()
解法三:使用工具Perfmon.exe
具体我就不列出来了,大家有兴趣去看书吧。(我这样给这本书打广告了,大神不会告我侵权了吧……^_^)
但是,这些方法都没有考虑到多核和多CPU的情况,书中提到对于多CPU的问题首先需要获得系统的CPU信息。可以使用GetProcessorInfo()获得多处理器的信息,然后指定进程在哪一个处理器上运行。其中指定运行使用的是SetThreadAffinityMask()函数。
另外,还可以使用RDTSC指令获取当前CPU核心运行周期数。
在x86平台定义函数:
inline unsigned_int64 GetCPUTickCount()
{
_asm
{
rdtsc;
}
}
在x64平台上定义:
#define GetCPUTickCount()_rdtsc()
使用CallNtPowerInformation API得到CPU频率,从而将周期数转化为毫秒数。(这边这段有点不知所云了……)
这边我给出两段代码,分别用C++和Python实现,通过动态获取CPU的状态,调整线程的数量来实现让CPU保持在某一个值,且考虑了多CPU情况。
1 #define _WIN32_WINNT 0x0502 2 3 #include <cstdio> 4 #include <cstdlib> 5 #include <ctime> 6 7 #include <Windows.h> 8 #include <process.h> 9 10 #define TARGET_CPU_RATE (80.0) 11 12 extern "C" { 13 typedef struct _CONTROL_PARAM 14 { 15 volatile LONG m_exit; 16 volatile LONGLONG m_rest; 17 } CONTROL_PARAM; 18 }; 19 20 static CONTROL_PARAM g_param; 21 22 unsigned __stdcall task_thread(void *pparam) 23 { 24 if (!pparam) 25 return 0; 26 27 CONTROL_PARAM *pctrl = (CONTROL_PARAM *)pparam; 28 29 LONGLONG rest64 = 0; 30 while (true) 31 { 32 if (rest64 > pctrl->m_rest) 33 { 34 Sleep(1); 35 rest64=0; 36 } 37 else 38 rest64++; 39 } 40 41 return 0; 42 } 43 44 inline unsigned __int64 u64_time(FILETIME &_ft) 45 { 46 unsigned __int64 u64; 47 u64 = _ft.dwHighDateTime; 48 u64 = u64 << 32; 49 u64 |= _ft.dwLowDateTime; 50 return u64; 51 } 52 53 int main() 54 { 55 SYSTEM_INFO sys_info; 56 ZeroMemory(&sys_info, sizeof(SYSTEM_INFO)); 57 58 GetSystemInfo(&sys_info); 59 int cpu_cnt = (int)sys_info.dwNumberOfProcessors; 60 61 if (0 == cpu_cnt) 62 cpu_cnt = 1; 63 printf("cpu count: %d\n", cpu_cnt); 64 65 g_param.m_rest = (DWORD)-1; 66 for (int i=0; i<cpu_cnt; ++i) 67 { 68 _beginthreadex(NULL,0,task_thread,&g_param,0,NULL); 69 } 70 71 FILETIME idleTime; 72 FILETIME kernelTime; 73 FILETIME userTime; 74 75 FILETIME last_idleTime; 76 FILETIME last_kernelTime; 77 FILETIME last_userTime; 78 79 bool initialized = false; 80 81 while (true) 82 { 83 if (GetSystemTimes(&idleTime,&kernelTime,&userTime)) 84 { 85 if (initialized) 86 { 87 unsigned __int64 usr = u64_time(userTime) - u64_time(last_userTime); 88 unsigned __int64 ker = u64_time(kernelTime) - u64_time(last_kernelTime); 89 unsigned __int64 idl = u64_time(idleTime) - u64_time(last_idleTime); 90 91 double sys = ker + usr; 92 double cpu = (sys - (double)idl) / sys * 100.0; 93 94 double dif = TARGET_CPU_RATE - cpu; 95 g_param.m_rest = (LONGLONG)((double)g_param.m_rest * (1.0 + dif/100.0)); 96 97 printf("rest = %I64d, cpu = %d\n", g_param.m_rest, (int)cpu); 98 } 99 else 100 initialized = true; 101 102 last_idleTime = idleTime; 103 last_kernelTime = kernelTime; 104 last_userTime = userTime; 105 } 106 107 Sleep(300); 108 } 109 110 return getchar(); 111 }
Python程序:
1 from ctypes import windll, Structure, byref, c_longlong 2 from ctypes.wintypes import DWORD 3 4 import win32api 5 import multiprocessing 6 7 WinGetSystemTimes = windll.kernel32.GetSystemTimes 8 9 class FILETIME(Structure): 10 _fields_ = [ ("dwLowDateTime", DWORD), ("dwHighDateTime", DWORD) ] 11 12 def pyGetSystemTimes(): 13 idleTime, kernelTime, userTime = FILETIME(), FILETIME(), FILETIME() 14 WinGetSystemTimes(byref(idleTime), byref(kernelTime), byref(userTime)) 15 return (idleTime, kernelTime, userTime) 16 17 def longTime(ft): 18 tm = ft.dwHighDateTime 19 tm = tm << 32 20 tm = tm | ft.dwLowDateTime 21 return tm 22 23 TARGET_CPU_RATE = 70.0 24 25 def worker(val): 26 rest = 0 27 while (True): 28 if rest > val.value: 29 rest = 0 30 win32api.Sleep(1) 31 else: 32 rest += 1 33 34 if __name__ == '__main__': 35 sys_info = win32api.GetSystemInfo() 36 cpu_cnt = sys_info[5] 37 38 val = multiprocessing.Value(c_longlong, 100, lock=False) 39 print type(val.value) 40 41 threads = [] 42 for i in range(cpu_cnt): 43 p = multiprocessing.Process(target=worker, args=(val,)) 44 p.start() 45 threads.append(p) 46 47 initialized = False 48 last_times = (FILETIME(), FILETIME(), FILETIME()) 49 while (True): 50 cur_times = pyGetSystemTimes() 51 52 if initialized: 53 usr = longTime(cur_times[2]) - longTime(last_times[2]) 54 ker = longTime(cur_times[1]) - longTime(last_times[1]) 55 idl = longTime(cur_times[0]) - longTime(last_times[0]) 56 57 sys = float(ker + usr) 58 cpu = (sys - float(idl)) / sys * 100.0; 59 60 dif = TARGET_CPU_RATE - cpu; 61 val.value = long(float(val.value) * (1.0 + dif / 100.0)); 62 63 print "rest=", val.value, ", cpu=", int(cpu) 64 else: 65 initialized = True 66 67 last_times = cur_times 68 win32api.Sleep(300)