基于 Buildroot + QEMU 的 ARM Linux 远程CPU心跳监控系统实现
本文介绍了在Linux环境下使用Buildroot+QEMU搭建ARM开发环境的实践过程。项目通过C/S架构实现系统监控功能:ARM开发板(客户端)采集CPU负载等数据,Ubuntu主机(服务器)接收并展示数据。重点解决了网络驱动配置(SMC91c111网卡)和SSH持久化(Buildroot Overlay机制)等关键问题。文章详细阐述了C语言客户端和Python服务端的实现方法,包括TCP通信
1.前言
项目背景:这是我在Linux上的第一个学习项目,通过Buildroot+QEMU来模拟真实的ARM开发环境,可以实现对CPU的平均负载、当前进程数以及进程ID的显示。
环境介绍:
- 宿主机:Ubuntu
- 模拟器:QEMU(模拟 ARM Versatile PB 开发板)
- 构建系统:Buildroot 2024.08
最终效果展示:

2.核心难点与环境搭建
- 网络驱动的大坑:
Virtio 网卡在 Versatile PB 上不工作,需要启动开发板的原生 SMC91c111 网卡驱动(需要开启 GPIO Support 才能看到 SMC91x 驱动的依赖关系)。
- SSH配置的持久化(Overlay):
问题:为了允许 SSH 远程登录,我们需要修改 /etc/ssh/sshd_config 文件(添加 PermitRootLogin yes)。但由于 QEMU 使用的是基于内存的根文件系统 (rootfs.cpio.gz),每次重启 QEMU 后,所有的修改都会丢失,必须重新手动配置,极其繁琐。
原因分析:Buildroot 生成的 cpio 镜像在启动时被加载到内存(RAM Disk)中。运行时的任何修改都只存在于内存里,关机即焚。我们需要一种方法,在编译阶段就把配置文件“焊死”在镜像里。
解决方法:使用 buildroot 的 Overlay 机制,在编译时注入配置(PermitRootLogin yes)。
3.系统架构设计

C/S架构:
- Client(ARM板):C 语言编写,负责采集数据。
- Server(Ubuntu):Python 编写,负责展示数据。
数据流:读取 /proc/loadavg ,经Socket打包,再由TCP发送,使用Python解码展示。
4.代码实现
服务端(Python):
服务端部署在 Ubuntu 宿主机上,负责监听特定端口,接收并打印来自开发板的数据。
新建文件 monitor_server.py:
import socket
# 监听配置
HOST = '0.0.0.0' # 监听所有网卡接口
PORT = 9999 # 自定义端口号
print(f"=== Server Started on port {PORT} ===")
# 创建 TCP Socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# 允许端口复用,防止程序退出后短时间内无法重启
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen()
print("Waiting for connection...")
conn, addr = s.accept() # 阻塞等待连接
with conn:
print(f"Connected by {addr}")
while True:
data = conn.recv(1024)
if not data:
break
# 解码并打印接收到的数据
print(f"[ARM Board]: {data.decode().strip()}")
关键点解析:
-
0.0.0.0:这里不写 127.0.0.1 也不写具体 IP,而是用 0.0.0.0,确保无论是本地回环还是来自虚拟网卡的数据都能被接收到。
-
SO_REUSEADDR:在调试阶段,如果我们频繁强制关闭服务,端口可能会处于 TIME_WAIT 状态。开启此选项可以让我们立即重启服务,无需等待。
客户端(C语言):
客户端运行在 QEMU 模拟的 ARM 开发板上。我们需要读取系统的 CPU 负载信息,并通过网络发送。
新建文件 sys_monitor.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
/* QEMU User Network 特性:
* 在 QEMU 内部,网关地址通常是 10.0.2.2
* 该地址会直接映射到宿主机 (Ubuntu) 的 Loopback 接口
*/
#define SERVER_IP "10.0.2.2"
#define SERVER_PORT 9999
#define BUFFER_SIZE 128
int main() {
int sock;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
FILE *fp;
char load_data[64];
// 1. 创建 Socket (IPv4, TCP)
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation failed");
return -1;
}
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
// 将 IP 地址从字符串转换为二进制
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("Invalid address");
return -1;
}
// 2. 连接到服务端
printf("Connecting to Server %s:%d ...\n", SERVER_IP, SERVER_PORT);
if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("Connection failed");
return -1;
}
printf("Connected! Sending data...\n");
// 3. 循环采集与发送
while (1) {
// 打开 /proc/loadavg 获取负载信息
fp = fopen("/proc/loadavg", "r");
if (fp == NULL) {
perror("Failed to read /proc/loadavg");
break;
}
// 读取一行数据
if (fgets(load_data, sizeof(load_data), fp) != NULL) {
// 拼接发送字符串
snprintf(buffer, BUFFER_SIZE, "CPU Load: %s", load_data);
// 发送数据
send(sock, buffer, strlen(buffer), 0);
}
fclose(fp); // 务必关闭文件句柄
sleep(2); // 设置 2 秒心跳间隔
}
close(sock);
return 0;
}
关键点解析:
-
关于 10.0.2.2:
这是 QEMU 用户模式网络(User Networking)的一个特殊约定。
10.0.2.15:QEMU开发板自己的IP。
10.0.2.2:QEMU所在宿主机的IP别名。
5.交叉编译与部署
代码写好了,但我们不能直接使用 Ubuntu 系统自带的 gcc 进行编译。因为 Ubuntu 是 x86 架构,而我们的 QEMU 模拟的是 ARM 架构。如果直接编译,在板子上运行时会报错 Exec format error。
- 交叉编译:
Buildroot 在构建系统的同时,已经为我们生成了一套完整的工具链,位于 output/host/bin/ 目录下
# 使用 Buildroot 生成的专用 gcc 编译器
./output/host/bin/arm-buildroot-linux-gnueabi-gcc sys_monitor.c -o sys_monitor
编译完成后,务必使用 file 命令检查生成的可执行文件类型,确保它是 ARM 格式:
file sys_monitor
检查命令的预期输出:
sys_monitor: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, ...
这里要看到 ARM 字样才算成功,如果是 x86-64 ,就说明误用了宿主机的编译器,板子会无法启动。
- 部署(SCP)
由于我们之前配置好了 SSH 服务,并将 QEMU 的 22 端口映射到了宿主机的 2222 端口,我们可以使用 scp (Secure Copy) 命令像操作本地文件一样传输文件。
# -P 2222: 指定映射端口
# sys_monitor: 本地文件
# root@localhost: 目标用户和地址
# :/root/: 目标路径
scp -P 2222 sys_monitor root@localhost:/root/
-
关于 SSH Host Key 冲突
在开发过程中,如果我们重新编译了 Buildroot 镜像(特别是执行了 make clean 后),QEMU 重启后会生成新的 SSH 主机密钥(Host Key)。此时再次使用 SCP 或 SSH 连接时,Ubuntu 会报错并拦截连接,提示 "REMOTE HOST IDENTIFICATION HAS CHANGED!":
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
...
Host key verification failed.
lost connection
这是因为 SSH 客户端发现目标主机的“身份证”变了,怀疑遭遇了中间人攻击。但在嵌入式开发中,这是正常现象。解决方法如下,
使用以下命令清除旧的密钥记录,然后重试即可:
ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[localhost]:2222"
6.运行测试
为了清晰地展示数据流向,我们需要打开两个中断窗口配合操作。
-
启动服务端 (Ubuntu)
首先,在 Ubuntu 宿主机的终端中启动 Python 服务端脚本。它将挂起并等待来自开发板的连接。
# 在 Ubuntu 终端执行
python3 monitor_server.py
输出状态:
=== Server Started on port 9999 ===
Waiting for connection...
此时,服务端进入监听状态。
-
启动客户端 (ARM 开发板)
保持服务端窗口不动,打开一个新的终端窗口(或使用之前的 QEMU 窗口),通过 SSH 登录到开发板,运行我们在上一节传输进去的监控程序。
# 1. SSH 登录开发板
ssh -p 2222 root@localhost
# 2. 赋予可执行权限 (首次运行需要)
chmod +x sys_monitor
# 3. 运行监控程序
./sys_monitor
输出状态:
Connecting to Server 10.0.2.2:9999 ...
Connected! Sending data...
- 结果验证与数据解读
客户端显示 Connected! 后,服务端窗口就会开始刷新数据:
Connected by ('127.0.0.1', 45678)
[ARM Board]: CPU Load: 0.12 0.05 0.01 1/34 112
[ARM Board]: CPU Load: 0.12 0.05 0.01 1/34 112
[ARM Board]: CPU Load: 0.11 0.05 0.01 1/34 112
...
这段数字包含如下系统信息:
0.12 0.05 0.01:分别代表系统在最近 1分钟、5分钟、15分钟 内的平均负载。
1/34:分母 34 表示系统中总共有 34 个进程/线程;分子 1 表示当前只有 1 个进程处于运行状态(就是我们的 sys_monitor)。
112:表示最近创建的进程 PID。
- 停止运行
停止客户端:在 SSH 窗口按 Ctrl + C,程序终止,停止发送心跳。
停止服务端:在 Python 窗口按 Ctrl + C,停止监听。
7.总结与展望
总结:本次项目有几个关键的知识点
-
依赖管理的艺术:在 Buildroot 中,软件包的显示与否往往取决于依赖关系(如 SMC91x 网卡驱动依赖于 GPIO Support)。学会使用 / 搜索并查看 Depends on 是解决配置问题的金钥匙。
-
文件系统的定制:通过 Overlay 机制,我们学会了如何在编译阶段“注入”配置文件,解决了嵌入式设备掉电配置丢失的痛点。这是产品化必不可少的一步。
-
跨架构开发的思维:时刻保持清醒——代码在哪里写?编译器用的是哪个?程序在哪里跑?交叉编译是嵌入式开发与纯软件开发最大的区别。
-
软硬结合:代码写得再好,如果内核没有开启对应的硬件驱动(如 PCI vs MMIO vs 原生网卡),系统依然跑不起来。
展望:目前的监控系统虽然能跑,但还比较简陋。基于当前的环境,还可以继续挑战更有趣的功能:
-
数据可视化:在 Buildroot 中集成 Web 服务器(如 lighttpd 或 nginx),将 C 语言采集的数据通过 CGI 或 WebSocket 推送到网页端,实现酷炫的仪表盘展示。
-
数据持久化:移植 SQLite 数据库,将采集到的负载历史数据保存到本地文件中,而不是仅仅打印在屏幕上。
-
内核级开发:不再满足于读取 /proc 文件,尝试编写一个真正的 Linux 内核模块 (Kernel Module),在内核态直接获取系统信息,甚至模拟一个字符设备驱动。
更多推荐
所有评论(0)