Ubuntu C++ 实时系统监控指南(网速 & CPU)



面向纯新手,基于 Linux 内核虚拟文件系统(sysfs / procfs),用 C++ 实现“每秒刷新”的实时监控工具。





1. 基础知识:数据从哪来?


Linux 系统把内核的运行状态以虚拟文件的形式暴露给用户。我们只需要像读普通文本文件一样读取这些文件,就能拿到硬件和设备的信息。






















监控项目 数据源路径 作用
实时网速 /sys/class/net/[网卡名]/statistics/ 记录网卡自开机以来收发数据的总字节数(rx_bytes / tx_bytes)。
CPU 使用率 /proc/stat 第一行(cpu 开头)记录 CPU 自开机以来在各种状态下的累计运行时间(时钟滴答数)。


新手提示:/sys/class 按设备功能分类(如 net/ 放网卡),/proc/stat 是内核统计信息的汇总。两者都是实时动态生成的,每次读取都会返回最新数值。





2. 项目一:实时网速监控(单网卡)


2.1 原理说明



  1. 读取 接收字节数(rx_bytes)和 发送字节数(tx_bytes)。

  2. 等待 1 秒,再次读取。

  3. 计算两次读数的差值,即为这一秒内的实时网速(B/s)。

  4. 自动将单位转换为 KB/s 或 MB/s 以方便阅读。


2.2 完整代码(network_speed.cpp)


#include <iostream>
#include <fstream>
#include <string>
#include <unistd.h>
#include <iomanip>

using namespace std;

// 读取指定网卡、指定类型的累计字节数(rx_bytes / tx_bytes)
long long getBytes(const string& interface, const string& type) {
string path = "/sys/class/net/" + interface + "/statistics/" + type;
ifstream file(path);
long long bytes = 0;
if (file.is_open()) {
file >> bytes;
file.close();
}
return bytes;
}

// 将 B/s 格式化为带单位的字符串
string formatSpeed(long long bytesPerSec) {
if (bytesPerSec < 1024) {
return to_string(bytesPerSec) + " B/s";
} else if (bytesPerSec < 1024 * 1024) {
return to_string(bytesPerSec / 1024) + " KB/s";
} else {
return to_string(bytesPerSec / (1024 * 1024)) + " MB/s";
}
}

int main() {
// 【重要】请将 "eth0" 改成你实际的网卡名(用 ifconfig 或 ip a 查看)
string interface = "eth0";

cout << "正在监控网卡: " << interface << endl;
cout << "按 Ctrl+C 退出监控\n" << endl;

// 首次读取,作为基准值
long long prev_rx = getBytes(interface, "rx_bytes");
long long prev_tx = getBytes(interface, "tx_bytes");

while (true) {
sleep(1); // 每秒刷新一次

long long curr_rx = getBytes(interface, "rx_bytes");
long long curr_tx = getBytes(interface, "tx_bytes");

long long rx_speed = curr_rx - prev_rx; // 下载速度
long long tx_speed = curr_tx - prev_tx; // 上传速度

prev_rx = curr_rx;
prev_tx = curr_tx;

// 使用 \r 让光标回到行首,实现原地刷新效果
cout << "\r下载: " << setw(10) << formatSpeed(rx_speed)
<< " 上传: " << setw(10) << formatSpeed(tx_speed) << flush;
}

return 0;
}



3. 项目二:CPU 使用率监控(多核心版)


3.1 原理说明




  1. 读取 /proc/stat 中所有以 cpu 开头的行(第一行是总数据,后面的 cpu0、cpu1 是各核心数据)。




  2. 每行包含 10 个数字,我们只需要前 8 个:



    • [0] user:用户态程序

    • [1] nice:低优先级用户态

    • [2] system:内核态

    • [3] idle:空闲(重要)

    • [4] iowait:等待 I/O(也算空闲)

    • [5] irq:硬中断

    • [6] softirq:软中断

    • [7] steal:被虚拟机偷走




  3. 使用率公式:


    使用率=(总时间增量−空闲时间增量)总时间增量×100%使用率=总时间增量(总时间增量−空闲时间增量)×100%


    其中“空闲时间增量” = idle 增量 + iowait 增量。




3.2 完整代码(cpu_monitor.cpp)


#include <iostream>
#include <fstream>
#include <string>
#include <unistd.h>
#include <iomanip>
#include <sstream>
#include <map>
#include <vector>

using namespace std;

// 读取 /proc/stat 中所有 cpu 开头的行,返回 map(键=核心名,值=时间数组)
map<string, vector<long long>> readCPUTimes() {
map<string, vector<long long>> result;
ifstream file("/proc/stat");
string line;

while (getline(file, line)) {
// 只处理以 "cpu" 开头的行(必须放在开头)
if (line.rfind("cpu", 0) != 0) continue;

stringstream ss(line);
string name;
ss >> name; // 取出开头的 "cpu" / "cpu0" / "cpu1"

// 去掉可能出现的干扰项(虽然一般不会)
if (name != "cpu" && (name.size() < 4 || name[3] < '0' || name[3] > '9')) {
continue;
}

vector<long long> times;
long long val;
while (ss >> val) {
times.push_back(val);
}

// 只要前 8 个(多余的 guest 等忽略)
if (times.size() >= 8) {
times.resize(8);
result[name] = times;
}
}
return result;
}

int main() {
cout << "实时监控 CPU(总体 + 每个核心),按 Ctrl+C 退出\n" << endl;

// 首次读取作为基准
map<string, vector<long long>> prev_data = readCPUTimes();

while (true) {
sleep(1);

map<string, vector<long long>> curr_data = readCPUTimes();

// 清空当前行准备刷新
cout << "\r" << string(120, ' ') << "\r";

bool is_first = true;

// 遍历所有核心(C++98/11 兼容写法)
for (map<string, vector<long long>>::iterator it = curr_data.begin();
it != curr_data.end(); ++it) {

const string& name = it->first;
const vector<long long>& curr_times = it->second;

if (prev_data.find(name) == prev_data.end()) continue;
const vector<long long>& prev_times = prev_data[name];

// 计算总时间增量
long long prev_total = 0, curr_total = 0;
long long prev_idle = 0, curr_idle = 0;

for (int i = 0; i < 8; i++) {
prev_total += prev_times[i];
curr_total += curr_times[i];
}

// 空闲 = idle (下标3) + iowait (下标4)
prev_idle = prev_times[3] + prev_times[4];
curr_idle = curr_times[3] + curr_times[4];

long long total_diff = curr_total - prev_total;
long long idle_diff = curr_idle - prev_idle;

double usage = 0.0;
if (total_diff > 0) {
usage = (double)(total_diff - idle_diff) / total_diff * 100.0;
}

// 显示名称美化
string display_name;
if (name == "cpu") {
display_name = "总";
} else {
display_name = "核" + name.substr(3); // "cpu0" -> "核0"
}

// 用 "|" 分隔不同核心
if (!is_first) {
cout << " | ";
}
is_first = false;

cout << display_name << ": " << fixed << setprecision(1) << usage << "%";

// 更新基准值供下一次循环使用
prev_data[name] = curr_times;
}
cout << flush;
}

return 0;
}



4. 核心知识点详解


4.1 什么是 stringstream?(摘“路牌”工具)


在 CPU 代码中,我们有一行 stringstream ss(line); 和 ss >> name;。



  • 作用:把字符串 "cpu0 50948 0 ..." 变成一个“输入流”,然后可以用 >> 像读 cin 一样逐个单词提取。

  • 为什么需要:因为每行的开头是 cpu0(字符串),后面都是数字。如果不先摘掉开头的字符串,后面的 while(ss >> val) 试图将 cpu0 读成 long long 就会报错。


4.2 /proc/stat 第一行(cpu 行)数值对照表





























































下标 字段名 含义 是否算“空闲”
0 user 用户态普通程序运行时间 ❌
1 nice 低优先级用户态程序时间 ❌
2 system 内核(系统调用、驱动)运行时间 ❌
3 idle 完全闲置(发呆) ✅
4 iowait 等待硬盘/磁盘 I/O(也是闲着) ✅
5 irq 硬中断处理时间 ❌
6 softirq 软中断处理时间 ❌
7 steal 虚拟机偷走的时间 ❌

4.3 为什么网速代码要读 statistics 目录?


因为 /sys/class/net/[网卡]/statistics/ 直接由网卡驱动维护,数据精准且开销极低。无需调用复杂的 ioctl 或 socket API,这是 Linux 系统监控工具(如 nethogs、iftop)最底层的取数方式。




5. 编译与运行指南


5.1 准备工作




  1. 将上面的两段代码分别保存为 network_speed.cpp 和 cpu_monitor.cpp。




  2. 查看自己的网卡名(用于网络监控):


    ip a
    # 或
    ifconfig

    找出类似 eth0、enp0s3、wlp2s0 的设备名,修改 network_speed.cpp 第 23 行的 interface 变量。




5.2 编译


建议使用 C++11 标准(兼容性最好):


# 编译网络监控
g++ -std=c++11 -o network_speed network_speed.cpp

# 编译 CPU 监控
g++ -std=c++11 -o cpu_monitor cpu_monitor.cpp

5.3 运行


# 运行网速监控
./network_speed

# 运行 CPU 监控(另开一个终端)
./cpu_monitor

退出方式:按 Ctrl + C 终止进程。




6. 运行效果示例




  • 网速监控(会原地刷新):


    下载:   1.2 MB/s  上传: 345 KB/s



  • CPU 监控(双核输出示例):


    总: 5.3%  |  核0: 2.1%  |  核1: 8.5%





7. 常见问题(FAQ)


Q1:编译时提示 to_string 未定义?
A:在编译命令中加上 -std=c++11 即可。


Q2:网络监控显示 0 B/s,但明明在下载东西?
A:检查网卡名是否正确。可以用 cat /sys/class/net/你的网卡/statistics/rx_bytes 手动验证是否有数据增长。


Q3:CPU 监控显示所有核心使用率都是 0.0%?
A:这很正常,说明你的电脑非常空闲。打开浏览器或编译大型程序,数值就会跳动起来。


Q4:rfind("cpu", 0) 是什么意思?
A:rfind 是从字符串末尾往前找,但第二个参数传 0 时表示“从位置 0 开始查找”,结合判断 != 0,就是检查字符串是否以 "cpu" 开头(标准库的 rfind 在这里作用同 find,只是写法上为了兼顾老版本 C++)。




8. 进阶思考



  1. 自动检测网卡名:遍历 /sys/class/net/ 目录,跳过名为 lo(本地回环)的虚拟网卡,自动绑定第一个物理网卡。

  2. 显示进程级别的流量:可以结合 netlink 或 libpcap 抓包实现。

  3. 图形化显示:将输出的数据通过管道传给 gnuplot 或 Python 画图。