注:在Linux系统下实现。
1. 首先我们看看程序运行的结果
上图是开启两个线程CPU占用率的一个曲线,这是一个余弦曲线。
2. 如何计算CPU的使用率计算CPU的使用率我们都是计算一段时间内的CPU使用率,不是计算某个时刻的计算机使用率。
查看/proc/[pid]/stat这个文件,其中[pid]是需要查看的进程的id,我们可以通过ps命令查看。我们打开stat文件,内容大概长下面这个样子,每一项用空格隔开,每个进程项目相同,各个项目的数字不同。
$ cat /proc/16743/stat
16743 (fake_process) S 505 16743 505 34819 16743 1077952512 160 0 0 0 135317 814 0 0 20 0 3 0 802402 22978560 403 18446744073709551615 94252505616384 94252505629301 140722538756160 0 0 0 0 0 0 0 0 0 17 5 0 0 0 0 0 94252505648072 94252505649184 94252509548544 140722538758747 140722538758762 140722538758762 140722538762217 0
文档中具体每一项的含义可以通过以下命令查询(鉴于大家都用手机看帖子,把不必要的内容删了)
$ man proc
.... # 省略/proc/[pid]目录下其它文件说明,我们直接看stat文件
/proc/[pid]/stat
...........
(14) utime %lu
Amount of time that this process has been scheduled in user mode, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). This in‐cludes guest time, guest_time (time spent running a virtual CPU, see below), so that applications that are not aware of the guest time field do not lose that time from their calculations.
(15) stime %lu
Amount of time that this process has been scheduled in kernel mode, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)).
......... #省略其它说明
man proc中详细地说明了每一项的含义。对于CPU的使用率我们主要关心第14项和第15项。utime表示进程从开始运行到目前占用的用户态时间,后面跟的%lu表示这是一个unsigned long类型的字段;stime表示进程从开始运行到目前占用的内核态时间,后面跟的%lu表示这是一个unsigned long类型的字段。这里需要特别注意的是占用时间单位是clock ticks。
clock ticks,使用sysconf(_SC_CLK_TCK)表示一秒内有多少个clock ticks,一般是100,即1秒被分成100个clock ticks,1个clock ticks表示1/100秒。也就是说我们可以用下面公式表示CPU一段时间的使用率
long last = utime stime;
// 间隔一段时间:这里用一秒, 可以任意时间间隔
/* ...... */
int period = 1; // 1秒
long curr = utime stime; // 这里表示重新从/proc/[pid]/stat里读取utime和stime
double ratio = (curr - last) * 1.0 / (period * sysconf(_SC_CLK_TCK)); /* 这里乘1.0是为了获取浮点数*/
比如我们需要让进程在一段时间间隔(我们设为P)内CPU使用率为50%,则我们让其中P/2的时间CPU满负荷运行,让后P/2时间进程让出CPU,则整个时间段内,进程的CPU使用率是50%,如图:
在上图中方案1,让CPU前50%的时间运行后50%时间让出CPU,这样导致进程占用CPU的时间不均匀,这种方式运行出来的CPU占用率曲线出现突刺的概率比较大,曲线不够平滑。方案2中,把一段时间平均分成N份,随机让其中n份运行,这样CPU占用率为n/N,曲线效果比较好。在要求不高的情况下,使用第一种方案比较简单、容易实现。
4. show me the code-代码实现实现思路,首先选择曲线:
一个线程根据曲线公式计算每个时刻(为了方便说明和计算,我们以1秒为一个计算周期)的y的值,y的值即CPU的占用率;然后把1秒平分成100份,然后从1到100,根据某种方式选择其中的y份打开运行开关,其中的100-y份关闭运行开关。比如可以写出如下伪代码,详细代码最后贴出:
void load(int percent) {
// 确保 0 <= percent <= 100
percent = std::max(0, percent);
percent = std::min(100, percent);
for (size_t i = 0; i < 100; i ) {
bool busy = false;
if (i < percent) busy = true;
// g_busy是线程是否运行的开关
g_busy = busy;
// 把一秒分成100份,休眠10ms让其它线程运行
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
其它的一个或者多个线程根据运行开关的开闭来确定自己是否运行。
std::unique_lock<std::mutex> lock(_m);
while (!g_busy) {
g_cond.wait(lock);
}
busy(g_cycles); // 运行
现在我们的重点就要确定我们的曲线和busy函数。
代码中我们实现让CPU使用率是一条余弦曲线,当然自己可以按这个思路实现其它曲线。
余弦曲线
y的取值范围为[-1, 1], x为(-无穷, 无穷) ,而且一个曲线周期是2 * PI。我们用其中x代表我们的时间,y代表CPU的占用率。那么x >= 0, 0 <= y < 100 (100%),且我们可以定义自己的曲线周期。最后的代码是这样的:
for (size_t i = 0; i < 200; i ) { // 以200为周期
// i * 3.141592 / 100 的范围为[0, 2*PI],假设闭区间
// (1.0 cos(i * 3.141592 / 100)) 的范围为[0,2]
// (1.0 cos(i * 3.141592 / 100)) / 2 的范围为[0,1]
// (1.0 cos(i * 3.141592 / 100)) / 2 * max_percent 为[0, max_percent], max_percent为我们设置的最大CPU占用率
// 最后加0.5做精度损失的修正,同时防止出现负数
int percent = static_cast<int>((1.0 cos(i * 3.141592 / 100)) / 2 * g_percent 0.5);
load(percent);
}
在excel里画出来的曲线是这样的:
确定了我们的曲线,然后就是确定我们的运行函数了,运行函数最好最大程度的使用我们的CPU,我们选择开平方根函数,也可以选择其它CPU密集的函数。
for (size_t i = 0; i < cycles; i ) {
result = sqrt(i) * sqrt(i 1);
}
这里有一个难点就是确定cycles的大小,让我们的运行函数尽量运行约10ms时间。
以下是完整的代码,以及在实现过程中遇到的坑。
#include <cmath>
#include <algorithm>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <thread>
#include <atomic>
#include <iostream>
#include <unistd.h>
#include <chrono>
std::mutex _m;
std::condition_variable g_cond;
bool g_busy = false;
std::atomic<int> g_done;
int g_cycles = 0;
int g_percent = 82;
void load(int percent) {
percent = std::max(0, percent);
percent = std::min(100, percent);
// Bresenham's line algorithm !!!
int err = 2 * percent - 100;
int count = 0;
for (size_t i = 0; i < 100; i ) {
bool busy = false;
if (err > 0) {
busy = true;
err = 2 * (percent - 100);
count;
} else {
err = 2 * percent;
}
{
std::unique_lock<std::mutex> lock(_m);
g_busy = busy;
g_cond.notify_all();
}
// 把一秒分成100份
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
double timedifference(std::chrono::steady_clock::time_point start,
std::chrono::steady_clock::time_point end) {
std::chrono::duration<double> d
= std::chrono::duration_cast<std::chrono::duration<double>>(end - start);
return d.count();
}
double busy(int cycles) {
double result = 0;
for (size_t i = 0; i < cycles; i ) {
result = sqrt(i) * sqrt(i 1);
}
return result;
}
void func() {
while (g_done.load() == 0) {
{ // 这里主要是要释放锁,防止某个线程一直占用锁,而主线程无法修改busy
std::unique_lock<std::mutex> lock(_m);
while (!g_busy) {
g_cond.wait(lock);
}
}
busy(g_cycles);
}
}
void cosine() {
while (true) {
for (size_t i = 0; i < 200; i ) { // 以200为周期
int percent = static_cast<int>((1.0 cos(i * 3.141592 / 100)) / 2 * g_percent 0.5);
load(percent);
}
}
}
double get_seconds(int cycles) {
std::chrono::steady_clock::time_point tp = std::chrono::steady_clock::now();
busy(cycles);
return timedifference(tp, std::chrono::steady_clock::now());
}
void find_cycles() {
g_cycles = 800;
while (get_seconds(g_cycles) < 0.01) { // 找到运行约10毫秒的cycle数
g_cycles = g_cycles g_cycles / 4;
}
}
int main(int argc, char const *argv[]) {
std::vector<std::thread> ths;
for (size_t i = 0; i < 2; i ) {
ths.emplace_back(func);
}
find_cycles();
std::cout << "pid : " << ::getpid() << std::endl;
cosine();
g_done = 1;
{
std::unique_lock<std::mutex> lock(_m);
g_busy = true;
g_cond.notify_all();
}
for (auto& t : ths) {
t.join();
}
return 0;
}
代码注意点提示:
- find_cycles函数,这个函数是为了找到使busy运行大约10ms时,需要循环的次数。
- func函数里,锁一定要在busy运行之前释放,如果不释放;可能导致本线程一直获取到锁;即计算曲线函数的线程一直获取不到锁(即我们平时说的线程被饿死),不能改变busy的状态,运行线程一直运行,导致CPU一直100%运行。
- load函数里用了布雷森汉姆直线演算法来开闭运行开关。
这一节写了进程怎么抛出任意CPU占用率的方法,下一节看看怎么把我们的进程参数发送到浏览器客户端展示,倒是我们会用到boost的beast库和asio库。
,