尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Rust+Tauri实战:打造Windows内存优化工具RAMGuard Pro

Rust+Tauri实战:打造Windows内存优化工具RAMGuard Pro RAMGuard Pro 是一个面向 Windows 的实时内存优化工具项目标题给出的技术栈组合是 Rust Tauri。这类工具的核心价值在于它要在长期驻留、低资源占用、系统 API 调用和前端可视化之间找到平衡点。本文不会只停留在功能描述而是直接带着一个最小可运行版本走完整个链路从 Windows 内存模型讲起搭建 Tauri 2 Rust 项目实现内存快照、进程枚举、工作集清理、托盘与开机启动最后补充常见乱码、权限、invoke 报错和杀软误报的排查路径。读完这篇文章你可以独立复刻一个与 RAMGuard Pro 同类的 Windows 内存优化器也能理解为什么“内存优化”不能被简单理解成“让可用内存越高越好”。1. 内存优化器的真实边界先理解 Windows 内存模型再动手1.1 从物理内存到工作集优化工具到底在优化什么Windows 的物理内存由内核统一管理应用程序申请虚拟内存后内核会在进程真正访问页面时把对应物理页映射进去。一个进程当前正在使用的物理内存页集合称为工作集Working Set。任务管理器里看到的“内存(活动工作集)”就是这个集合的大小。内存优化工具最常做的一件事就是把进程工作集中“暂时不活跃”的物理页面写回页面文件让这些物理页进入备用列表从而抬高可用物理内存。这里有两个关键点需要明确。第一清理工作集不等于释放进程申请的内存。进程的虚拟内存空间、已提交内存、私有字节数都不会因为清理而减少。如果程序随后继续访问这些页面Windows 会产生硬错误把页面从页面文件重新换入物理内存Cleaner 带来的可用内存收益会迅速回落。第二Windows 自身也有工作集修剪机制系统内存压力增大时内核会主动修剪进程工作集。第三方工具做的事情本质上是把这个动作提前或扩大。所以RAMGuard Pro 这类工具真正应该回答的问题是能不能在系统响应变慢之前主动把内存中的冷页换出而不是等到内存耗尽才被系统强制修剪。基于这个目标工具需要的核心能力是内存快照、进程枚举、按需清理和可视化反馈而不是“点击一下可用内存暴涨”的短期效果。1.2 为什么 RAMGuard Pro 选择 Rust Tauri同样是做 Windows 桌面内存优化工具可选的方案很多常见组合是 Electron、C# WPF、C/Qt以及本文使用的 Rust Tauri。技术选型的关键矛盾在于工具本身要长期驻留后台内存占用必须低到不能成为系统负担同时又要提供一个可操作、可展示图表和进程表格的界面。对比起来几个方案的差异非常明显。技术栈可执行体积运行内存占用Windows API 调用能力开发效率适合场景Electron通常 150MB 以上较高几十 MB 到上百 MB需要通过 Node 原生模块间接调用高重前端应用C# WPF中等中等原生P/Invoke 方便高纯 Windows 桌面工具C/Qt较小低原生中等对性能和发布体积要求极高Rust Tauri通常 10MB 以内低Rust 直接调用 Windows API中等跨平台且需要系统调用的工具类应用Tauri 比较适合 RAMGuard Pro 的一个原因是它让后端和系统交互这一层完全掌握在 Rust 手里。Windows API 的 OpenProcess、GetProcessMemoryInfo、EmptyWorkingSet 都可以直接调用不需要通过桥接层绕路而窗口、按钮、列表、图表仍然可以用 Web 技术快速搭建。代价也很明确Rust 编译器对 Windows API 封装版本的差异比较敏感依赖更新后字段名或方法签名可能变化需要开发者持续跟进锁定版本。2. 搭建 Tauri 2 Rust 项目骨架2.1 环境准备Rust、Node.js 和 WebView2 要一起对齐在 Windows 上运行 Tauri 项目需要有 Rust stable 工具链、Node.js 和 npm以及 WebView2 Runtime。Windows 10 和 Windows 11 系统一般自带 WebView2但如果系统版本较旧需要单独确认。打开终端后先确认工具链版本rustc --version cargo --version node --version npm --version如果 Rust 还没有安装可以使用 rustup 安装默认 stable 工具链。这里要注意工具链后缀的选择在 Windows 上默认推荐 MSVC 工具链因为后续链接系统库和启用 Windows 长路径都更省心。rustup 配置中如果使用了 GNU 工具链在调用部分 Windows API 或链接系统库时可能遇到 ABI 层面的兼容问题因此学习阶段优先使用x86_64-pc-windows-msvc。Tauri 2 还需要 Microsoft C Build Tools 中的链接器。安装 Visual Studio Build Tools 时勾选“使用 C 的桌面开发”工作负载即可。如果本机是刚装好的 Windows容易漏掉这一项导致npm run tauri dev第一次编译时在链接阶段报LINK : fatal error LNK1104或找不到link.exe。2.2 创建项目并理解目录结构使用官方脚手架创建项目模板这里选择 vanilla-ts最小且容易看清结构npm create tauri-applatest ramguard-pro -- --template vanilla-ts cd ramguard-pro npm install创建完成后项目目录结构如下ramguard-pro/ ├─ src/ │ ├─ main.ts │ └─ styles.css ├─ src-tauri/ │ ├─ src/ │ │ ├─ main.rs │ │ └─ lib.rs │ ├─ Cargo.toml │ ├─ build.rs │ ├─ tauri.conf.json │ ├─ capabilities/default.json │ └─ icons/ ├─ package.json └─ vite.config.ts这里最容易犯的错是把前端命令和后端命令混在一起。前端部分由 Vite 和 npm 管理Rust 部分由 Cargo 管理。日常开发运行npm run tauri devTauri CLI 会先启动 Vite 开发服务器再编译 Rust最终把一个 WebView 窗口加载到本机地址同时把前端指向 devUrl。如果直接执行cargo run会因为没有前端页面而得到一个空白窗口或直接报错。2.3 配置 Cargo 国内源避免依赖下载卡住项目第一次npm run tauri dev时Cargo 需要拉取大量 crate。如果默认源访问不稳定最先要做的是配置国内镜像源而不是等待或重试。在~/.cargo/config.toml中写入[source.crates-io] replace-with rsproxy-sparse [source.rsproxy-sparse] registry sparsehttps://rsproxy.cn/index/对于 rustup 工具链更新可以设置环境变量RUSTUP_DIST_SERVERhttps://rsproxy.cn RUSTUP_UPDATE_ROOThttps://rsproxy.cn/rustupnpm 依赖下载慢时也可以切换到 npm 镜像。镜像源只影响下载速度不影响依赖逻辑也不需要调整业务代码。这个配置对 Tauri 项目尤其重要因为 Tauri 2 的依赖树比较大windowscrate、tauri本体和前端构建链加在一起首次全量编译等待时间可能很长。3. 实现系统内存快照与进程枚举3.1 使用 GlobalMemoryStatusEx 获取系统内存状态系统内存信息是整个 RAMGuard Pro 前端仪表盘的数据基础。在 Windows 中最直接的接口是GlobalMemoryStatusEx它能返回物理内存总量、可用物理内存、页面文件总量、虚拟内存总量等信息。在src-tauri/src下新增一个memory.rs先定义对应的 Rust 结构体use serde::Serialize; use windows::Win32::System::Memory::{GlobalMemoryStatusEx, MEMORYSTATUSEX}; #[derive(Serialize)] #[serde(rename_all camelCase)] pub struct MemoryStatus { pub total_phys: u64, pub avail_phys: u64, pub used_phys: u64, pub total_page_file: u64, pub avail_page_file: u64, pub memory_load: u32, } pub fn get_memory_status() - MemoryStatus { let mut status MEMORYSTATUSEX::default(); status.dwLength std::mem::size_of::MEMORYSTATUSEX() as u32; unsafe { GlobalMemoryStatusEx(mut status).expect(GlobalMemoryStatusEx failed); } let total_phys status.ullTotalPhys; let avail_phys status.ullAvailPhys; MemoryStatus { total_phys, avail_phys, used_phys: total_phys - avail_phys, total_page_file: status.ullTotalPageFile, avail_page_file: status.ullAvailPageFile, memory_load: status.dwMemoryLoad, } }MEMORYSTATUSEX的关键字段含义如下。字段含义在工具中的作用dwMemoryLoad当前内存使用百分比顶部仪表盘的百分比ullTotalPhys物理内存总量计算使用率时的分母ullAvailPhys可用物理内存判断是否触发自动清理ullTotalPageFile当前提交限制帮助判断系统是否接近提交上限ullAvailPageFile可用提交空间排查系统虚拟内存耗尽问题注意这个函数返回的是瞬时快照。桌面工具显示数字和清理后的效果时不要指望它是稳定的趋势线必须配合定时刷新。3.2 枚举进程列表并读取工作集内存优化器需要展示所有用户可见进程的内存占用并允许逐个清理。核心流程是调用EnumProcesses获取当前系统所有 PID。对每个 PID 调用OpenProcess打开进程对象。调用GetProcessMemoryInfo获取工作集和私有字节数。调用QueryFullProcessImageNameW获取进程完整路径。如果OpenProcess因为权限不足失败跳过该进程并记录原因。实现代码如下use windows::core::PWSTR; use windows::Win32::Foundation::{CloseHandle, HANDLE}; use windows::Win32::System::ProcessStatus::{ EnumProcesses, GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS, }; use windows::Win32::System::Threading::{ OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION, }; #[derive(Serialize, Clone)] #[serde(rename_all camelCase)] pub struct ProcessInfo { pub pid: u32, pub name: String, pub path: String, pub working_set: u64, pub private_bytes: u64, } fn read_process_name(handle: HANDLE) - String { let mut buffer [0u16; 32768]; let mut size buffer.len() as u32; let result unsafe { QueryFullProcessImageNameW( handle, PROCESS_NAME_WIN32, PWSTR(buffer.as_mut_ptr()), mut size, ) }; if result.is_ok() size 0 { String::from_utf16_lossy(buffer[..size as usize]) } else { String::from(unknown) } } pub fn list_processes() - VecProcessInfo { let mut pids [0u32; 4096]; let mut bytes_returned: u32 0; unsafe { EnumProcesses( pids.as_mut_ptr(), std::mem::size_of_val(pids) as u32, mut bytes_returned, ) .expect(EnumProcesses failed); } let pid_count (bytes_returned as usize) / std::mem::size_of::u32(); let mut result Vec::new(); for pid in pids.iter().take(pid_count) { let process_handle unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) }; let Ok(handle) process_handle else { continue; }; let mut pmc PROCESS_MEMORY_COUNTERS::default(); let mem_result unsafe { GetProcessMemoryInfo( handle, mut pmc, std::mem::size_of::PROCESS_MEMORY_COUNTERS() as u32, ) }; let path read_process_name(handle); unsafe { CloseHandle(handle) }; if mem_result.is_ok() { result.push(ProcessInfo { pid, name: path.rsplit(\\).next().unwrap_or().to_string(), path, working_set: pmc.WorkingSetSize, private_bytes: pmc.PrivateUsage, }); } } result.sort_by_key(|p| std::cmp::Reverse(p.working_set)); result }这段代码有几个细节要理解。EnumProcesses返回的是字节数所以进程数量要用字节数除以u32大小得到。OpenProcess打开的句柄必须通过CloseHandle关闭否则长时间运行会句柄泄漏。PROCESS_QUERY_LIMITED_INFORMATION是权限最小的查询权限如果换成PROCESS_QUERY_INFORMATION很多系统进程会拒绝访问反而拿不到数据。进程名取完整路径的反斜杠分隔最后一段中文路径在这个地方最容易出现乱码后面排错部分会单独讲。这里使用的 windows crate 版本是 0.58 左右。不同版本的函数签名和结构体字段命名存在差异如果编译报错优先查当前版本的windowscrate 文档而不是机械照抄旧代码。3.3 把快照数据序列化给前端为了让前端一次拿到完整数据把内存状态和进程列表打包成一个快照#[derive(Serialize)] #[serde(rename_all camelCase)] pub struct MemorySnapshot { pub memory: MemoryStatus, pub processes: VecProcessInfo, } pub fn collect_snapshot() - MemorySnapshot { MemorySnapshot { memory: get_memory_status(), processes: list_processes(), } }在lib.rs中注册命令#[tauri::command] fn get_memory_snapshot() - MemorySnapshot { memory::collect_snapshot() } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![get_memory_snapshot]) .run(tauri::generate_context!()) .expect(error while running tauri application); }serde的rename_all camelCase会把total_phys转成前端看到的totalPhys这样 JavaScript 侧对象风格统一避免下划线来回转换。4. 实现工作集清理EmptyWorkingSet 与权限控制4.1 EmptyWorkingSet 的原理和限制清理进程工作集的核心 API 是EmptyWorkingSet它可以把进程工作集修剪到最小。调用后进程仍然拥有完整的虚拟地址空间但其中不活跃的物理页会被写回页面文件物理内存可用量会立刻上升。这个 API 的真正意义在于告诉 Windows当前进程的冷页可以被回收了。它不会抢救已经泄漏的内存也不改变进程的 Commit Size。最常见的错误理解是“清理后某个程序的内存占用应该变小”实际上任务管理器里那个程序的工作集可能变小但提交大小没有变化。对于 RAMGuard Pro 来说合理的策略是手动清理、低频自动清理而不是每秒钟把所有进程的工作集都打空。系统本身有内存管理和缓存机制过度清理会让进程重新访问内存时产生大量硬错误反而造成卡顿。4.2 清理单个进程并返回清理前工作集清理一个进程需要两个权限读进程信息和设置进程配额。对应的权限标志是PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SET_QUOTA。use windows::Win32::System::Memory::EmptyWorkingSet; use windows::Win32::System::Threading::{PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SET_QUOTA}; #[tauri::command] fn clean_process_by_pid(pid: u32) - Resultu64, String { let handle unsafe { OpenProcess( PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SET_QUOTA, false, pid, ) } .map_err(|e| format!(OpenProcess failed, pid{pid}, error{e}))?; let mut pmc PROCESS_MEMORY_COUNTERS::default(); unsafe { GetProcessMemoryInfo( handle, mut pmc, std::mem::size_of::PROCESS_MEMORY_COUNTERS() as u32, ) } .map_err(|e| format!(GetProcessMemoryInfo failed, pid{pid}, error{e}))?; let before pmc.WorkingSetSize; unsafe { EmptyWorkingSet(handle) } .map_err(|e| format!(EmptyWorkingSet failed, pid{pid}, error{e}))?; unsafe { CloseHandle(handle) }.ok(); Ok(before) }命令返回清理前的工作集前端可以计算出本次清理释放了多少字节。这里计算出来的“释放量”只是工作集减少量不能把它宣传成“释放了这么多内存”因为页面换回时又会被重新统计。4.3 全量清理、进程白名单与系统保护全量清理就是遍历进程列表逐个执行上面的清理函数。但有些进程不能碰直接清理会带来系统不稳定或蓝屏风险。至少需要跳过以下类型PID 为 0 的 System Idle Process。PID 为 4 的 System 进程和核心系统进程。当前 RAMGuard Pro 自己以及它所依赖的 WebView2 子进程。返回错误码 5拒绝访问的受保护进程。用户配置到白名单中的进程例如正在运行的数据库、虚拟机、开发工具等。全量清理实现#[tauri::command] fn clean_all_processes(whitelist: OptionVecu32) - ResultCleanResult, String { let processes memory::list_processes(); let whitelist whitelist.unwrap_or_default(); let mut cleaned 0u32; let mut freed 0u64; for process in processes { if whitelist.contains(process.pid) || process.pid 4 || process.pid std::process::id() { continue; } if let Ok(before) clean_process_by_pid(process.pid) { cleaned 1; freed before; } } Ok(CleanResult { cleaned, freed }) }4.4 管理员权限没有提权很多进程根本碰不到即使代码使用了正确的权限标志普通权限进程依然无法打开高完整性级别进程。RAMGuard Pro 要看到并清理尽可能多的进程就需要以管理员权限运行。在 Tauri 2 项目中可以通过在src-tauri下添加app.manifest并在build.rs中嵌入资源assembly xmlnsurn:schemas-microsoft-com:asm.v1 manifestVersion1.0 trustInfo xmlnsurn:schemas-microsoft-com:asm.v3 security requestedPrivileges requestedExecutionLevel levelrequireAdministrator uiAccessfalse / /requestedPrivileges /security /trustInfo /assemblyfn main() { tauri_build::build(); let manifest_path std::path::PathBuf::from(std::env::var(CARGO_MANIFEST_DIR).unwrap()).join(app.manifest); println!(cargo:rerun-if-changed{}, manifest_path.display()); embed_resource::compile(app.manifest, embed_resource::NONE); }对应的Cargo.toml需要添加embed-resource构建依赖。提权是双刃剑程序如果被注入或利用影响范围会更大。RAMGuard Pro 属于系统工具提权可以接受但必须严格
返回列表