直接读取/proc/net/dev可获取实时网卡流量,无需root权限;需正确解析接口名(处理冒号)和字段偏移(第17位rx_bytes、第25位tx_bytes),推荐原子写入带纳秒时间戳的日志。

直接读取 /proc/net/dev 文件就能拿到实时流量,无需 root 或额外权限
Linux 内核通过 /proc/net/dev 暴露网卡收发字节数(rx_bytes 和 tx_bytes),格式固定、无延迟、不依赖外部工具。C++ 只需用 std::ifstream 逐行解析,注意跳过前两行表头和空格分隔不一致的问题。
常见错误是直接用 operator>> 按空格切分——但第一列接口名后可能有多个空格,且某些发行版(如 CentOS 7)在 lo 后带冒号(lo:),导致字段偏移错乱。正确做法是先 std::getline 整行,再用 find_first_of(' ') + find_first_not_of(' ') 定位首尾空白,提取接口名;之后用 std::stringstream 配合 ignore() 跳过前 16 个数字字段(含 rx/tx 的 8 个计数器),第 17 个才是 rx_bytes,第 25 个是 tx_bytes。
示例关键片段:
std::string line;
while (std::getline(file, line)) {
auto start = line.find_first_not_of(" \t");
if (start == std::string::npos || line[start] == '\0') continue;
auto end = line.find_first_of(" \t", start);
std::string iface = line.substr(start, end - start);
if (iface != "eth0" && iface != "ens33" && iface != "wlan0") continue; // 按需过滤
std::stringstream ss(line.substr(end));
uint64_t dummy, rx_bytes, tx_bytes;
// 忽略前 16 个字段(rx_packets 到 rx_compressed),取第 17(rx_bytes)和第 25(tx_bytes)
for (int i = 0; i < 16; ++i) ss >> dummy;
ss >> rx_bytes;
for (int i = 0; i < 7; ++i) ss >> dummy;
ss >> tx_bytes;
}
fopen + sscanf 更轻量,但要注意字段对齐和接口名末尾冒号
比 std::ifstream 开销更低,适合高频采样(如每秒一次)。问题在于 /proc/net/dev 第二行是字段名,第三行开始才是数据,且接口名后可能带冒号(eth0:),而 sscanf 格式串若写成 "%s" 会把冒号吞进去,导致后续数字读错。
立即学习“C++免费学习笔记(深入)”;
稳妥做法是用 "%[^:]:%n" 显式匹配冒号并记录偏移,或直接用 "%20[^ ]" 限制接口名长度,再手动跳过冒号:
- 先用
fgets读整行,检查是否以空格或字母开头(跳过表头) - 用
sscanf(buf, "%20s", ifname)读接口名,然后if (ifname[strlen(ifname)-1] == ':') ifname[strlen(ifname)-1] = '\0' - 用
sscanf(buf + offset, "%*d %*d %*d %*d %*d %*d %*d %*d %lu %*d %*d %*d %*d %*d %*d %*d %lu", &rx, &tx)—— 注意这里跳过的是前 8 个 rx 字段和中间 7 个 tx 前字段,第 9 个是rx_bytes,第 17 个是tx_bytes(顺序见 proc(5) 文档)
写日志时必须加时间戳和原子写入,否则多进程并发会丢数据
如果程序被 systemd 管理或用 cron 启动,fork() 后子进程可能同时写同一个日志文件,导致行断裂(例如某次写入只落了半行)。不要用 std::endl 或 \n 直接 flush,而应构造完整字符串后调用 write() 系统调用(它本身是原子的,只要单行 ≤ PIPE_BUF,通常是 4096 字节)。
时间戳建议用 clock_gettime(CLOCK_MONOTONIC, &ts),避免系统时间回拨影响流量差值计算;日志格式推荐: "%ld.%09ld eth0 rx=%lu tx=%lu\n"(秒+纳秒,方便后续 awk/gnuplot 处理)。
别用 std::ofstream 默认的缓冲模式——它会在用户态缓存,进程崩溃就丢最后一段。要么设 file.setf(std::ios::unitbuf),要么每次 file ,但更稳的是 <code>write() + fsync()(仅在关键场景,如每 5 分钟刷盘一次)。
两次读取算增量时,要处理内核计数器回绕和接口热插拔
/proc/net/dev 的字节数是无符号 64 位,理论上不会溢出(10Gbps 满速跑也要 584 年才到 2⁶⁴),但实际中可能因网卡驱动 bug 或虚拟化环境出现异常回绕(比如从极大值突变为小值)。不能直接做减法,得判断:若新值 2⁶³,则大概率是回绕,此时增量 = 新值 + (2⁶⁴ − 旧值)。
更常见的是接口消失:比如 WiFi 断开后 wlan0 不再出现在 /proc/net/dev 中。代码里不能假设每次都能读到同一组接口。建议每次全量读取所有接口,用 std::map<:string std::pair uint64_t>></:string> 存上次值,本次缺失的接口跳过增量计算,或记为 0(视监控需求而定)。
另外,容器环境里可能看到 vethXXXXXX 这类临时接口,如果没过滤,日志会爆炸。生产环境务必白名单过滤(eth0, ens33, enp0s3 等物理/常用名)。



















