ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用Qt开发Linux图形化任务管理器:/proc解析与CPU计算详解

用Qt开发Linux图形化任务管理器:/proc解析与CPU计算详解 简介面向Linux下需要掌握进程监控与Qt界面开发的读者这套基于Qt实现的简易任务管理器完整工程源码是一个轻量入口。项目以C为核心共7个文件含2个cpp源文件、1个ui界面文件、1个头文件及1个pro工程文件等压缩包仅7KB结构紧凑适合直接导入Qt Creator学习或二次改造。源码演示了通过/proc文件系统读取进程CPU、内存等状态信息并在此基础上实现进程列表刷新、结束进程等交互操作同时覆盖QMainWindow、QTableWidget、定时器及按钮事件等常用组件的配合方式。已有411人学习下载对于想在系统编程与GUI开发之间建立联系的初学者或开发者是一份轻量实用的参考样例。 我用 Qt 在 Linux 上撸了一个图形化的任务管理器过程比想象中顺畅也踩了不少坑。文章的起因很简单Linux 下面看进程状态很多人习惯开个终端敲 top 或者 htop但如果是做给普通用户用的工具或者你想把这套监控能力嵌到自己的 Qt 应用里一个图形化的任务管理器就很有必要了。项目本身不大却把 Linux 系统编程、/proc 文件系统、Qt 模型视图、信号槽、权限处理这些点全串起来了。适合 Qt 入门到进阶的开发者也适合要在嵌入式或者国产 Linux 环境里做轻量监控工具的朋友参考。先给个全景图我用 Qt Widgets 写主界面核心数据源是 Linux 的 /proc 虚拟文件系统CPU 占用率用两次采样差值计算内存读取 /proc/meminfo进程列表遍历 /proc/[pid] 目录再配合 QTimer 定时刷新。窗口里有一个进程表格顶部加了“结束进程”“刷新”按钮底部状态栏实时显示系统总 CPU、内存和进程数。整套东西从设计到跑通前后大概两天时间。1. 整体设计与方案选型1.1 为什么是 Qt Widgets 而不是 QML这个项目本质上是个工具型桌面应用不是那种需要炫酷动画的产品界面。Qt Widgets 的优势是成熟稳重QTableWidget、QStatusBar、QMenu 这些现成控件直接拼开发效率非常高。QML 虽然做动态效果漂亮但对我来说任务管理器这种界面用 QML 属于杀鸡用牛刀还引入额外的 QML 运行时依赖部署体积也大。如果你是做嵌入式 Linux 或者国产化适配Widgets 的兼容性和资源占用也更友好。另外 Qt 的信号槽机制特别适合这种“定时器触发界面刷新”的场景。QTimer 发出 timeout 信号槽函数里做数据采集和表格更新天然线程安全也不需要我自己管理回调线程。相比直接调系统 API 加 C 接口回调Qt 这套抽象让代码结构干净很多。1.2 架构分层与职责划分虽然这个项目不大我依然按三个层次拆了代码后续扩展才不吃力数据采集层负责读 /proc/stat、/proc/meminfo、/proc/[pid]/stat 等文件封装成 SystemInfoCollector 类返回结构体数组。逻辑计算层负责把原始数值换算成 CPU 百分比、内存百分比、进程占用的 KB/MB这部分我揉进了采集类内部避免单独一层搞得太虚。界面展示层MainWindow 负责布局表格控件展示进程列表状态栏展示系统级指标定时器驱动刷新。这样分的好处很明显。如果后续想支持别的平台比如 Windows我只需要替换数据采集层的实现把 /proc 换成 Win32 API界面层完全不用动。或者我想把进程数据导出成 JSON也只需要依赖采集层的结果和 UI 解耦。1.3 为什么读 /proc 而不是调 ps -aux最直接的方案其实是让 Qt 去执行ps -aux或者top -b -n 1然后解析标准输出。我最早就是这么干的但用了几分钟就放弃了。第一ps 的输出格式在不同发行版和不同语言环境下有差异解析文本太脆弱。第二每刷一次界面就要 fork 一个子进程几百毫秒一次的开销不说进程多的时候 ps 本身也要遍历一次系统明显浪费。第三ps 给的信息不够底层比如你拿不到进程的 utime/stime用户态和内核态 CPU 时间自然就算不出准确的进程 CPU 占用率。/proc 文件系统直接把内核数据结构暴露成文件读取效率高字段稳定而且不依赖任何外部命令。这一点在做嵌入式系统时特别重要因为裁剪过的系统里未必有 ps 命令但 /proc 一定还在。2. 核心数据采集与计算逻辑2.1 系统 CPU 占用率怎么算才准CPU 占用率不是读一次文件就能得到的必须采样两次求差值。原理很简单/proc/stat 第一行是系统总 CPU 时间单位是 USER_HZ通常是 100 分之一秒字段依次是 user、nice、system、idle、iowait、irq、softirq、steal。你开机后这些值一直在增长要算“最近这一秒的占用率”就用第二次读到的值减去第一次读到的值看在这段时间里非空闲时间占比多少。struct CpuTimes { quint64 user, nice, system, idle, iowait; quint64 irq, softirq, steal; }; double calcCpuPercent(const CpuTimes now, const CpuTimes prev) { quint64 prevIdle prev.idle prev.iowait; quint64 nowIdle now.idle now.iowait; quint64 prevTotal prev.user prev.nice prev.system prevIdle prev.irq prev.softirq prev.steal; quint64 nowTotal now.user now.nice now.system nowIdle now.irq now.softirq now.steal; quint64 totalDelta nowTotal - prevTotal; if (totalDelta 0) return 0.0; return (1.0 - (nowIdle - prevIdle) / (double)totalDelta) * 100.0; }刷新间隔我设置成 1000ms也就是 1 秒一次。间隔太短比如 100ms数值会剧烈跳动毫无参考价值间隔太长比如 5 秒又失去实时性。1 秒是任务管理器的行业标准Windows 任务管理器默认也就是 1 秒左右。2.2 内存信息与进程内存内存信息从 /proc/meminfo 拿字段是键值对形式按 KB 为单位。读取时逐行解析冒号前面是键后面是值。QFile file(/proc/meminfo); if (file.open(QIODevice::ReadOnly)) { while (!file.atEnd()) { QString line file.readLine(); QStringList parts line.split(QRegularExpression(\\s)); if (parts.size() 2) memInfo[parts[0].remove(:)] parts[1].toULongLong(); } }注意这里有个坑老一代教程喜欢用MemFree Buffers Cached来计算可用内存但在新版内核里文件缓存、slab 这些内存的回收逻辑很复杂这样算出来误差很大。正确做法是直接用MemAvailable这个字段是内核自己估算的“可分配给新程序的物理内存”比手动算准确得多。内存使用率用(MemTotal - MemAvailable) / MemTotal * 100计算。我用MemAvailable算出来的数值和 htop 显示的几乎一致。进程内存方面/proc/[pid]/stat 里第 24 个字段索引 23是 RSS单位是页。需要乘以系统页大小才能换算成字节Linux 下页大小可以用sysconf(_SC_PAGESIZE)获取绝大多数 x86 机器是 4096 字节。更直观的方式是读 /proc/[pid]/status 里的 VmRSS 字段单位是 kB直接转换成 MB 展示。两者本质是同一个数据看你怎么选。2.3 解析 /proc/[pid]/stat 的正确姿势这是全项目最容易翻车的地方。/proc/[pid]/stat 格式乍一看是空格分隔的字段但它的第二个字段comm是进程名被圆括号包着而进程名里完全可以出现空格和括号。比如一个进程叫(chrome) --typerenderer括号里的内容就乱了。所以不能简单用split( )。正确做法是找第一个(和最后一个)把中间内容提取成进程名剩下的部分再按空格切分。QFile f(QString(/proc/%1/stat).arg(pid)); if (!f.open(QIODevice::ReadOnly)) return; QString text f.readAll(); int left text.lastIndexOf((); int right text.lastIndexOf()); QString comm text.mid(left 1, right - left - 1); QStringList parts text.mid(right 1).trimmed().split( ); // parts[0] - state // parts[1] - ppid // parts[11] - utime // parts[12] - stime bool ok; qint64 utime parts.value(11).toLongLong(ok); qint64 stime parts.value(12).toLongLong(ok);进程 CPU 占用率同样需要两次采样。进程的 CPU 时间就是 utime stime进程在这段时间内占用 CPU 的比例就是进程时间增量除以全局 CPU 总时间增量。注意一个问题多核机器上一个进程同时跑满多个核占用率是可以超过 100% 的。如果你想让数值看着“单核归一化”就再除以逻辑核心数这个细节决定用户看到的数字是否符合直觉。我最终选择显示超过 100% 的实际值和 top 默认行为保持一致懂的人自然懂。进程状态字段同样是从这里取比如R是运行S是睡眠Z是僵尸。展示的时候可以把字符映射成中文描述。用户 ID 我可以从 stat 里拿不到所以用/proc/[pid]/status里的Uid:字段第一个值再配合getpwuid()转成用户名。3. 界面实现与刷新交互3.1 主窗口与表格布局主窗口用一个 QMainWindow中央区域放 QTableWidget顶部工具栏放“刷新”“结束进程”两个按钮底部 QStatusBar 显示系统总体信息。表格列我设计了 PID、进程名、状态、用户、CPU%、内存%、内存大小共 7 列。列头加粗默认按 CPU% 降序排序。这里我权衡过 QTableWidget 和 QTableView QAbstractTableModel 的选型。QTableWidget 是 QTableView 的便捷封装内置数据存储适合这种行数不多、结构简单的场景QTableView 配自定义 Model 适合数据量特别大、需要延迟加载的场景。任务管理器通常同时有几百个进程最多上千行QTableWidget 在 1 秒刷新一次的频率下完全扛得住没必要上 Model那会增加很多样板代码。3.2 定时刷新与表格复用刷新逻辑最初我图省事每次clearContents()然后重新插入所有行。结果发现两个问题一是表格闪烁明显尤其进程多的时候二是排序状态和选中项被清掉用户体验很差。改进后的思路是“复用行”。采集到当前所有进程后新出现的 PID插入新行。已存在的 PID只更新对应单元格的数据。上次存在、这次消失的 PID删除对应行。这样表格的改动量最小闪烁几乎消失。关键实现是维护一个QHashint, int m_rowByPid把 PID 映射到行号。排序导致的错位问题用刷新前临时setSortingEnabled(false)、刷新后再恢复并执行sortItems()来解决。选中和滚动位置则在刷新前记录 PID刷新后重新定位。QTimer 部分用QTimer::setInterval(1000)然后连接timeout信号到刷新槽函数。如果进程数量特别大读取 /proc 本身耗时极低真正耗时的是表格刷新。实测在几千个进程的极端环境下一次刷新也就 20 毫秒左右完全不需要引入多线程。如果你非要加线程记得 UI 更新必须回到主线程否则就是给自己找麻烦。3.3 CPU/内存曲线的扩展方向任务管理器只有数字不够直观趋势曲线能让你一眼看出系统是不是在“抽风”。这里我用了 QCustomPlot它是轻量级的第三方绘图库集成简单和 Qt Widgets 混搭很自然。做法是开一个固定大小的环形缓存区每秒钟采集一次系统 CPU 和内存占用率追加进 QCustomPlot 的 graph然后replot()。只保留最近 60 个点相当于一分钟的历史曲线。这里有个经验不要每次刷新把所有点重新 addData那样会反复触发重绘性能很差。直接用graph-data()-add()追加新点再设置坐标轴范围只显示最近 60 秒实测在嵌入式设备上也能流畅跑。这块思路其实和用 QCustomPlot 把时域波形转频域展示类似都是高频追加数据、实时刷新的套路。4. 结束进程与安全性处理4.1 从界面到 kill 的一整套链路结束进程按钮的交互是获取表格当前选中的 PID弹出确认对话框用户确认后执行终止操作。终止操作我用的是标准 C 库的kill()函数不是去执行kill命令。这样做省去创建子进程的开销错误码处理也更直接。#include signal.h if (::kill(pid, SIGTERM) 0) { statusBar()-showMessage(QString(已向进程 %1 发送终止信号).arg(pid)); } else { statusBar()-showMessage(QString(终止进程 %1 失败: %2) .arg(pid).arg(strerror(errno))); }这里必须强调一个原则优先发送 SIGTERM让进程有机会清理文件和释放资源不要一上来就 SIGKILL。SIGKILL 是直接杀死进程没有任何善后机会可能留下临文件或者损坏数据。如果过几秒发现 SIGTERM 没生效再考虑 SIGKILL。Windows 上那种“结束任务”其实也是先走 WM_CLOSE 再强制终止理念是一样的。4.2 权限不足与拒绝访问普通用户只能终止属于自己的进程别的用户的进程会返回 EPERMroot 进程更是碰都不能碰。界面上要体现这一点读 /proc/[pid] 目录时权限不足会导致某些进程的信息缺失比如状态、用户名读取失败我统一显示成“未知”执行 kill 失败时错误信息直接透传给用户。如果你确实需要杀掉别的用户的进程常规做法是通过 polkit 调用 pkexec 提权执行。但这个话题牵扯到系统权限模型不是任务管理器这种工具随便能碰的我在项目里没有做提权功能。我的建议是工具本身保持普通用户权限杀不了就让用户自己去终端处理这是对系统稳定性的负责任态度。4.3 防误杀设计任务管理器这种工具特别怕用户手滑误杀。我在列风险项之前先做了三个保护PID 为 1 的进程systemd 或 init直接禁止终止事件过滤器拦截一下即可。当前正在运行的这个程序自身进程也要禁止终止否则用户杀完发现窗口消失了会一头雾水。内核线程默认没有权限杀可以隐藏它们的 ppid 为 0 或者进程名是 [kworker/0:0] 这种中括号形式。另外确认对话框里要完整展示进程名、PID 和危险等级。比如杀 Chrome 标签页进程问题不大杀 sshd 或者数据库进程就要重点警示。这个我写成了一个通用函数根据进程名判断是否属于“系统关键进程”列表是的话多打一行红色警告。这是真实场景里用户最需要的贴心功能。5. 打包部署与踩坑记录5.1 Linux 下 Qt 程序的发布写完代码之后最大的坑在部署。Qt 项目在开发机上跑得好好的换一台机器就跑不起来这是所有 Qt 开发者的噩梦。Linux 下 Qt 依赖动态库最常见的问题是目标机器没有安装 Qt 开发环境。对这个问题我的经验是用 linuxdeployqt 打包成 AppImage。打包时注意linuxdeployqt 需要在 Qt 的 qmake 所在目录下运行它会自动扫描可执行文件的依赖库把 Qt 库全部打包进去。但 glibc 这类基础库它是不会打进去的而 glibc 向后兼容、不向前兼容。也就是说在 Ubuntu 24.04 上打包拿到 Ubuntu 20.04 上大概率跑不起来反过来在旧系统上打包新系统基本都能跑。所以我做发布版时会在 CentOS 7 或者 Ubuntu 18.04 这种老环境的容器里交叉编译这样兼容面最广。如果你只在内网用直接在目标机器上编译安装就行绕开这个问题。5.2 常见问题速查表问题现象原因解决办法启动报could not connect to display没有图形显示环境或 DISPLAY 变量不对检查是否在 SSH 环境无头测试先export QT_QPA_PLATFORMoffscreen表格刷新闪烁每次清空重建行记录 PID 到行号的映射复用已有行只增删变化部分CPU 占用率超过 100%多核机器上进程同时使用多个核心属正常现象若想单核归一化除以逻辑核心数kill 返回Permission denied权限不足提示用户用 sudo 或换用有权限的账户执行打包到别的机器缺库动态链接了系统里没有的 Qt 库用 linuxdeployqt 打包尽量在低版本 glibc 环境构建进程名出现截断或者乱码stat 字段解析用了简单的空格 split用lastIndexOf(()和lastIndexOf())提取 comm刷新时间不准QTimer 在高负载下可能延迟以实际采样间隔计算 CPU 占用率别硬编码 1 秒5.3 一个容易被忽视的优化点关于 QTimer 和采样间隔我想多说一句。我在调试时发现系统负载高的时候 QTimer 的 timeout 信号可能延迟触发如果用固定“两次采样间隔 1 秒”去算 CPU 占用率结果会偏大。稳妥做法是记录上一次采样的QElapsedTimer计算的时候用真实经过的时间修正。这个细节一般教程不会讲但真实系统里很常见尤其是你把这套代码搬到慢速嵌入式设备上时。再分享一个小技巧采集过程中如果遇到一个进程正在退出读 /proc/[pid]/stat 会直接失败这是正常的不是 bug。遇到这种文件读取失败的情况跳过这个进程即可不必报错界面下次刷新自然就把这个进程从列表去掉了。到目前为止这个 Qt 实现的 Linux 任务管理器已经能在我的开发机上稳定跑起来后来我还把它改装成了一个嵌入式设备上的资源监控小面板只保留 CPU、内存和几个关键进程的图表曲线后台跑了半个多月内存占用一直很稳定。在我看来这个项目最大的价值不在于界面多好看而在于把 Linux 系统编程里的 /proc 文件系统、进程模型、信号机制和 Qt 的定时器、信号槽、表格控件完整地串了一遍。做完之后再看任何系统监控类工具的代码都会觉得思路清晰得多。如果你也想练手建议从最简单的进程列表开始慢慢往上加功能每个模块都相对独立做起来会很有成就感。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进