Linux 性能分析与优化实战指南:perf / 火焰图 / Heaptrack 全流程

Linux 性能分析与优化实战指南 基于 Hical 项目的 Ubuntu 24.04 VM 环境(VirtualBox,8 CPU / 16GB RAM)。 前置条件:已完成 Hical-Linux开发环境 和 VM编译运行Hical-Benchmark流程 的环境搭建。 目录 零、工具安装 一、perf stat:硬件计数器分析 二、perf record + 火焰图:CPU 热点定位 三、Heaptrack:内存分配分析 四、缓存层次与 cache line 五、实战:Hical 性能分析全流程 六、速查卡 零、工具安装 0.1 一键安装所有性能工具 1 2 3 4 5 6 7 8 9 10 11 # perf(必须匹配内核版本) sudo apt install -y linux-tools-$(uname -r) linux-tools-generic # heaptrack(内存分配分析) sudo apt install -y heaptrack heaptrack-gui # FlameGraph(火焰图生成脚本) git clone --depth 1 https://github.com/brendangregg/FlameGraph.git ~/FlameGraph # 辅助工具 sudo apt install -y valgrind strace sysstat hwloc 0.2 内核参数调整(perf / heaptrack 权限) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # ── perf 权限 ── # 查看当前值(默认通常是 4,限制很严) cat /proc/sys/kernel/perf_event_paranoid # 临时放开(重启失效) sudo sysctl -w kernel.perf_event_paranoid=-1 sudo sysctl -w kernel.kptr_restrict=0 # ── ptrace 权限(heaptrack --pid 运行时附着需要) ── # 查看当前值(默认 1,禁止非父进程 ptrace) cat /proc/sys/kernel/yama/ptrace_scope # 临时放开(重启失效) sudo sysctl -w kernel.yama.ptrace_scope=0 # ── 永久生效(写入配置文件) ── cat << 'EOF' | sudo tee /etc/sysctl.d/99-perf.conf kernel.perf_event_paranoid = -1 kernel.kptr_restrict = 0 kernel.yama.ptrace_scope = 0 EOF sudo sysctl --system 各级别含义: ...

May 15, 2026 · 25 min · 5176 words

perf + 火焰图:5 分钟定位 C++ 程序的 CPU 瓶颈

perf + 火焰图:5 分钟定位 C++ 程序的 CPU 瓶颈 你的服务器 CPU 跑满了,QPS 却上不去。top 告诉你"忙",但不告诉你忙在哪。怎么办? 故事:从 27K 到 136K QPS 我开发了一个 C++20/26 Web 框架(Hical),第一次压测只有 27K QPS,而同场景下 Drogon 和 Cinatra 都在 160K+。CPU 使用率 100%,top 没用,gdb 打断点太慢。 最终靠 perf record + 火焰图,5 分钟定位到瓶颈不在我的框架代码(仅占 2% CPU),而在 Boost.Asio 的调度层——跨线程 epoll_ctl 和 per-request timer 合计吃了 27% CPU。 优化后 QPS 从 27K → 136K。 这篇文章把我整套分析流程分享出来。不需要你用过 Hical,任何 C++ 服务器程序都适用。 一、工具安装(2 分钟搞定) 1 2 3 4 5 6 7 8 9 # perf(必须匹配内核版本) sudo apt install -y linux-tools-$(uname -r) linux-tools-generic # FlameGraph 脚本(Brendan Gregg 出品) git clone --depth 1 https://github.com/brendangregg/FlameGraph.git ~/FlameGraph # 放开 perf 权限(否则只能看到自己的进程) sudo sysctl -w kernel.perf_event_paranoid=-1 sudo sysctl -w kernel.kptr_restrict=0 验证: ...

May 15, 2026 · 5 min · 971 words

Hical Linux 开发环境搭建

Hical Linux 开发环境(实操记录) 概述 将 Hical C++20 Web 框架的日常开发环境从 Windows 本地迁移到 Linux 虚拟机,通过 Oracle VM VirtualBox 运行 Ubuntu 24.04.3 LTS Server 虚拟机,使用 Tabby Terminal + VS Code Remote SSH 进行开发。 为什么迁移 痛点 Linux 解决 MSYS2/vcpkg 路径混乱、ABI 不兼容 原生 apt 包管理,toolchain 统一 liburing/io_uring 仅 Linux 可用 原生支持,解锁异步文件 I/O 最优路径 CI 环境是 Ubuntu,本地是 Windows 开发即 CI,减少"本地能跑 CI 挂"的问题 clang-format/clang-tidy 版本对齐困难 直接装 LLVM APT 源的 clang-20 Sanitizer(ASan/UBSan)Windows 支持差 Linux 原生完美支持 宿主机配置 项目 值 处理器 Intel Core i7-11700K @ 3.60GHz(8核16线程) 内存 32 GB 操作系统 Windows 10 Enterprise LTSC 2021 VM 存放磁盘 D:\ (900 GB,SSD) 一、VirtualBox 虚拟机创建 1.1 下载 软件 版本 下载地址 VirtualBox 7.1+ https://www.virtualbox.org/wiki/Downloads Ubuntu ISO ubuntu-24.04.3-live-server-amd64.iso(3.1 GiB) https://mirrors.tuna.tsinghua.edu.cn/ubuntu-releases/24.04/ 选择 Server 版(无桌面 GUI),减少资源占用。国内推荐清华镜像下载更快。 ...

May 8, 2026 · 17 min · 3460 words

VM 编译运行 Hical Benchmark 流程:不走 Docker 的本地压测方案

VM 直接编译运行 Hical Benchmark 流程 不走 Docker,直接在 VM 上编译项目源码 + bench_server,用 wrk 压测。 适用于需要验证本地代码改动的场景(如性能优化后的 A/B 对比)。 前置条件 VM 里已有 Hical 项目源码:~/projects/Hical/ 已安装编译依赖(GCC 14+、CMake 3.20+、Boost 1.82+、OpenSSL) 挂接点 /mnt/hical_host/ 对应宿主机 d:/hical/Hical/ 一、同步宿主机代码改动到 VM 如果在宿主机上修改了源码,需要先拷贝到 VM 项目目录: 1 2 3 4 5 # 示例:拷贝 3 个改动文件 cp /mnt/hical_host/src/core/HttpServer.h \ /mnt/hical_host/src/core/HttpServer.cpp \ /mnt/hical_host/src/core/HttpSessionImpl.cpp \ ~/projects/Hical/src/core/ 或者整体同步 src 目录: 1 rsync -av /mnt/hical_host/src/ ~/projects/Hical/src/ 二、编译 1 2 3 4 5 6 7 8 9 10 cd ~/projects/Hical # 清理旧构建(可选,首次或 CMake 配置变更时执行) rm -rf build # 配置:Release + 编译 bench_server cmake -B build -DCMAKE_BUILD_TYPE=Release -DHICAL_BUILD_BENCH=ON # 编译(-j 自动取 CPU 核数) cmake --build build -j$(nproc) 编译产物:build/bench_server(直接链接本地 hical_core 库,代码改动即生效)。 ...

May 8, 2026 · 6 min · 1193 words

深入学习 io_uring(三):C++ 封装、协程集成与高性能架构

系列导航:入门篇 | 进阶篇 | 实战篇 前置知识 已阅读入门篇和进阶篇,掌握 io_uring 双环形缓冲区和 liburing API 了解 C++20 协程基础(co_await、coroutine_handle、promise_type) 建议先阅读 深入学习 Boost.Asio(三):实战篇 中的协程部分作为对照 1. RAII 封装:安全管理 io_uring 资源 1.1 为什么需要 C++ 封装 直接使用 liburing 的 C API 有三个痛点: io_uring_queue_init / io_uring_queue_exit 手动配对,容易遗漏 user_data 是 void* 或 uint64_t,类型安全全靠人肉 提交→收割的事件循环代码高度模板化,每个项目重写一遍 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 封装层次: 应用代码(协程/回调) │ ▼ ┌──────────────────────┐ │ IoUringAwaitable │ ← 协程集成层(co_await 一个 I/O 操作) └──────────────────────┘ │ ▼ ┌──────────────────────┐ │ IoUringContext │ ← 事件循环层(submit / wait / dispatch) └──────────────────────┘ │ ▼ ┌──────────────────────┐ │ IoUring (RAII) │ ← 资源管理层(init / exit) └──────────────────────┘ │ ▼ liburing C API 1.2 IoUring:RAII 包装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 // IoUring.hpp — RAII 封装 io_uring 实例 // 编译:g++ -std=c++20 -O2 xxx.cpp -luring -o xxx #pragma once #include <liburing.h> #include <stdexcept> #include <string> #include <cstring> class IoUring { public: // 构造时初始化 io_uring,指定 SQ 大小和可选标志 explicit IoUring(unsigned entries, unsigned flags = 0) { int ret = io_uring_queue_init(entries, &ring_, flags); if (ret < 0) { throw std::runtime_error( "io_uring_queue_init 失败: " + std::string(strerror(-ret))); } } // 禁止拷贝(io_uring 资源不可共享) IoUring(const IoUring&) = delete; IoUring& operator=(const IoUring&) = delete; // 允许移动 IoUring(IoUring&& other) noexcept : ring_(other.ring_) { other.moved_ = true; } // 析构时自动清理 ~IoUring() { if (!moved_) { io_uring_queue_exit(&ring_); } } // 获取 SQE(SQ 满时自动 submit 腾出空间) io_uring_sqe* getSqe() { io_uring_sqe* sqe = io_uring_get_sqe(&ring_); if (!sqe) { // SQ 满了,先提交当前积压的请求 io_uring_submit(&ring_); sqe = io_uring_get_sqe(&ring_); if (!sqe) { throw std::runtime_error("SQ 空间不足,即使 submit 后仍无法获取 SQE"); } } return sqe; } int submit() { return io_uring_submit(&ring_); } // 阻塞等待至少一个 CQE io_uring_cqe* waitCqe() { io_uring_cqe* cqe = nullptr; int ret = io_uring_wait_cqe(&ring_, &cqe); if (ret < 0) { throw std::runtime_error( "io_uring_wait_cqe 失败: " + std::string(strerror(-ret))); } return cqe; } // 非阻塞查看 CQE io_uring_cqe* peekCqe() { io_uring_cqe* cqe = nullptr; int ret = io_uring_peek_cqe(&ring_, &cqe); if (ret == -EAGAIN) return nullptr; // 无就绪 CQE if (ret < 0) { throw std::runtime_error( "io_uring_peek_cqe 失败: " + std::string(strerror(-ret))); } return cqe; } // 标记 CQE 已消费 void seenCqe(io_uring_cqe* cqe) { io_uring_cqe_seen(&ring_, cqe); } // 访问底层 io_uring(高级用法需要) io_uring* raw() { return &ring_; } private: io_uring ring_{}; bool moved_ = false; }; 设计原则:RAII 保证 io_uring_queue_exit 一定被调用,即使异常传播也不会泄漏内核资源。getSqe() 中自动 submit 是防御性编程——避免 SQ 满导致的隐性 bug。 ...

October 3, 2025 · 14 min · 2887 words

深入学习 io_uring(二):高级特性与 TCP 网络编程

系列导航:入门篇 | 进阶篇 | 实战篇 前置知识 已阅读入门篇,理解 SQ/CQ 双环形缓冲区和 liburing 基本 API 熟悉 TCP socket 编程基础(socket、bind、listen、accept) 1. SQPOLL:零系统调用提交 1.1 常规模式的瓶颈 入门篇中每次 io_uring_submit() 底层都会调用 io_uring_enter() 系统调用: 1 2 3 4 5 6 7 8 9 10 常规模式: 用户态 内核态 │ │ │ SQE 写入共享内存 │ │ │ │ io_uring_enter(to_submit=N) │ ├────────系统调用──────────────→│ ← 仍有上下文切换 │ │ 读取 SQ,执行 I/O │ 返回 │ │←─────────────────────────────┤ 对于超高频提交场景(如高频交易、高吞吐数据库),连这一次系统调用都嫌多。 ...

October 2, 2025 · 15 min · 3155 words

深入学习 io_uring(一):从原理到第一个异步程序

系列导航:入门篇 | 进阶篇 | 实战篇 引言:epoll 之后,还能更快吗? 假设你用 epoll 写了一个高并发 TCP 服务器,性能已经不错——C10K 问题解决了。但当你把连接数推到 C1M(百万级) 时: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 epoll 的瓶颈: 用户态 内核态 │ │ │ epoll_wait() │ ├───────系统调用─────────────→│ ← 每次至少一次上下文切换 │ │ │ 返回就绪的 fd 列表 │ │←───────────────────────────┤ │ │ │ recv(fd_1, buf, ...) │ ├───────系统调用─────────────→│ ← 每个 fd 又一次系统调用! │ │ │ send(fd_1, resp, ...) │ ├───────系统调用─────────────→│ ← 再一次! │ │ │ recv(fd_2, buf, ...) │ ├───────系统调用─────────────→│ ← N 个连接 = 2N+ 次系统调用 问题:epoll 只解决了"哪些 fd 就绪"的问题,每次 I/O 操作仍然需要独立的系统调用。百万连接下,系统调用的开销成为主要瓶颈——上下文切换、数据拷贝、内核锁竞争。 ...

October 1, 2025 · 13 min · 2737 words