为 C++ Web 框架设计三层 PMR 内存池:从原理到实战

为 C++ Web 框架设计三层 PMR 内存池:从原理到实战 本文以 Hical 框架为例,深入讲解如何利用 C++17 PMR(Polymorphic Memory Resource)为高并发 Web 服务器构建三层内存池架构。 为什么 Web 服务器需要自定义内存管理? 一个 HTTP 请求的生命周期中,框架需要分配大量临时对象:解析缓冲区、路径字符串、JSON 值、响应体。在高并发场景下(如 50K QPS),new/delete 的全局锁竞争会成为显著瓶颈: 1 2 3 50,000 请求/秒 × 每请求 ~20 次分配 = 1,000,000 次/秒 new/delete ↓ 全局堆锁竞争 → CPU 空转 传统方案是自研内存池,但 C++17 提供了标准化的解决方案 —— PMR。 PMR 速览 PMR 的核心思想:把内存分配策略从容器类型中解耦。 1 2 3 4 5 // 传统方式:分配器绑定在类型中 std::vector<int> vec; // 永远用 std::allocator // PMR 方式:运行时切换分配策略 std::pmr::vector<int> vec(&myPool); // 用自定义内存池 标准库提供了三种现成的内存资源: ...

April 12, 2026 · 3 min · 438 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