ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

用Qt打造Linux图形化任务管理器:从/proc到实时曲线

用Qt打造Linux图形化任务管理器:从/proc到实时曲线 简介基于Qt实现的Linux任务管理器完整源码工程旨在帮助Qt及Linux开发者掌握进程监控类应用的设计思路也适合作为高校操作系统或GUI课程的实战练习。压缩包共7个文件包含2个C实现文件、1个头文件、1个Qt Designer界面布局文件、1个qmake工程文件、1个Qt Creator用户配置及1份说明文档整体仅7KB结构小巧打开工程即可运行查看。代码围绕Linux下/proc文件系统展开解析status、statm等节点获取进程PID、内存与CPU占用使用QTimer定时刷新QTableWidget动态展示支持通过kill命令结束进程、按名称或PID搜索过滤并交代了无效PID与权限不足时的基础处理思路同时以信号槽解耦刷新线程与UI主线程体现轻量级多线程协作方式借助QSS还可进一步定制界面观感。已有411人学习/下载资源体量虽小但覆盖了Qt事件循环、模型视图、进程通信和异步刷新等关键知识点无论是要完成课程设计、快速搭建监控面板还是深入学习Linux系统编程都具备直接参考价值也可视为系统监控类工具的开发起点。 最近折腾Linux桌面环境时明显感觉缺一个趁手的可视化任务管理器。终端里敲top、htop虽然够用可终究不够直观想结束一个卡死的程序还得先查PID再敲命令。干脆自己动手用Qt在Linux下写了一个带图形界面的任务管理器功能对标Windows任务管理器进程列表、CPU/内存实时曲线、结束进程、暂停恢复。这个项目对于想深入Qt实战、又对Linux/proc文件系统感兴趣的朋友来说是一块非常合适的练手材料既避开了纯理论的无聊又能把图形界面、文件解析、进程信号这些知识点串成一条线。下面把整个设计和实现过程完整拆开讲。1. 整体设计与思路拆解1.1 需求定位你不是在写第二个htop做之前得先想清楚Linux下已经有好用的gnome-system-monitor终端党也有htop为什么还要自己造轮子我当时的诉求很实际第一想做一个足够轻量、界面干净、能随时定制行为逻辑的桌面工具第二想借这个项目把Qt的Model/View架构、信号槽跨线程通信、QCustomPlot绘图这些平时零散用到的技术完整走一遍。第三本地跑着的是一个精简版桌面环境自带的监控工具太简陋装一个完整重量级的系统监视器又显得臃肿。所以这个项目定位不是去挑战htop而是做一个“够用、可控、能扩展”的个人工具。界面设计上参考Windows任务管理器的信息密度顶部放CPU和内存占用曲线中间是进程表格右键弹菜单做操作。所有数据来源都用Linux标准的/proc虚拟文件系统不依赖任何非标准命令这样在主流发行版上都能跑起来。1.2 技术选型思考为什么锁定Qt选择Qt而不是GTK或者直接上Electron原因有几个。第一Qt Widgets在处理高频刷新表格和图表时性能远好于Web方案内存占用也低一个桌面工具常驻后台不应该吃掉几百兆内存。第二Qt的QAbstractTableModel和QStyledItemDelegate天生适合做可排序、可筛选的进程列表信号槽机制在跨线程刷新数据时非常自然。第三QCustomPlot这个第三方绘图库和Qt配合得很顺几行代码就能画出平滑的实时曲线。第四团队里做桌面工具的历史工程大多是Qt后续要维护或者移植到WindowsQt的跨平台性让这份代码不至于被锁死在Linux上。版本选择上我用的Qt 5.15.2 LTS稳定性优先。Qt 6其实也行但5.15.2对老编译器和第三方库兼容性更好QCustomPlot在这版下跑得也最稳。1.3 功能范围与界面布局最终确定的模式是“主窗体 左右两栏布局”。左边是实时曲线区绘制CPU总占用率和内存占用率两条曲线右边是进程表格展示PID、进程名、CPU占用、内存占用、运行状态、实际用户几个关键列。进程操作上支持结束进程、暂停进程、恢复进程三个动作。结束进程前会弹确认框避免误杀系统关键进程。顶部放一个系统信息栏显示操作系统名称、内核版本、CPU型号、总内存、已用内存、运行时间。底部状态栏显示进程总数、当前筛选结果数和上次刷新耗时方便评估采集性能。这样规划下来功能不多不少但已经覆盖了一个任务管理器该有的核心体验看得见状态、查得到进程、动得了进程。2. 核心细节与关键技术点2.1 数据来源Linux系统状态都写在/proc里Linux下几乎所有系统状态都能在/proc里找到这个目录本质上是内核暴露给用户态的内存视图不是磁盘上的真实文件。进程信息在/proc/[PID]/目录下每个进程一个数字命名的子目录里面有stat、status、cmdline、comm等文件。比如/proc/1234/comm返回的是进程名/proc/1234/status里的State字段返回进程状态VmRSS返回物理内存占用。要枚举所有进程最简单的方式是用QDir扫描/proc目录下所有纯数字的目录名QDir procDir(/proc); QStringList entries procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString entry : entries) { bool isPid false; int pid entry.toInt(isPid); if (isPid) { // entry 就是PID } }这种做法的好处是不依赖外部命令解析性能也足够。实测在进程数300个左右的系统上完整扫一遍所有进程并解析关键字段耗时在几十毫秒量级完全够用。后续如果还要看每个进程的详细CPU时间就去读/proc/[PID]/stat它的第14和第15个字段分别是utime和stime也就是用户态和内核态的CPU时间。2.2 CPU占用率的正确计算方式CPU占用率不能直接读一次就得出必须采样两次取差值。/proc/stat的第一行以cpu开头后面跟着8到10个递增的数字分别表示系统开机以来处于不同状态的累计jiffies系统时钟滴答数。常见字段顺序是user、nice、system、idle、iowait、irq、softirq、steal、guest、guest_nice。计算某一时间段内的CPU占用率需要记录两次采样值先算总时间差和空闲时间差再用公式得出使用率quint64 total user nice system idle iowait irq softirq steal; quint64 idleAll idle iowait; // 两次采样后 double totalDelta total - prevTotal; double idleDelta idleAll - prevIdle; double cpuUsage (1.0 - idleDelta / totalDelta) * 100.0;注意一定得用差值而不是绝对值否则算出来的永远是“开机以来的平均占用率”不是当前负载。我第一个版本踩了这个坑曲线永远是压在某个低值附近后来才意识到问题出在这里。空闲时间通常要包含iowait否则磁盘繁忙时CPU会被算成高占用显示就会失真。2.3 进程控制的正确姿势信号与kill进程操作在Linux里本质是发信号。结束进程发SIGKILL或SIGTERM暂停进程发SIGSTOP恢复进程发SIGCONT。Qt提供了QProcess但杀进程没必要起一个完整子进程在C里直接调用POSIX接口更干净。比较麻烦的是权限问题。普通用户只能操作属于自己的进程处理root进程会收到EPERM错误返回码是“Operation not permitted”。这种场景下我选择直接调用外部kill命令因为可以通过startDetached异步运行不阻塞UI线程而且用QProcess::exitCode()能拿到命令执行结果方便弹提示。QStringList args; args -9 QString::number(pid); QProcess::startDetached(kill, args);这里还有个容易被忽略的点Linux下结束父进程后子进程可能并不会跟着退出会变成孤儿进程被init收养继续占用CPU和内存。结合这个现象我在进程表里额外加了一列PPID父进程PID排查应用残留时就方便多了顺着PPID看父子链路能很快定位到底是谁没被清干净。2.4 图表与UI刷新的线程策略初版界面卡顿的问题几乎全部来自在主线程里直接读/proc。虽然单次读取不慢但每个刷新周期都要遍历几百个进程叠加UI重绘、图表数据更新在低配机器上就会出现明显的拖拽延迟。最终的方案是用QThreadPool跑一个后台采集任务后台线程只负责读文件、解析数据把结果封装成一个ProcessInfoList通过信号槽以队列连接发回主线程。主线程收到数据后只做一件最便宜的事更新表格模型和图表曲线。刷新频率我设置了两个档位前台运行时每秒刷新一次如果窗口失去焦点、用户切到别的应用自动降到每3秒刷新一次。这样既保证了监控数据的实时性又不至于让一个后台工具一直空转消耗性能。实测在双核虚拟机里运行很平稳CPU占用率本体的开销能控制在1%以内。3. 实操过程与核心功能实现3.1 工程搭建Qt版本、依赖与QCustomPlot接入工程用qmake管理项目文件最简单几行配置就行QT core gui widgets printsupport TARGET linux-task-manager TEMPLATE app SOURCES main.cpp MainWindow.cpp \ ProcessTableModel.cpp SystemMonitor.cpp HEADERS MainWindow.h \ ProcessTableModel.h SystemMonitor.h \ qcustomplot.h SOURCES qcustomplot.cppQCustomPlot虽然叫单文件库但如果你把它的.cpp加入工程必须在QT里加上printsupport否则编译会报一堆QPrinter相关的错误这是网上问得最多的编译问题之一。装好Qt后建议用5.15.2或者更新的6.5以上LTS编译器用g即可不用额外装其他库。3.2 进程表格模型从/proc到QAbstractTableModel进程列表没有用简单的QTableWidget而是选择了QAbstractTableModel核心原因是数据量可能上百行需要排序、过滤、局部更新Model/View架构能把这些操作委托给View层的ProxyModel处理性能和耦合度都更优。关键重写方法就四个rowCount、columnCount、data、headerData。数据后端是QListProcessInfo每行对应一个进程快照。int rowCount(const QModelIndex) const override { return m_processes.size(); } QVariant data(const QModelIndex index, int role) const override { if (!index.isValid() || role ! Qt::DisplayRole) return QVariant(); const ProcessInfo info m_processes.at(index.row()); switch (index.column()) { case 0: return info.pid; case 1: return info.name; case 2: return QString(%1%).arg(info.cpuUsage, 0, f, 1); case 3: return QString(%1 MB).arg(info.memRss / 1024); case 4: return info.state; case 5: return info.user; default: return QVariant(); } }后台采集完数据后主线程会调用updateProcesses(newList)更新模型。如果全量替换就用beginResetModel()/endResetModel()但这种方式会让排序和选中状态丢失体验不好。我最后优化成只对变化行发dataChanged信号这样表格不会每秒钟闪一下视觉上平滑了很多。3.3 系统监控采集器与实时曲线CPU和内存采集我单独封装成一个SystemMonitor类对外暴露readCpuUsage()和readMemInfo()两个方法。内存数据从/proc/meminfo解析最关键是MemTotal和MemAvailable两个字段。注意不要用MemFree因为Linux下有一部分内存被用作文件缓存会被系统在内存压力下自动释放MemFree直接算可用内存会把数值算得很低造成“明明内存够用却显示75%占用”的假象。曲线绘制用的QCustomPlot初始化时设置两个graph和坐标轴范围// CPU曲线 customPlot-addGraph(); customPlot-graph(0)-setPen(QPen(QColor(52, 152, 219))); customPlot-yAxis-setRange(0, 100); // 内存曲线 customPlot-addGraph(); customPlot-graph(1)-setPen(QPen(QColor(231, 76, 60))); customPlot-yAxis-setRange(0, 100); customPlot-xAxis-setRange(0, 60, Qt::AlignRight);每轮刷新时对新数据只addData一个点然后检查点数量超过60个点就把最旧的丢弃。X轴用滚动模式看起来就像Windows任务管理器那根持续向右推移的曲线。有朋友之前问过QCustomPlot有没有类似QtCharts的append高效追加接口答案是有graph()-addData(double key, double value)它内部是红黑树结构增量插入效率很高不需要每帧setData全量赋值。3.4 右键菜单与进程操作交互给进程表格加右键菜单主要是三步重写customContextMenuRequested信号、弹QMenu、执行动作。因为用了QTableView直接把setContextMenuPolicy(Qt::CustomContextMenu)打开在槽函数里拿当前行对应的进程信息connect(ui-tableView, QTableView::customContextMenuRequested, this, MainWindow::onContextMenu); void MainWindow::onContextMenu(const QPoint pos) { QModelIndex index ui-tableView-indexAt(pos); if (!index.isValid()) return; int pid m_model-getProcess(index.row()).pid; QMenu menu(this); QAction* killAction menu.addAction(结束进程); QAction* stopAction menu.addAction(暂停进程); QAction* contAction menu.addAction(恢复进程); QAction* chosen menu.exec(ui-tableView-viewport()-mapToGlobal(pos)); // 根据chosen执行对应操作 }结束操作前加了一个确认弹窗内容是“确定要结束进程[xxx] (PID)吗”这样能有效防止误点。实际操作后我发现有些用户很容易把“暂停”误当成“结束”所以暂停动作执行完会立刻把该行状态从R改为T同时在状态栏给一条提示让用户知道进程并没有被销毁可以随时恢复。4. 常见问题与排查技巧实录4.1 界面卡顿问题别在主线程读文件这是整个项目里最典型的问题。初版直接在QTimer::timeout里同步读取所有/proc数据再刷新表格和图表编译后在普通机器上跑着还行一旦系统里开着浏览器、编译任务任务管理器自己就开始掉帧。后来我把采集逻辑抽到QRunnable里用QThreadPool::globalInstance()-start()丢后台线程执行。注意一点背景线程不能碰任何QWidget只能把结果包成结构体通过信号槽发回主线程。这里如果忘了指定连接类型Qt会默认在emit那一刻把参数拷贝一份对于QListProcessInfo这种自定义类型记得在声明信号前调用qRegisterMetaTypeQListProcessInfo()否则连接会失败。4.2 结束进程提示权限不足怎么办普通用户尝试结束root拥有的进程时kill返回1错误信息是Operation not permitted。我在代码里做了错误捕获弹窗提示“没有权限结束该进程建议以管理员权限重试”。严格来说图形界面应用不建议默认用sudo整个程序安全风险太大。遇到确实需要杀root进程的场景更稳妥的做法是调pkexec弹出一个polkit授权框用户临时授权执行一次kill命令。因为pkexec本身也是个外部程序QProcess::startDetached(pkexec, {kill, -9, pid})就能直接复用现有逻辑。4.3 CPU使用率显示超过100%的坑多核机器上如果不做归一化CPU使用率可以超过100%。比如8核CPU上单核满载就显示12.5%如果进程开了8个线程跑满8核显示就是100%而某些工具可能显示800%。Windows任务管理器的习惯是“总使用率不超过100%”就是在总数上除以了CPU核心数。我的做法是给界面加了一个开关默认显示“总CPU占用率”0-100%选中后切换为“每核心平均占用率”这个对判断系统是否整体过载很有用。如果不想做开关直接在计算时除以核数也可以看个人喜好。4.4 跨发行版兼容与高分屏适配/proc接口在所有主流Linux发行版上基本一致真正容易出问题的是环境差异。比如某发行版没有安装中文字体界面里进程名是乱码比如在容器里运行/proc下的进程数不完整部分字段可能缺失。解决办法是界面用系统默认字体不做硬编码同时解析字段时做好缺失判断读取失败就填N/A保证程序不崩溃。高分屏适配必须提一句。Qt 5.15可以直接在main函数里打开HiDPI属性QApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true);如果不开在2K或4K屏上整个界面会糊成一团字体和图标都发虚。Qt 6默认自带高分屏支持不需要手动开。下面整理一个速查表汇总我踩过的主要问题和对应的解决思路现象原因处理方式UI卡顿、拖动窗口掉帧主线程频繁读取/proc采集放到后台线程结果通过信号槽回传CPU曲线数值偏低、不波动用累计值直接算占用率必须两次采样取差值内存占用显示偏高用了MemFree判断可用内存改用MemAvailable结束进程无效普通用户操作root进程没有权限捕获错误并提示用pkexec授权表格每秒闪烁、选中丢失每次全量reset模型只在变化行发dataChanged更新高分屏字体模糊Qt未启用HiDPI开启AA_EnableHighDpiScaling结束父进程后子进程还在进程树关系未清理追加PPID列并支持按父进程过滤5. 扩展方向与个人体会5.1 往系统监视器方向扩展有了进程列表这个底座后续扩展空间其实很大。比如从/proc/net/dev解析各网卡流量画一条实时的上行下行曲线从/proc/diskstats读磁盘读写速率还可以通过libsystemd直接拉起systemd服务管理面板一键重启或停止系统服务那就更接近完整的系统监视器了。对图形界面部分还会考虑加一个“启动应用程序”功能相当于在任务管理器里快速启动一些常用工具。结合一些帖子提到的启动残留问题更实用的方案是在进程表里支持“杀掉整个进程树”也就是同时向某个进程的所有子孙进程发SIGKILL。这个递归遍历/proc/[pid]/task/[pid]/children的逻辑不算复杂但确实能把很多顽固应用彻底清干净。5.2 编码过程中的真实感受整个项目从零到基本可用前后花了一个多星期大部分时间其实不是花在写Qt控件上而是花在理解Linux怎么把系统状态暴露给用户态。/proc这套文件接口初看很零散但它有一种很朴素的哲学一切都是文件读文件就是读状态写文件就是改配置发信号就是控制进程。理解了这一层后续再做任何Linux系统工具思路都会清晰很多。最后再分享一个小技巧在调试CPU采集算法时别只看平均值记得做一个“空跑”对比实验——程序刚启动时曲线应该接近0持续跑一个死循环线程时曲线应该稳定在一个接近单核满载的数值。如果你的曲线波动剧烈或者和top对不上优先检查是不是采样间隔太短导致两次/proc/stat差值过小产生抖动。我最后把采样间隔稳定在1000毫秒曲线平滑度和实时性之间的平衡最理想。本文还有配套的精品资源点击获取
返回列表