news 2026/9/8 11:24:12

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用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) --type=renderer,括号里的内容就乱了。所以不能简单用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,删除对应行。

这样表格的改动量最小,闪烁几乎消失。关键实现是维护一个QHash<int, 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 权限不足与拒绝访问

普通用户只能终止属于自己的进程,别的用户的进程会返回 EPERM,root 进程更是碰都不能碰。界面上要体现这一点:读 /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_PLATFORM=offscreen
表格刷新闪烁每次清空重建行记录 PID 到行号的映射,复用已有行,只增删变化部分
CPU 占用率超过 100%多核机器上进程同时使用多个核心属正常现象;若想单核归一化,除以逻辑核心数
kill 返回Permission denied权限不足提示用户用 sudo 或换用有权限的账户执行
打包到别的机器缺库动态链接了系统里没有的 Qt 库用 linuxdeployqt 打包,尽量在低版本 glibc 环境构建
进程名出现截断或者乱码stat 字段解析用了简单的空格 splitlastIndexOf('(')lastIndexOf(')')提取 comm
刷新时间不准QTimer 在高负载下可能延迟以实际采样间隔计算 CPU 占用率,别硬编码 1 秒

5.3 一个容易被忽视的优化点

关于 QTimer 和采样间隔,我想多说一句。我在调试时发现,系统负载高的时候 QTimer 的 timeout 信号可能延迟触发,如果用固定“两次采样间隔 = 1 秒”去算 CPU 占用率,结果会偏大。稳妥做法是记录上一次采样的QElapsedTimer,计算的时候用真实经过的时间修正。这个细节一般教程不会讲,但真实系统里很常见,尤其是你把这套代码搬到慢速嵌入式设备上时。

再分享一个小技巧:采集过程中如果遇到一个进程正在退出,读 /proc/[pid]/stat 会直接失败,这是正常的,不是 bug。遇到这种文件读取失败的情况,跳过这个进程即可,不必报错,界面下次刷新自然就把这个进程从列表去掉了。


到目前为止,这个 Qt 实现的 Linux 任务管理器已经能在我的开发机上稳定跑起来,后来我还把它改装成了一个嵌入式设备上的资源监控小面板,只保留 CPU、内存和几个关键进程的图表曲线,后台跑了半个多月,内存占用一直很稳定。在我看来,这个项目最大的价值不在于界面多好看,而在于把 Linux 系统编程里的 /proc 文件系统、进程模型、信号机制,和 Qt 的定时器、信号槽、表格控件完整地串了一遍。做完之后,再看任何系统监控类工具的代码,都会觉得思路清晰得多。如果你也想练手,建议从最简单的进程列表开始,慢慢往上加功能,每个模块都相对独立,做起来会很有成就感。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 11:23:46

计算机单片机毕设实战-基于 STM32 的心率血氧体温检测与跌倒报警设备设计 基于 STM32 的可穿戴式生理参数及运动信息采集系统设计(013307)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/8 11:22:54

Java复刻巨洞冒险:经典文字冒险游戏的设计与实现

简介&#xff1a;这是一份面向Java初、中级学习者及课程设计场景的完整实践资源&#xff0c;围绕经典文字冒险游戏“巨洞冒险”的功能扩充与工程化开发展开。资源以Java面向对象编程为基础&#xff0c;覆盖了从阅读源码、添加Javadoc注释、绘制EA类图&#xff0c;到使用IDEA开发…

作者头像 李华
网站建设 2026/9/8 11:22:03

CIMPro云渲染API携手AI助手:三维场景的自然语言控制实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 11:21:32

用Java写一个迷你编译器:从源码到可执行代码的完整实现

简介&#xff1a;面向Java学习者的Java编译器实现源码包&#xff0c;围绕Java源代码到JVM字节码的编译过程&#xff0c;为理解编译原理和JVM机制提供可读的参考实现。压缩包共2个文件&#xff0c;包含1个Java源文件与1份Markdown说明文档&#xff0c;整体体积仅2KB&#xff0c;…

作者头像 李华
网站建设 2026/9/8 11:20:38

三款AI论文写作软件横评:从大纲到降重怎么选才不踩坑?

写论文这事&#xff0c;最怕的不是写不出来&#xff0c;而是写得心里没底。 题目改了七八版还怕选重了&#xff0c;文献下载了两百篇越读越乱&#xff0c;参考文献格式调到崩溃&#xff0c;交稿前还得担心重复率和AIGC检测。今年开学季一到&#xff0c;又有一波人在搜“AI论文工…

作者头像 李华
网站建设 2026/9/8 11:19:11

UnityPlayer.dll报错闪退?三步排查法从根因修复游戏启动故障

上个月一个朋友发来一张截图&#xff0c;游戏启动画面闪了一下&#xff0c;紧接着弹窗提示“由于找不到 UnityPlayer.dll&#xff0c;无法继续执行代码”。他问我是不是显卡坏了&#xff0c;要不要重装系统。我说你先别拆机也别重装&#xff0c;按顺序查三样东西——游戏文件、…

作者头像 李华