[{"content":"C++ 模板元编程一直是\u0026quot;工程师对机器\u0026quot;的对抗赛。你用 std::enable_if、if constexpr、折叠表达式这些工具在编译期辗转腾挪，每多一层抽象就多一层模板参数、多一层 typename...。\nP2996（Reflection for C++26）试图终结这种状态。它给 C++ 引入一组真正的**编译期内省（introspection）和代码注入（injection）**机制，让你能在编译期遍历类的成员、获取名称/类型、生成新代码——而不再依赖宏、外部代码生成器、或 Boost.Hana 这种\u0026quot;用模板模拟反射\u0026quot;的黑科技。\n背景：C++ 为什么需要反射 在 Java、C#、Python 这类语言里，反射是标准库的一部分：你能在运行时获取一个类型的所有字段、调用任意方法、甚至动态创建对象。C++ 一直缺少这个能力，因为两个核心约束：\n零开销原则：不能为不用反射的人付出任何运行时成本 编译期模型：C++ 的类型信息主要在编译期可用，运行时大多已擦除 C++ 社区的回应是\u0026quot;用模板元编程模拟反射\u0026quot;——std::tuple、std::is_same_v、std::decay_t、type traits……这些本质上都是用模板在编译期\u0026quot;计算\u0026quot;出类型信息。但它们是碎片化的：你需要为每种具体的元操作写一个 trait，没有统一的 API 来遍历\u0026quot;某个类型的所有成员\u0026quot;。\nP2996 的方案很直接：编译期反射，零运行时开销。所有的反射操作都在 consteval 上下文中执行，生成代码片段（splicer）注入到 AST 中。\n核心机制：^^ 与 [::] P2996 引入了两个核心语法：\n^^（reflection operator）：把语法结构反射成 std::meta::info 类型的值 [::]（splicer）：把 std::meta::info \u0026ldquo;拼接\u0026quot;回语法结构中 最简单的例子：\n1 2 3 constexpr auto r = ^^int; // 反射类型 int typename [:r:] x = 42; // 等价于：int x = 42; typename [:^^char:] c = \u0026#39;*\u0026#39;; // 等价于：char c = \u0026#39;*\u0026#39;; 你看，^^int 产生的 r 是一个反射值（类型是 std::meta::info），它不代表 int 本身，而是关于 int 的元信息。typename [:r:] 把这个元信息重新拼回一个类型声明——C++ 标准的术语叫 splicing。\n同一个反射值可以用于不同的上下文：\n1 2 3 4 5 constexpr auto r = ^^int; typename [:r:] // 类型上下文 → int sizeof([:r:]) // 表达式上下文 → sizeof(int) typename [:^^char:] // 也可以直接嵌套 ^^ 为什么用单一的 std::meta::info 类型 一个常见疑问是：为什么不按语言元素分类定义反射类型？比如 std::meta::variable、std::meta::type、std::meta::function？\n论文明确指出这是故意的设计选择：如果把语言设计编码到类型系统中，会让未来的语言演化极其困难——C++11 扩展了 variable 的语义包含引用，如果当时已经固定了 std::meta::variable 类型，就会面临破坏性变更。单一 opaque 类型让委员会保留了对语言演化的所有自由度。\n遍历类的成员 反射的真正威力在于遍历。以 std::meta::nonstatic_data_members_of(^^T) 为例，它返回一个 vector\u0026lt;meta::info\u0026gt;，包含类型 T 的所有非静态数据成员。配合 C++26 的另一个核心提案 P1306 Expansion Statements，你可以这样遍历：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include \u0026lt;meta\u0026gt; template \u0026lt;typename T\u0026gt; void print_all(T const\u0026amp; v) { template for (constexpr auto e : std::meta::nonstatic_data_members_of(^^T)) { std::println(\u0026#34;.{} = {}\u0026#34;, std::meta::identifier_of(e), v.[:e:]); } } struct Point { int x, y; double z; }; int main() { Point p{1, 2, 3.14}; print_all(p); // 输出: // .x = 1 // .y = 2 // .z = 3.14 } template for 是 P1306 引入的编译期展开循环：它的循环体在编译期被展开成 N 份，每一份用循环变量的具体值替换。上面的代码等价于编译期展开为：\n1 2 3 std::println(\u0026#34;.x = {}\u0026#34;, v.x); std::println(\u0026#34;.y = {}\u0026#34;, v.y); std::println(\u0026#34;.z = {}\u0026#34;, v.z); 实现一个通用序列化 有了反射和 expansion statements，很多过去需要宏或外部工具的任务变得非常简单。比如 enum → string：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 template \u0026lt;typename E\u0026gt; requires std::is_enum_v\u0026lt;E\u0026gt; constexpr std::string enum_to_string(E value) { template for (constexpr auto e : std::meta::enumerators_of(^^E)) { if (value == [:e:]) { return std::string(std::meta::identifier_of(e)); } } return \u0026#34;\u0026lt;unknown\u0026gt;\u0026#34;; } enum class Color { Red, Green, Blue }; static_assert(enum_to_string(Color::Red) == \u0026#34;Red\u0026#34;); 再比如 struct → JSON：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 template \u0026lt;typename T\u0026gt; std::string to_json(T const\u0026amp; v) { std::string result = \u0026#34;{\u0026#34;; bool first = true; template for (constexpr auto e : std::meta::nonstatic_data_members_of(^^T)) { if (!first) result += \u0026#34;, \u0026#34;; first = false; result += \u0026#34;\\\u0026#34;\u0026#34;; result += std::meta::identifier_of(e); result += \u0026#34;\\\u0026#34;: \u0026#34;; if constexpr (std::is_arithmetic_v\u0026lt;decltype(v.[:e:])\u0026gt;) { result += std::to_string(v.[:e:]); } else if constexpr (std::is_same_v\u0026lt;decltype(v.[:e:]), std::string\u0026gt;) { result += \u0026#34;\\\u0026#34;\u0026#34; + v.[:e:] + \u0026#34;\\\u0026#34;\u0026#34;; } // else ... 递归处理嵌套 struct } result += \u0026#34;}\u0026#34;; return result; } 这不需要任何宏、不需要外部代码生成器、也不需要 Boost——所有逻辑在编译期一次 resolve，运行时只是一个普通的函数调用，没有任何反射开销。\n更激进的例子：编译期 struct 变换 反射不仅能读，还能写——也就是代码注入（code injection）。你可以根据现有类型在编译期构造出新的类型：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 consteval std::meta::info make_point_type() { // 动态构造一个 struct 类型定义 std::vector\u0026lt;std::meta::info\u0026gt; members; members.push_back(std::meta::data_member_spec(^^double, {.identifier = \u0026#34;x\u0026#34;})); members.push_back(std::meta::data_member_spec(^^double, {.identifier = \u0026#34;y\u0026#34;})); return std::meta::define_aggregate(^^Point, members); } // 在编译期注入新的类型定义 [::make_point_type():]; int main() { Point p{1.0, 2.0}; // Point 此时已存在 } 这叫 compile-time type generation，已在 EDG 和 Bloomberg Clang fork 中实现。\n实现状态 目前有两个可用的编译器实现，都可以在 Compiler Explorer 上在线测试：\n实现方 编译器 状态 EDG (Edison Design Group) EDG C++ Front End 覆盖 P2996 大部分特性，可直接使用 Bloomberg Clang fork (clang-p2996) 开源实现，已支持模板/命名空间 splicer 论文 P2996R13（2025-06-20）是当前最新版。P1306（Expansion Statements）也已进入 CWG 审查阶段。两者组合使用可以覆盖绝大多数反射场景。\n与已有元编程方案的对比 方案 类型遍历 成员名获取 代码注入 编译器支持 C++17 type traits 无（需手写） 无 无 所有编译器 C++20 requires + concepts 部分（条件约束） 无 无 所有编译器 Boost.Hana 有（模板模拟） 无 无 所有主流编译器 Clang __reflection 扩展 部分 有 有限 Clang only P2996 有 有 有 EDG / Bloomberg Clang P2996 + P1306 有 有 有（含展开） EDG / Bloomberg Clang 今日可执行动作 在线体验：打开 Compiler Explorer，选择 \u0026ldquo;EDG (experimental reflection)\u0026rdquo; 编译器，复制上面的 enum_to_string 例子跑一遍 本地编译：如果你用 Clang，克隆 Bloomberg 的 clang-p2996，按 README 构建后测试 阅读原文：阅读 P2996R13 的第 3 节（Examples）和第 4 节（Proposed Features），里面有 17 个完整的可运行示例 参考 P2996R13: Reflection for C++26 P1306R4: Expansion Statements Bloomberg clang-p2996 (GitHub) P2996 on Compiler Explorer (EDG) — 选择 EDG experimental reflection 编译器 ","date":"2026-05-25T00:00:00Z","permalink":"/posts/cpp-c++26-%E5%8F%8D%E5%B0%84%E4%BB%8E%E6%A8%A1%E6%9D%BF%E5%85%83%E7%BC%96%E7%A8%8B%E5%88%B0%E7%BC%96%E8%AF%91%E6%9C%9F%E5%86%85%E7%9C%81/","title":"C++26 反射：从模板元编程到编译期内省"},{"content":"背景 Linux 的异步 I/O 一直是个尴尬的话题。\n老的 POSIX AIO（aio_read/aio_write）在用户态用线程池模拟，内核态 AIO（io_submit）只支持 O_DIRECT 文件，连 socket 都做不了。epoll 虽然解决了网络 I/O 的 C10K 问题，但对文件 I/O 无能为力——epoll 对普通文件 fd 永远返回 ready，于是 Node.js 的 libuv 只能开线程池来处理文件读写。\n2019 年，Jens Axboe（当时在 Facebook）向内核提交了一套全新的异步 I/O 接口，命名为 io_uring。它用两个共享 ring buffer 实现了内核-用户态零拷贝通信，支持文件、网络、甚至 accept() 等操作，并且提供 SQPOLL 模式让应用可以完全免去系统调用。Linux 5.1 合入主线，之后每个版本都在扩充 opcode。\nio_uring 的核心设计：两个 ring buffer io_uring 的命名来自它的核心数据结构——两个 ring buffer（环形缓冲区）：\nSubmission Queue (SQ)：用户态写入 I/O 请求（SQE） Completion Queue (CQ)：内核写入 I/O 完成结果（CQE） 两个队列通过 mmap() 映射到用户态和内核态共享的内存区域。这意味着：大部分情况下，用户态和内核态不需要拷贝数据，也不需要系统调用来传递结构体。\n数据结构层面 SQE（Submission Queue Entry）描述一个 I/O 操作：\n1 2 3 4 5 6 7 8 9 10 struct io_uring_sqe { __u8 opcode; /* IORING_OP_READV, IORING_OP_WRITEV ... */ __u8 flags; /* IOSQE_IO_LINK 等链式标记 */ __s32 fd; /* 操作目标 fd */ __u64 off; /* 文件偏移 */ void *addr; /* buffer 或 iovec 数组指针 */ __u32 len; /* buffer 大小或 iovec 数量 */ __u64 user_data; /* 用户自定义标记，关联 CQE */ __u16 buf_index; /* fixed buffer 索引 */ }; CQE（Completion Queue Event）返回结果：\n1 2 3 4 5 struct io_uring_cqe { __u64 user_data; /* 原样返回 SQE 中设置的 user_data */ __s32 res; /* 结果码（类似 read/write 返回值） */ __u32 flags; }; 工作流程 1 2 3 4 5 6 7 用户态 内核态 | | |— 填充 SQE (写 SQ ring) | |— io_uring_enter() ————————→|— 消费 SQ ring | |— 执行 I/O 操作 |— 读取 CQE (读 CQ ring) ←——|— 填充 CQ ring | | 关键优势：多个 I/O 请求只需要一次系统调用。传统的 read() 一次调用一个，epoll 也是等事件然后逐个调用。io_uring 可以在 SQ 里批量提交 256 个请求，一次 io_uring_enter() 全部下发。\nSQPOLL 模式：零系统调用 I/O 如果应用对延迟有极致要求，io_uring 提供 IORING_SETUP_SQPOLL 标志。启动后内核创建一个内核线程（sqpoll），持续轮询 SQ ring 是否有新的 SQE。应用只需写 SQ → 内核线程自动消费 → 写 CQ。应用全程不需要调用 io_uring_enter()。\n在 Spectre/Meltdown 修复之后，系统调用的开销显著增加（因为页表隔离和 TLB 刷新）。SQPOLL 模式完全消除了这个成本。\n实战：用 liburing 写一个零系统调用读文件 原始 io_uring 系统调用接口很底层：需要 io_uring_setup() + 3 次 mmap() 映射不同区域 + 手动管理 ring buffer 的 head/tail 指针。liburing 封装了这些细节。\n安装 1 2 git clone https://github.com/axboe/liburing cd liburing \u0026amp;\u0026amp; ./configure \u0026amp;\u0026amp; make \u0026amp;\u0026amp; sudo make install 代码：io_uring 版的文件读取器 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 #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;sys/stat.h\u0026gt; #include \u0026lt;liburing.h\u0026gt; #define QUEUE_DEPTH 1 #define BLOCK_SZ 4096 off_t get_file_size(int fd) { struct stat st; if (fstat(fd, \u0026amp;st) \u0026lt; 0) return -1; return st.st_size; } int main(int argc, char *argv[]) { if (argc \u0026lt; 2) { fprintf(stderr, \u0026#34;Usage: %s \u0026lt;file\u0026gt;\\n\u0026#34;, argv[0]); return 1; } /* 1. 初始化 io_uring —— 一个 ring 搞定 */ struct io_uring ring; io_uring_queue_init(QUEUE_DEPTH, \u0026amp;ring, 0); int fd = open(argv[1], O_RDONLY); off_t file_sz = get_file_size(fd); char *buf = malloc(file_sz); /* 2. 获取一个 SQE，准备 read 操作 */ struct io_uring_sqe *sqe = io_uring_get_sqe(\u0026amp;ring); io_uring_prep_read(sqe, fd, buf, file_sz, 0); /* 3. 提交到内核 */ io_uring_submit(\u0026amp;ring); /* 4. 等待完成，读取 CQE */ struct io_uring_cqe *cqe; io_uring_wait_cqe(\u0026amp;ring, \u0026amp;cqe); if (cqe-\u0026gt;res \u0026lt; 0) { fprintf(stderr, \u0026#34;read failed: %s\\n\u0026#34;, strerror(-cqe-\u0026gt;res)); } else { write(STDOUT_FILENO, buf, cqe-\u0026gt;res); } /* 5. 标记 CQE 已消费 */ io_uring_cqe_seen(\u0026amp;ring, cqe); io_uring_queue_exit(\u0026amp;ring); free(buf); close(fd); return 0; } 对比传统的同步读取，这段代码的关键区别是：io_uring_submit() 立即返回，不阻塞，你可以在这期间做别的计算。io_uring_wait_cqe() 才真正等待 I/O 完成。\n编译 1 gcc -o iouring-cat iouring-cat.c -luring 进阶：用 io_uring 写一个完整的 HTTP 服务器 下面是一个基于 io_uring 的简化版 HTTP 服务器框架。它用 io_uring 同时处理 accept、readv 和 writev，全程只需要 3 个系统调用变体：io_uring_queue_init、io_uring_submit、io_uring_wait_cqe。\n核心事件循环：\n1 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 #define QUEUE_DEPTH 256 enum { EVENT_ACCEPT, EVENT_READ, EVENT_WRITE }; struct conn { int event_type; int client_fd; struct iovec iov[]; }; void server_loop(int server_fd) { struct io_uring ring; io_uring_queue_init(QUEUE_DEPTH, \u0026amp;ring, 0); /* 初始注册一个 accept 请求 */ struct io_uring_sqe *sqe = io_uring_get_sqe(\u0026amp;ring); io_uring_prep_accept(sqe, server_fd, NULL, NULL, 0); io_uring_sqe_set_data(sqe, new_req(EVENT_ACCEPT, 0)); io_uring_submit(\u0026amp;ring); while (1) { struct io_uring_cqe *cqe; io_uring_wait_cqe(\u0026amp;ring, \u0026amp;cqe); struct conn *req = (struct conn *)cqe-\u0026gt;user_data; switch (req-\u0026gt;event_type) { case EVENT_ACCEPT: { int client_fd = cqe-\u0026gt;res; /* accept() 返回的客户端 fd */ /* 再注册下一个 accept（持续监听）*/ sqe = io_uring_get_sqe(\u0026amp;ring); io_uring_prep_accept(sqe, server_fd, NULL, NULL, 0); io_uring_sqe_set_data(sqe, new_req(EVENT_ACCEPT, 0)); /* 注册读客户端请求 */ sqe = io_uring_get_sqe(\u0026amp;ring); io_uring_prep_readv(sqe, client_fd, /* ... */); io_uring_sqe_set_data(sqe, new_req(EVENT_READ, client_fd)); break; } case EVENT_READ: { /* 解析 HTTP 请求 → 打开文件 → 注册 writev 写回响应 */ handle_http(req); sqe = io_uring_get_sqe(\u0026amp;ring); io_uring_prep_writev(sqe, req-\u0026gt;client_fd, req-\u0026gt;iov, n, 0); io_uring_sqe_set_data(sqe, req); break; } case EVENT_WRITE: /* 写完关闭连接 */ close(req-\u0026gt;client_fd); free(req); break; } io_uring_cqe_seen(\u0026amp;ring, cqe); io_uring_submit(\u0026amp;ring); /* 批量提交所有新注册的请求 */ } } 这是单线程异步模型的极致形态——一个 io_uring_submit 提交所有类型的 I/O（accept、read、write），一个 io_uring_wait_cqe 等待任何完成。不需要 epoll、不需要线程池、不需要区分网络 I/O 和文件 I/O。\nZeroHTTPd（Shuveb Hussain 的开源项目）基于这个架构做了 benchmark：在单核 VM 上，io_uring 版本比 epoll + 线程池版本吞吐提升约 30-50%，延迟降低更明显，因为没有了线程切换和系统调用开销。\nio_uring 生态现状 场景 项目 说明 数据库 RocksDB 6.15+ 开始集成 io_uring 做文件 I/O 存储 SPDK / FIO SPDK 的 io_uring 引擎，FIO 原生支持 io_uring 网络代理 Envoy 社区有 io_uring 集成 PR 编程语言 Rust (tokio-uring) tokio-uring 项目将 io_uring 引入 Rust 异步生态 文件系统 XFS / Btrfs 内核原生支持，io_uring 绕过 VFS 的某些路径 Rust 的 tokio-uring 值得一提——它把 io_uring 封装成 Rust 的 async/await 接口，实现了\u0026quot;真正零开销异步 I/O\u0026quot;：\n1 2 3 4 5 6 7 8 9 10 11 use tokio_uring::fs::File; fn main() -\u0026gt; io::Result\u0026lt;()\u0026gt; { tokio_uring::start(async { let file = File::open(\u0026#34;hello.txt\u0026#34;).await?; let buf = vec![0u8; 4096]; let (res, buf) = file.read_at(buf, 0).await; println!(\u0026#34;read {} bytes\u0026#34;, res?); Ok(()) }) } 今日可执行动作 本地实验：sudo apt install -y liburing-dev 然后编译上面的 io_uring cat 代码。用 strace -e io_uring_enter,io_uring_setup,io_uring_register ./iouring-cat 观察实际发生了几次系统调用。对比普通 cat 走 read() 的次数。\n理解性能差异：运行 fio --engine=io_uring --rw=randread --bs=4k --size=1G --runtime=10 对比 --engine=psync，观察 IOPS 差距。\n读源码：liburing 的 src/queue.c 只有 200 行。看 io_uring_submit 如何管理 SQ tail 指针、io_uring_wait_cqe 如何决定走 io_uring_enter 还是直接从 CQ ring 读取——这是理解\u0026quot;批处理 + 共享内存\u0026quot;设计的最佳入口。\n参考 LWN: Ringing in a new asynchronous I/O API — Jonathan Corbet 对 io_uring 的原始报道 Lord of the io_uring — Shuveb Hussain 编写的完整 io_uring 指南，附带 ZeroHTTPd 源码 liburing GitHub — Jens Axboe 维护的用户态封装库 tokio-uring — Rust 生态的 io_uring 异步运行时 Linux kernel source: fs/io_uring.c — 编译后的 io_uring 内核实现约 10000+ 行 ","date":"2026-05-24T14:52:36+08:00","permalink":"/posts/2026-05-24-linux-io_uring-%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%E4%BB%8E-ring-buffer-%E5%88%B0%E9%9B%B6%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8-i/o/","title":"io_uring 完全指南：从 ring buffer 到零系统调用 I/O"},{"content":"背景 如果说 2024 年是\u0026quot;百模大战\u0026quot;，那么 2025-2026 年无疑是 Agent 元年。你不再只用 API 调用一个模型回答问题，而是让模型拥有工具、记忆和自主决策能力，去完成复杂任务。\n然而，Agent 开发面临三个核心问题：\n模型怎么像人一样使用工具？ —— 每个模型提供商都有自己的工具调用格式，接入不同数据源需要重复适配。 不同 Agent 之间怎么对话？ —— 不同团队、不同框架构建的 Agent 是孤岛，无法协作。 开发者怎么快速构建多 Agent 应用？ —— 每次都要从头实现 Agent 循环、工具编排、记忆管理，重复造轮子。 针对这三个问题，业界在 2025 年给出了三个重量级的答案：\nMCP (Model Context Protocol) — Anthropic 推出的开放协议，统一模型与外部工具/数据的连接标准，被称为\u0026quot;AI 界的 USB-C\u0026quot;。 A2A (Agent-to-Agent Protocol) — Google 贡献给 Linux 基金会的开放协议，解决 Agent 与 Agent 之间的通信与协作。 OpenAI Agents SDK — OpenAI 开源的轻量级 Python 框架，提供开箱即用的多 Agent 编排能力。 本文将从核心原理、代码实战、生态对比三个维度深度解析三门技术，帮你理清它们的定位和适用场景。\n核心原理 MCP：模型连接世界的\u0026quot;万能转接头\u0026quot; MCP（Model Context Protocol）是 Anthropic 在 2024 年底推出的开放协议。它解决的问题很直接：每个 AI 应用如果要连接数据库、文件系统或第三方 API，都需要写大量的胶水代码。MCP 提供了统一的接口标准，让模型能通过标准化的方式发现和使用外部工具与数据。\nMCP 的架构采用 客户端-服务器（Client-Server） 模型：\nMCP Host：发起请求的应用程序（如 Claude Desktop、VS Code AI 插件） MCP Client：负责与 Server 建立 1:1 连接的通道 MCP Server：对外暴露资源（Resources）、工具（Tools）和提示词（Prompts）的轻量级服务 MCP 定义了三种核心原语：\n原语 作用 类比 Resources 暴露数据（文件、数据库记录、API 响应） GET 请求 Tools 可被模型调用的函数（创建文件、发送邮件） POST 请求 Prompts 预定义的提示词模板 路由模板 传输层支持 stdio（本地进程通信）、SSE（Server-Sent Events）和 Streamable HTTP。通信协议基于 JSON-RPC 2.0，协议版本目前为 2025-11-25。\nA2A：Agent 之间的\u0026quot;社交协议\u0026quot; A2A（Agent-to-Agent Protocol）由 Google 在 2025 年 4 月发布，并于同年 9 月贡献给 Linux 基金会。它解决的是 MCP 没有覆盖的问题——Agent 与 Agent 之间的通信。\nMCP 的连接方向是：Model → Tools/Data。而 A2A 的连接方向是：Agent → Agent。它们是互补关系——你可以用 MCP 让 Agent 接入工具，再用 A2A 让这个 Agent 与另一个 Agent 协作。\nA2A 的核心设计包括：\nAgent Card：每个 Agent 通过一个 JSON 格式的\u0026quot;名片\u0026quot;发布自己的能力、端点和认证方式。客户端通过 Agent Card 发现和连接 Agent。 JSON-RPC 2.0 over HTTP(S)：标准化的通信协议，支持同步请求/响应、SSE 流式传输、以及异步推送通知。 Task 生命周期管理：A2A 定义了标准的任务状态机（submitted → working → input-required → completed → failed），支持长时间运行的任务。 内容协商（Content Negotiation）：Agent 之间可以协商交互格式（纯文本、结构化 JSON、文件等）。 A2A 的一个重要设计哲学是 Preserving Opacity（保持不透明性） ——Agent 之间协作时不需要暴露内部状态、记忆或工具实现细节，这对安全性和知识产权保护至关重要。\nOpenAI Agents SDK：极简的多 Agent 编排框架 OpenAI Agents SDK 是 OpenAI 在 2025 年开源的 Python 框架（npm 上也提供了 JS/TS 版本）。它不是协议，而是一个开发框架。其核心概念包括：\nAgent：配置了指令、工具、护栏（Guardrails）和转交权（Handoffs）的 LLM 实例。 Handoff：当前 Agent 可以将任务转交给另一个 Agent，形成多 Agent 协作。 Guardrails：在输入和输出阶段进行检查的安全机制。 Tool：可以是普通 Python 函数、MCP 工具或 Hosted Tool。 Session：自动管理对话历史，跨多次运行保持上下文。 Tracing：内置的可观测性工具，追踪每个 Agent 的运行轨迹。 SDK 提供两种 Agent 模式：\n标准 Agent：轻量级，使用 LLM 的 function calling 进行工具调用。 Sandbox Agent（v0.14+）：在隔离的文件系统环境中运行，适合需要写代码、操作文件的场景。 OpenAI Agents SDK 的一个重要特点是 provider-agnostic（提供商无关）——它不仅支持 OpenAI 自己的 Responses API 和 Chat Completions API，还通过 any-llm 和 LiteLLM 支持 100+ 其他 LLM。\n代码实战 MCP：用 Python 构建一个天气查询工具 首先安装 MCP Python SDK：\n1 pip install \u0026#34;mcp[cli]\u0026#34; 以下代码实现了一个 MCP Server，暴露两个工具：get_forecast（获取天气预报）和 get_alerts（获取天气预警）：\n1 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 # weather_server.py import httpx from mcp.server.fastmcp import FastMCP # 创建 MCP Server mcp = FastMCP(\u0026#34;Weather Service\u0026#34;, json_response=True) # 定义工具：获取天气预报 @mcp.tool() async def get_forecast(latitude: float, longitude: float) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;获取指定坐标的天气预报\u0026#34;\u0026#34;\u0026#34; url = f\u0026#34;https://api.weather.gov/points/{latitude},{longitude}\u0026#34; async with httpx.AsyncClient() as client: resp = await client.get(url, headers={\u0026#34;User-Agent\u0026#34;: \u0026#34;mcp-demo/1.0\u0026#34;}) resp.raise_for_status() forecast_url = resp.json()[\u0026#34;properties\u0026#34;][\u0026#34;forecast\u0026#34;] forecast_resp = await client.get(forecast_url) forecast_resp.raise_for_status() return forecast_resp.json()[\u0026#34;properties\u0026#34;][\u0026#34;periods\u0026#34;][:3] # 定义工具：获取天气预警 @mcp.tool() async def get_alerts(state: str) -\u0026gt; list[dict]: \u0026#34;\u0026#34;\u0026#34;获取指定州的天气预警\u0026#34;\u0026#34;\u0026#34; url = f\u0026#34;https://api.weather.gov/alerts/active/area/{state}\u0026#34; async with httpx.AsyncClient() as client: resp = await client.get(url, headers={\u0026#34;User-Agent\u0026#34;: \u0026#34;mcp-demo/1.0\u0026#34;}) resp.raise_for_status() data = resp.json() return [ {\u0026#34;event\u0026#34;: a[\u0026#34;properties\u0026#34;][\u0026#34;event\u0026#34;], \u0026#34;headline\u0026#34;: a[\u0026#34;properties\u0026#34;][\u0026#34;headline\u0026#34;]} for a in data.get(\u0026#34;features\u0026#34;, [])[:5] ] # 定义资源：暴露静态数据 @mcp.resource(\u0026#34;weather://supported-states\u0026#34;) async def get_supported_states() -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;返回支持的美国州代码列表\u0026#34;\u0026#34;\u0026#34; return \u0026#34;AL, AK, CA, NY, TX, FL, WA, OR\u0026#34; if __name__ == \u0026#34;__main__\u0026#34;: mcp.run(transport=\u0026#34;stdio\u0026#34;) 运行方式：\n1 2 3 4 5 6 7 8 9 10 11 # 开发模式（带 MCP Inspector 调试 UI） uv run mcp dev weather_server.py # 直接运行（stdio 传输） python weather_server.py # Streamable HTTP 传输 python -c \u0026#34; from weather_server import mcp mcp.run(transport=\u0026#39;streamable-http\u0026#39;) \u0026#34; 代码要点：\n@mcp.tool() 装饰器将函数注册为 Tool，模型可以通过 function calling 自动调用。 @mcp.resource() 注册静态资源，支持 URI 模式匹配。 FastMCP 自动处理 JSON-RPC 消息序列化和反序列化。 json_response=True 让工具返回结构化 JSON 而非纯文本。 A2A：让两个 Agent 互相协作 安装 A2A Python SDK：\n1 pip install a2a-sdk 以下代码实现了一个简单的 A2A Server（翻译 Agent）和 A2A Client（请求翻译的编排 Agent）：\nStep 1: A2A Server — 翻译 Agent\n1 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 # a2a_translation_server.py from a2a_sdk.server import AgentCard, A2AServer from a2a_sdk.types import ( Task, TaskState, TaskStatus, Message, TextContent, AgentCard as AgentCardModel, ) class TranslationAgent: \u0026#34;\u0026#34;\u0026#34;翻译 Agent，支持中英文互译\u0026#34;\u0026#34;\u0026#34; async def get_agent_card(self) -\u0026gt; AgentCardModel: return AgentCardModel( name=\u0026#34;Translation Agent\u0026#34;, description=\u0026#34;中英文翻译服务\u0026#34;, url=\u0026#34;http://localhost:8080\u0026#34;, version=\u0026#34;1.0.0\u0026#34;, capabilities={ \u0026#34;translation\u0026#34;: { \u0026#34;source_languages\u0026#34;: [\u0026#34;zh\u0026#34;, \u0026#34;en\u0026#34;], \u0026#34;target_languages\u0026#34;: [\u0026#34;zh\u0026#34;, \u0026#34;en\u0026#34;, \u0026#34;ja\u0026#34;, \u0026#34;ko\u0026#34;, \u0026#34;fr\u0026#34;], } }, ) async def handle_task(self, task: Task) -\u0026gt; Task: \u0026#34;\u0026#34;\u0026#34;处理翻译任务\u0026#34;\u0026#34;\u0026#34; # 从任务消息中提取待翻译文本 message = task.messages[-1] text = message.content.text # 简单翻译逻辑（生产环境应调用 LLM 或翻译 API） translations = { \u0026#34;hello\u0026#34;: \u0026#34;你好\u0026#34;, \u0026#34;world\u0026#34;: \u0026#34;世界\u0026#34;, \u0026#34;你好\u0026#34;: \u0026#34;Hello\u0026#34;, \u0026#34;世界\u0026#34;: \u0026#34;World\u0026#34;, } translated = translations.get(text.strip().lower(), f\u0026#34;[翻译]{text}\u0026#34;) task.status = TaskStatus(state=TaskState.COMPLETED) task.messages.append( Message( role=\u0026#34;agent\u0026#34;, content=TextContent(text=translated), ) ) return task # 启动 A2A Server if __name__ == \u0026#34;__main__\u0026#34;: import uvicorn from a2a_sdk.server import create_app agent = TranslationAgent() app = create_app(agent, host=\u0026#34;0.0.0.0\u0026#34;, port=8080) uvicorn.run(app, host=\u0026#34;0.0.0.0\u0026#34;, port=8080) Step 2: A2A Client — 编排 Agent 调用翻译服务\n1 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 # a2a_client_example.py from a2a_sdk.client import A2AClient from a2a_sdk.types import Task, TaskState, Message, TextContent async def translate_via_a2a(): \u0026#34;\u0026#34;\u0026#34;通过 A2A 协议调用翻译 Agent\u0026#34;\u0026#34;\u0026#34; client = A2AClient(base_url=\u0026#34;http://localhost:8080\u0026#34;) # 1. 获取 Agent Card（发现能力） card = await client.get_agent_card() print(f\u0026#34;发现 Agent: {card.name}\u0026#34;) print(f\u0026#34;能力: {list(card.capabilities.keys())}\u0026#34;) # 2. 发送翻译任务 task = Task( messages=[ Message( role=\u0026#34;user\u0026#34;, content=TextContent(text=\u0026#34;hello\u0026#34;), ) ] ) result = await client.send_task(task) # 3. 轮询任务结果 while result.status.state != TaskState.COMPLETED: result = await client.get_task(result.id) print(f\u0026#34;翻译结果: {result.messages[-1].content.text}\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: import asyncio asyncio.run(translate_via_a2a()) A2A 的关键工作流：\n服务发现：Client 通过 get_agent_card() 获取 Agent Card，了解对方的能力。 任务提交：Client 通过 send_task() 提交包含消息的 Task。 状态轮询/推送：Server 通过 Task 状态机（submitted → working → completed/failed）通知 Client。 多轮对话：如果 Agent 需要更多信息，可以返回 input-required 状态，Client 补充输入后继续。 OpenAI Agents SDK：三行代码搭建多 Agent 系统 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 # multi_agent_demo.py import asyncio from agents import Agent, Runner, set_trace_processors from agents.tracing.processors import ConsoleTracingProcessor # 启用控制台追踪 set_trace_processors([ConsoleTracingProcessor()]) # 定义三个专业 Agent triage_agent = Agent( name=\u0026#34;Triage Agent\u0026#34;, instructions=\u0026#34;你是客服分流 Agent。根据用户的问题类型，将任务转交给对应的专业 Agent。\u0026#34;, handoffs=[\u0026#34;billing_agent\u0026#34;, \u0026#34;tech_support_agent\u0026#34;], ) billing_agent = Agent( name=\u0026#34;Billing Agent\u0026#34;, instructions=\u0026#34;你是账单 Agent。回答用户关于账单、发票和支付的问题。\u0026#34;, ) tech_support_agent = Agent( name=\u0026#34;Tech Support Agent\u0026#34;, instructions=\u0026#34;你是技术支持 Agent。帮助用户解决产品使用问题和技术故障。\u0026#34;, ) # 也可以用函数作为工具 async def get_account_info(account_id: str) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34;获取用户账户信息\u0026#34;\u0026#34;\u0026#34; return { \u0026#34;account_id\u0026#34;: account_id, \u0026#34;name\u0026#34;: \u0026#34;张三\u0026#34;, \u0026#34;plan\u0026#34;: \u0026#34;pro\u0026#34;, \u0026#34;balance\u0026#34;: 199.00, } billing_agent.tools = [get_account_info] async def main(): # 运行 Agent result = await Runner.run( triage_agent, input=\u0026#34;我的账单上个月扣了 199 元，能帮我查一下吗？\u0026#34;, ) print(result.final_output) if __name__ == \u0026#34;__main__\u0026#34;: asyncio.run(main()) Sandbox Agent 模式（OpenAI Agents SDK v0.14+）：\n1 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 # sandbox_agent_demo.py from agents import Runner from agents.run import RunConfig from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig from agents.sandbox.entries import GitRepo from agents.sandbox.sandboxes import UnixLocalSandboxClient agent = SandboxAgent( name=\u0026#34;Code Reviewer\u0026#34;, instructions=\u0026#34;检查克隆下来的仓库代码，发现 Bug 并修复。\u0026#34;, default_manifest=Manifest( entries={ \u0026#34;my_project\u0026#34;: GitRepo( repo=\u0026#34;openai/openai-agents-python\u0026#34;, ref=\u0026#34;main\u0026#34;, ), } ), ) result = Runner.run_sync( agent, \u0026#34;检查这个项目的 README，并告诉我它解决什么问题。\u0026#34;, run_config=RunConfig( sandbox=SandboxRunConfig(client=UnixLocalSandboxClient()) ), ) print(result.final_output) 生态现状 \u0026amp; 对比 MCP 生态（截至 2026 年 5 月） MCP 的生态发展最为成熟：\n客户端支持：Claude Desktop、ChatGPT、VS Code（GitHub Copilot）、Cursor、JetBrains IDE、Eclipse 官方 SDK：Python（mcp）、TypeScript（@modelcontextprotocol/sdk）、Go、Java 预构建 Server：官方和社区提供了数百个现成的 MCP Server（文件系统、数据库、GitHub、Slack、Notion、Figma 等） 托管平台：ModelScope MCP、Cloudflare Workers MCP MCP Inspector：官方调试工具，可视化查看 Server 暴露的工具和资源 A2A 生态（截至 2026 年 5 月） A2A 虽然年轻，但发展迅速：\n发起方：Google（2025-04 发布 → 2025-09 捐赠给 Linux 基金会） 官方 SDK：Python（a2a-sdk）、Go、JS/TS、Java、.NET DeepLearning.AI 课程：与 Google Cloud、IBM Research 合作的专项课程 合作伙伴：Google Cloud、IBM、LangChain（LangGraph 集成） 示例项目：多 Agent 医疗系统（A2A + LangGraph）、跨框架 Agent 协作 OpenAI Agents SDK 生态 开源协议：Apache 2.0 安装量：PyPI 高下载量，社区活跃 支持模型：OpenAI 全系列 + 100+ 第三方 LLM（via any-llm / LiteLLM） 集成：内置 MCP 工具支持（可与 MCP Server 互连）、Tracing 可视化、Pydantic 数据结构 Sandbox 模式：支持 Manifest 声明式环境配置、GitRepo 克隆、Docker 容器 三者核心对比 维度 MCP A2A OpenAI Agents SDK 定位 协议（Model ↔ Tools） 协议（Agent ↔ Agent） 开发框架（多 Agent 编排） 发起方 Anthropic Google（Linux 基金会） OpenAI 核心问题 模型如何统一调用工具 Agent 之间如何协作 如何快速构建多 Agent 应用 通信协议 JSON-RPC 2.0 JSON-RPC 2.0 over HTTP(S) Python SDK（内部编排） 传输层 stdio / SSE / Streamable HTTP HTTP(S) / SSE / Push N/A（内存进程间） 服务发现 资源 URI 模式 Agent Card（JSON） 代码静态定义 任务模式 请求-响应 异步任务状态机 Runner.run() / Handoff 学习曲线 中等 中等 低 成熟度 成熟（2024-11 发布） 发展中（2025-04 发布） 成熟（2025 年开源） 供应商锁定 无（开放协议） 无（开放协议） 低（支持第三方 LLM） 可观测性 需自行集成 标准日志 内置 Tracing 适用场景 工具集成、数据接入 跨组织 Agent 协作 单体多 Agent 应用 如何选择？ 你需要让 LLM 访问数据库/文件/API？ → MCP。开发一个 MCP Server，所有支持 MCP 的客户端都能用它。 你有多个 Agent 需要互相协作？ → A2A。通过 Agent Card 发现能力，用标准任务协议通信。 你只想快速构建一个多 Agent 应用？ → OpenAI Agents SDK。Handoff、Guardrails、Session 开箱即用。 三者可以组合使用：Agent（OpenAI SDK）→ 通过 MCP 接入工具 → 通过 A2A 与其他 Agent 协作。 今日可执行动作 跑通 MCP 天气 Server 示例：安装 pip install mcp，运行上面的 weather_server.py，然后用 MCP Inspector (uv run mcp dev weather_server.py) 查看可视化界面，体验工具调用的全过程。\n尝试 A2A 翻译 Agent 协作：安装 pip install a2a-sdk，启动翻译 Server 后运行 Client 脚本，观察 Agent 之间如何通过 Agent Card 发现能力并完成翻译任务。\n用 OpenAI Agents SDK 改写一个简单脚本：把你平时需要手动调 LLM API 的小工具（如摘要生成、翻译工具），用 Agent + Tool 的方式重写，体验 Agent 自动规划调用链的能力。\n参考 MCP 官方文档: https://modelcontextprotocol.io/docs/getting-started/intro MCP 协议规范: https://github.com/modelcontextprotocol/specification MCP Python SDK: https://github.com/modelcontextprotocol/python-sdk A2A Protocol 文档: https://a2a-protocol.org/latest/ A2A GitHub 仓库: https://github.com/a2aproject/A2A A2A Python SDK: https://github.com/a2aproject/a2a-python A2A 协议中文资源: https://www.a2aprotocol.org/zh OpenAI Agents SDK: https://github.com/openai/openai-agents-python OpenAI Agents SDK 文档: https://openai.github.io/openai-agents-python/ 深度长文：谷歌 A2A 协议权威详解 - 知乎: https://zhuanlan.zhihu.com/p/1894797987739324876 MCP 一篇就够了 - 知乎: https://zhuanlan.zhihu.com/p/29001189476 深入理解 MCP 协议 - JavaGuide: https://javaguide.cn/ai/agent/mcp.html AI Agent 框架全景指南: https://www.cnblogs.com/qiniushanghai/p/19952939 ","date":"2026-05-24T00:00:00Z","permalink":"/posts/system-ai-agent-%E5%BC%80%E5%8F%91%E6%A1%86%E6%9E%B6%E4%B8%89%E5%BC%BA%E4%BA%89%E9%9C%B8mcpa2a-%E4%B8%8E-openai-agents-sdk-%E6%B7%B1%E5%BA%A6%E5%AF%B9%E6%AF%94/","title":"AI Agent 开发框架三强争霸：MCP、A2A 与 OpenAI Agents SDK 深度对比"},{"content":"背景 C++ 的 RTTI（运行时类型信息）被诟病了二十年——慢、不透明、只支持多态类型。模板元编程虽然能做到编译期类型推断，但它的表达能力受限于模板语法：你无法在编译期遍历一个 struct 的成员，无法将一个 enum 自动转为字符串，无法动态查询一个类的布局。\n这就是 Reflection（反射）要解决的问题。经过十年的提案迭代，C++26 即将引入基于 P2996 的编译期反射，核心贡献者包括 Wyatt Childers、Peter Dimov、Barry Revzin 和 Daveed Vandevoorde（C++ 模板元编程的奠基人之一）。\n这不是运行时反射（像 Java/Python 那种），而是纯编译期的——所有反射操作在编译时求值，零运行时开销。编译器生成的反射信息通过新的 \u0026lt;meta\u0026gt; 头文件暴露给 constexpr 代码。\n核心原理 两个新语法元素 C++26 反射引入两个核心操作：\n^^ 反射运算符（reflection operator）：获取类型的反射值\n1 constexpr auto r = ^^int; // r 的类型是 std::meta::info [: :] 拼接运算符（splicer）：将反射值拼回源代码\n1 typename[:^^int:] x = 42; // 等价于 int x = 42; 新类型和中转 1 2 3 ^^ [:] 源代码实体 ───────→ std::meta::info ───────────→ 代码实体 (type/func/enum) (不透明 handle) (代换回源码中) std::meta::info 是一个不透明的 handle 类型。你不能直接\u0026quot;看\u0026quot;它的内部，只能通过 \u0026lt;meta\u0026gt; 头文件中的元函数来查询：\n元函数 作用 std::meta::members_of(r) 获取类型的所有成员（函数、变量、类型） std::meta::nonstatic_data_members_of(r) 仅获取非静态数据成员 std::meta::enumerators_of(r) 获取 enum 的所有枚举项 std::meta::type_of(r) 获取实体的类型 std::meta::identifier_of(r) 获取实体的名字（字符串） std::meta::size_of(r) 获取类型的大小 std::meta::dealias(r) 去掉类型别名 std::meta::reflect_constant(r) 获取枚举的值 枚举转字符串（最实用的例子） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include \u0026lt;meta\u0026gt; #include \u0026lt;string\u0026gt; #include \u0026lt;string_view\u0026gt; enum class Color { Red, Green, Blue }; consteval std::string color_to_string(Color c) { std::string result; // 反射：获取 Color 的所有枚举项 for (auto e : std::meta::enumerators_of(^^Color)) { auto val = std::meta::reflect_constant(e); // e.g. 0, 1, 2 auto name = std::meta::identifier_of(e); // e.g. \u0026#34;Red\u0026#34; if (val == (int)c) { result = name; break; } } return result; } static_assert(color_to_string(Color::Red) == \u0026#34;Red\u0026#34;); static_assert(color_to_string(Color::Green) == \u0026#34;Green\u0026#34;); 这里没有宏，没有运行时字符串对比，没有反射库——纯标准 C++26，全部编译期确定。static_assert 证明了这一点。\nStruct 成员反射（序列化的基础） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct Point { int x; int y; int z; }; consteval auto member_names() { std::vector\u0026lt;std::string_view\u0026gt; names; for (auto m : nonstatic_data_members_of(^^Point)) { names.push_back(identifier_of(m)); } return names; } static_assert(member_names().size() == 3); // member_names() == {\u0026#34;x\u0026#34;, \u0026#34;y\u0026#34;, \u0026#34;z\u0026#34;} 这意味着自动序列化/反序列化可以用十几行通用代码实现，不再需要手写 to_json/from_json 或者依赖第三方宏库。\n代码实战：一个通用的 struct-to-tuple 转换器 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 #include \u0026lt;meta\u0026gt; #include \u0026lt;tuple\u0026gt; #include \u0026lt;type_traits\u0026gt; template \u0026lt;typename T\u0026gt; requires std::is_aggregate_v\u0026lt;T\u0026gt; constexpr auto struct_to_tuple(T\u0026amp;\u0026amp; obj) { constexpr auto members = nonstatic_data_members_of(^^std::remove_cvref_t\u0026lt;T\u0026gt;); // 注意：完整的实现需要展开成员（这里展示思路） // 每个成员的类型通过 type_of() 获取 // 值通过拼接访问 return [\u0026amp;]\u0026lt;std::size_t... I\u0026gt;(std::index_sequence\u0026lt;I...\u0026gt;) { return std::make_tuple( obj.[:members[I]:]... // 拼接语法访问每个成员 ); }(std::make_index_sequence\u0026lt;members.size()\u0026gt;{}); } // 使用 struct Config { int port; std::string host; bool enable_ssl; }; auto cfg = Config{8080, \u0026#34;localhost\u0026#34;, true}; auto tup = struct_to_tuple(cfg); // tup 的类型是 std::tuple\u0026lt;int, std::string, bool\u0026gt; // 可以结构化绑定: auto [port, host, ssl] = tup; 这段代码在 C++20 里需要几十行 SFINAE+ 递归元组生成，在 C++26 中只需要一个 constexpr 循环和拼接语法。\n编译器支持状态 编译器 反射支持状态 EDG (Compiler Explorer) ✅ 完整原型，可在线试用 Clang (Bloomberg fork) ✅ 开源版（github.com/bloomberg/clang-p2996） GCC 16 ⚠️ 部分支持进行中 MSVC ❌ 尚未开始 生态现状 C++26 反射的影响面很广：\n领域 影响 序列化 struct→JSON/Protobuf 自动生成，不再需要宏/代码生成器 日志/调试 自动打印任意 struct 字段名+值 ORM 从 struct 定义自动生成 SQL schema 映射 配置解析 命令行参数自动绑定到 struct 成员 测试框架 自动注册测试函数（不再需要手动枚举） 枚举工具 enum→string、string→enum、enum 值范围检查 Bloomberg 的 reflection 实现已经用在内部金融系统中——struct A 数据布局变化后，序列化代码自动适配，不再需要手写同步。\n今日可执行动作 在 Compiler Explorer 上试用：打开 godbolt.org，选择 EDG 编译器（experimental），-std=c++26 标志，粘贴上面的 color_to_string 示例。亲眼确认 static_assert 通过——这是体验\u0026quot;纯编译期反射\u0026quot;最直接的方式。\n对比 C++20 反射方案：写一个相同的 color_to_string 用 C++20 实现（宏版或魔术枚举库版），对比代码量和可维护性。C++20 最接近的方案也只能用宏 + X_MACRO 模式。\nBloomberg 的 Clang fork：git clone https://github.com/bloomberg/clang-p2996，按 README 构建。运行 test/P2996/ 目录下的测试用例，理解 substitute() 和 define_aggregate() 等高级 API（可以用反射创建新的类型）。\n参考 P2996R13: Reflection for C++26 — WG21 标准提案 Bloomberg clang-p2996 — 开源 Clang 参考实现 Compiler Explorer EDG — 选 EDG + -std=c++26 在线测试 Daveed Vandevoorde: C++ Reflection — CppCon 2024 演讲 P3096R12: Function Reflection — 函数反射扩展 P3293R3: constexpr reflected pointers — constexpr 反射指针 ","date":"2026-05-24T00:00:00Z","permalink":"/posts/cpp-c++26-%E7%BC%96%E8%AF%91%E6%9C%9F%E5%8F%8D%E5%B0%84reflection%E6%9D%A5%E4%BA%86p2996-%E5%85%A8%E8%A7%A3%E6%9E%90/","title":"C++26 编译期反射（Reflection）来了：P2996 全解析"},{"content":"背景 Go 的开发节奏近年明显加快。2024 年 8 月的 Go 1.23 和 2025 年 2 月的 Go 1.24，连续两个版本交出了重量级的答卷：\nGo 1.23：正式支持 range-over-func 迭代器（range 可直接遍历任意函数），配套的 iter、slices、maps 标准库包全面适配 Go 1.24：引入 Swiss Table 作为内置 map 的底层实现，CPU 开销平均降低 2–3%；同时正式支持泛型类型别名 这两个特性解决了 Go 开发者在日常编码中两个最实际的痛点：\n没有泛型迭代器：过去要遍历一个自定义容器或 database cursor，要么显式写 for 循环 + next() 调用，要么自己搓一个 channel goroutine。代码分散、不易组合、还容易漏 close。 map 性能瓶颈：Go 的内置 map 在 1.24 之前用的是 2013 年 C 版本衍生的 hash 表，多年未大改。在大规模 map 操作（如缓存、去重、聚合计算）中，GC 压力和内存带宽消耗都偏高。 本文逐一拆解。\n核心原理 Range-over-Func 迭代器 Go 1.23 引入了一个新的约定：任何签名为 func(yield func(T) bool) 的函数类型，都可以直接出现在 range 语句的右侧。\n核心签名在标准库 iter 包中定义：\n1 2 // Seq 是元素序列的迭代器 type Seq[T any] func(yield func(T) bool) 当你在 range 中写 for v := range seq 时，编译器会自动把 body 编译成一个 yield 回调函数传给 seq。yield 返回 false 时迭代立即终止（对应 break 或 return）。\n关键优势：\n零成本抽象：回调方式避免了 channel 的 goroutine 调度和栈复制开销 惰性求值：只在 range 执行时才真正推动迭代，天然支持无限序列 可组合：slices.Collect、maps.Keys、maps.Values 等函数可以直接操作任意 Seq/Seq2 Swiss Table Map Go 1.24 将内置 map 的底层实现替换为 Swiss Table（源自 Google 的 Abseil C++ 库，C++17 标准提案 P2248R5 的变体）。\n传统 Go map 使用链式哈希表（bucket + overflow bucket + 链表），而 Swiss Table 的核心结构是：\n一个控制字节数组（Control Array），每个 slot 用 1 字节元信息标记状态（Empty / Deleted / Occupied + 7-bit hash） 一个密集数组（Data Array）连续存放 key-value pair 查询时，利用 SIMD（SSE2/NEON）一次比对 16 个控制字节，找到候选 slot 后直接访问数据数组 带来的收益：\n指标 旧 map Swiss Table 查询吞吐 1x ~1.3–1.5x 写入吞吐 1x ~1.2–1.4x 删除后内存回收 需 GC 扫描 立即回收 随机遍历顺序 保证随机 保证随机 大 map GC 压力 高（每个 bucket 独立对象） 低（连续内存块） Go 团队在 benchmark 中测得整体 CPU 开销降低 2–3%，对于 map-heavy 的应用（如 HTTP header 解析、JSON 解码、聚合缓存）收益尤其明显。\n代码实战 实战 1：用 range-over-func 遍历树形结构 旧写法——手动递归加回调：\n1 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 // 旧：显式递归，调用者每次要传回调 type TreeNode struct { Value int Left *TreeNode Right *TreeNode } func WalkPreorder(node *TreeNode, fn func(v int) bool) bool { if node == nil { return true } if !fn(node.Value) { return false } if !WalkPreorder(node.Left, fn) { return false } return WalkPreorder(node.Right, fn) } // 使用 var result []int WalkPreorder(root, func(v int) bool { result = append(result, v) return true }) 新写法——返回迭代器，直接 range：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // 新：返回 iter.Seq[int]，调用者用 range func (n *TreeNode) All() iter.Seq[int] { return func(yield func(int) bool) { var walk func(*TreeNode) bool walk = func(node *TreeNode) bool { if node == nil { return true } if !yield(node.Value) { return false } if !walk(node.Left) { return false } return walk(node.Right) } walk(n) } } // 使用 —— 像 range slice 一样自然 for v := range root.All() { fmt.Println(v) } iter.Seq2 支持 key-value 对，适合遍历 map 或数据库行：\n1 2 3 for k, v := range maps.All(myMap) { fmt.Println(k, v) } 实战 2：用 iter 包组合操作 标准库 slices 和 maps 包为迭代器提供了丰富的辅助函数：\n1 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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;iter\u0026#34; \u0026#34;maps\u0026#34; \u0026#34;slices\u0026#34; ) func main() { // 从 map 中取出所有值并翻转切片 m := map[string]int{\u0026#34;a\u0026#34;: 1, \u0026#34;b\u0026#34;: 2, \u0026#34;c\u0026#34;: 3} vals := slices.Collect(maps.Values(m)) slices.Reverse(vals) fmt.Println(vals) // [3 2 1]（顺序取决于 map 遍历顺序） // 过滤 + 映射 —— 组合迭代器 seq := func(yield func(int) bool) { for i := 0; ; i++ { if !yield(i) { return } } } // 取前 5 个偶数 i := 0 for v := range seq { if v%2 == 0 { fmt.Println(v) i++ if i \u0026gt;= 5 { break } } } // 输出：0 2 4 6 8 } // Collect 的签名是： // func Collect[E any](seq iter.Seq[E]) []E 实战 3：性能对比——Swiss Table map 创建测试文件 map_bench_test.go：\n1 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 package main import ( \u0026#34;testing\u0026#34; ) const N = 1_000_000 func BenchmarkMapInsert(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { m := make(map[int]int, N) for j := 0; j \u0026lt; N; j++ { m[j] = j } } } func BenchmarkMapLookup(b *testing.B) { m := make(map[int]int, N) for j := 0; j \u0026lt; N; j++ { m[j] = j } b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; N; j++ { _ = m[j] } } } 用 Go 1.23 vs Go 1.24 分别运行：\n1 2 3 4 5 6 7 8 # Go 1.23 $ go version go version go1.23.0 linux/amd64 $ go test -bench=. -benchmem -count=5 ./... # Go 1.24 $ go version go version go1.24.0 linux/amd64 $ go test -bench=. -benchmem -count=5 ./... Go 团队在官方博客中公布的典型数据（Intel Xeon, linux/amd64）：\nBenchmark Go 1.23 Go 1.24 提升 MapInsert/1M 45.2 ms 37.8 ms ~16% MapLookup/1M 38.1 ms 29.3 ms ~23% MapDelete/1M 42.6 ms 28.9 ms ~32% Swiss Table 在密集写入/查询/删除场景下提升显著。\n生态现状 特性 最低版本 状态 range-over-func 迭代器 Go 1.23 正式发布，go vet 会检查 yield 使用 iter 包 (Seq/Seq2/Pull) Go 1.23 稳定，无后续变更计划 slices.Collect/slices.Sorted Go 1.23 可直接配合任意 iter.Seq maps.Keys/maps.Values/maps.All Go 1.23 返回 iter.Seq/iter.Seq2 Swiss Table map Go 1.24 对用户完全透明，无需修改代码 泛型类型别名 Go 1.24 稳定，用于渐进式 API 迁移 第三方库迁移状态：\n主流的 ORM 和数据库库（如 pgx、go-sql-driver/mysql）已有实验性分支返回 iter.Seq2[col1, col2] 替代逐行 Scan golang.org/x/exp 已不再需要维护 slices/maps 扩展 —— 全部移至标准库 建议：如果你在写库（library），可以考虑为 Range(ctx) 类方法提供 iter.Seq2 返回；如果你是应用开发者，从 Go 1.23 以上的迭代器迁移是零成本的 今日可执行动作 升级 Go 版本：执行 go install golang.org/dl/go1.24.2@latest \u0026amp;\u0026amp; go1.24.2 download，然后将项目 go.mod 中的 go 指令改为 go 1.23 或 go 1.24，体验 range-over-func 和 Swiss Table 替换手写迭代逻辑：找到项目中的 for { next, ok := iter.Next(); if !ok { break } } 或类似模式，改为 for v := range myIter.All() 风格。可以先用 slices.Collect + slices.Backward 替换反向遍历 运行 Map 基准测试：在 Go 1.23 和 Go 1.24 环境下跑同一组 map 密集型 benchmark，用 benchstat 对比报告，确认你项目中的收益 参考 Go 1.24 is released! — go.dev blog (2025-02-11) Faster Go maps with Swiss Tables — Michael Pratt (2025-02-26) Range Over Function Types — Ian Lance Taylor (2024-08-20) Go 1.23 is released — go.dev blog (2024-08-13) What\u0026rsquo;s in an (Alias) Name? — Robert Griesemer (2024-09-17) iter package docs — pkg.go.dev Abseil Swiss Table — Google C++ Library ","date":"2026-05-24T00:00:00Z","permalink":"/posts/backend-go-%E7%94%9F%E6%80%81%E6%96%B0%E5%8A%A8%E5%90%91range-over-func-%E8%BF%AD%E4%BB%A3%E5%99%A8%E4%B8%8E-swiss-table-map-%E5%AE%9E%E6%88%98/","title":"Go 生态新动向：Range-over-Func 迭代器与 Swiss Table Map 实战"},{"content":"引言 IM（Instant Messaging）应用开发的\u0026quot;终局问题\u0026quot;之一，是消息系统架构。\n不管你用 Netty、gRPC、WebSocket 还是裸 TCP，不管你用 MySQL、Redis 还是自研分布式存储——最终你都要面对两个核心问题：消息怎么同步到接收方的每个端？消息怎么持久化，让用户随时翻出半年前的聊天记录？\n业界对这两个问题的解法，经过十几年演化，收敛到了一个经典的范式——\u0026ldquo;两个消息库\u0026rdquo;：一个消息同步库（Sync Store），一个消息存储库（Message Store / Archive Store）。\n这个范式最早由阿里云 TableStore 团队在 2017 年前后系统性地提出并落地（参见《现代IM系统中消息推送和存储架构的实现》），至今依然是微信、钉钉、QQ 等主流 IM 产品在消息层面的核心架构基础。\n本文围绕\u0026quot;两个消息库\u0026quot;展开讨论，从为什么需要两个库、Timeline 模型、写扩散 vs 读扩散、物理存储选型到工程中的取舍，希望能讲清楚这个看似简单实则充满权衡的设计问题。\n一、传统架构：先同步，后存储 在分析现代架构之前，先看看\u0026quot;原始\u0026quot;做法。\n传统架构是先同步后存储。流程大致是：\n1 2 3 发送方 → 服务端 → (接收方在线?) → 是 → 在线推送 → 接收方 ↓ 否 离线库(暂存) → 接收方上线后拉取 → 删除离线消息 关键特征：\n服务端不持久化消息。消息同步到接收方后，服务端就丢弃了 离线消息是临时缓存，接收方拉取后就删除 不支持消息漫游（换个设备看历史消息？不存在） 写放大几乎没有，但读放大严重（每条消息都要问：接收方在不在？发不发推送？） 这种架构在今天来看已经非常落伍，但它的思路相当直觉——消息嘛，传到了就行了，留着干嘛？\n然而用户说：不行，我要在手机上看昨天聊的内容，晚上回家在 Mac 上接着看。这就引出了多端同步和消息漫游的需求，传统架构完全做不到。\n二、现代架构：先存储，后同步 现代架构的核心思想是先存储，后同步。消息从发送方发出，服务端不是先考虑\u0026quot;怎么传给接收方\u0026quot;，而是先存起来。\n1 2 3 发送方 → 服务端 → ①写入消息存储库 → ②写入消息同步库 → ③在线推送(优化路径) ↓ 失败/离线 接收方主动拉取同步库 这里的核心变化是引入了两个独立的存储系统：\n2.1 消息存储库（Message Store） 按会话（Conversation / Session）组织，每个会话一个 Timeline 保存该会话的全量消息，永不过期（理论上） 用于支持消息漫游：用户在新设备上登录，从这里拉取任意会话的历史消息 写入频率稳定：一次会话一条消息就是一次写入 数据量最大：需支撑全量持久化 2.2 消息同步库（Sync Store） 按接收端（Device / Client）组织，每个接收端一个 Timeline 保存待同步给这个端的所有消息（可能来自多个会话） 用于支持多端同步：手机端、PC 端、Pad 端各自独立拉取各自的同步 Timeline 写入频率高：一条消息可能触发 N 次写入（群聊 N 个成员） 数据有生命周期：同步完毕后可以清理（取决于设计） 为什么要分成两个库？因为两者的访问模式完全不同：\n维度 消息存储库 消息同步库 主键 会话 ID 接收端 ID 访问模式 按会话拉全量历史 按设备拉增量消息 写入频率 1 次/消息 1~N 次/消息 数据量 全量 有状态的增量 TTL 永久 可淘汰 核心能力 消息漫游 多端同步 强行用一个库同时支撑这两种模式，要么读性能爆炸，要么写放大失控。\n三、Timeline 模型：统一抽象 要想理解两个库的具体实现，首先要理解Timeline这个抽象模型。\nTimeline 是一个有序消息队列，具有以下三个特性：\n每一条消息都有一个顺序 ID（SeqId），后面的消息 SeqId 一定比前面的大 新消息永远追加到尾部，不插入中间 支持按 SeqId 随机定位和范围读取 这三条特性赋予了 Timeline 极强的表达能力：\n消息同步：接收方的每个端维护一个本地 cursor（即最新已同步的 SeqId），每次拉取时问\u0026quot;从 cursor+1 开始，给我最新的消息\u0026quot;即可。服务端无需维护每个端的状态 消息漫游：直接从会话 Timeline 按 SeqId 范围批量读取 多端独立进度：B1、B2、B3 三个端各自记录自己的 cursor，互不干扰 从这个角度看，消息同步库本质上是一个按设备维度组织的 Timeline 集合，消息存储库是一个按会话维度组织的 Timeline 集合。\n四、消息同步的选择：写扩散 vs 读扩散 这是 IM 架构中最经典的 trade-off 讨论。\n4.1 读扩散（Fan-out on Read） 每条消息只写入存储库一次，各接收端主动去每个会话的 Timeline 拉取新消息。\n场景 写入次数 单聊 1 次 群聊（N 人） 1 次 优点：写入极少，非常适合写密集型场景 缺点：读放大严重。假设用户有 50 个活跃会话，每次同步需要拉 50 次。大量无效拉取（很多会话并没有新消息）。读写比从 10:1 放大到 100:1 4.2 写扩散（Fan-out on Write） 每条消息写入存储库 + 所有接收方的同步库。\n场景 写入次数 单聊（A→B） 1（存储）+ 2（A和B的同步库）= 3 次 群聊（N 人） 1（存储）+ N 次 优点：接收方只需拉一次自己的同步 Timeline，即可拿到所有未读消息。读压力极低 缺点：写入放大严重，群聊场景尤其恐怖（万人大群每条消息写 1 万次） 4.3 主流选择：写扩散为主，混合为辅 IM 场景的特点是读多写少。一条消息产生一次，但会被读取多次（手机端读一次，PC 端读一次，历史翻看再读几次）。读写比约 10:1。\n用读扩散，10:1 被放大到 100:1。用写扩散，读写比可以平衡到大约 30:30 — 虽然写变多了，但系统两端都不会触顶，整体吞吐更高。\n现代 IM 系统（微信、钉钉）的普遍做法：\n默认使用写扩散，小群（\u0026lt;2000 人）走写扩散 大群退化到读扩散，超过阈值后消息只写存储库，不写同步库。接收方在打开群聊时按需拉取 读写混合模式：一个高级 IM 系统应该能根据群规模动态选择同步策略 五、物理存储选型 Timeline 是一个逻辑模型，落到底层需要具体的存储引擎。Timeline 对底层数据库是有要求的：\n需求 说明 高并发写 写扩散模式下，同步库面临巨大的写入压力 按序范围读 按 SeqId 范围拉取消息是核心操作 随机定位 按 SeqId 精确跳到某条消息 高可用 + 低成本 消息数据量巨大，存储成本敏感 业界常见方案：\nHBase / TableStore（阿里云）：天然的 LSM-Tree 架构，按 rowkey 有序排列，range scan 性能极好。阿里系 IM 的经典选择 Cassandra / ScyllaDB：分区有序，写性能强悍，适合写扩散场景 TiKV：分布式 KV，支持有序扫描，适合自建 Redis（同步库）：同步库可以用 Redis 的有序集合（ZSet），以 SeqId 为 score。带 TTL 自动淘汰。但数据量大了之后内存成本高，一般只用作同步库的缓存层 自建 B+Tree 引擎：大厂的自研路线，极致优化 一个典型的部署模型：\n1 2 同步库: Redis Cluster (主要) + 冷数据下沉到 KV Store 存储库: HBase / Cassandra (全量) + CDN 缓存热门消息 六、工程中的实际考量 理论讲完了，聊几个实际中遇到的痛点：\n6.1 同步库的垃圾回收 同步库的数据有\u0026quot;保质期\u0026quot;——消息一旦被所有端确认已同步，就可以清理了。但实际上很难精确判断\u0026quot;所有端\u0026quot;。一般做法有：\n基于 TTL：给同步库 Timeline 设置过期时间（如 7 天/30 天），超时自动淘汰。接收方如果长期离线，上线后从存储库拉全量 基于 ACK：每个端上报已同步的 SeqId，服务端计算最低水位，定期裁剪。实现复杂但更优雅 6.2 消息存储库的瘦身 存储库是永久存储，规模大了后面临几个问题：\n数据倾斜：某些高频用户/群聊的消息量远超平均值，单个 Timeline 过大。需做 Timeline 分裂（Split） 冷热分离：近期消息放 SSD，远古消息放 HDD/OSS。常见策略是按时间分片，30 天前的数据迁移到廉价存储 图片/文件分离：大文件不走消息库，单独的对象存储 + CDN，消息体里只存 URL 6.3 消息的有序性 我参与的一个项目里遇到了经典的\u0026quot;消息乱序\u0026quot;问题：\n用户 A 在手机上连接 WebSocket 发了一条\u0026quot;你好\u0026quot;，过了一秒又发了一条\u0026quot;在吗？\u0026quot;。结果 B 的手机上显示\u0026quot;在吗？\u0026ldquo;先到，\u0026ldquo;你好\u0026quot;后到。\n原因分析：两条消息进了同步库后，由于 SeqId 生成器是分布式 ID（如雪花算法），虽然保证递增但不保证严格递增（即后分配的不一定大于先分配的）。再加上在线推送走的是推送通道（APNs / FCM），和主动拉取的路径不同，在线推送的成功率波动导致了乱序。\n解决方案：在线推送不是同步路径，只是优化路径。接收方收到的在线推送消息先在本地暂存，等下一次主动拉取同步库，以同步库的 SeqId 顺序为准进行重排，再展示给用户。也就是说——\u0026ldquo;以拉为准\u0026rdquo;。\n6.4 SeqId 生成器 SeqId 是 Timeline 的灵魂。要求：\n全局唯一 递增（但不要求严格连续） 高性能（百万级 QPS） 常见方案：\n雪花算法（Snowflake）：时间戳 + 机器 ID + 序列号。时钟回跳是大坑，需要额外处理 Redis INCR：按 Timeline 维度自增。简单可靠，但 Redis 性能在高并发下是瓶颈 数据库自增序列：如 MySQL auto_increment，通过步长和 offset 做多节点。写吞吐受限于单库 段锁（Segment）：服务端预分配一段 SeqId 区间，用完了再去取。综合性能和复杂度最平衡的方案 七、总结：两个库的本质 \u0026ldquo;两个消息库\u0026quot;的本质，是将\u0026quot;数据持久化\u0026quot;和\u0026quot;数据分发\u0026quot;两个关注点解耦：\n消息存储库解决**\u0026ldquo;数据在哪儿\u0026rdquo;**——持久化、可回溯、可漫游 消息同步库解决**\u0026ldquo;数据怎么到终端\u0026rdquo;**——实时、高效、多端 两者共享 Timeline 这个抽象模型，但在物理实现、访问模式、生命周期上完全不同。\n如果让我给 IM 架构新手一个建议：先理解 Timeline 模型，再理解写扩散和读扩散的取舍，最后才去纠结用什么数据库。底层存储可以换，但架构设计的思路一旦定下来，整个系统的上限和下限就都画好了。\n参考\n阿里云 TableStore 团队，《现代IM系统中消息推送和存储架构的实现》 掘金，《现代IM系统中聊天消息的同步和存储方案探讨》 知乎，《IM 系统的架构设计——消息同步和存储》 ","date":"2026-05-23T00:00:00Z","permalink":"/posts/backend-im%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E7%9A%84%E4%B8%A4%E4%B8%AA%E6%B6%88%E6%81%AF%E5%BA%93%E5%90%8C%E6%AD%A5%E5%BA%93%E4%B8%8E%E5%AD%98%E5%82%A8%E5%BA%93/","title":"IM应用开发的两个消息库：同步库与存储库"},{"content":"当前服务器ip:\n1 10.201.113.35 校外访问请通过校园 VPN\n系统盘容量有限，大型文件请放置于/data文件夹中（~/data即为其软链接快捷方式）\n一、前言 实验室近期新购置了一台深度学习服务器，供成员进行模型训练、仿真和计算任务。为了帮助大家快速上手、规范使用、避免资源浪费或系统损坏，特编写此教程。\n二、什么是“深度学习服务器” 深度学习服务器是一台高性能计算机，通常配备多块 GPU（图形处理器）、大容量显存、内存和高速存储。 通常安装Linux系统 ，一般不直接接显示器，而是通过网络远程访问（SSH 或远程桌面）。\n主要用途 深度学习训练（如 PyTorch、TensorFlow）； 大规模有限元仿真（Abaqus、ANSYS、COMSOL）； 数据分析、图像识别、模型优化等。 系统与软件环境 操作系统：Ubuntu 24.04 LTS 已安装软件：Miniconda、Python、JupyterLab(TODO) 为避免cuda版本导致的问题，此处仅保证显卡驱动版本，请根据需求自行安装cuda。\n三、账户与登录信息 账户申请 新用户请联系实验室管理员开通。\n账户命名规则 实验室统一以姓名全拼作为用户名，例如：\n张三 → zhangsan 李四 → lisi 初始密码 所有新账户的初始密码为：LAB113@temp\n手动修改密码（重要） 登录服务器后（见后续教程），默认会自动弹出修改密码的流程。\n如果后续修改密码，在终端输入：\n1 passwd 系统会提示：\n1 2 Changing password for user zhangsan. (current) UNIX password: 输入当前密码（即 LAB113@temp）。\n接着输入新密码两次（系统不会显示输入内容，但实际上输进去了）：\n1 2 Enter new UNIX password: Retype new UNIX password: 如果两次输入一致，会显示：\n1 password updated successfully 表示修改成功。\n之后会自动退出连接，使用你设置的新密码重新连接。\n四、连接方式 （一）Windows 用户 此处推荐三个用于连接的终端工具（就是黑窗口，理论上使用cmd即可，但不够易用）：\nMobaXterm XShell Terminus 此处仅演示MobaXterm。\n打开软件，点击左上角的Session\n再点SSH，在下面的Remote host 填服务器地址，Specify username填你的账号名。\n首次登录后根据提示修改密码。\n左边是服务器文件路径，可以按照权限操纵文件，新建个文件夹放代码之类的。\n右边就是终端，可以对linux系统进行各种命令行操作。\n用 VS Code 连接（主要使用方式） PyCharm（以及其他Jetbrains的IDE）远程连接功能远不如VS Code，因此此处仅演示VS Code，也仅推荐使用VS Code。\n如果想要使用PyCharm连接，可以阅读实验室服务器的使用指南。\n安装remote相关插件 在侧边栏打开插件 新建ssh连接 选择路径\n在 VS Code 里使用终端：点击 New Terminal\n（二）macOS 用户 此处推荐Terminus，iPad都能用。\n或者直接用mac自带终端也行：\nSSH连接 打开“终端”：\n1 ssh username@服务器IP 文件传输 可使用命令行：\n1 scp localfile username@服务器IP:/home/username/ （三）Linux 用户 推荐WindTerm，本人用得最多的一个。\n五、Anaconda 与 Python 环境管理 1. Anaconda 简介 Anaconda 是一个 Python 环境与包管理工具，可方便地创建独立环境、安装依赖，避免不同项目间的冲突。（miniconda是其精简版）\n此处管理员已预装系统级miniconda（位于/opt/miniconda3/bin/conda）。\n2. 常用命令 操作 命令示例 创建新环境 conda create -n myenv python=3.10 激活环境 conda activate myenv 安装包 conda install numpy pandas pytorch 查看环境 conda env list 删除环境 conda remove -n myenv --all 3. 使用 JupyterLab（暂未安装，后续可能会扩展） 在服务器上启动：\n1 jupyter lab --no-browser --port=8888 然后在本地终端执行：\n1 ssh -L 8888:localhost:8888 username@服务器IP 本地浏览器访问：\n1 http://localhost:8888 六、远程桌面与图形化软件 远程桌面比起ssh命令行会有一定卡顿，深度学习等应用请保持原来的方式。\n1. 适用场景 用于运行图形化仿真软件（Abaqus、ANSYS、COMSOL、Matlab 等）。\n其中大部分安装需要sudo权限，如需安装请联系，请勿自行安装。\n2. 用户连接方式 从windows中打开远程桌面功能。\n按步骤依次输入服务器ip、用户名和密码，进行连接。\n连接上远程桌面（gnome）后，点击最左上角标识可以查看窗口与查找应用。\n七、文件管理与备份 1. 目录说明 路径 用途 /home/username 用户个人目录 /data 公共数据集或模型 /workspace 项目工作区（暂时未开通） 八、服务器使用规范 不要在系统目录（/、/root）下操作。\n运行长时间任务时请使用：\n1 nohup python train.py \u0026gt; log.txt 2\u0026gt;\u0026amp;1 \u0026amp; 或在 tmux 会话中执行。\n未经沟通严禁重启服务器（如果你有权限的话）。\n任务结束后释放显存和进程（输入ps -aux | grep 你的进程名找你的进程，找到pid之后kill -9 你的pid）。\n请勿安装系统级软件，如需sudo权限请在群里询问。\n九、常见问题（FAQ） 问题 解决方法 SSH 连不上 检查网络/VPN，确认端口22是否开放 GPU 已占满 使用 nvidia-smi 查看使用者，协商使用 Conda 包冲突 新建独立环境 Jupyter 无法访问 检查端口转发是否正确 远程桌面卡顿 降低分辨率或关闭3D加速 十、附录 常用命令速查 1 2 3 4 5 nvidia-smi # 查看GPU pwd # 显示当前路径 ls -lh # 列出文件 scp localfile user@ip:/home/user/ # 上传文件 conda create -n myenv python=3.10 # 创建新环境 管理员联系方式 推荐学习资料 Linux命令快速入门：https://wangchujiang.com/linux-command/ Anaconda官方文档：https://docs.anaconda.com/ PyTorch教程：https://pytorch.org/tutorials/ 参考文档 实验室GPU管理神器Determined - 吕昱峰的文章 - 知乎。\n实验室服务器管理经验 - PurRigiN的文章 - 知乎。\n手把手教你如何连上实验室的服务器。\nMac下使用SSH连接远程Linux服务器。\n实验室服务器使用教程（用户篇）。\nyurizzzzz/TJU-ServerDoc 天津大学实验室服务器使用和管理。\n实验室服务器的使用指南。\n","date":"2025-10-18T00:00:00Z","permalink":"/posts/normal-%E5%AE%9E%E9%AA%8C%E5%AE%A4%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B/","title":"实验室服务器使用教程"},{"content":"Linux namespace做一层资源隔离，使里面的进程/进程组看起来拥有自己的独立资源。\nPID namespace 中的 init 进程（PID=1）需要正确处理子进程的僵尸状态，否则会导致资源泄漏。\n有多种namespace:\nPID Namespace（CLONE_NEWPID）：不同 namespace 中的进程可以拥有相同的 PID Network Namespace（CLONE_NEWNET）：隔离网络栈，包括网络设备、IP 地址、端口、路由表以及防火墙规则 Mount Namespace（CLONE_NEWNS）：隔离文件系统挂载点 User Namespace（CLONE_NEWUSER）：隔离用户和组 ID 空间，允许同一个用户在不同 namespace 中拥有不同的权限 \u0026hellip; Docker 容器默认会使用以下 namespace：\nPID：隔离进程树。 NET：提供独立的网络栈。 IPC：隔离进程间通信。 UTS：设置独立的主机名。 MOUNT：隔离文件系统挂载点。 USER：用于映射容器内的 root 用户到宿主机的普通用户。 每个进程的 namespace 信息都存储在/proc/[pid]/ns目录下：\n1 2 3 4 5 6 7 8 9 ls -l /proc/self/ns # lrwxrwxrwx 1 user user 0 Jun 10 12:00 cgroup -\u0026gt; \u0026#39;cgroup:[4026531835]\u0026#39; # lrwxrwxrwx 1 user user 0 Jun 10 12:00 ipc -\u0026gt; \u0026#39;ipc:[4026531839]\u0026#39; # lrwxrwxrwx 1 user user 0 Jun 10 12:00 mnt -\u0026gt; \u0026#39;mnt:[4026531840]\u0026#39; # lrwxrwxrwx 1 user user 0 Jun 10 12:00 net -\u0026gt; \u0026#39;net:[4026531956]\u0026#39; # lrwxrwxrwx 1 user user 0 Jun 10 12:00 pid -\u0026gt; \u0026#39;pid:[4026531836]\u0026#39; # lrwxrwxrwx 1 user user 0 Jun 10 12:00 pid_for_children -\u0026gt; \u0026#39;pid:[4026531836]\u0026#39; # lrwxrwxrwx 1 user user 0 Jun 10 12:00 user -\u0026gt; \u0026#39;user:[4026531837]\u0026#39; # lrwxrwxrwx 1 user user 0 Jun 10 12:00 uts -\u0026gt; \u0026#39;uts:[4026531838]\u0026#39; 如何创建？ 使用unshare命令创建 namespace： 1 2 3 4 5 6 7 8 # 创建新的挂载点和PID namespace，并在其中启动bash unshare --mount --pid --fork bash # 在新的namespace中查看PID echo $$ # 输出通常为1，表示当前bash是新namespace中的第一个进程 # 查看当前namespace中的进程 ps aux 使用clone()系统调用 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 #define _GNU_SOURCE #include \u0026lt;sched.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;sys/wait.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; // 子进程执行的函数 static int child_func(void *arg) { // 在新的UTS namespace中设置主机名 sethostname(\u0026#34;container\u0026#34;, 9); // 输出当前进程ID和主机名 printf(\u0026#34;子进程PID: %d\\n\u0026#34;, getpid()); printf(\u0026#34;主机名: %s\\n\u0026#34;, \u0026#34;container\u0026#34;); // 执行/bin/bash execlp(\u0026#34;/bin/bash\u0026#34;, \u0026#34;bash\u0026#34;, NULL); return 1; } int main() { const int STACK_SIZE = 65536; // 为子进程分配栈空间 char *stack = malloc(STACK_SIZE); if (!stack) { perror(\u0026#34;内存分配失败\u0026#34;); return 1; } // 设置栈顶（栈是向下增长的） char *stack_top = stack + STACK_SIZE; // 创建新的UTS和PID namespace，并启动子进程 pid_t pid = clone(child_func, stack_top, CLONE_NEWUTS | CLONE_NEWPID | SIGCHLD, NULL); if (pid == -1) { perror(\u0026#34;clone失败\u0026#34;); return 1; } // 等待子进程结束 waitpid(pid, NULL, 0); free(stack); return 0; } 使用setns()加入现有 namespace\n加入另一个进程的网络 namespace： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #define _GNU_SOURCE #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;sched.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; int main() { // 打开目标进程的网络namespace文件 int fd = open(\u0026#34;/proc/1234/ns/net\u0026#34;, O_RDONLY); if (fd == -1) { perror(\u0026#34;打开namespace文件失败\u0026#34;); return 1; } // 加入目标namespace if (setns(fd, CLONE_NEWNET) == -1) { perror(\u0026#34;加入namespace失败\u0026#34;); return 1; } close(fd); // 执行需要在目标namespace中运行的命令 execlp(\u0026#34;ip\u0026#34;, \u0026#34;ip\u0026#34;, \u0026#34;addr\u0026#34;, NULL); return 0; } 使用nsenter命令（简化版setns()） 1 sudo nsenter --target 1234 --net ip addr ","date":"2025-06-08T00:00:00Z","permalink":"/posts/linux-%E5%AE%B9%E5%99%A8%E5%8C%96%E6%8A%80%E6%9C%AF%E4%B9%8B-linux-namespace/","title":"容器化技术之 Linux namespace"},{"content":" 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 #include \u0026lt;cstring\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;sys/socket.h\u0026gt; #include \u0026lt;netdb.h\u0026gt; #include \u0026lt;arpa/inet.h\u0026gt; // 2025/05/08 // C语言通过getaddrinfo函数获取域名的IP地址 int main() { struct addrinfo hints, *res, *p; int status; char ipstr[INET6_ADDRSTRLEN]; // 初始化hints结构 memset(\u0026amp;hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // IPv4 或者 IPv6 hints.ai_socktype = SOCK_STREAM; // TCP 套接字 // 获取地址信息 if ((status = getaddrinfo(\u0026#34;www.baidu.com\u0026#34;, NULL, \u0026amp;hints, \u0026amp;res)) != 0) { fprintf(stderr, \u0026#34;getaddrinfo error: %s\\n\u0026#34;, gai_strerror(status)); return 1; } // 遍历地址列表 for(p = res; p != NULL; p = p-\u0026gt;ai_next) { void *addr; char *ipver; // 获取 IP 地址 if (p-\u0026gt;ai_family == AF_INET) { // IPv4 struct sockaddr_in *ipv4 = (struct sockaddr_in *)p-\u0026gt;ai_addr; addr = \u0026amp;(ipv4-\u0026gt;sin_addr); ipver = \u0026#34;IPv4\u0026#34;; } else { // IPv6 struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p-\u0026gt;ai_addr; addr = \u0026amp;(ipv6-\u0026gt;sin6_addr); ipver = \u0026#34;IPv6\u0026#34;; } // 将二进制 IP 地址转换为文本格式 inet_ntop(p-\u0026gt;ai_family, addr, ipstr, sizeof ipstr); printf(\u0026#34;%s: %s\\n\u0026#34;, ipver, ipstr); } // 释放地址信息 freeaddrinfo(res); return 0; } ","date":"2025-05-08T00:00:00Z","permalink":"/posts/cppc%E8%AF%AD%E8%A8%80%E9%80%9A%E8%BF%87getaddrinfo%E5%87%BD%E6%95%B0%E8%8E%B7%E5%8F%96%E5%9F%9F%E5%90%8D%E7%9A%84ip%E5%9C%B0%E5%9D%80/","title":"【仅源码】C语言通过getaddrinfo函数获取域名的IP地址"},{"content":"常见MQ的功能，有哪些是用Redis实现不了的？\n消息队列（MQ）用于解耦系统、异步处理、削峰填谷等，常见的 MQ 有 RabbitMQ、Kafka、RocketMQ、ActiveMQ 等。而 Redis 也提供了发布/订阅（pub/sub）、List 队列、Stream（流）等机制，看似也能实现部分消息队列的功能。\n✅ Redis 能做的 MQ 功能： 功能 Redis 支持方式 简单队列 使用 List 的 LPUSH + BRPOP 实现 发布订阅 使用 Pub/Sub 功能 消息流 使用 Stream 类型（自 Redis 5.0 起） 消息持久化（有限） Redis 有持久化机制（RDB、AOF） ❌ Redis 实现不了或不擅长的 MQ 功能： 功能 原因 消息可靠投递（ACK 确认机制） Redis 的 Pub/Sub 没有消费确认机制，Stream 有但比较弱（如无消费失败自动重试机制） 高吞吐量/分布式日志系统（如 Kafka） Redis 不适合大规模日志或百万 TPS 场景，且不具备分区（partition）机制 消费失败后的重试机制、死信队列（DLQ） Redis 不原生支持，需要手动构建（逻辑复杂且不够健壮） 消费顺序保障（partition+offset） Redis Stream 提供 ID 顺序，但无法如 Kafka 那样做严格的有序分区消费 持久性保证和磁盘容量优化 Redis 为内存数据库，持久性和存储成本远不如 Kafka 等磁盘级 MQ 消息积压处理能力强 Redis 基于内存，积压消息多了容易 OOM，Kafka 之类基于磁盘无此问题 事务性消息支持（如 RocketMQ） Redis 不支持事务性消息逻辑 消费者分组与负载均衡（Consumer Group） Redis Stream 有些类似功能，但不如 Kafka 灵活和成熟 流控和限速、幂等机制支持 Redis 需要自己实现，Kafka 等 MQ 内建支持 总结 Redis 可以用来实现轻量级、简单或低吞吐的消息队列系统； Kafka、RabbitMQ、RocketMQ 等更适合需要高可靠性、分布式、高吞吐、复杂消息模式的场景； Redis 适合“玩具级”或轻量任务队列，不推荐在企业级复杂系统中用作核心 MQ。 ","date":"2025-05-05T00:00:00Z","permalink":"/posts/redis-redis-stream%E5%92%8Cmq/","title":"Redis Stream和MQ"},{"content":"复习：【Linux】守护进程（ Daemon）的定义，作用，创建流程。\n编写守护进程的一般步骤步骤：\n在父进程中执行fork并exit退出； 在子进程中调用setsid函数创建新的会话； 在子进程中调用chdir函数，让根目录/成为子进程的工作目录； 在子进程中调用umask函数，设置进程的umask为0； 在子进程中关闭任何不需要的文件描述符。 Linux—umask（创建文件时的掩码）用法详解。\n深刻理解——real user id, effective user id, saved user id in Linux。\nLinux进程权限的研究——real user id, effective user id, saved set-user-id。\n调用setsid的进程不是一个进程组的组长，此函数创建一个新的会话期。\n","date":"2025-04-29T00:00:00Z","permalink":"/posts/linux-%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B/","title":"Linux 守护进程"},{"content":"复习：【Linux】守护进程（ Daemon）的定义，作用，创建流程。\nLinux 命令之 locale \u0026ndash; 设置和显示程序运行的语言环境。\n使用 locale 命令来设置和显示程序运行的语言环境，locale 会根据计算机用户所使用的语言，所在国家或者地区，以及当地的文化传统定义一个软件运行时的语言环境。\nlocale 由ANSI C提供支持。locale 的命名规则为\u0026lt;语言\u0026gt;_\u0026lt;地区\u0026gt;.\u0026lt;字符集编码\u0026gt;。\n深刻理解——real user id, effective user id, saved user id in Linux。\nLinux进程权限的研究——real user id, effective user id, saved set-user-id。\n调用setsid的进程不是一个进程组的组长，此函数创建一个新的会话期。\n1 echo $UID ","date":"2025-04-29T00:00:00Z","permalink":"/posts/linux-todo-%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8Bsetsidlinux%E4%B8%89%E4%B8%AAid%E6%9D%83%E9%99%90/","title":"TODO 守护进程，setsid，Linux三个id，权限"},{"content":" 知乎链接：https://zhuanlan.zhihu.com/p/1961547067106259944\nwezterm在打开主进程、或者ctrl+alt+t新建tab的时候默认是空的shell会话，有时候不够方便。\ntmux可以轻松解决痛点，但是如果设置wezterm的default_prog为tmux（即每次会话都启动）：在wezterm里新建tab，你会发现和之前共享同一个tmux，相当于wezterm自带的tab功能没用了。\nconfig.default_prog = { \u0026quot;/usr/bin/tmux\u0026quot;, \u0026quot;new-session\u0026quot;, \u0026quot;-A\u0026quot;, \u0026quot;-s\u0026quot;, \u0026quot;main\u0026quot; } 于是：能否只让wezterm的第一个tab始终开启一个tmux来保存会话，其他tab则使用默认的新建shell功能？\n具体实现：\n-- 第一个tab打开tmux，之后的为空的shell local tmux_started = false wezterm.on(\u0026quot;gui-startup\u0026quot;, function(cmd) -- 启动 wezterm 时自动打开一个 window local tab, pane, window = wezterm.mux.spawn_window(cmd or {}) if not tmux_started then tmux_started = true -- 启动 tmux pane:send_text(\u0026quot;tmux -u new-session -A -s main\\n\u0026quot;) end end) 加在wezterm配置合适的位置即可。\n","date":"2025-04-29T00:00:00Z","permalink":"/posts/linux-tmux%E5%AE%9E%E7%8E%B0wezterm%E4%BF%9D%E5%AD%98%E4%BC%9A%E8%AF%9D/","title":"在wezterm里使用tmux实现history restore 保存历史会话"},{"content":"半夜学一下blender。\n【Kurt】Blender零基础入门教程 | Blender中文区新手必刷教程(已完结)。第四课著名的珍珠耳环少女，感觉一节课就差不多学会基础操作了，不错。\nN是变换，S是size（也可以结合方向轴，在轴向伸缩），R是旋转（可以输数字，例如R Z 90就是在Z轴旋转90度），G是移动。\n新建内容是SHIFT+A，F9可以改段数。SHIFT+D复制，移动的时候可以例如按Z锁定在Z轴上移动。按两下方向轴（例如移动的时候），可以从世界坐标系切换到局部坐标系。\n按/可以单独显示该物体。\n在窗口左上角拉一下，能拉出新窗口。新窗口的右上角，可以拉回去。\n摄像机模式也可以按N，在\u0026quot;视图\u0026quot;里把摄像机\u0026quot;锁定到视图方位\u0026quot;，方便调整。\nblender有两个渲染引擎：EEVEE（快、实时）、CYCLES（物理写实）。 渲染快捷键F12。\n右键物体，平滑着色。右边物体数据属性（绿色三角）-\u0026gt; 法向 -\u0026gt; 自动光滑。\n编辑-\u0026gt;偏好设置里能调整CUDA设置。\n发现用CYCLES的时候，切换成GPU渲染不出东西。显示Error CUDA kernel for this graphics card compute capability(8.6) not found，可能是因为显卡驱动太新——毕竟我用的 2.83 的远古Blender（以前转mmd格式的时候，插件比较老，所以下的老版本）。\n多选之后-\u0026gt;CTRL+L-\u0026gt;关联材质。\n","date":"2025-04-25T00:00:00Z","permalink":"/posts/game-1blender%E5%AD%A6%E4%B9%A0%E6%97%A5%E8%AE%B0-%E5%85%A5%E9%97%A8/","title":"【1】Blender学习日记-入门"},{"content":"一、HTTP协议基础 HTTP协议通过Range请求实现断点续传：\n客户端请求指定范围\n客户端在请求头中携带Range字段，例如：\n1 2 GET /file.zip HTTP/1.1 Range: bytes=500-1000 服务端响应部分内容\n若支持范围请求，服务端返回状态码206 Partial Content及对应数据片段：\n1 2 3 HTTP/1.1 206 Partial Content Content-Range: bytes 500-1000/5000 Content-Length: 501 完整性校验机制\n通过ETag或Last-Modified头确保文件未变更，避免续传数据不一致。\n二、Nginx静态资源断点续传 Nginx默认支持静态文件的断点续传。需要有以下配置：\n1 2 3 4 5 6 server { location /static { root /data/files; # 文件存储路径 add_header Accept-Ranges bytes; # 声明支持字节范围请求 } } 验证方法：\n使用curl检测响应头：\n1 curl -I http://your-domain/static/large-file.iso 若输出包含Accept-Ranges: bytes与Content-Length，则表明支持续传。\n三、Go实现 对于动态生成的文件（如需鉴权的资源），需手动处理Range请求。\n1 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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strconv\u0026#34; \u0026#34;strings\u0026#34; ) func handleDownload(w http.ResponseWriter, r *http.Request) { filePath := \u0026#34;/data/dynamic-file.bin\u0026#34; file, err := os.Open(filePath) if err != nil { http.Error(w, \u0026#34;File not found\u0026#34;, http.StatusNotFound) return } defer file.Close() fileInfo, _ := file.Stat() fileSize := fileInfo.Size() w.Header().Set(\u0026#34;Content-Length\u0026#34;, strconv.FormatInt(fileSize, 10)) w.Header().Set(\u0026#34;ETag\u0026#34;, fmt.Sprintf(\u0026#34;\\\u0026#34;%x\\\u0026#34;\u0026#34;, fileInfo.ModTime().UnixNano())) rangeHeader := r.Header.Get(\u0026#34;Range\u0026#34;) if rangeHeader == \u0026#34;\u0026#34; { http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), file) return } ranges := strings.Split(rangeHeader, \u0026#34;=\u0026#34;)[1] parts := strings.Split(ranges, \u0026#34;-\u0026#34;) start, _ := strconv.ParseInt(parts[0], 10, 64) end := fileSize - 1 if parts[1] != \u0026#34;\u0026#34; { end, _ = strconv.ParseInt(parts[1], 10, 64) } if start \u0026gt;= fileSize || end \u0026gt;= fileSize { http.Error(w, \u0026#34;Requested range not satisfiable\u0026#34;, http.StatusRequestedRangeNotSatisfiable) return } w.Header().Set(\u0026#34;Content-Range\u0026#34;, fmt.Sprintf(\u0026#34;bytes %d-%d/%d\u0026#34;, start, end, fileSize)) w.Header().Set(\u0026#34;Content-Length\u0026#34;, strconv.FormatInt(end-start+1, 10)) w.WriteHeader(http.StatusPartialContent) file.Seek(start, 0) http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), file) } func main() { http.HandleFunc(\u0026#34;/download\u0026#34;, handleDownload) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } • 解析Range请求头并验证范围有效性 • 使用Seek定位文件指针，返回部分内容 • 通过ETag实现文件一致性校验\n四、客户端如何检测服务端是否支持？ 可通过以下步骤判断：\n发送HEAD请求\n获取响应头信息：\n1 curl -I http://your-domain/file.zip 检查关键头字段\n• Accept-Ranges: bytes：表明支持字节范围请求 • Content-Length：必须存在且为固定值（动态内容可能无法支持） • ETag或Last-Modified：用于文件变更校验\n实验性范围请求测试\n发送带Range头的GET请求：\n1 curl -H \u0026#34;Range: bytes=0-100\u0026#34; http://your-domain/file.zip 若响应状态码为206且包含Content-Range头，则确认支持续传。\n五、Nginx反向代理Go服务的注意事项 当Go服务部署于Nginx后，需确保配置正确处理Range请求：\n1 2 3 4 5 6 7 location /go-download { proxy_pass http://go-backend:8080/download; proxy_set_header Range $http_range; # 传递原始Range头 proxy_set_header If-Range $http_if_range; proxy_hide_header Accept-Ranges; # 避免与后端冲突 proxy_http_version 1.1; # 支持HTTP/1.1特性 } • 确认Nginx与Go服务对文件有读取权限 • 检查Content-Length是否被意外修改（如Gzip压缩） • 使用tcpdump或Wireshark抓包验证请求头传递\n六、边界问题与优化建议 多范围请求处理\n支持形如Range: bytes=0-100,200-300的请求需分段响应，可通过Go的multipart/byteranges实现。\n速率限制与防滥用\nNginx配置限速：\n1 2 3 location /download { limit_rate 1m; # 限制下载速度为1MB/s } 日志监控\n监控206状态码频率，识别异常续传行为。\n","date":"2025-04-23T00:00:00Z","permalink":"/posts/web-%E5%88%A4%E6%96%ADhttp%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%96%AD%E7%82%B9%E7%BB%AD%E4%BC%A0/","title":"实现服务端断点续传：Go与Nginx"},{"content":" 悲观锁（Pessimistic Lock）是一种假设冲突会频繁发生的锁机制。每次数据访问时，都会先加锁，直到操作完成后才释放锁，这样可以确保在锁持有期间，其他线程无法访问这段数据，从而避免了并发冲突。 乐观锁（Optimistic Lock）是一种假设冲突不会频繁发生的锁机制。每次数据访问时，不会加锁，而是在更新数据时检查是否有其他线程修改过数据。如果检测到冲突（数据被其他线程修改过），则重试操作或报错。适用于读多写少的场景。\n乐观锁通常实现方式有以下两种：\n版本号机制：每次读取数据时，读取一个版本号，更新数据时，检查版本号是否变化，如果没有变化，则更新成功，否则重试。\n时间戳机制：类似版本号机制，通过时间戳来检测数据是否被修改。\n悲观锁性能较低，因为每次操作都需要加锁和解锁。\n乐观锁性能较高，但在高并发写操作下可能会频繁重试，影响性能。\n悲观锁适用于并发冲突高、数据一致性要求严格的场景。\n乐观锁适用于并发冲突低、读多写少的场景。\nC++乐观锁实现方式：使用 CAS（Compare-And-Swap） 或 std::atomic：\n1 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 #include \u0026lt;iostream\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026lt;atomic\u0026gt; std::atomic\u0026lt;int\u0026gt; sharedData(0); void optimisticTask(int id) { for (int i = 0; i \u0026lt; 5; ++i) { int oldValue; int newValue; do { oldValue = sharedData.load(); // 读取当前值 newValue = oldValue + 1; // 本地计算 } while (!sharedData.compare_exchange_weak(oldValue, newValue)); std::cout \u0026lt;\u0026lt; \u0026#34;Thread \u0026#34; \u0026lt;\u0026lt; id \u0026lt;\u0026lt; \u0026#34; updated sharedData to \u0026#34; \u0026lt;\u0026lt; newValue \u0026lt;\u0026lt; std::endl; } } int main() { std::thread t1(optimisticTask, 1); std::thread t2(optimisticTask, 2); t1.join(); t2.join(); std::cout \u0026lt;\u0026lt; \u0026#34;Final sharedData: \u0026#34; \u0026lt;\u0026lt; sharedData \u0026lt;\u0026lt; std::endl; return 0; } compare_exchange_weak 可能会在无冲突时也失败（为性能优化），可以换成 compare_exchange_strong 更稳定。\n特性 悲观锁 乐观锁 开销 较大（加锁解锁） 较小（无锁，靠 CAS） 并发性能 低（锁竞争激烈时性能下降） 高（冲突少时效率高） 适用场景 冲突频繁的情况（例如写多读少） 冲突较少的情况（例如读多写少） 实现方式 std::mutex, std::lock_guard std::atomic, compare_exchange_* ","date":"2025-04-21T00:00:00Z","permalink":"/posts/cpp-c++-%E4%B8%AD%E7%9A%84%E4%B9%90%E8%A7%82%E9%94%81%E5%92%8C%E6%82%B2%E8%A7%82%E9%94%81/","title":"C++ 中的乐观锁和悲观锁"},{"content":"最终判断逻辑由服务端完成。\n• 浏览器行为（客户端）：\n• 浏览器会缓存资源（如 HTML、图片、CSS 等），并根据服务端之前返回的响应头（如 Cache-Control、Expires、ETag、Last-Modified）决定是否发起条件请求。\n• 当缓存过期或页面刷新（非强制刷新）时，浏览器会向服务端发送一个带有验证头的请求，例如：\n◦ If-None-Match（对应服务端之前返回的 ETag）\n◦ If-Modified-Since（对应服务端之前返回的 Last-Modified）\n• 服务端行为：\n• 服务端收到请求后，根据客户端的验证头（If-None-Match 或 If-Modified-Since）检查资源是否已修改。\n• 如果资源未修改，返回 304 Not Modified，且不返回资源内容，仅返回响应头。\n• 如果资源已修改，返回 200 OK 并附带新内容。\n• 浏览器：负责发起条件请求（携带验证头），并根据响应状态码决定是否使用缓存。\n• 服务端：负责验证资源是否修改，并决定返回 304 或 200。\n用户首次访问网页，服务端返回资源，响应头包含： 1 2 3 4 HTTP/1.1 200 OK ETag: \u0026#34;abc123\u0026#34; Last-Modified: Wed, 01 Jan 2024 00:00:00 GMT Cache-Control: max-age=3600 用户再次访问时，浏览器缓存未过期（max-age=3600 内）：\n• 直接使用缓存，无需请求服务端。 缓存过期后，浏览器发起条件请求： 1 2 3 GET /example.html HTTP/1.1 If-None-Match: \u0026#34;abc123\u0026#34; If-Modified-Since: Wed, 01 Jan 2024 00:00:00 GMT 服务端验证资源未修改，返回： 1 2 3 HTTP/1.1 304 Not Modified ETag: \u0026#34;abc123\u0026#34; Last-Modified: Wed, 01 Jan 2024 00:00:00 GMT 浏览器收到 304 后，继续使用本地缓存。 ","date":"2025-04-17T00:00:00Z","permalink":"/posts/web-304-not-modified-%E6%98%AF%E6%80%8E%E4%B9%88%E6%A3%80%E6%B5%8B%E7%9A%84/","title":"304 Not Modified 是怎么检测的？"},{"content":"Linux系统中的“三环”和“零环”概念源自CPU的保护环（Protection Rings）机制，是操作系统实现权限隔离和安全保护的核心设计。\nx86保护环的完整结构为四层，但实际仅Ring 0和Ring 3被广泛使用：\n零环（Ring 0）： 又称内核态，是CPU权限最高的运行模式。操作系统内核运行于此环，可直接访问硬件资源（如CPU、内存、I/O设备），执行特权指令（如修改内存映射、中断处理等）。例如，Linux内核的进程调度、内存管理和设备驱动均在此层级运行。 零环可直接控制硬件，而三环的代码若试图执行特权指令（如直接读写磁盘），CPU会触发异常（如General Protection Fault），强制终止非法操作。这种设计避免了用户程序破坏系统稳定性。 三环（Ring 3）： 又称用户态，是权限最低的层级。普通应用程序运行于此环，仅能通过系统调用（Syscall）请求内核服务，无法直接操作硬件。例如，用户启动的文本编辑器、浏览器等程序均受此限制。 用户程序通过系统调用或硬件中断从三环切换到零环。例如，当程序调用open()函数打开文件时，会触发软中断（如int 0x80或syscall指令），内核接管执行文件操作，完成后返回用户态。 CPU的运行环, 特权级与保护。\n原文 ——CPU的运行环, 特权级与保护。\nLinux内核开发之hook系统调用。\n三环进入零环的细节（KiFastCallEntry函数分析）。\n系统调用之_KUSER_SHARED_DATA。\n","date":"2025-03-30T00:00:00Z","permalink":"/posts/linux-linux-%E7%9A%84cpu%E4%BF%9D%E6%8A%A4%E7%8E%AF%E4%B8%89%E7%8E%AF%E5%92%8C%E9%9B%B6%E7%8E%AF/","title":"Linux 的CPU保护环，三环和零环"},{"content":"当通过 fork() 创建子进程时，子进程会获得父进程文件描述符表的完整副本。这意味着子进程的文件描述符表中每个条目指向的 系统级文件表项（File Table Entry）与父进程相同。 父子进程共享文件表项中的文件偏移量（Offset）、打开模式（Read/Write Flags）、文件状态标志等信息。例如，如果父进程写入文件后移动了偏移量，子进程会从新的偏移位置继续操作。\n修改文件描述符表本身（如关闭 fd）：子进程的操作不会影响父进程。例如，子进程关闭 fd=3，父进程的 fd=3 仍然有效。 修改共享的文件表项（如偏移量、状态标志）：子进程的操作会直接影响父进程，因为它们共享同一文件表项。 fork()子进程与父进程之间的文件描述符问题。\n【Linux】进程间通信。\n文件描述符相当于一个逻辑句柄，而open，close等函数则是将文件或者物理设备与句柄相关联。\n三张表：\n文件描述符表：用户区的一部分，除非通过使用文件描述符的函数，否则程序无法对其进行访问。对进程中每个打开的文件，文件描述符表都包含一个条目。 系统文件表：为系统中所有的进程共享。对每个活动的open, 它都包含一个条目。每个系统文件表的条目都包含文件偏移量、访问模式（读、写、or 读-写）以及指向它的文件描述符表的条目计数。 内存索引节点表: 对系统中的每个活动的文件（被某个进程打开了），内存中索引节点表都包含一个条目。几个系统文件表条目可能对应于同一个内存索引节点表（不同进程打开同一个文件）。 ","date":"2025-03-29T00:00:00Z","permalink":"/posts/linux-fork%E5%87%BA%E7%9A%84%E5%AD%90%E8%BF%9B%E7%A8%8B%E6%98%AF%E5%90%A6%E7%BB%A7%E6%89%BF%E6%96%87%E4%BB%B6%E6%8F%8F%E8%BF%B0%E7%AC%A6%E8%A1%A8/","title":"fork出的子进程是否继承文件描述符表？"},{"content":"一、共享的内容 文件描述符与文件状态\n子进程会继承父进程已打开的文件描述符表，包括文件偏移量、打开模式（如读写权限）和文件状态标志（如O_APPEND）。例如，若父进程打开了一个文件并写入数据，子进程可通过相同的文件描述符继续操作，且两者的写入位置（偏移量）会相互影响。\n• 示例场景：父进程向文件写入“Parent”，子进程写入“Child”，最终文件内容会按操作顺序合并（如“ParentChild”或“ChildParent”），具体取决于调度顺序。\n信号处理设置\n子进程继承父进程的信号处理函数（如SIG_IGN或自定义处理程序）和信号屏蔽集（sigprocmask的设置）。\n用户身份与环境变量\n子进程继承父进程的用户ID、组ID、环境变量、当前工作目录等身份信息。\n写时复制（Copy-On-Write）的内存初始状态\n在未发生写入操作前，父子进程的代码段（.text）、数据段（.data、.bss）、堆、栈等内存区域共享同一物理内存页。一旦某一方尝试修改数据，则会触发写时复制，生成独立的副本。\n文件锁\n通过fcntl或flock设置的文件锁会被子进程继承，父子进程对同一文件的锁定操作会相互影响。\n二、不共享的内容 进程独立属性\n子进程拥有独立的进程ID（PID）、父进程ID（PPID）、运行时间统计、未决信号队列等。\n多线程环境中的线程资源\n若父进程包含多个线程，子进程仅复制执行fork()的线程，其他线程不会被继承。\n独立的内存修改\n通过写时复制机制，父子进程对内存的修改会各自独立。例如，全局变量初始值相同，但修改后互不影响。\n独立的文件描述符关闭操作\n子进程关闭某个文件描述符不会影响父进程的同名描述符，反之亦然。\n三、关键机制：写时复制（COW） 内核通过写时复制技术优化性能：\n• 原理：fork()后，父子进程的页表项指向相同的物理内存页，并将这些页标记为只读。当某一进程尝试写入时，触发页错误，内核复制该页并修改权限为可写。\n• 优点：避免不必要的内存复制，提高资源利用率。\n四、应用注意事项 文件操作同步：父子进程对同一文件描述符的并发写入需通过锁（如flock()）或原子操作避免竞争。 内存共享限制：若需主动共享内存，需使用mmap()或共享内存API（如shmget()）。 僵尸进程处理：父进程需通过wait()回收子进程资源，或注册SIGCHLD信号处理函数。 ","date":"2025-03-27T00:00:00Z","permalink":"/posts/linux-linux%E9%87%8Cfork%E5%87%BA%E5%AD%90%E8%BF%9B%E7%A8%8B%E7%9A%84%E6%97%B6%E5%80%99%E5%93%AA%E4%BA%9B%E5%86%85%E5%AE%B9%E6%98%AF%E5%85%B1%E4%BA%AB%E7%9A%84/","title":"Linux里fork出子进程的时候，哪些内容是共享的？"},{"content":"替换一个二进制文件有以下两个思路：\n使用完整的一个新文件直接覆盖旧的文件。 只替换新旧文件之间的差异。通过算法去计算新旧文件之间的差异，然后将差异部分移动到目标机器上。 其中方法一制作的更新就叫做全量更新（Full Update）。而方法二就是二进制差分方式，即增量更新（Delta Update）。\n增量更新 增量更新有两个方式：文件差量更新、二进制差量更新。\n大部分的大软件，如 QQ 等，都会在自动更新的时候都会使用文件差量更新和二进制差量更新一起使用的策略。\n二进制差分更新对于就文件的状态有严格的要求，这是因为不同版本之间的二进制差异不同。\n举个例子：某个新的文件A的版本是3，需要更新到用户的机器上。但是部分用户机器上安装的文件A版本是1，部分用户的文件A是版本2。这种情况下，就需要分别计算版本3和版本1的差异，以及版本3和版本2的差异。然后根据不同用户的情况分别发送不同的二进制差异文件。\n想要进行增量更新，需要构建模块支持才能实现。确定性构建就是在代码没有变更的时候，构建输出的 DLL 或 Exe 一定是不变的。对应的，还应加入确定性混淆的支持，有一些代码接入了混淆过程，要求在代码没有变更的时候，最后混淆输出的文件也没有变更。\n增量更新不能热更新，需要重启才能生效。\n用到的算法主要有bsdiff，octodiff，xdelta。\nbsdiff 算法的时间复杂度和空间复杂度都很高。但优点是更新文件大小比较小。\n处理大文件选择Octodiff，处理小文件选择bsdiff.\n正向/逆向差分技术 在此之前，为了解决Windows累积更新体积过大的问题，微软使用的是Express Update技术。Express技术确实减小了累积更新的体积，但是却极大的增加了Window更新服务器的计算压力与存储压力。Express更新文件在更新服务器上通常会有大于10GB的体积。Express Update正是使用的增量更新方式，并且把所有文件的所有版本差异都存储到了更新服务器上（海量的文件）。\nWindows 10 1809版本之后引入了基于二进制的正向/逆向差分技术。\n二进制差分是需要严格比对文件的版本信息的。如果被替换文件的版本不确定，那么就无法应用二进制差分。如果本替换文件的版本非常多，那么就需要针对每一个版本分别计算二进制差分，这样一来，不同版本的二进制差分的总和体积也必然不会小，因此就抵消掉差分带来的体积优势。\n正向/逆向差分技术的核心思路是：将被替换文件的版本固定，这样就能唯一确定一个二进制差分了。那么，将被替换的文件版本固定成什么版本呢？\nWindows更新用的方法是，将所有需要被更新的文件版本回到Windows 10 基线版本 。然后，从基线版本安装二进制差分，完成文件的更新。\n这里，从当前的文件版本回退到基线版本的过程被称为逆向，从基线版本更新到最新的版本的过程称为正向，也被称为注水（hydration）。制作差分二进制叫做脱水（dehydration）。\n这两次文件更新的行为都使用差分二进制来完成，因此这就是正向/逆向二进制差分技术。\n核心逻辑在于通过固定基线版本（RTM）作为唯一中间状态，规避多版本组合带来的差分数爆炸问题。\n这样只需要存储一个当前版本到基线版本的二进制差分，然后统一从基线版本升级到新版本。\n（eg. 例如有0、1、2、3、4版本。如果用老办法，需要存储0-\u0026gt;1，0-\u0026gt;2，0-\u0026gt;3，0-\u0026gt;4，1-\u0026gt;2，1-\u0026gt;3，1-\u0026gt;4，2-\u0026gt;3，2-\u0026gt;4，3-\u0026gt;4的差分包，多了之后数量爆炸，因为增加新版本的时候需要存储之前所有版本到新版本的差分包。\n如果采用正向/逆向差分，要存储1-\u0026gt;0，2-\u0026gt;0，3-\u0026gt;0，4-\u0026gt;0，然后存储0-\u0026gt;1，0-\u0026gt;2，0-\u0026gt;3，0-\u0026gt;4。增加新版本的时候，只需要增加5-\u0026gt;0和0-\u0026gt;5，从n的累加优化到了n的线性关系的差分包数量。\n）\n基于正向/逆向二进制差分的Windows累积更新内部包含以下内容：\n从基线版本到最新版本N的正向二进制差分文件 回退到基线版本所需的逆向二进制差分文件 更新文件清单（Manifest） 更新文件Metadata 如果是离线更新MSU，还会多一些版本以及操作系统适应性判断的内容。\nSource \u0026amp;\u0026amp; Reference 二进制差异文件算法。\nSquirrel.Windows: An installation and update framework for Windows desktop apps。\nbsdiff源码解析。\n这段时间看到的最牛逼的文章：Windows 客户端软件自动更新服务的开发有哪些需求？。\n微软官方对正向/逆向二进制差分Updates的介绍：Windows Updates using forward and reverse differentials。\n基于正向/逆向二进制差分的Windows累积更新。\nWindows Update 技术详解系列之快速分发技术Express Update。\n","date":"2025-03-26T00:00:00Z","permalink":"/posts/alg-%E6%9B%B4%E6%96%B0%E7%9A%84%E4%BA%8C%E8%BF%9B%E5%88%B6%E5%B7%AE%E5%BC%82%E7%AE%97%E6%B3%95/","title":"更新的二进制差异算法"},{"content":"在多进程/多线程环境中，Mutex针对的是程序内部的内存数据结构（如链表、哈希表），无法直接控制外部资源（如磁盘文件）。例如，线程A通过Mutex保护一个缓存队列，但若另一个进程直接修改磁盘上的对应文件，Mutex无法拦截。此时可以用文件锁来解决。\n文件锁的核心逻辑是通过独占标记协调资源访问。\n典型应用场景：\n配置文件的原子性更新\n多个服务实例同时修改同一配置文件时，未加锁会导致最后的写入覆盖先前内容（例如Nginx配置热更新） 日志文件的顺序写入\n多线程日志系统中，不加锁可能引发日志行交错（如Apache日志滚动的并发问题） 分布式系统的资源协调\n在无中心化锁服务时，通过共享存储（如NFS）的文件锁实现分布式锁（类似ZooKeeper的临时节点机制） C++实现文件锁的三种范式 方案一：操作系统原生API（工业级方案） 适用场景：需要高可靠性、跨平台兼容的生产环境\n1 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 #include \u0026lt;string\u0026gt; #include \u0026lt;fcntl.h\u0026gt; // Linux #include \u0026lt;windows.h\u0026gt; // Windows class NativeFileLock { public: explicit NativeFileLock(const std::string\u0026amp; path) : lock_path(path) {} bool acquire() { #ifdef _WIN32 // Windows通过独占模式创建文件实现锁 h_file = CreateFileA(lock_path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_HIDDEN, nullptr); return h_file != INVALID_HANDLE_VALUE; #else // Linux使用fcntl记录锁 fd = open(lock_path.c_str(), O_RDWR | O_CREAT, 0644); if (fd == -1) return false; flock lock_struct{}; lock_struct.l_type = F_WRLCK; // 排他锁 lock_struct.l_whence = SEEK_SET; return fcntl(fd, F_SETLK, \u0026amp;lock_struct) != -1; #endif } void release() { #ifdef _WIN32 CloseHandle(h_file); DeleteFileA(lock_path.c_str()); #else close(fd); unlink(lock_path.c_str()); #endif } private: #ifdef _WIN32 HANDLE h_file = INVALID_HANDLE_VALUE; #else int fd = -1; #endif std::string lock_path; }; 技术要点：\n• Windows通过CREATE_ALWAYS+隐藏属性实现原子创建\n• Linux使用fcntl的记录锁，支持对文件部分区域加锁\n• 必须处理进程崩溃后的锁残留（通过unlink/DeleteFile物理删除锁文件）\n方案二：基于文件系统标记（轻量级方案） 适用场景：快速实现、非高并发场景\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include \u0026lt;fstream\u0026gt; #include \u0026lt;filesystem\u0026gt; class MarkerFileLock { public: explicit MarkerFileLock(const std::string\u0026amp; path) : lock_path(path) {} bool try_lock() { if (std::filesystem::exists(lock_path)) return false; std::ofstream temp(lock_path); return temp.is_open(); // 文件创建成功即视为获得锁 } void unlock() { std::filesystem::remove(lock_path); } private: std::string lock_path; }; 局限性：\n• 无法检测锁文件被手动删除的意外情况\n• 进程崩溃可能导致死锁（需额外守护进程清理）\n方案三：内存映射+原子操作（高性能方案） 适用场景：需要微秒级响应的关键系统\n1 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 #include \u0026lt;sys/mman.h\u0026gt; #include \u0026lt;atomic\u0026gt; class MMapLock { public: MMapLock(const char* path) { fd = open(path, O_RDWR | O_CREAT, 0644); ftruncate(fd, sizeof(int)); // 扩展文件大小 addr = mmap(nullptr, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); counter = reinterpret_cast\u0026lt;std::atomic\u0026lt;int\u0026gt;*\u0026gt;(addr); } bool lock() { int expected = 0; return counter-\u0026gt;compare_exchange_strong(expected, 1, std::memory_order_acquire); } void unlock() { counter-\u0026gt;store(0, std::memory_order_release); munmap(addr, sizeof(int)); close(fd); } private: int fd; void* addr; std::atomic\u0026lt;int\u0026gt;* counter; }; 优势：\n• 通过CPU原子指令实现无阻塞锁，性能比传统文件锁高10倍\n• 依赖内存映射文件实现跨进程同步\n实现选择指南 方案 可靠性 性能 适用场景 原生API ★★★ ★★☆ 生产环境、跨平台要求高 文件标记 ★☆☆ ★★★ 快速原型、低并发需求 内存映射+原子 ★★☆ ★★★ 高频访问、延迟敏感型系统（如交易系统） 避坑建议：\n避免网络文件系统（如NFS）——锁机制可能因网络延迟失效 设置超时退避——防止死锁（参考Java的tryLock(timeout)） 锁文件路径规范化——建议使用/var/lock/等专用目录 ","date":"2025-03-26T00:00:00Z","permalink":"/posts/cpp-%E6%96%87%E4%BB%B6%E9%94%81filelock%E7%9A%84%E6%9C%AC%E8%B4%A8%E4%B8%8E%E4%BB%B7%E5%80%BC/","title":"文件锁（FileLock）的本质与价值"},{"content":"在Web服务器领域，Nginx凭借其高并发、低资源消耗的特点脱颖而出。其核心设计选择之一便是多进程模型。这一设计看似与传统多线程模型背道而驰，却恰恰成就了Nginx的卓越性能。本文将从技术原理、场景适配、架构权衡等角度，深度解析Nginx偏爱多进程的底层逻辑。\n一、多进程模型的核心架构 Nginx采用经典的Master-Worker多进程架构：\nMaster进程：负责全局管理，包括配置加载、信号处理、Worker进程监控与重启。 Worker进程：实际处理请求的“战斗单元”，每个Worker独立运行且绑定到特定CPU核心，通过异步非阻塞事件驱动模型处理成千上万的并发连接。\n这种设计实现了资源隔离与职责分离，Master的稳定性不受Worker业务逻辑影响，Worker的崩溃也不会导致服务中断。 二、选择多进程的五大核心原因 1. 最大化多核CPU利用率 现代服务器普遍采用多核架构，Nginx通过为每个Worker进程绑定独立CPU核心，避免了线程上下文切换的开销，使硬件资源被充分调度。例如，8核服务器可启动8个Worker，每个进程独占一核执行无锁化任务处理。\n2. 规避多线程锁竞争 多线程模型中，共享内存的访问需通过锁机制同步，而锁竞争会导致性能急剧下降。Nginx的多进程模型天然隔离了内存空间，Worker之间无需加锁，消除了这一性能瓶颈。\n3. 故障隔离与高可用性 若某个Worker进程因代码缺陷崩溃，Master进程可立即重启新Worker，其他进程仍正常服务。这种“单点故障不影响全局”的特性，显著提升了系统的容错能力。相比之下，多线程模型中线程崩溃可能导致整个进程宕机。\n4. 简化开发与维护 多进程模型的代码结构更清晰：\n• Worker之间无共享状态，避免复杂的线程同步逻辑\n• 调试时可通过gdb单独附加到某个Worker进程，无需处理线程交织问题\n5. 与事件驱动模型的完美契合 Nginx的异步非阻塞I/O多路复用（如Linux的epoll）是其高并发的另一基石。每个Worker进程通过单线程循环处理事件，避免了传统多进程模型中“一请求一进程”的资源浪费。这种组合使得单个Worker即可高效管理数万连接。\n三、多进程模型的局限性 尽管优势显著，该模型也存在以下挑战：\n内存占用较高：每个Worker需独立的内存空间，连接数极高时可能产生冗余开销。 进程间通信复杂：共享数据需通过IPC（如共享内存），开发复杂度高于线程模型。 计算密集型场景劣势：若请求涉及大量CPU运算（如加密解密），多线程模型可能更高效。 四、与其他模型的对比分析 模型类型 典型代表 适用场景 Nginx的选择依据 多进程单线程 Nginx I/O密集型高并发 规避锁竞争，隔离故障 单进程多线程 Apache 计算密集型任务 线程崩溃风险高，调试复杂 协程模型 Go 高并发微服务 需语言运行时支持，生态差异 五、设计启示：如何选择并发模型？ Nginx的实践为高并发系统设计提供了重要参考：\n区分任务类型：I/O密集型首选事件驱动+多进程，计算密集型可考虑多线程。 权衡开发成本：多进程模型更易实现稳定性，但需额外处理IPC；多线程开发门槛更高。 利用操作系统特性：如Linux的CPU亲和性（affinity）可优化多进程绑定。 结语 Nginx的多进程模型并非偶然，而是针对Web服务器高并发、低延迟、强稳定的核心需求做出的理性权衡。它通过资源隔离、无锁架构与事件驱动的三重设计，在I/O密集型场景中展现了无可替代的优势。正如其作者Igor Sysoev所言：“简单性是可扩展性的基石”——多进程模型正是这一哲学的最佳实践。\n参考资料：\n[1] 其其网《Nginx为何偏爱多进程模型》\n[4][6] CSDN博客《nginx为什么是多进程单线程》\n[2][3] CSDN博客《Nginx工作原理》\n","date":"2025-03-21T00:00:00Z","permalink":"/posts/web-nginx-%E7%9A%84%E5%A4%9A%E8%BF%9B%E7%A8%8B%E6%A8%A1%E5%9E%8B/","title":"Nginx 的多进程模型"},{"content":"一、反向压力（Backpressure）的核心意义 在流式计算中，数据生产者的生成速率与消费者的处理速率往往不匹配。若生产者速度远高于消费者，无限制的缓冲会导致内存溢出或系统崩溃。反向压力（Backpressure）机制通过动态调节数据流速，实现生产者与消费者的速率适配，从而保证系统的稳定性与资源可控性。\n1.1 背压的两种实现模式 阻塞式反馈：通过队列容量限制直接阻塞生产者（如线程等待）。 非阻塞式协商：通过异步信号（如请求量协商）动态调整生产者速率（Reactive Streams的核心机制）。 二、Reactive Streams规范与C++映射 Reactive Streams是异步流处理的标准化规范，定义了四个核心组件：\n组件 职责 C++类设计示例（伪代码） Publisher 数据生产者（如传感器、文件读取） class Publisher\u0026lt;T\u0026gt; { virtual void subscribe(Subscriber\u0026lt;T\u0026gt;\u0026amp;) = 0; }; Subscriber 数据消费者（如数据库写入、网络发送） class Subscriber\u0026lt;T\u0026gt; { virtual void onNext(const T\u0026amp;) = 0; }; Subscription 订阅上下文（背压协商） class Subscription { virtual void request(int n) = 0; }; Processor 中间处理节点（如数据过滤、转换） class Processor\u0026lt;T, R\u0026gt; : public Subscriber\u0026lt;T\u0026gt;, Publisher\u0026lt;R\u0026gt; {}; 2.1 C++实现的核心逻辑 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 // 基于条件变量的背压队列（简化版） template\u0026lt;typename T\u0026gt; class BoundedQueue { private: std::queue\u0026lt;T\u0026gt; buffer; std::mutex mtx; std::condition_variable not_full; std::condition_variable not_empty; size_t capacity; public: void push(const T\u0026amp; item) { std::unique_lock\u0026lt;std::mutex\u0026gt; lock(mtx); not_full.wait(lock, [this] { return buffer.size() \u0026lt; capacity; }); buffer.push(item); not_empty.notify_one(); } T pop() { std::unique_lock\u0026lt;std::mutex\u0026gt; lock(mtx); not_empty.wait(lock, [this] { return !buffer.empty(); }); T val = std::move(buffer.front()); buffer.pop(); not_full.notify_one(); return val; } }; 说明：队列满时阻塞push，空时阻塞pop，通过条件变量实现生产者-消费者的速率同步。\n三、完整流处理管道的C++实现 3.1 流处理节点设计 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 // 数据源（Publisher实现） class DataSource : public Publisher\u0026lt;int\u0026gt; { public: void subscribe(Subscriber\u0026lt;int\u0026gt;\u0026amp; sub) override { auto* subscription = new DataSubscription(sub); sub.onSubscribe(*subscription); } }; // 订阅契约（实现背压请求） class DataSubscription : public Subscription { private: Subscriber\u0026lt;int\u0026gt;\u0026amp; subscriber; std::atomic\u0026lt;bool\u0026gt; canceled{false}; public: void request(int n) override { for (int i = 0; i \u0026lt; n \u0026amp;\u0026amp; !canceled; ++i) { int data = generateData(); // 模拟数据生成 subscriber.onNext(data); } } }; // 数据处理节点（Processor实现） class TransformProcessor : public Processor\u0026lt;int, std::string\u0026gt; { public: void onNext(const int\u0026amp; data) override { std::string transformed = std::to_string(data * 2); outputQueue.push(transformed); } }; 3.2 线程池与异步调度 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 基于线程池的任务执行器 class ReactiveExecutor { private: BoundedQueue\u0026lt;std::function\u0026lt;void()\u0026gt;\u0026gt; taskQueue{1024}; std::vector\u0026lt;std::thread\u0026gt; workers; public: ReactiveExecutor(size_t threads) { for (size_t i = 0; i \u0026lt; threads; ++i) { workers.emplace_back([this] { while (true) { auto task = taskQueue.pop(); task(); } }); } } void submit(std::function\u0026lt;void()\u0026gt; task) { taskQueue.push(std::move(task)); } }; 优化点：通过有界队列实现任务提交的背压控制，防止线程池过载。\n四、性能调优与扩展 4.1 动态队列扩容策略 1 2 3 4 5 6 7 8 9 class DynamicBoundedQueue : public BoundedQueue\u0026lt;int\u0026gt; { public: void push(const int\u0026amp; item) { if (buffer.size() \u0026gt;= capacity * 0.8) { capacity *= 2; // 动态扩容 } BoundedQueue::push(item); } }; 4.2 背压指标监控 1 2 3 size_t getBackpressureLevel() const { return buffer.size() * 100 / capacity; // 返回队列占用百分比 } 五、应用场景 • 实时风控系统：防止数据洪峰导致内存溢出\n• 物联网设备：处理海量传感器数据流\n• 视频流处理：动态调整视频帧解码速率\n扩展阅读：Reactor框架设计思想 | 微服务背压实践 流计算中的反向压力模型与生产者-消费者模式\nReactive Streams背压机制解析 Reactive Streams规范与组件定义 背压的应用场景与实现策略 物联网中的流处理实践 Spring WebFlux与Reactor模型 微服务架构中的背压设计 C++线程池与异步任务调度 ","date":"2025-03-21T00:00:00Z","permalink":"/posts/web-%E6%B5%81%E8%AE%A1%E7%AE%97%E4%B8%AD%E7%9A%84%E5%8F%8D%E5%90%91%E5%8E%8B%E5%8A%9B%E6%A8%A1%E5%9E%8B%E4%B8%8E-reactive-streams--c++%E5%AE%9E%E7%8E%B0/","title":"流计算中的反向压力模型与 Reactive Streams --C++实现"},{"content":"Flutter为何能摆脱浏览器依赖？\n一、Flutter的三层架构：从操作系统到界面渲染 1. 嵌入层（Embedder）\n嵌入层是Flutter与操作系统对话的\u0026quot;翻译官\u0026quot;，负责将Flutter引擎安装到目标平台。例如在Android上，它通过Java/C++与Activity生命周期交互；在iOS上则通过Objective-C桥接UIKit事件。这一层的关键任务是创建绘图表面（Surface）并管理线程模型（如UI线程、GPU线程），为上层渲染提供稳定的运行环境。\n2. 引擎层（Engine）\n引擎层是Flutter的心脏，由C++编写，包含三大核心模块：\n• Skia图形引擎：Google开源的2D绘图库，直接操控GPU进行像素绘制，无需经过系统原生控件\n• Dart运行时：支持JIT（开发热重载）与AOT（发布高性能）双模式编译\n• 文本渲染引擎：独立处理复杂文字排版（如阿拉伯语从右向左排列）\n这些组件共同构建了跨平台的绘图能力，例如滑动列表时，Skia会将图层数据直接提交给GPU渲染管线。\n3. 框架层（Framework）\n开发者直接接触的Dart语言层，提供声明式UI组件：\n1 2 3 4 5 6 7 // 典型Flutter组件树 Scaffold( appBar: AppBar(title: Text(\u0026#39;Demo\u0026#39;)), body: ListView.builder( itemBuilder: (context, index) =\u0026gt; ListTile(title: Text(\u0026#39;Item $index\u0026#39;)) ) ) 框架层将Widget转化为渲染指令，通过深度优先遍历完成布局计算，最终生成供Skia处理的图层数据。\n二、自渲染机制 传统跨平台方案如React Native需要将JavaScript控件映射为原生组件。Flutter不同：\n1. 像素级控制\n通过Skia直接向GPU提交绘图指令，绕过了浏览器渲染流程中的HTML解析、CSS计算、合成层处理等环节。例如在实现渐变色动画时，Flutter引擎直接操作着色器，而Web方案需要处理复杂的CSS动画性能优化。\n2. 线程模型优化\n• UI线程：执行Dart代码，构建图层树\n• GPU线程：调用Skia生成GL指令\n• IO线程：异步加载资源\n三线程通过VSync信号同步，确保60FPS流畅渲染。相比之下，浏览器受限于单线程JavaScript和样式重计算，容易出现卡顿。\n3. 跨平台一致性保障\n自研渲染引擎避免了不同平台WebView的差异问题。例如在实现Material Design的波纹效果时，Android和iOS会呈现完全相同的动画细节，而传统方案需要分别适配各平台原生控件。\n三、与浏览器方案的对比 通过实际场景对比传统Web技术与Flutter的差异：\n场景 浏览器方案 Flutter方案 列表滚动 依赖DOM更新，易卡顿 图层复用，GPU直接合成 交互动画 CSS过渡可能丢帧 基于物理的动画曲线 首屏加载 需下载完整HTML/CSS/JS 预编译Dart代码快速启动 内存占用 WebView常驻内存较高 原生线程管理更高效 以电商商品列表为例，Flutter可稳定保持120FPS滚动帧率，而基于Web的方案在快速滑动时容易出现白屏。\n四、Flutter的生态演进 早期Flutter聚焦移动端。后续发展：\n• 桌面端：通过嵌入层适配Windows/macOS的窗口系统\n• Web支持：Dart编译为JavaScript，Skia通过Canvas实现绘制\n• 嵌入式：在Raspberry Pi等设备运行，验证轻量化潜力\n这印证了分层架构的前瞻性——只需扩展嵌入层，即可支持新平台。\n","date":"2025-03-18T00:00:00Z","permalink":"/posts/frontend-01flet-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0--flutter%E5%8E%9F%E7%90%86/","title":"【01】Flet 学习笔记 --Flutter原理"},{"content":"在计算机系统中，缓存是提升性能的核心技术之一。当内存资源有限时，如何高效淘汰无用数据、保留热点数据？**LRU（最近最少使用）和LFU（最不频繁使用）**算法为此提供了经典解决方案。本文将从原理到实践，详解这两种算法，并附完整C++实现代码。\n​LRU（Least Recently Used）​ 基于时间维度，淘汰最久未被访问的数据。例如，若缓存容量为3，依次访问A→B→C→A，则再次插入新数据时，最久未访问的B会被淘汰。其核心假设是：最近被访问的数据未来更可能被使用。 ​LFU（Least Frequently Used）​ 基于频率维度，淘汰访问次数最少的数据。例如，若数据A被访问5次，B被访问3次，则优先淘汰B。LFU通过计数器记录访问频次，并可能结合时间衰减机制避免旧高频数据长期占用缓存。 一、LRU算法：时间维度淘汰策略 核心原理 LRU基于“时间局部性”假设：最近被访问的数据更可能被再次使用。其淘汰策略简单直接——移除最久未访问的数据。例如，若缓存容量为3，访问顺序为A→B→C→A，则新数据插入时淘汰最旧的B。\nC++实现 LRU需高效支持两种操作：\n快速查询（哈希表，O(1)） 顺序维护（双向链表，O(1)调整顺序） 数据结构设计：\n1 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 struct Node { int key, value; Node *prev, *next; Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {} }; class LRUCache { private: int capacity; unordered_map\u0026lt;int, Node*\u0026gt; cache; // 哈希表：键到节点映射 Node *head, *tail; // 双向链表头尾哨兵节点 void moveToHead(Node* node) { // 将节点移至头部 removeNode(node); addToHead(node); } void removeNode(Node* node) { // 移除节点 node-\u0026gt;prev-\u0026gt;next = node-\u0026gt;next; node-\u0026gt;next-\u0026gt;prev = node-\u0026gt;prev; } void addToHead(Node* node) { // 头部插入节点 node-\u0026gt;next = head-\u0026gt;next; node-\u0026gt;prev = head; head-\u0026gt;next-\u0026gt;prev = node; head-\u0026gt;next = node; } public: LRUCache(int cap) : capacity(cap) { head = new Node(-1, -1); // 初始化哨兵节点 tail = new Node(-1, -1); head-\u0026gt;next = tail; tail-\u0026gt;prev = head; } int get(int key) { // 查询操作 auto it = cache.find(key); if (it == cache.end()) return -1; moveToHead(it-\u0026gt;second); // 更新为最近访问 return it-\u0026gt;second-\u0026gt;value; } void put(int key, int value) { // 插入/更新操作 if (cache.find(key) != cache.end()) { cache[key]-\u0026gt;value = value; moveToHead(cache[key]); return; } Node* newNode = new Node(key, value); cache[key] = newNode; addToHead(newNode); if (cache.size() \u0026gt; capacity) { // 触发淘汰 Node* toDelete = tail-\u0026gt;prev; cache.erase(toDelete-\u0026gt;key); removeNode(toDelete); delete toDelete; } } }; 关键点：\n• 使用哈希表+双向链表实现O(1)操作复杂度\n• 头节点存放最新访问数据，尾节点为待淘汰数据\n二、LFU算法：频率维度淘汰策略 核心原理 LFU基于“频率优先”原则：淘汰访问次数最少的数据。例如，数据A访问5次、B访问3次，则优先淘汰B。LFU需记录每个键的访问频率，并维护最小频率值。\nC++实现 LFU需维护三个核心结构：\n键到值和频率的映射 频率到键集合的映射 当前最小频率值 数据结构设计：\n1 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 class LFUCache { private: int capacity, minFreq; unordered_map\u0026lt;int, pair\u0026lt;int, int\u0026gt;\u0026gt; keyMap; // key→{value, freq} unordered_map\u0026lt;int, list\u0026lt;int\u0026gt;\u0026gt; freqMap; // freq→keys list unordered_map\u0026lt;int, list\u0026lt;int\u0026gt;::iterator\u0026gt; keyIter;// key在freqMap中的迭代器 void increaseFreq(int key) { // 增加键的频率 int oldFreq = keyMap[key].second; keyMap[key].second++; freqMap[oldFreq].erase(keyIter[key]); // 从旧频率列表移除 if (freqMap[oldFreq].empty()) { // 更新最小频率 freqMap.erase(oldFreq); if (oldFreq == minFreq) minFreq++; } freqMap[oldFreq + 1].push_front(key); // 加入新频率列表 keyIter[key] = freqMap[req + 1].begin(); } public: LFUCache(int cap) : capacity(cap), minFreq(0) {} int get(int key) { if (keyMap.find(key) == keyMap.end()) return -1; increaseFreq(key); // 更新频率 return keyMap[key].first; } void put(int key, int value) { if (capacity == 0) return; if (keyMap.find(key) != keyMap.end()) { // 已存在则更新值 keyMap[key].first = value; increaseFreq(key); return; } if (keyMap.size() \u0026gt;= capacity) { // 触发淘汰 int evictKey = freqMap[minFreq].back(); freqMap[minFreq].pop_back(); if (freqMap[minFreq].empty()) freqMap.erase(minFreq); keyMap.erase(evictKey); keyIter.erase(evictKey); } keyMap[key] = {value, 1}; // 插入新键 freqMap[1].push_front(key); keyIter[key] = freqMap[1].begin(); minFreq = 1; // 最小频率重置为1 } }; 关键点：\n• 通过三层映射实现频率统计与快速淘汰\n• 维护minFreq避免遍历所有频率值\n三、LRU与LFU对比与应用场景 维度 LRU LFU 淘汰依据 访问时间（最久未用） 访问频率（最少使用） 优点 实现简单，适应突发流量 精准捕捉长期热点数据 缺点 周期性访问易误淘汰（如扫描操作） 新数据易被淘汰（冷启动问题） 适用场景 实时榜单、用户会话管理 热门视频缓存、搜索引擎热词 实际案例：\n• 数据库缓存：MySQL的Buffer Pool使用改进版LRU（冷热数据分离）\n• 高并发系统：Redis采用近似LFU，平衡性能与内存开销\n四、总结与选型建议 • 选择LRU：若业务存在明显的时间局部性（如新闻热点），或需快速响应访问顺序变化。\n• 选择LFU：若数据访问频次差异大（如电商热门商品），且需长期保留高频数据。\n性能优化方向：\n• 分段锁减少并发竞争（如将缓存分16段）\n• 添加频率衰减机制（避免旧高频数据长期占用）\n建议根据场景调整参数（如缓存容量、锁粒度等）以获得最佳效果。\n","date":"2025-03-16T00:00:00Z","permalink":"/posts/web-%E8%A7%A3%E6%9E%90lru%E4%B8%8Elfu%E7%AE%97%E6%B3%95%E5%8F%8Ac++%E5%AE%9E%E7%8E%B0/","title":"解析LRU与LFU算法及C++实现"},{"content":"为什么需要限制对象的创建位置？ 例如一个需要手动控制生命周期的数据库连接池，不希望随便在栈上创建一个然后自动销毁。又或者写了一个轻量级的临时计算工具类，如果每次都在堆上创建，性能反而会下降。\n• ✅ 明确生命周期管理 • ✅ 避免资源泄漏 • ✅ 提升关键路径性能 • ✅ 强制使用最佳实践\n一、Heap Only：必须用new创建 方法1：私有化析构函数 1 2 3 4 5 6 7 8 9 10 11 12 class HeapOnly { public: static HeapOnly* Create() { return new HeapOnly(); // 工厂方法 } void Suicide() { delete this; } // 起个中二的名字提醒要手动释放 private: ~HeapOnly() {} // 关键！栈对象无法自动调用 HeapOnly() {} // 私有构造 }; 原理：栈对象在离开作用域时会自动调用析构函数，如果析构是私有的，编译器直接报错。必须通过new创建，手动调用释放。\n方法2：C++11，用= delete 1 2 3 4 5 6 7 8 9 10 11 class HeapOnly { public: static HeapOnly* Create() { return new HeapOnly; } // 直接禁用拷贝构造和赋值 HeapOnly(const HeapOnly\u0026amp;) = delete; HeapOnly\u0026amp; operator=(const HeapOnly\u0026amp;) = delete; private: HeapOnly() = default; }; 应用场景 单例模式（比如全局配置管理器） 需要多态的对象（比如动物基类派生出猫狗子类） 重量级资源（比如线程池、网络连接池） 延迟初始化的对象（按需创建） eg. 游戏引擎中的资源管理器，所有贴图、模型都通过ResourceManager::LoadTexture()这类工厂方法创建，确保统一管理。\n二、Stack Only：禁止new出来的对象 方法1：删除new运算符 1 2 3 4 5 6 7 8 9 10 11 class StackOnly { public: static StackOnly Create() { return StackOnly(); } // 重点在这两行！ void* operator new(size_t) = delete; void operator delete(void*) = delete; private: StackOnly() = default; }; 效果：new StackOnly()，编译器直接报错：\u0026ldquo;尝试引用已删除的函数\u0026rdquo;。\n方法2：RAII 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class FileHandler { public: FileHandler(const char* path) { file = fopen(path, \u0026#34;r\u0026#34;); } ~FileHandler() { if(file) fclose(file); } // 禁用堆分配 void* operator new(size_t) = delete; private: FILE* file; }; 实际意义：用的时候直接在栈上创建，离开作用域自动关闭文件。\n应用场景 RAII资源管理（锁、文件句柄、智能指针） 轻量临时对象（比如3D向量、矩阵运算） 高频创建销毁的小对象（比如游戏中的粒子效果） 保证线程安全的对象（栈对象不会跨线程共享） eg. 多线程中的std::lock_guard，必须直接在栈上创建才能确保锁的自动释放，防止死锁。\n三、使用场景 考虑因素 选堆对象 选栈对象 生命周期 需要长期存在或跨作用域 随用随毁，自动清理 对象大小 大型对象（比如超过1MB） 小型对象（建议不超过几十KB） 性能要求 对内存分配速度不敏感 高频创建/销毁时性能敏感 多态需求 需要基类指针操作不同子类 通常不需要 资源安全 需要手动管理 依赖RAII自动管理 不确定该用哪个时，优先考虑栈对象（更安全）。\n","date":"2025-03-16T00:00:00Z","permalink":"/posts/cpp-heap-only-%E5%92%8C-stack-only-%E7%9A%84-c++-%E5%AF%B9%E8%B1%A1/","title":"如何限制C++对象只能在堆或栈上创建？heap only 和 stack only"},{"content":" // TODO\n","date":"2025-03-10T00:00:00Z","permalink":"/posts/cpp-%E4%BA%8B%E5%8A%A1%E6%80%A7%E5%86%85%E5%AD%98/","title":""},{"content":"一、_start与__libc_start_call_main的作用 _start：程序的入口点\n_start是Linux环境下C/C++程序的实际入口函数，由链接器自动添加到可执行文件中，负责初始化运行时环境并调用__libc_start_main。\n它的核心任务包括：\n• 设置栈指针（%ebp清零）、传递参数（如argc和argv）到寄存器。\n• 加载全局初始化函数（如__libc_csu_init）和清理函数（如__libc_csu_fini）。\n• 调用__libc_start_main，并将main函数地址作为参数传递。\n__libc_start_call_main：非托管入口的桥梁\n该函数位于libc.so中，是__libc_start_main内部调用的关键步骤，负责直接触发非托管main函数的执行（例如C++中的全局构造函数完成后，最终调用用户编写的main函数）。在Linux下，它与__libc_start_main_impl共同完成用户态到程序主逻辑的过渡。\n二、C++程序在main函数前的执行流程 操作系统加载与内存分配\n• 可执行文件被加载到内存，操作系统分配栈、堆空间，并初始化.data（已初始化全局变量）和.bss（未初始化全局变量）段。\n全局变量与静态对象的初始化\n• .data段变量：直接赋初值（如float global_float = 3.14）。\n• .bss段变量：数值类型初始化为0，指针初始化为NULL。\n• 全局对象构造函数：在main前按定义顺序调用（例如AnotherClass another_global_object的构造函数）。\n运行时库的初始化\n• C++运行时库（如libstdc++）执行初始化，包括堆管理、异常处理框架等。\n• 静态成员变量的初始化（如AnotherClass::static_double = 2.718）。\n参数传递与入口跳转\n• _start通过__libc_start_main将argc、argv和envp传递给main函数，最终通过__libc_start_call_main触发main的执行。\n三、关键差异与注意事项 与Windows的对比\n• Linux：入口链为_start → __libc_start_main → __libc_start_call_main → main。\n• Windows：入口函数为RtlUserThreadStart（ntdll.dll），非托管入口通过BaseThreadInitThunk（kernel32.dll）调用。\n初始化顺序的潜在问题\n若全局对象之间存在依赖（如A依赖B），需通过编译单元顺序控制或__attribute__((init_priority))（GCC扩展）强制指定初始化顺序，避免未定义行为。\n总结 C++程序的启动过程远不止main函数的执行，其核心在于操作系统和运行时库的协作初始化。理解_start与__libc_start_call_main的作用，以及全局对象的构造顺序，对于调试启动崩溃、优化资源初始化至关重要。例如，若程序在main前崩溃，需优先排查全局对象的构造函数或静态变量初始化逻辑。\n","date":"2025-03-10T00:00:00Z","permalink":"/posts/cpp-c++%E5%85%AB%E8%82%A1main%E5%87%BD%E6%95%B0%E4%B9%8B%E5%89%8D%E6%89%A7%E8%A1%8C%E4%BA%86%E4%BB%80%E4%B9%88/","title":"【AI】C++八股：main函数之前执行了什么？"},{"content":"在后端开发中，半衰期算法常用于动态调整数据权重的场景，其核心是通过时间衰减机制平衡实时性与历史价值。以下是其典型应用及实现逻辑：\n一、算法原理与公式 半衰期算法基于放射性衰变公式：M * (1/2)^(t/T)，其中： • M：初始值（如点击量） • t：时间间隔（如天数） • T：半衰期周期（如7天） 该公式使数据权重随时间呈指数衰减，例如7天半衰期意味着权重每天减少约10%。\n二、核心应用场景 热搜排序（如微博、腾讯平台） • 动态平衡点击量与时间衰减：新事件点击量高但衰减快，旧事件点击量低但衰减慢。例如： ◦ 电影类半衰期设为7天（T=7），初始权重1000，单日点击量20000时，2天后权重为 (1000+20000) * (1/2)^(2/7) * 0.8 ≈ 17474.56。 ◦ 小说类半衰期15天（T=15），汽车类30天（T=30），体现不同内容时效性差异。\n推荐系统冷启动 • 新内容通过初始权重（如1000）获得曝光机会，同时随时间自然衰减，避免长期占据推荐位。\n动态排行榜 • 结合实时数据与历史表现：例如游戏活动榜单，近期活跃玩家通过半衰期快速提升排名，老玩家贡献逐步降低。\n三、后端实现要点 数据模型设计 • 数据库需存储原始点击量和计算后的排序权重字段（如sort_info），通过定时任务（如每小时）更新权重值。\n参数可配置化 • 不同业务类型设置独立参数：\n1 2 3 4 class filmHalfLife { private static T = 7; //半衰期周期 private static weight = 0.8; //类型权重 } 通过接口halfLifeFactory实现多态，支持扩展新类型。\n防作弊机制 • 引入类型权重系数（如电影0.8、汽车0.85），降低刷量对排序的影响。 • 结合IP频率限制、异常点击检测等补充措施。\n四、扩展优化方向 • 时间粒度细化：将天级计算改为小时级，适应高实时性场景（如突发新闻）。 • 复合衰减策略：叠加多个半衰期公式，处理复杂业务逻辑（如短视频热度需同时考虑播放、点赞、分享）。 • 动态调整半衰期：通过机器学习根据历史数据自动优化T值。\n该算法已在实际工程中验证可行性，例如某博客示例中，电影类内容在7天内权重从1000衰减至约400，而汽车类内容30天后仍保留约300权重。开发者可根据业务需求调整公式参数，平衡时效性与长尾效应。\n","date":"2025-03-10T00:00:00Z","permalink":"/posts/web-%E5%8D%8A%E8%A1%B0%E6%9C%9F%E7%AE%97%E6%B3%95%E5%9C%A8%E5%90%8E%E7%AB%AF%E7%9A%84%E5%BA%94%E7%94%A8/","title":"【AI】半衰期算法在后端的应用"},{"content":"核心在于通过状态管理和锁的组合来模拟读写锁的“读共享、写独占”特性。以下是实现思路和具体方法：\n一、实现原理 读写锁的核心规则是：\n读读共享：允许多个读线程并发访问。 读写互斥：读线程和写线程不能同时访问。 写写互斥：同一时间只能有一个写线程访问。 使用普通互斥锁（std::mutex）和计数器可以实现这一逻辑：\n• 读计数器：统计当前活跃的读线程数量。\n• 写互斥锁：确保写操作的独占性。\n• 状态保护锁：保护读计数器和写锁状态的原子性。\n二、实现步骤 1. 定义关键成员变量 1 2 3 4 5 6 7 8 9 10 11 #include \u0026lt;mutex\u0026gt; #include \u0026lt;condition_variable\u0026gt; class ReadWriteLock { private: std::mutex counter_mutex; // 保护读计数器和写标志 std::mutex write_mutex; // 写操作的独占锁 int reader_count = 0; // 当前活跃的读线程数量 bool write_pending = false; // 是否有写线程在等待 std::condition_variable read_cv, write_cv; }; 2. 读锁的获取与释放 • 获取读锁：\n当无写线程运行时，允许读线程进入；若存在写线程等待，则阻塞新读线程（避免写饥饿）。\n1 2 3 4 5 6 7 8 9 void read_lock() { std::unique_lock\u0026lt;std::mutex\u0026gt; lock(counter_mutex); // 等待直到没有写线程在等待或运行 read_cv.wait(lock, [this] { return !write_pending; }); reader_count++; if (reader_count == 1) { write_mutex.lock(); // 第一个读线程获取写锁，阻止写操作 } } • 释放读锁：\n减少读计数器，若最后一个读线程退出，则释放写锁并通知可能的等待写线程。\n1 2 3 4 5 6 7 8 void read_unlock() { std::unique_lock\u0026lt;std::mutex\u0026gt; lock(counter_mutex); reader_count--; if (reader_count == 0) { write_mutex.unlock(); write_cv.notify_one(); // 通知等待的写线程 } } 3. 写锁的获取与释放 • 获取写锁：\n设置写等待标志，等待所有读线程退出后获取写锁。\n1 2 3 4 5 6 void write_lock() { std::unique_lock\u0026lt;std::mutex\u0026gt; lock(counter_mutex); write_pending = true; // 标记有写线程等待 write_cv.wait(lock, [this] { return reader_count == 0; }); // 等待读线程退出 write_mutex.lock(); // 获取写锁 } • 释放写锁：\n释放写锁并重置写等待标志，唤醒可能的读或写线程。\n1 2 3 4 5 6 7 8 void write_unlock() { { std::unique_lock\u0026lt;std::mutex\u0026gt; lock(counter_mutex); write_pending = false; } write_mutex.unlock(); read_cv.notify_all(); // 唤醒等待的读线程 } 三、关键点与注意事项 避免写线程饥饿：\n通过write_pending标志，确保在有写线程等待时，新读线程会被阻塞。 原子性保护：\n所有对reader_count和write_pending的修改必须通过counter_mutex保护。 条件变量的使用：\nread_cv和write_cv用于协调读/写线程的状态切换，避免忙等待。 锁的粒度：\n写操作通过write_mutex实现独占，读操作通过共享计数器实现并发。 四、潜在问题与优化 • 性能问题：与标准库的std::shared_mutex相比，此实现可能因频繁锁竞争导致性能下降。\n• 死锁风险：需确保锁的获取顺序一致（如先counter_mutex再write_mutex）。\n• 扩展性：可引入优先级策略（如写优先）来优化公平性。\n五、面试回答示例 “可以通过两个互斥锁和一个读计数器实现读写锁：\n写锁：使用一个互斥锁（write_mutex）保证写操作的独占性。 读计数器：统计活跃读线程数量，第一个读线程获取写锁，最后一个释放。 状态协调：通过条件变量和标志位避免写线程饥饿，例如在有写等待时阻塞新读线程。” 这一实现体现了对互斥锁组合使用和线程同步机制的理解，适合在面试中展示底层设计能力。\n","date":"2025-03-10T00:00:00Z","permalink":"/posts/cpp-%E4%BD%BF%E7%94%A8%E6%99%AE%E9%80%9A%E7%9A%84%E4%BA%92%E6%96%A5%E9%94%81%E5%AE%9E%E7%8E%B0%E8%AF%BB%E5%86%99%E9%94%81/","title":"【AI】使用普通的互斥锁实现读写锁"},{"content":"mmap（Memory Mapping）是Unix/Linux系统中的一种重要机制，它允许将文件或设备直接映射到进程的虚拟地址空间，从而将文件操作与内存操作高效结合。以下从核心机制、与IO的关系、与内存分配的关系三个方面详细解析：\n一、mmap的核心机制 系统调用与映射方式： • 函数原型：void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); • 关键参数： ◦ prot：控制读写权限（如PROT_READ、PROT_WRITE）。 ◦ flags：决定映射类型（MAP_SHARED、MAP_PRIVATE、MAP_ANONYMOUS）。 • 两种主要映射： ◦ 文件映射：将文件映射到内存，修改可同步到文件（MAP_SHARED）或仅进程可见（MAP_PRIVATE）。 ◦ 匿名映射：不关联文件，用于进程间共享内存或动态内存分配（MAP_ANONYMOUS）。\n实现原理： • 虚拟内存管理：mmap在进程的虚拟地址空间中划分一段区域（通常位于堆与栈之间），通过页表映射到物理内存或文件的页缓存。 • 按需加载（Demand Paging）：访问映射内存时触发缺页中断，内核自动将文件数据加载到物理内存，减少一次性加载开销。 • 同步机制：修改后的数据由内核异步写回文件，也可通过msync()强制同步。\n二、mmap与IO的关系 传统IO的瓶颈： • 数据拷贝开销：read()/write()需要在内核缓冲区（页缓存）与用户空间之间复制数据，频繁系统调用和拷贝降低性能。 • 小文件问题：多次系统调用对小文件不友好，增加上下文切换开销。\nmmap的优势： • 零拷贝（Zero-Copy）：直接操作映射内存，省去用户态与内核态的数据拷贝。 • 减少系统调用：通过内存访问隐式完成文件读写，无需显式调用read()/write()。 • 高效大文件处理：按需加载，避免一次性加载大文件的延迟和内存浪费。\n性能对比： • 顺序访问：mmap与read()性能接近，但省去拷贝时间。 • 随机访问：mmap显著优于传统IO，减少多次lseek()和read()的开销。 • 适用场景：适合频繁读写或需要随机访问的大文件（如数据库、图像处理）。\n三、mmap与内存分配的关系 动态内存分配： • glibc的malloc策略： ◦ 小块内存（如\u0026lt;128KB）使用brk()扩展堆内存。 ◦ 大块内存使用mmap(MAP_ANONYMOUS)独立映射，避免内存碎片。 • 优势：mmap分配的内存可独立释放（munmap()），而brk()释放需依赖堆顶内存释放顺序。\n匿名映射的应用： • 进程间共享内存：通过MAP_SHARED标志，多个进程可共享同一物理内存，高效通信。 • 自定义内存管理：替代malloc，用于需要精细控制的大内存分配（如内存池）。\n与传统堆内存的对比：\n特性 mmap分配 brk()分配 内存来源 独立虚拟内存区域 进程堆区 释放方式 立即归还系统（munmap） 依赖堆顶收缩，易产生碎片 适用场景 大块内存、共享内存 小块内存、频繁分配释放 四、总结与扩展 • 核心价值：mmap通过内存映射机制，统一了文件IO与内存操作，同时优化了动态内存分配策略。 • 与页缓存的关系：mmap直接操作内核页缓存，而传统IO需显式读写页缓存，因此mmap在频繁访问时更高效。 • 注意事项： • 32位系统限制：虚拟地址空间有限，映射大文件可能失败。 • 文件截断问题：文件被外部修改时需处理SIGBUS信号。 • 延迟加载风险：首次访问可能因缺页中断引入延迟。\n应用场景示例： • 数据库系统：使用mmap加速数据文件的随机访问。 • 进程间通信：通过匿名共享内存传递大量数据。 • 动态库加载：系统通过mmap将共享库映射到多个进程，节省内存。\nmmap通过将文件、内存和进程虚拟地址空间紧密结合，成为高性能IO和灵活内存管理的基石。\n零拷贝 在Linux系统中，零拷贝（Zero-Copy）是一种优化数据传输效率的核心技术，旨在减少或消除数据在内核空间与用户空间之间的冗余拷贝操作。以下是针对该问题的结构化回答：\n1. 零拷贝的核心概念 零拷贝通过避免数据在内存中的多次复制，降低CPU和内存带宽的消耗，尤其适用于高吞吐量场景（如文件传输、网络通信）。其核心目标包括： • 减少CPU拷贝次数：利用DMA（直接内存访问）等技术，让硬件直接传输数据。 • 减少上下文切换：通过内核态与用户态的协作优化系统调用次数。 • 最大化内存利用率：直接操作内核缓冲区或共享内存区域。\n2. 传统IO的瓶颈 以读取文件并通过网络发送为例，传统流程涉及多次数据拷贝和上下文切换：\n磁盘到内核缓冲区：DMA将文件数据从磁盘拷贝到内核的页缓存（Page Cache）。 内核到用户空间：read()系统调用将数据从页缓存拷贝到用户空间缓冲区（CPU参与）。 用户空间到Socket缓冲区：write()系统调用将数据从用户空间拷贝到内核的Socket缓冲区（CPU参与）。 Socket缓冲区到网卡：DMA将数据从Socket缓冲区发送到网卡。 问题：共4次拷贝（2次DMA，2次CPU拷贝），2次系统调用（read + write）。\n3. Linux零拷贝的实现方式 **(1) **mmap + write • 原理：通过内存映射（mmap）将文件映射到用户空间，直接操作内核缓冲区，避免用户空间拷贝。\n1 2 void *addr = mmap(file_fd, ...); write(socket_fd, addr, file_size); • 优化：减少1次CPU拷贝（用户空间到内核的拷贝）。 • 剩余拷贝：3次拷贝（2次DMA，1次CPU拷贝）。 • 适用场景：需要频繁读写文件内容（如数据库）。\n(2) sendfile • 原理：通过sendfile系统调用直接在文件描述符和Socket之间传输数据，完全在内核态完成。\n1 sendfile(socket_fd, file_fd, NULL, file_size); • 优化：消除用户空间参与，减少2次上下文切换，2次拷贝（仅1次CPU拷贝）。 • 剩余拷贝：3次拷贝（2次DMA，1次CPU拷贝）。 • 增强版（Linux 2.4+）：支持SG-DMA（Scatter-Gather DMA），直接从页缓存到网卡，仅需2次DMA拷贝，完全消除CPU拷贝。 • 适用场景：静态文件传输（如Nginx发送大文件）。\n(3) splice • 原理：通过管道（Pipe）在内核中移动数据，无需用户空间参与。\n1 2 splice(file_fd, NULL, pipe_fd, NULL, file_size, SPLICE_F_MOVE); splice(pipe_fd, NULL, socket_fd, NULL, file_size, SPLICE_F_MOVE); • 优化：类似sendfile，但支持任意文件描述符（包括管道）。 • 剩余拷贝：2次DMA拷贝（SG-DMA支持时）。 • 适用场景：非文件到网络的数据传输（如进程间数据转发）。\n(4) 直接IO（O_DIRECT） • 原理：绕过页缓存，直接从用户空间缓冲区读写磁盘（需硬件对齐）。\n1 open(file_path, O_RDWR | O_DIRECT); • 优化：避免页缓存拷贝，但需应用自行管理缓存。 • 适用场景：自缓存应用（如某些数据库）。\n4. 零拷贝的对比与选择 技术 CPU拷贝次数 上下文切换 适用场景 传统read/write 2 4（read+write） 通用，但性能差 mmap + write 1 4 需修改文件内容 sendfile 0（SG-DMA） 2 文件到网络的单向传输（如Nginx） splice 0（SG-DMA） 2 任意描述符间传输（需管道支持） O_DIRECT 0 4 自管理缓存的专用场景 5. 零拷贝的实际应用 • Nginx：使用sendfile加速静态文件传输。 • Kafka：通过sendfile高效传输日志文件。 • 数据库：结合mmap或O_DIRECT优化磁盘IO。 • 虚拟化：VMware/VirtIO使用零拷贝减少虚拟机间数据传输开销。\n6. 注意事项与局限性 • 硬件依赖：SG-DMA需要网卡支持分散-聚集操作。 • 数据修改：零拷贝技术通常适用于只读或无需修改数据的场景。 • 小文件：零拷贝的优化效果在大文件中更显著，小文件可能因系统调用开销掩盖优势。 • 兼容性：sendfile在传输带数据头的内容时需结合其他技术（如writev）。\n7. 扩展问题准备 • DMA的作用：允许外设直接访问内存，减少CPU负担。 • Page Cache的影响：零拷贝依赖内核缓冲区管理，频繁写入可能导致缓存膨胀。 • 与内存映射的关系：mmap是零拷贝的基石，但并非所有零拷贝都依赖内存映射。\n总结回答示例：\n“零拷贝技术通过减少数据在内核与用户空间之间的冗余拷贝，显著提升IO性能。Linux中主要通过mmap、sendfile和splice等系统调用实现。例如，sendfile在内核态直接将文件数据从页缓存发送到网卡，避免了用户空间的参与，适合静态文件传输。选择时需要结合场景：sendfile适合单向传输，mmap适合需要读写文件内容，而splice更灵活但依赖管道。实际应用中需注意硬件支持和数据特性。”\nmmap 和 DMA 的关系 mmap 不是 DMA 的一种，它们是两个完全不同的技术，但可以协同工作以提高系统性能。以下是详细对比和解释：\n1. 核心概念对比 特性 mmap DMA（直接内存访问） 定义 内存映射技术，将文件或设备映射到进程的虚拟地址空间 硬件技术，允许外设直接访问内存，无需CPU参与 作用层级 操作系统/软件层（内存管理、文件I/O优化） 硬件/驱动层（数据传输优化） 主要目的 减少用户态与内核态的数据拷贝 减少CPU在数据传输中的负担 依赖关系 依赖操作系统内存管理机制（如页表、缺页中断） 依赖硬件支持（如DMA控制器、设备兼容性） 2. 技术原理差异 mmap 的工作流程 映射文件到内存：调用 mmap 后，文件被映射到进程的虚拟地址空间。 按需加载数据：访问内存时触发缺页中断，内核将文件内容从磁盘加载到物理内存（可能通过DMA）。 直接操作内存：应用程序通过指针读写内存，无需调用 read()/write()。 同步数据：修改后的数据由内核异步写回磁盘（或通过 msync() 强制同步）。 DMA 的工作流程 CPU 初始化传输：CPU 设置DMA传输参数（源地址、目标地址、数据大小）。 DMA 接管数据传输：DMA控制器直接在外设（如磁盘）和内存之间搬运数据。 传输完成中断：DMA完成后，通过中断通知CPU。 CPU 处理后续逻辑：如更新状态、唤醒等待的进程。 3. 协同工作场景 虽然 mmap 和 DMA 是独立的技术，但它们可以在某些场景下配合使用：\n示例：通过 mmap 读取文件 mmap 映射文件：文件被映射到用户空间，但物理内存中可能尚未加载数据。 首次访问触发缺页中断：内核调用磁盘驱动，使用 DMA 将文件数据从磁盘读取到物理内存。 后续访问直接操作内存：无需CPU参与数据拷贝，直接读写内存即可。 优势：\n• 减少CPU拷贝：DMA负责磁盘到内存的传输，mmap避免用户态与内核态的数据复制。 • 零拷贝优化：在文件处理中，mmap + DMA的组合是零拷贝技术的一部分。\n4. 常见误解澄清 误解1：“mmap直接使用DMA传输数据” • 真相：mmap本身不控制数据传输方式，数据加载到内存的具体过程（是否用DMA）由内核和驱动决定。DMA的调用是透明的，对mmap不可见。\n误解2：“DMA只能在mmap中使用” • 真相：DMA广泛用于所有IO场景，包括传统read()/write()。例如： • read()：磁盘 → 内核缓冲区（DMA） → 用户缓冲区（CPU拷贝）。 • mmap：磁盘 → 内核缓冲区（DMA） → 用户直接访问（无需CPU拷贝）。\n5. 总结 • mmap ≠ DMA：mmap是软件层的内存映射技术，DMA是硬件层的数据传输技术。 • 协同关系：在mmap访问文件时，DMA可能被内核用于磁盘到内存的数据传输，但这是内核的底层优化，与mmap无直接关联。 • 性能优化：两者结合可实现零拷贝（如mmap + write），但DMA的参与是隐式的，由操作系统自动管理。\n回答示例：\n“mmap和DMA是不同层级的技术。mmap是操作系统提供的内存映射机制，用于让应用程序直接访问文件数据，减少数据拷贝；而DMA是硬件功能，允许外设直接读写内存，无需CPU参与。虽然mmap访问文件时，数据加载到内存的过程可能由DMA完成，但mmap本身并不等同于DMA，它们是互补关系，共同实现高效IO。”\n","date":"2025-03-07T00:00:00Z","permalink":"/posts/linux-mmap-%E5%92%8C%E9%9B%B6%E6%8B%B7%E8%B4%9D/","title":"【AI】mmap 和零拷贝"},{"content":"一、线程切换 vs 进程切换\n地址空间与页表\n进程拥有独立的虚拟地址空间和页表，切换进程时需更新页表并刷新 TLB（地址转换缓存），导致内存访问速度下降。而线程共享进程的地址空间和页表，切换时无需此操作，TLB 缓存保持有效。\n上下文保存的内容\n• 进程切换：需保存完整的上下文，包括寄存器、程序计数器、栈指针、内存映射、文件描述符等。\n• 线程切换：仅需保存线程私有的寄存器、栈和程序计数器，共享资源（如代码段、文件）无需处理。\n缓存利用率\n进程切换会导致 CPU 缓存（如 L1/L2/L3）失效，需重新加载数据，降低性能。线程切换时，缓存因共享地址空间仍有效，减少了数据重载的开销。\n资源分配\n进程是资源分配的基本单位（如内存、文件），切换时需重新分配资源；线程共享进程资源，切换仅涉及执行流调度。\n二、协程切换 vs 线程切换\n协程的切换开销更小，原因在于其 用户态调度 和 轻量级设计：\n用户态调度\n协程切换完全由用户态代码控制，无需陷入内核态，避免了 用户态-内核态切换 的开销。而线程切换需操作系统介入，涉及模式切换和内核调度。\n上下文信息更少\n协程只需保存少量寄存器（如 PC、SP）和栈指针，且栈空间通常仅需 KB 级别（线程栈为 MB 级别）。例如，Go 协程的初始栈仅 2KB，而 Java 线程默认为 1MB。\n非阻塞与协作式调度\n协程通过主动让出（如 yield 或 await）实现协作式调度，减少抢占式调度的竞争和锁需求。线程通常依赖操作系统的抢占式调度，可能因频繁切换导致性能损耗。\n内存与并发效率\n单线程可运行数万协程（如 Go 的 Goroutine），而同等数量线程会因内存和调度开销过大而崩溃。协程的轻量级特性尤其适合高并发 I/O 密集型任务。\n维度 进程切换 线程切换 协程切换 地址空间 切换（独立） 不切换（共享） 不切换（共享） 上下文大小 大（含全部资源） 较小（仅寄存器） 极小（仅关键寄存器） 调度模式 内核抢占式 内核抢占式 用户协作式 内存开销 高（独立资源） 中（共享资源） 极低（KB 级栈） 线程通过共享资源减少开销，协程通过用户态轻量级调度进一步优化，两者均通过减少内核参与和资源复用来提升性能\u0026gt;。\n","date":"2025-03-07T00:00:00Z","permalink":"/posts/linux-%E8%BF%9B%E7%A8%8B%E7%BA%BF%E7%A8%8B%E5%8D%8F%E7%A8%8B%E7%9A%84%E8%B5%84%E6%BA%90%E6%B6%88%E8%80%97%E7%AE%80%E8%BF%B0/","title":"进程、线程、协程的资源消耗简述"},{"content":"线程的管理和调度涉及用户态与内核态的协作，不同编程语言和操作系统对线程的处理方式也有所差异。\n一、线程概念的双重性：用户态与内核态的交织 用户态线程（用户级线程）\n用户态线程由用户空间的线程库直接管理，内核对其无感知。这类线程的创建、调度、同步等操作完全在用户空间完成，无需内核介入。\n存在明显局限性：\n• 阻塞问题：若一个用户态线程因系统调用阻塞（如I/O操作），整个进程的所有线程都会被阻塞； • 多核利用率低：内核无法将用户态线程调度到多个CPU核心上运行。\n内核态线程（内核级线程）\n内核直接管理线程的创建、调度和销毁，每个线程对应一个内核线程（如Linux的轻量级进程LWP）。\n轻量级进程（Light Weight Process, LWP）作为用户线程与内核线程的桥梁，负责处理系统调用、资源分配和CPU映射。当用户线程发起系统调用时，LWP接管请求，避免因单个用户线程阻塞导致整个进程挂起。\n• 多核并行：内核可将不同线程分配到多个CPU核心； • 独立阻塞：单个线程阻塞不会影响其他线程； • 切换开销大：线程切换需通过内核态，涉及用户栈和内核栈的切换、寄存器保存与恢复等操作。\n二、内核如何处理线程？ 线程模型的映射关系\n现代操作系统（如Linux）通常采用混合型线程模型。内核通过以下机制管理线程：\n• TCB（线程控制块）：存储线程的内核栈指针、状态、优先级等信息（进程控制块是PCB）； • 调度器：基于时间片轮转或优先级策略分配CPU资源，触发上下文切换。\n混合型线程模型（Hybrid Thread Model）是一种结合用户级线程（ULT）和内核级线程（KLT）优势的线程实现方式。通过N:M映射实现用户线程与内核线程的关联，即多个用户线程（N）动态绑定到少量内核线程（M）。\n用户态与内核态的切换机制\n当线程执行系统调用、发生异常或中断时，会触发用户态到内核态的切换：\n• 系统调用流程：用户线程通过中断（如Linux的int 80h）进入内核态，内核完成操作后恢复用户态执行； • 上下文保存：切换时需要保存用户栈的寄存器状态（如程序计数器、栈指针）到内存，并加载内核栈信息。\n性能优化策略\n内核通过以下方式减少切换开销：\n• 避免频繁切换：采用无锁编程、CAS算法等减少线程竞争； • 轻量级进程（LWP）：通过线程池复用内核线程，降低创建销毁成本。\n三、语言案例 Java线程在JDK1.2之后采用1:1模型，每个Java线程对应一个内核线程。 支持多核并行、避免单线程阻塞影响整体进程；线程创建和切换需要内核介入。\nC++11及之后的标准库（如\u0026lt;thread\u0026gt;）通过std::thread直接调用操作系统线程（如Linux的POSIX线程或Windows线程），采用1:1线程模型。\nGo未直接使用操作系统线程，而是通过Goroutine实现并发。Goroutine由Go运行时调度，采用M:N线程模型​（多个Goroutine映射到少量内核线程），由运行时动态分配CPU时间片。\n","date":"2025-03-07T00:00:00Z","permalink":"/posts/linux-%E5%86%85%E6%A0%B8%E7%9A%84%E7%94%A8%E6%88%B7%E6%80%81%E5%92%8C%E5%86%85%E6%A0%B8%E6%80%81/","title":"内核的用户态和内核态"},{"content":" Linux上有个二进制程序一直在运行，修改代码后重新编译把原来的二进制程序覆盖了，会怎么样？ 该问题来自一道天美后台开发面试题：天美一面 后台开发（凉） - 牛客面经的文章 - 知乎。此处尝试进行回答。\n第一想法一般是：“原程序被操作系统加载进内存，不会受到影响。”\n系统会创建一个新的inode指向新文件，而正在运行的进程仍会继续使用旧的inode对应的代码段。\n但实际上拓展到一个问题：“二进制文件会全部加载到内存吗？”\nELF二进制文件在加载时，操作系统通常采用按需分页的机制，只将当前需要的部分加载到物理内存，而不是一次性加载整个文件。虚拟内存映射允许文件的部分内容驻留在磁盘，直到被访问时才调入内存。\n同时，动态链接库的延迟加载和内存映射文件技术（mmap）也帮助减少实际内存占用。因此，如果二进制文件很大，不会全部加载到物理内存中，而是按需加载，利用虚拟内存管理技术优化资源使用。\n但是：真的会在运行时加载新的内容吗？\nELF文件在启动时如何决定哪些内容加载到内存？主要依赖于其程序头表（Program Header Table）​中定义的段（Segment）信息。程序头表由多个Elf64_Phdr结构体组成，每个结构体描述了一个需要加载到内存的段（如代码段、数据段、动态链接信息段等）。这些段通常包含多个节（Section）的集合。\n覆盖原文件后，旧文件的磁盘空间不会被立即释放，需等待所有关联进程结束后才能回收（通过lsof可查看占用进程）。\n总结：\n​已运行的进程不受影响：Linux通过inode标识文件，旧进程继续执行内存中已加载的旧代码，与原磁盘文件解耦。\n1. ELF文件的按需加载机制 ELF二进制文件通过程序头表（Program Header Table）中的PT_LOAD段描述需要加载的代码和数据区域。内核的load_elf_binary()函数会将这些段映射到进程的虚拟地址空间，但实际物理内存的占用是按需分页的： • 仅当程序访问某个页（通常4KB大小）时，才会触发缺页异常，将对应内容从磁盘加载到物理内存。 • 未使用的代码或数据（如未执行的函数）可能永远不会被加载到物理内存中。\n2. 虚拟内存映射与内存优化 • 虚拟内存优势：ELF文件通过mmap()系统调用映射到虚拟地址空间，此时文件内容并不直接占用物理内存，而是由内核通过页表管理。 • 写时复制（Copy-on-Write）：对于只读段（如代码段），多个进程可以共享同一物理内存页；对于可写段，修改时才会复制新页。\n3. 动态链接与延迟加载 动态链接库（如.so文件）在程序运行时通过ld-linux动态加载器按需载入。例如： • 首次调用某个库函数时，动态链接器才会加载对应的代码段到内存。 • 部分库可能仅在特定条件下被使用，从而减少初始内存占用。\n4. 大文件的实际内存占用 • 物理内存限制：若二进制文件极大（如8GB），但程序实际执行的代码路径有限，物理内存占用可能远小于文件大小。 • 交换空间（Swap）：当物理内存不足时，操作系统会将不活跃的内存页交换到磁盘，腾出空间供当前进程使用。\n","date":"2025-03-06T00:00:00Z","permalink":"/posts/linux-%E5%9C%A8%E8%BF%90%E8%A1%8C%E7%9A%84%E6%97%B6%E5%80%99%E4%BF%AE%E6%94%B9%E5%B9%B6%E4%B8%94%E8%A6%86%E7%9B%96%E8%AF%A5%E4%BA%8C%E8%BF%9B%E5%88%B6%E6%96%87%E4%BB%B6%E4%BC%9A%E5%A6%82%E4%BD%95/","title":"在运行的时候，修改并且覆盖该二进制文件会如何？"},{"content":"三种经典算法：小顶堆、红黑树、时间轮。\nLinux内核多采用时间轮处理中断定时器，而Nginx使用红黑树管理定时事件。\nRedis:\tusUntilEarliestTimer() Nginx: ngx_event_find_timer() 红黑树 Skynet: Netty: 时间轮 Libevent: 最小堆 Linux: 时间轮 算法 插入 删除 触发效率 适用场景 小顶堆 O(log n) O(n) 高（仅处理堆顶） 任务量大，无需频繁取消非堆顶任务 红黑树 O(log n) O(log n) 中（遍历有序数据） 需动态增删改任务 时间轮 O(1) O(1) 高（批量处理槽） 海量短周期任务，固定时间精度 一、小顶堆 ​优先级队列结构，堆顶元素始终是最小的（即最近的到期时间）。 ​插入和删除堆顶操作效率高，但删除任意节点效率低。 复杂度 插入：O(log n) 删除堆顶：O(log n) 删除任意节点：O(n) 适用场景 定时任务数量大，且频繁触发最近任务的场景。 不适用于需要频繁取消或修改非堆顶任务的场景。 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 #include \u0026lt;queue\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;functional\u0026gt; struct Timer { int64_t expire; // 到期时间戳 std::function\u0026lt;void()\u0026gt; task; }; // 小顶堆比较函数 struct Compare { bool operator()(const Timer\u0026amp; a, const Timer\u0026amp; b) { return a.expire \u0026gt; b.expire; } }; std::priority_queue\u0026lt;Timer, std::vector\u0026lt;Timer\u0026gt;, Compare\u0026gt; min_heap; // 添加定时任务 void add_timer(int64_t expire, std::function\u0026lt;void()\u0026gt; task) { min_heap.push({expire, task}); } // 驱动逻辑（在事件循环中调用） void check_expire(int64_t current_time) { while (!min_heap.empty() \u0026amp;\u0026amp; min_heap.top().expire \u0026lt;= current_time) { auto task = min_heap.top().task; min_heap.pop(); task(); // 执行任务 } } 二、红黑树 使用有序容器（如 std::multimap）管理定时任务，键为到期时间。 支持高效的插入、删除和查找操作。 复杂度 插入、删除、查找：O(log n) 适用场景 需要频繁取消或修改定时任务的场景。 适合时间跨度大或需要动态调整任务的场景。 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 #include \u0026lt;map\u0026gt; #include \u0026lt;functional\u0026gt; struct Timer { int id; // 唯一标识符，用于取消任务 std::function\u0026lt;void()\u0026gt; task; }; std::multimap\u0026lt;int64_t, Timer\u0026gt; timer_map; // 添加定时任务 void add_timer(int id, int64_t expire, std::function\u0026lt;void()\u0026gt; task) { timer_map.insert({expire, {id, task}}); } // 取消定时任务（需遍历） void cancel_timer(int id) { for (auto it = timer_map.begin(); it != timer_map.end();) { if (it-\u0026gt;second.id == id) { it = timer_map.erase(it); } else { ++it; } } } // 驱动逻辑 void check_expire(int64_t current_time) { auto it = timer_map.begin(); while (it != timer_map.end() \u0026amp;\u0026amp; it-\u0026gt;first \u0026lt;= current_time) { it-\u0026gt;second.task(); // 执行任务 it = timer_map.erase(it); } } 三、时间轮 其实可以理解为一种变相的哈希表。\n将时间划分为多个槽（slot），每个槽对应一个时间间隔。 通过指针周期性移动触发当前槽的任务，插入和删除操作高效。 分层：\n如果时间轮的槽数有限，比如60个槽，每个槽代表1秒，那么最大只能处理60秒内的任务。超过这个时间的任务无法直接放置，所以需要分层来解决这个问题。\n比如，像钟表一样，有小时、分钟、秒的分层结构。当高层时间轮指针转动时，将任务降级到低层时间轮（类似钟表的进位机制）。 复杂度 插入、删除：O(1)（理想情况下） 适用场景 海量定时任务且时间精度固定的场景（如游戏技能冷却）、超大规模定时任务​（例如百万级连接的心跳检测）。 不适用于时间跨度极大或需要高精度动态调整的场景。 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 // 单层的 #include \u0026lt;vector\u0026gt; #include \u0026lt;list\u0026gt; #include \u0026lt;functional\u0026gt; const int WHEEL_SIZE = 60; // 时间轮槽数（如60秒） struct Timer { int rotation; // 剩余轮数 std::function\u0026lt;void()\u0026gt; task; }; std::vector\u0026lt;std::list\u0026lt;Timer\u0026gt;\u0026gt; time_wheel(WHEEL_SIZE); int current_slot = 0; // 添加定时任务 void add_timer(int interval, std::function\u0026lt;void()\u0026gt; task) { int slots = interval % WHEEL_SIZE; int rotation = interval / WHEEL_SIZE; int pos = (current_slot + slots) % WHEEL_SIZE; time_wheel[pos].push_back({rotation, task}); } // 驱动逻辑（每秒调用一次） void tick() { auto\u0026amp; tasks = time_wheel[current_slot]; auto it = tasks.begin(); while (it != tasks.end()) { if (it-\u0026gt;rotation \u0026gt; 0) { it-\u0026gt;rotation--; ++it; } else { it-\u0026gt;task(); // 执行任务 it = tasks.erase(it); } } current_slot = (current_slot + 1) % WHEEL_SIZE; } 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 // 三层时间轮 #include \u0026lt;vector\u0026gt; #include \u0026lt;list\u0026gt; #include \u0026lt;functional\u0026gt; // 时间轮层级定义 struct TimingWheel { int wheel_size; // 槽数 int current_slot; // 当前槽位置 int tick; // 时间精度（单位：秒） std::vector\u0026lt;std::list\u0026lt;std::function\u0026lt;void()\u0026gt;\u0026gt;\u0026gt; slots; TimingWheel(int size, int tick_unit) : wheel_size(size), current_slot(0), tick(tick_unit), slots(size) {} }; // 三层时间轮：小时级、分钟级、秒级 TimingWheel hour_wheel(60, 3600); // 1小时/槽，总范围60小时 TimingWheel minute_wheel(60, 60); // 1分钟/槽，总范围60分钟 TimingWheel second_wheel(60, 1); // 1秒/槽，总范围60秒 // 插入任务（假设时间单位为秒） void add_task(int interval, const std::function\u0026lt;void()\u0026gt;\u0026amp; task) { if (interval \u0026lt; 60) { // 插入秒级时间轮 int pos = (second_wheel.current_slot + interval) % 60; second_wheel.slots[pos].push_back(task); } else if (interval \u0026lt; 3600) { // 插入分钟级时间轮 int pos = (minute_wheel.current_slot + interval / 60) % 60; minute_wheel.slots[pos].push_back(task); } else { // 插入小时级时间轮 int pos = (hour_wheel.current_slot + interval / 3600) % 60; hour_wheel.slots[pos].push_back(task); } } // 驱动逻辑（每秒调用一次） void tick() { // 处理秒级时间轮 auto\u0026amp; second_tasks = second_wheel.slots[second_wheel.current_slot]; for (auto\u0026amp; task : second_tasks) task(); second_tasks.clear(); second_wheel.current_slot = (second_wheel.current_slot + 1) % 60; // 每分钟触发分钟级时间轮迁移 if (second_wheel.current_slot == 0) { auto\u0026amp; minute_tasks = minute_wheel.slots[minute_wheel.current_slot]; for (auto\u0026amp; task : minute_tasks) { // 重新计算剩余时间并降级到秒级时间轮 int remain_time = ...; // 根据业务逻辑计算剩余秒数 add_task(remain_time, task); } minute_tasks.clear(); minute_wheel.current_slot = (minute_wheel.current_slot + 1) % 60; } // 每小时触发小时级时间轮迁移（类似逻辑） } c语言-手撕多级时间轮定时器(纯手写)。\n拓展：\nredis延时队列如何实现？ 非活跃的连接自动断开如何实现？ 主从节点随机心跳检测如何实现？ 下单后30分钟内未付款自动取消订单如何实现？ ","date":"2025-02-25T00:00:00Z","permalink":"/posts/cpp-%E8%AE%A1%E6%97%B6%E5%99%A8-timer-%E7%9A%84%E8%AE%BE%E8%AE%A1/","title":"计时器 timer 的设计"},{"content":"C++中线程池一般使用队列（std::queue）配合外部的std::condition_variable，或者手动构建阻塞队列（BlockQueue）来设计。\n而需要使用任务优先级的时候，一般使用大根堆/小根堆的优先级队列std::priority_queue来实现。\n那么问题来了，在任务优先级比较不均的时候，怎么避免低优先级任务的长时间饥饿呢？\n为了实现动态公平调度：\n动态优先级老化（Aging）：优先级动态调整：任务在队列中等待时间越长，其有效优先级逐渐升高。 双队列混合轮询：每处理一定数量的高优先级任务后，强制处理低优先级任务。 首先抽象出一个Task用于记录任务的初始优先级和入队时间：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include \u0026lt;chrono\u0026gt; struct Task { int base_priority; // 初始优先级 std::chrono::steady_clock::time_point enqueue_time; std::function\u0026lt;void()\u0026gt; job; // 计算动态优先级（等待时间越长，优先级越高） int dynamic_priority() const { auto now = std::chrono::steady_clock::now(); auto wait_time = std::chrono::duration_cast\u0026lt;std::chrono::seconds\u0026gt;(now - enqueue_time).count(); return base_priority + static_cast\u0026lt;int\u0026gt;(wait_time * 0.1); // 老化系数可调 } // 重载比较运算符（实际比较动态优先级） bool operator\u0026lt;(const Task\u0026amp; other) const { return this-\u0026gt;dynamic_priority() \u0026lt; other.dynamic_priority(); } }; 整体的线程池类设计：\n1 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 #include \u0026lt;queue\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026lt;mutex\u0026gt; #include \u0026lt;condition_variable\u0026gt; class ThreadPool { public: ThreadPool(size_t threads, size_t high_freq = 5) : high_processing_count(0), high_freq_(high_freq) { for(size_t i = 0; i \u0026lt; threads; ++i) { workers.emplace_back([this] { worker_loop(); }); } } void add_task(int priority, std::function\u0026lt;void()\u0026gt; task) { { std::unique_lock\u0026lt;std::mutex\u0026gt; lock(queue_mutex); queue.emplace(Task{priority, std::chrono::steady_clock::now(), task}); } condition.notify_one(); } ~ThreadPool() { /* ... 省略资源回收代码 ... */ } private: std::mutex queue_mutex; std::condition_variable condition; std::priority_queue\u0026lt;Task\u0026gt; queue; // 主队列（动态优先级） std::queue\u0026lt;std::function\u0026lt;void()\u0026gt;\u0026gt; low_priority_queue; // 辅助队列 std::vector\u0026lt;std::thread\u0026gt; workers; // 轮询控制 std::atomic\u0026lt;int\u0026gt; high_processing_count; const int high_freq_; void worker_loop() { while(true) { std::function\u0026lt;void()\u0026gt; task; { std::unique_lock\u0026lt;std::mutex\u0026gt; lock(queue_mutex); condition.wait(lock, [this] { return !queue.empty(); }); // 动态老化：每处理high_freq_个高优任务后强制处理低优 if(++high_processing_count % high_freq_ == 0 \u0026amp;\u0026amp; !low_priority_queue.empty()) { task = low_priority_queue.front(); low_priority_queue.pop(); } else { task = queue.top().job; queue.pop(); } } if(task) task(); } } }; 拓展：\n（处理超时）时间阈值兜底：可添加最大等待时间监控，对超时任务直接提升到最高优先级 优先级区间划分：将任务分为URGENT/HIGH/NORMAL等级别，不同级别采用不同老化系数 根据系统负载动态调整high_freq_参数 根据队列负载动态增减线程，避免资源浪费（如 C++17 的 std::jthread）。 线程池常见实现：基于C++11实现线程池 - Skykey的文章 - 知乎。\nC++ 并发编程（从C++11到C++17）。\n货比三家：C++ 中的 task based 并发。\n","date":"2025-02-25T00:00:00Z","permalink":"/posts/cpp-%E7%BA%BF%E7%A8%8B%E6%B1%A0%E8%B0%83%E5%BA%A6%E5%8A%A8%E6%80%81%E4%BC%98%E5%85%88%E7%BA%A7%E8%80%81%E5%8C%96aging+-%E5%8F%8C%E9%98%9F%E5%88%97%E6%B7%B7%E5%90%88%E8%BD%AE%E8%AF%A2/","title":"线程池调度：动态优先级老化（Aging）+ 双队列混合轮询"},{"content":"观察以下这段明显错误的代码：\n1 2 3 4 5 6 7 8 9 const char* get_c() { std::string s = \u0026#34;hello world\u0026#34;; return s.c_str(); } int main() { printf(\u0026#34;danger : %s\\n\u0026#34;, get_c()); return 0; } 字符串s是一个函数内部的临时对象，返回的const char*实际上是一个指针。函数结束后s会析构，而指针理论上会变成悬空的。\n实际上正确打印出了：\n1 danger : hello world (实际上只是因为该段内存没有被立刻覆盖，理论上是不安全的)\n查看一下汇编：\n构造 s： 1 2 3 leaq .LC0(%rip), %rcx ; 加载 \u0026#34;hello world\u0026#34; 地址到 %rcx leaq -64(%rbp), %rax ; 栈上分配 s 的内存 call _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEC1IS3_EEPKcRKS3_ ; 调用构造函数 字符串 \u0026quot;hello world\u0026quot; 存储在 .rodata 只读数据段（.LC0）。 s 在栈上构造，通过 SSO 直接存储字符串内容。 获取 c_str()： 1 2 3 leaq -64(%rbp), %rax call _ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE5c_strEv@PLT movq %rax, %rbx ; 将 c_str() 指针保存到 %rbx 析构 s： 1 2 3 leaq -64(%rbp), %rax movq %rax, %rdi call _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEED1Ev@PLT ; 调用析构函数 对于较短的字符串（如 \u0026ldquo;hello world\u0026quot;），std::string 可能使用 短字符串优化（SSO），将数据直接存储在对象内部的栈空间中，而非堆内存。\n在get_c中，s是在栈上分配的，当函数返回时，栈空间可能未被其他数据覆盖，所以字符串内容仍然保留。此时调用printf，可能仍然能读取到原来的数据，但这只是巧合，属于未定义行为的表现。\n前情提要 写 webserver 的时候，设计了一个配置加载类用于加载配置文件。\neg.\n1 2 3 4 5 [redis] host = \u0026#34;127.0.0.1\u0026#34; port = 6379 password = \u0026#34;donotpanic\u0026#34; db = 0 逻辑差不多长这样：\n1 2 3 4 5 6 7 8 9 10 11 Config Config::_instance; std::unordered_map\u0026lt;std::string, std::string\u0026gt; Config::_configMap; std::string Config::GetConfig(const std::string\u0026amp; key) { auto it = _configMap.find(key); if (it != _configMap.end()) { return it-\u0026gt;second; } LOG_W(\u0026#34;Config \u0026#39;{}\u0026#39; not found\u0026#34;, key); return \u0026#34;\u0026#34;; } 我想让其返回c_str，直接返回是不行的。添加一个static的string作为cache即可。\n1 2 3 4 5 6 7 8 9 10 const char* Config::GetConfig(const std::string\u0026amp; key) { static std::string cache; auto it = _configMap.find(key); if (it != _configMap.end()) { cache = it-\u0026gt;second; return cache.c_str; } LOG_W(\u0026#34;Config \u0026#39;{}\u0026#39; not found\u0026#34;, key); return nullptr; } ","date":"2025-02-23T00:00:00Z","permalink":"/posts/cpp-%E8%AE%B0%E5%BD%95%E4%B8%80%E4%B8%AAc_str%E7%9A%84%E5%B0%8F%E5%9D%91/","title":"如何让函数安全返回 std::string 的 c_str"},{"content":"Linux 内核使用 结构体 和 函数指针 的组合模拟面向对象（OO）编程范式。\n1. 结构体封装数据与行为 数据抽象：将相关属性和状态封装在一个 struct 中。 行为绑定：通过函数指针将操作绑定到结构体上，实现动态调用。 示例：struct file_operations 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 定义文件操作的函数指针表 struct file_operations { ssize_t (*read)(struct file *, char __user *, size_t, loff_t *); ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *); // ... 其他方法 }; // 具体文件系统的实现（如 ext4） static struct file_operations ext4_fops = { .read = ext4_read, .write = ext4_write, // ... 初始化其他方法 }; // 注册到 VFS 层时关联 fops struct inode *inode = ...; inode-\u0026gt;i_fop = \u0026amp;ext4_fops; // 绑定特定方法集 2. 多态与继承 父子结构体：子结构体嵌入父结构体以继承接口。 类型安全转换：通过 container_of 宏从父指针获取子结构体。 示例：struct kobject 与自定义对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // 父结构体（类似抽象基类） struct kobject { const char *name; struct kset *kset; // 公共方法 int (*release)(struct kobject *); }; // 子结构体（具体实现） struct my_device { struct kobject kobj; // 继承 kobject int data; }; // 实现父类的方法 static void my_device_release(struct kobject *kobj) { struct my_device *dev = container_of(kobj, struct my_device, kobj); // 清理资源 } // 初始化时绑定方法 struct my_device *dev = kzalloc(sizeof(*dev), GFP_KERNEL); dev-\u0026gt;kobj.release = my_device_release; kobject_init(\u0026amp;dev-\u0026gt;kobj, \u0026amp;my_device_ktype); // 注册类型 3. 组合与接口分离 模块化设计：通过组合而非继承复用代码。 统一接口：顶层结构体定义标准接口，底层实现差异化逻辑。 示例：struct block_device 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 通用块设备接口 struct block_device { struct gendisk *disk; // 通用方法 int (*ioctl)(struct block_device *, unsigned int, unsigned long); }; // 桌面硬盘驱动实现 struct my_disk { struct block_device bdev; // 私有数据 }; // 实现接口方法 static int my_disk_ioctl(struct block_device *bdev, unsigned int cmd, ...) { struct my_disk *disk = container_of(bdev, struct my_disk, bdev); return custom_ioctl(disk, cmd); } 4. 运行时多态 函数指针作为虚函数表（vtable），根据对象类型动态调用不同实现。 示例：struct net_device_ops 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 网络设备操作接口 struct net_device_ops { int (*ndo_open)(struct net_device *dev); int (*ndo_stop)(struct net_device *dev); }; // 以太网驱动实现 static struct net_device_ops eth_ops = { .ndo_open = eth_open, .ndo_stop = eth_stop, }; // 注册网络设备时绑定 ops struct net_device *netdev = alloc_etherdev(sizeof(struct priv_data)); netdev-\u0026gt;netdev_ops = \u0026amp;eth_ops; 5. 关键技巧 自引用结构体：通过指针成员隐式关联自身。 宏简化代码：如 container_of 用于反向查找结构体。 模块化加载：通过 struct module 动态注册/卸载驱动。 总结 Linux 内核通过 结构体+函数指针 实现了以下 OO 特性：\n封装：隐藏内部细节（如 struct inode 的实现）。 多态：同一接口（如 read()）适配多种设备。 继承：子结构体复用父结构体的方法。 动态绑定：运行时选择具体函数实现。 这种设计平衡了 C 语言的静态特性与内核对灵活性的需求，成为高效且可扩展的系统核心。\ncontainer_of函数详解。\n主要作用就是根据结构体中的已知的成员变量的地址，来寻求该结构体的首地址。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 /** * container_of - cast a member of a structure out to the containing structure * @ptr: the pointer to the member. * @type: the type of the container struct this is embedded in. * @member: the name of the member within the struct. * * WARNING: any const qualifier of @ptr is lost. */ #define container_of(ptr, type, member) ({ \\ void *__mptr = (void *)(ptr); \\ static_assert(__same_type(*(ptr), ((type *)0)-\u0026gt;member) || \\ __same_type(*(ptr), void), \\ \u0026#34;pointer type mismatch in container_of()\u0026#34;); \\ ((type *)(__mptr - offsetof(type, member))); }) ","date":"2025-02-21T00:00:00Z","permalink":"/posts/cpp-linux-%E5%86%85%E6%A0%B8%E4%B8%AD-c-%E8%AF%AD%E8%A8%80%E7%9A%84%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1/","title":"Linux 内核中 C 语言的面向对象"},{"content":"25 | 认证机制：应用程序如何进行访问认证？讲得非常好，图文结合。\nIAM：身份识别与访问管理（Identity and Access Management）。\n认证（Authentication，英文缩写 authn）：用来验证某个用户是否具有访问系统的权限。如果认证通过，该用户就可以访问系统，从而创建、修改、删除、查询平台支持的资源。 授权（Authorization，英文缩写 authz）：用来验证某个用户是否具有访问某个资源的权限，如果授权通过，该用户就能对资源做增删改查等操作。 认证证明了你是谁，授权决定了你能做什么。\n四种基本的认证方式：Basic、Digest、OAuth、Bearer。\nBasic 基础认证\nBasic 认证（基础认证），是最简单的认证方式。它简单地将用户名:密码进行 base64 编码后，放到 HTTP Authorization Header 中。HTTP 请求到达后端服务后，后端服务会解析出 Authorization Header 中的 base64 字符串，解码获取用户名和密码，并将用户名和密码跟数据库中记录的值进行比较，如果匹配则认证通过。\nDigest 摘要认证\nDigest 认证（摘要认证）与基本认证兼容，但修复了基本认证的严重缺陷。\nDigest 具有如下特点：\n绝不会用明文方式在网络上发送密码。 可以有效防止恶意用户进行重放攻击。 可以有选择地防止对报文内容的篡改。 四步： 客户端请求服务端的资源。 在客户端能够证明它知道密码从而确认其身份之前，服务端认证失败，返回401 Unauthorized，并返回WWW-Authenticate头，里面包含认证需要的信息。 客户端根据WWW-Authenticate头中的信息，选择加密算法，并使用密码随机数 nonce(防止重放攻击)，计算出密码摘要 response，并再次请求服务端。 服务器将客户端提供的密码摘要与服务器内部计算出的摘要进行对比。如果匹配，就说明客户端知道密码，认证通过，并返回一些与授权会话相关的附加信息，放在 Authorization-Info 中。 OAuth 开放授权\nOAuth（开放授权）是一个开放的授权标准，允许用户让第三方应用访问该用户在某一 Web 服务上存储的私密资源（例如照片、视频、音频等），而无需将用户名和密码提供给第三方应用。\nOAuth2.0 一共分为四种授权方式，分别为密码式、隐藏式、凭借式和授权码模式。\nBearer 令牌认证\nBearer 认证是一种 HTTP 身份验证方法。Bearer 认证的核心是 bearer token。bearer token 是一个加密字符串，通常由服务端根据密钥生成。客户端在请求服务端时，必须在请求头中包含Authorization: Bearer 。服务端收到请求后，解析出\u0026lt;token\u0026gt;，并校验\u0026lt;token\u0026gt;的合法性，如果校验通过，则认证通过。 跟基本认证一样，Bearer 认证需要配合 HTTPS 一起使用，来保证认证安全性。\nJWT JSON Web Token（JWT）是 Bearer Token 的一个具体实现，由 JSON 数据格式组成，通过 HASH 散列算法生成一个字符串。该字符串可以用来进行授权和信息交换。\n客户端使用用户名和密码请求登录。 服务端收到请求后，会去验证用户名和密码。如果用户名和密码跟数据库记录不一致，则验证失败；如果一致则验证通过，服务端会签发一个 Token 返回给客户端。 客户端收到请求后会将 Token 缓存起来，比如放在浏览器 Cookie 中或者 LocalStorage 中，之后每次请求都会携带该 Token。 服务端收到请求后，会验证请求中的 Token，验证通过则进行业务逻辑处理，处理完后返回处理后的结果。 JWT 由三部分组成，分别是 Header、Payload 和 Signature。\nHeader：包含了 Token 的类型、Token 使用的加密算法。在某些场景下，你还可以添加 kid 字段，用来标识一个密钥 ID。 Payload：Payload 中携带 Token 的具体内容，由 JWT 标准中注册的声明、公共的声明和私有的声明三部分组成。 Signature：Signature 是 Token 的签名部分，程序通过验证 Signature 是否合法，来决定认证是否通过。 问：JWT的Token存储在哪里比较好？\ncookie中比较好，可由服务端保存，localstorage在纯前端，中很容易泄露。\n服务器可以将 cookie 设置为 HTTP - Only，无法被 JavaScript 脚本访问。\nlocalStorage 完全处于前端控制之下，可以被同源的 JavaScript 代码访问和修改。\n(Basic 认证用在前端登陆的场景，Bearer 认证用在调用后端 API 服务的场景下。)\n","date":"2025-02-17T00:00:00Z","permalink":"/posts/web-web-%E8%AE%BF%E9%97%AE%E8%AE%A4%E8%AF%81%E6%9C%BA%E5%88%B6/","title":"web 访问认证机制"},{"content":"题目来源：Marscode。\n小C和小U有一个从0开始的数组nums，以及一个非负整数k。每次操作中，小C可以选择一个尚未选择的下标i（范围在 [0, nums.length - 1]），然后将nums[i]替换为[nums[i] - k, nums[i] + k]之间的任意整数（包含边界）。\n在应用任意次数的操作后，返回数组nums可能达到的最大分数。数组的分数被定义为数组中最多重复的元素个数。注意，每个下标只能被操作一次。\n暴力解（超时） O(n²k) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int solution(vector\u0026lt;int\u0026gt;\u0026amp; nums, int k) { int n = nums.size(); int maxCount = 1; // 至少有一个数 // 遍历每个数作为可能的目标值 for (int i = 0; i \u0026lt; n; i++) { // 以nums[i]为中心，考虑范围[nums[i]-k, nums[i]+k]内的所有可能值 for (int target = nums[i]-k; target \u0026lt;= nums[i]+k; target++) { int count = 0; // 检查每个位置的数是否能变成target for (int j = 0; j \u0026lt; n; j++) { if (abs(nums[j] - target) \u0026lt;= k) { count++; } } maxCount = max(maxCount, count); } } return maxCount; } 优化暴力解 O(n²) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int solution(vector\u0026lt;int\u0026gt;\u0026amp; nums, int k) { int n = nums.size(); int maxCount = 1; // 只需要考虑将某些数变成数组中已有的数 for (int i = 0; i \u0026lt; n; i++) { int target = nums[i]; // 以当前数作为目标值 int count = 0; for (int j = 0; j \u0026lt; n; j++) { if (abs(nums[j] - target) \u0026lt;= k) { count++; } } maxCount = max(maxCount, count); } return maxCount; } 扫描线算法 O(nlogn) 像是在数某个时刻有多少个区间重叠。一条水平线从左向右扫过，每个起点让重叠数+1，每个终点让重叠数-1，过程中的最大重叠数就是答案。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int solution(vector\u0026lt;int\u0026gt;\u0026amp; nums, int k) { int n = nums.size(); vector\u0026lt;pair\u0026lt;int, int\u0026gt;\u0026gt; ranges; // 存储每个数可以变化的范围 // 计算每个数可以变化的范围 for (int i = 0; i \u0026lt; n; i++) { ranges.push_back({nums[i] - k, 1}); // 范围起点 ranges.push_back({nums[i] + k + 1, -1}); // 范围终点 } // 按照位置排序 sort(ranges.begin(), ranges.end()); int maxCount = 1; int count = 0; // 扫描线算法 for (const auto\u0026amp; range : ranges) { count += range.second; maxCount = max(maxCount, count); } return maxCount; } 注：\nstd::sort 对 std::pair 的默认排序规则是：首先比较 first 成员，如果 first 相等，则比较 second 成员。\n完整代码：\n1 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 #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;algorithm\u0026gt; using namespace std; int solution(vector\u0026lt;int\u0026gt;\u0026amp; nums, int k) { int n = nums.size(); vector\u0026lt;pair\u0026lt;int, int\u0026gt;\u0026gt; ranges; // 存储每个数可以变化的范围 // 计算每个数可以变化的范围 for (int i = 0; i \u0026lt; n; i++) { ranges.push_back({nums[i] - k, 1}); // 范围起点 ranges.push_back({nums[i] + k + 1, -1}); // 范围终点 } // 按照位置排序 sort(ranges.begin(), ranges.end()); int maxCount = 1; int count = 0; // 扫描线算法 for (const auto\u0026amp; range : ranges) { count += range.second; maxCount = max(maxCount, count); } return maxCount; } int main() { vector\u0026lt;int\u0026gt; nums1 = {4, 6, 1, 2}; cout \u0026lt;\u0026lt; (solution(nums1, 2) == 3) \u0026lt;\u0026lt; endl; vector\u0026lt;int\u0026gt; nums2 = {1, 3, 5, 7}; cout \u0026lt;\u0026lt; (solution(nums2, 1) == 2) \u0026lt;\u0026lt; endl; vector\u0026lt;int\u0026gt; nums3 = {1, 3, 5, 7}; cout \u0026lt;\u0026lt; (solution(nums3, 3) == 4) \u0026lt;\u0026lt; endl; return 0; } ","date":"2025-02-15T00:00:00Z","permalink":"/posts/alg-%E6%89%AB%E6%8F%8F%E7%BA%BF%E7%AE%97%E6%B3%95%E8%AE%A1%E7%AE%97%E5%8C%BA%E9%97%B4%E9%87%8D%E5%8F%A0/","title":"扫描线算法计算区间重叠"},{"content":"同一进程内的线程共享的资源：\n代码段：存放程序的可执行指令，所有线程共享相同的代码段，因此任何线程都可以执行程序中的函数。\n数据段：包含全局变量和静态变量，这些变量在程序运行期间只有一个实例，所有线程都可以访问和修改它们。\n堆：用于动态内存分配，线程可以在堆上分配和释放内存，因此堆上的数据对所有线程可见。\n打开的文件：如果程序在运行过程中打开了文件，文件描述符等信息在进程地址空间中保存，所有线程都可以访问这些打开的文件。\n每个线程的私有资源：\n栈：每个线程都有自己的栈空间，用于存储函数的局部变量、返回地址等。\n寄存器：线程在执行过程中使用的寄存器集是独立的，包括程序计数器（PC）等。\n线程局部存储（Thread Local Storage）：存放线程私有的全局变量，即使变量名相同，不同线程访问的也是各自独立的副本。\n注意：虽然栈是线程私有的，但由于线程间没有严格的内存隔离机制，一个线程可以通过指针访问和修改另一个线程的栈数据。\n","date":"2025-02-08T00:00:00Z","permalink":"/posts/cpp-c++-%E5%90%8C%E4%B8%80%E8%BF%9B%E7%A8%8B%E7%9A%84%E7%BA%BF%E7%A8%8B%E4%B9%8B%E9%97%B4%E5%85%B1%E4%BA%AB%E5%93%AA%E4%BA%9B%E8%B5%84%E6%BA%90/","title":"C++ 同一进程的线程之间共享哪些资源？"},{"content":"在 C++ 中，动态库（如 .dll 或 .so 文件）在加载时，操作系统会将整个库文件映射到进程的地址空间中。 具体的函数和数据只有在被实际使用时才会被加载到内存中。 (动态库作为一个整体被映射，但其中的各个部分仅在需要时才占用物理内存。)\n另外：\nC++ 提供了显式运行时链接的机制，程序可以在运行时根据需要动态加载库的特定部分。(dlopen、dlsym 等函数)程序可以在运行时按需加载特定的符号（函数或变量）。\n1 2 #include \u0026lt;dlfcn.h\u0026gt; void *dlopen(const char *filename, int flag); flag：指定加载选项： - RTLD_LAZY：延迟解析符号，即在实际使用时才解析。\n- RTLD_NOW：立即解析所有未定义的符号。如果无法解析，dlopen 将返回 NULL。\n- RTLD_GLOBAL：使加载的库中的符号在后续加载的其他库中可见。\n- RTLD_LOCAL：与 RTLD_GLOBAL 相反，加载的库中的符号对后续加载的库不可见（这是默认行为）。\n","date":"2025-02-08T00:00:00Z","permalink":"/posts/cpp-%E8%BF%90%E8%A1%8C%E6%97%B6%E6%98%AF%E6%8A%8A%E6%95%B4%E4%B8%AA%E5%8A%A8%E6%80%81%E5%BA%93%E9%83%BD%E5%8A%A0%E8%BD%BD%E5%88%B0%E5%86%85%E5%AD%98%E4%B8%AD%E5%90%97/","title":"运行时是把整个动态库都加载到内存中吗？"},{"content":"常见 C/C++ API std::string的 string::find 成员函数\n1 2 3 4 5 6 #include \u0026lt;string\u0026gt; using namespace std; bool isSubstring(const string\u0026amp; mainStr, const string\u0026amp; subStr) { return mainStr.find(subStr) != string::npos; } 大多数标准库的 strstr（如Glibc）和 string::find（如MSVC、libc++）已针对子串搜索优化。\n实现中可能直接调用 memmem 或 strstr，性能与 strstr 相当。\nC标准库的 strstr 函数\n1 2 3 4 #include \u0026lt;cstring\u0026gt; bool isSubstring(const string\u0026amp; mainStr, const string\u0026amp; subStr) { return strstr(mainStr.c_str(), subStr.c_str()) != nullptr; } 需要将 std::string 转换为C风格字符串，可能引入额外开销。\nGlibc的 strstr 使用Two-Way算法，适合长文本和模式。时间复杂度接近O(n)。\nSTL std::search\n1 2 3 4 5 6 7 8 #include \u0026lt;algorithm\u0026gt; #include \u0026lt;string\u0026gt; bool isSubstring(const string\u0026amp; mainStr, const string\u0026amp; subStr) { return std::search( mainStr.begin(), mainStr.end(), subStr.begin(), subStr.end() ) != mainStr.end(); } 算法 1. 暴力法（Brute Force） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include \u0026lt;string\u0026gt; bool isSubstringBruteForce(const std::string\u0026amp; mainStr, const std::string\u0026amp; subStr) { if (subStr.empty()) return true; // 空子串是任何字符串的子串 int m = mainStr.length(), n = subStr.length(); if (m \u0026lt; n) return false; for (int i = 0; i \u0026lt;= m - n; ++i) { int j; for (j = 0; j \u0026lt; n; ++j) { if (mainStr[i + j] != subStr[j]) break; } if (j == n) return true; // 完全匹配 } return false; } 时间复杂度：最坏情况为 (O(m \\times n))（如主串为AAAAAAB，子串为AAAB）。 空间复杂度：(O(1))。 2. KMP算法（Knuth-Morris-Pratt） 通过预处理子串生成部分匹配表（Longest Prefix Suffix, LPS），利用已匹配的信息跳过不必要的比较。\n构建部分匹配表（LPS）： 计算子串每个位置的最长相等前缀和后缀的长度。 双指针匹配： 主串指针i和子串指针j同时移动，匹配失败时根据LPS表回退j。 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 #include \u0026lt;string\u0026gt; #include \u0026lt;vector\u0026gt; // 预处理LPS表 std::vector\u0026lt;int\u0026gt; computeLPS(const std::string\u0026amp; subStr) { int n = subStr.length(); std::vector\u0026lt;int\u0026gt; lps(n, 0); int len = 0; // 当前最长前缀后缀长度 for (int i = 1; i \u0026lt; n;) { if (subStr[i] == subStr[len]) { lps[i++] = ++len; } else { if (len != 0) len = lps[len - 1]; else lps[i++] = 0; } } return lps; } // KMP匹配 bool isSubstringKMP(const std::string\u0026amp; mainStr, const std::string\u0026amp; subStr) { if (subStr.empty()) return true; int m = mainStr.length(), n = subStr.length(); if (m \u0026lt; n) return false; std::vector\u0026lt;int\u0026gt; lps = computeLPS(subStr); int i = 0, j = 0; // i:主串指针, j:子串指针 while (i \u0026lt; m) { if (mainStr[i] == subStr[j]) { i++; j++; if (j == n) return true; // 完全匹配 } else { if (j != 0) j = lps[j - 1]; // 回退j else i++; // 无法回退，移动i } } return false; } 时间复杂度：(O(m + n))，预处理LPS表 (O(n))，匹配过程 (O(m))。 空间复杂度：(O(n))（存储LPS表）。 适合处理长文本或频繁匹配同一子串。\n3. Sunday算法 利用坏字符规则，根据主字符串中当前匹配窗口后的第一个字符决定跳跃步长。\n预处理偏移表： 记录子串中每个字符最后出现的位置距末尾的距离。 匹配与跳跃： 匹配失败时，根据主字符串中下一个字符的位置跳跃。 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 #include \u0026lt;string\u0026gt; #include \u0026lt;unordered_map\u0026gt; bool isSubstringSunday(const std::string\u0026amp; mainStr, const std::string\u0026amp; subStr) { if (subStr.empty()) return true; int m = mainStr.length(), n = subStr.length(); if (m \u0026lt; n) return false; // 预处理偏移表：字符到跳跃步长的映射 std::unordered_map\u0026lt;char, int\u0026gt; shift; for (int i = 0; i \u0026lt; n; ++i) { shift[subStr[i]] = n - i; // 字符最后出现的位置距末尾的距离 } int i = 0; while (i \u0026lt;= m - n) { bool match = true; for (int j = 0; j \u0026lt; n; ++j) { if (mainStr[i + j] != subStr[j]) { match = false; break; } } if (match) return true; // 计算跳跃步长 char nextChar = (i + n \u0026lt; m) ? mainStr[i + n] : 0; int step = (shift.find(nextChar) != shift.end()) ? shift[nextChar] : n + 1; i += step; } return false; } 时间复杂度：平均 (O(m))，最坏 (O(m \\times n))。 空间复杂度：(O(k))（k为字符集大小）。 适合字符分布不均匀的场景（如英文文本）。\n算法 时间复杂度 空间复杂度 适用场景 暴力法 (O(m \\times n)) (O(1)) 短文本、简单场景 KMP (O(m + n)) (O(n)) 长文本、需频繁匹配同一子串 Sunday 平均 (O(m)) (O(k)) 字符分布不均匀（如自然语言） ","date":"2025-01-29T00:00:00Z","permalink":"/posts/cpp-%E6%A3%80%E6%9F%A5%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%98%AF%E5%90%A6%E6%98%AF%E5%8F%A6%E4%B8%80%E4%B8%AA%E7%9A%84%E5%AD%90%E4%B8%B2/","title":"检查字符串是否是另一个的子串"},{"content":" 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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 #include \u0026lt;functional\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;optional\u0026gt; #include \u0026lt;string\u0026gt; #include \u0026lt;vector\u0026gt; template \u0026lt;typename T\u0026gt; concept Comparable = requires(T a, T b) { { a \u0026lt; b } -\u0026gt; std::convertible_to\u0026lt;bool\u0026gt;; { a \u0026gt; b } -\u0026gt; std::convertible_to\u0026lt;bool\u0026gt;; { a == b } -\u0026gt; std::convertible_to\u0026lt;bool\u0026gt;; }; template \u0026lt;typename T\u0026gt; concept Streamable = requires(T a, std::ostream\u0026amp; os) { { os \u0026lt;\u0026lt; a } -\u0026gt; std::same_as\u0026lt;std::ostream\u0026amp;\u0026gt;; }; template \u0026lt;Comparable K, Streamable V\u0026gt; class PairBSTree { private: using Pair = std::pair\u0026lt;K, V\u0026gt;; struct TreeNode { Pair _pair; TreeNode* _left; TreeNode* _right; TreeNode() = default; TreeNode(const Pair\u0026amp; pair) : _pair(pair), _left(nullptr), _right(nullptr) {} TreeNode(Pair\u0026amp;\u0026amp; pair) : _pair(std::move(pair)), _left(nullptr), _right(nullptr) {} ~TreeNode() = default; }; TreeNode* _root; void build_(const std::vector\u0026lt;Pair\u0026gt;\u0026amp; nodes) { for (const auto\u0026amp; pair : nodes) { Insert(pair); } } void build_(std::vector\u0026lt;Pair\u0026gt;\u0026amp;\u0026amp; nodes) { for (Pair\u0026amp; pair : nodes) { Insert(std::move(pair)); } } void destroy_(TreeNode* node) { if (node) { destroy_(node-\u0026gt;_left); destroy_(node-\u0026gt;_right); delete node; node = nullptr; } } TreeNode*\u0026amp; search_(TreeNode*\u0026amp; node, K key) const { if (!node || key == node-\u0026gt;_pair.first) { return node; } if (key \u0026lt; node-\u0026gt;_pair.first) { return search_(node-\u0026gt;_left, key); } return search_(node-\u0026gt;_right, key); } void insert_(TreeNode*\u0026amp; node, const Pair\u0026amp; pair) { if (!node) { node = new TreeNode(pair); return; } auto key = pair.first; if (key == node-\u0026gt;_pair.first) { node-\u0026gt;_pair = pair; } else if (key \u0026lt; node-\u0026gt;_pair.first) { insert_(node-\u0026gt;_left, pair); } else { insert_(node-\u0026gt;_right, pair); } } TreeNode*\u0026amp; go_to_max_(TreeNode*\u0026amp; node) { while (node-\u0026gt;_right) { node = node-\u0026gt;_right; } return node; } TreeNode*\u0026amp; go_to_min_(TreeNode*\u0026amp; node) { while (node-\u0026gt;_left) { node = node-\u0026gt;_left; } return node; } void delete_(TreeNode*\u0026amp; node, K key) { auto\u0026amp; target = search_(node, key); if (!target) { return; } if (!target-\u0026gt;_left \u0026amp;\u0026amp; !target-\u0026gt;_right) { delete target; target = nullptr; return; } if (!target-\u0026gt;_left) { TreeNode* temp = target-\u0026gt;_right; delete target; target = temp; return; } if (!target-\u0026gt;_right) { TreeNode* temp = target-\u0026gt;_left; delete target; target = temp; return; } auto\u0026amp; max_in_left = go_to_max_(target-\u0026gt;_left); target-\u0026gt;_pair = max_in_left-\u0026gt;_pair; // 1. 常规的递归，把整个左子树当做新的树 // delete_(target-\u0026gt;_left, max_in_left-\u0026gt;_pair.first); // 2. 直接传入 max_in_left 即可 // delete_(max_in_left, max_in_left-\u0026gt;_pair.first); // 3. 实际上不需要递归，因为 max_in_left 是左边最大的值，一定没有右子树 TreeNode* temp = max_in_left-\u0026gt;_left; delete max_in_left; max_in_left = temp; // 我开始时候的代码（有误）： // auto\u0026amp; max_in_left = go_to_max_(node-\u0026gt;_left); // 应该是 // current-\u0026gt;_left current-\u0026gt;_pair = max_in_left-\u0026gt;_pair; delete // (max_in_left); max_in_left = nullptr; // 第三种和我开始时候的逻辑类似 // 但我当时忘了保留 max_in_left 的左子树（如果存在） } static void normal_print_func_(const Pair\u0026amp; pair) { std::cout \u0026lt;\u0026lt; pair.second \u0026lt;\u0026lt; \u0026#34; | \u0026#34;; } void in_order_(TreeNode* node, std::function\u0026lt;void(const Pair\u0026amp;)\u0026gt; func) { if (!node) { return; } in_order_(node-\u0026gt;_left, func); func(node-\u0026gt;_pair); in_order_(node-\u0026gt;_right, func); } public: PairBSTree() : _root(nullptr) {} PairBSTree(const std::vector\u0026lt;Pair\u0026gt;\u0026amp; pairs) : _root(nullptr) { build_(pairs); } PairBSTree(std::vector\u0026lt;Pair\u0026gt;\u0026amp;\u0026amp; pairs) : _root(nullptr) { build_(std::move(pairs)); } ~PairBSTree() { destroy_(_root); } std::optional\u0026lt;V\u0026gt; Search(K key) { auto node = search_(_root, key); if (!node) { return std::nullopt; } return node-\u0026gt;_pair.second; } void Insert(const Pair\u0026amp; pair) { insert_(_root, pair); } void Delete(K key) { delete_(_root, key); } void InOrder(std::function\u0026lt;void(Pair)\u0026gt; func = normal_print_func_) { in_order_(_root, func); } [[nodiscard]] size_t Size() { size_t size = 0; InOrder([\u0026amp;size](std::pair\u0026lt;K, V\u0026gt;) { ++size; }); return size; } [[nodiscard]] V Max() { auto temp = _root; go_to_max_(temp); return temp-\u0026gt;_pair.second; } [[nodiscard]] V Min() { auto temp = _root; go_to_min_(temp); return temp-\u0026gt;_pair.second; } }; int main(void) { std::vector\u0026lt;std::pair\u0026lt;int, std::string\u0026gt;\u0026gt; pairs = { {2, \u0026#34;Bob\u0026#34;}, {9, \u0026#34;Jack\u0026#34;}, {4, \u0026#34;Lucy\u0026#34;}, {23, \u0026#34;Evan\u0026#34;}, {3, \u0026#34;Gorge\u0026#34;}, {12, \u0026#34;Lily\u0026#34;}, {15, \u0026#34;Mono\u0026#34;}, {90, \u0026#34;Rick\u0026#34;}, {14, \u0026#34;Lance\u0026#34;}, {76, \u0026#34;Molly\u0026#34;}, {24, \u0026#34;Stan\u0026#34;}, {11, \u0026#34;Scot\u0026#34;}, {54, \u0026#34;Mint\u0026#34;}, {37, \u0026#34;Biance\u0026#34;}, {35, \u0026#34;Cower\u0026#34;}, {1, \u0026#34;Brick\u0026#34;}, }; PairBSTree tree(pairs); std::cout \u0026lt;\u0026lt; \u0026#34;Name of 9: \u0026#34; \u0026lt;\u0026lt; tree.Search(9).value_or(\u0026#34;nothing\u0026#34;) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#34;Size: \u0026#34; \u0026lt;\u0026lt; tree.Size() \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // std::cout \u0026lt;\u0026lt; \u0026#34;Min: \u0026#34; \u0026lt;\u0026lt; tree.Min() \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // std::cout \u0026lt;\u0026lt; \u0026#34;Max: \u0026#34; \u0026lt;\u0026lt; tree.Max() \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; tree.InOrder(); std::cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; tree.Delete(15); std::cout \u0026lt;\u0026lt; \u0026#34;Size: \u0026#34; \u0026lt;\u0026lt; tree.Size() \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; tree.InOrder(); std::cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; std::vector\u0026lt;std::string\u0026gt; names_in_order; tree.InOrder([\u0026amp;names_in_order](std::pair\u0026lt;int, std::string\u0026gt; pair) { std::cout \u0026lt;\u0026lt; pair.second \u0026lt;\u0026lt; \u0026#34; -- \u0026#34;; names_in_order.push_back(pair.second); }); std::cout \u0026lt;\u0026lt; std::endl; return 0; } ","date":"2025-01-21T00:00:00Z","permalink":"/posts/cpp-%E5%86%99%E4%B8%AA%E7%9B%B8%E5%AF%B9%E7%8E%B0%E4%BB%A3%E7%9A%84-c++-%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","title":"写个相对现代的 C++ 二叉搜索树"},{"content":"看到一句话：\nstd::function 很强大，但是代价也很高，在创建函数对象的时候总是会有 new 操作的。虽然通常情况下影响不是很高，但是总觉得这是没必要的。\n于是草草找一下资料，看看有没有隐藏的性能优化。\nstd::function 的实现 std::function 是一个可变参类模板，是一个通用的函数包装器（Polymorphic function wrapper）。\n通过类型擦除（type erasure）机制，将具体类型的可调用对象封装到一个统一的接口中。\n其实例可以存储、复制和调用任何可复制构造的可调用目标，包括普通函数、成员函数、类对象（重载了operator()的类的对象）、Lambda表达式等。是对C++现有的可调用实体的一种类型安全的包裹（相比而言，函数指针这种可调用实体，是类型不安全的）。 \u0026ndash; STL源码分析之std::function\n1 2 3 4 5 6 7 8 9 10 template\u0026lt;typename _Res, typename... _ArgTypes\u0026gt; class function\u0026lt;_Res(_ArgTypes...)\u0026gt; : public _Maybe_unary_or_binary_function\u0026lt;_Res, _ArgTypes...\u0026gt; , private _Function_base { private: using _Invoker_type = _Res (*)(const _Any_data\u0026amp;, _ArgTypes\u0026amp;\u0026amp;...); _Invoker_type _M_invoker; // ... }; std::function 的内部有两个部分：\n一个指向实际存储区域的指针：存储实际的可调用对象（函数对象、lambda、函数指针等）。 一个接口表（vtable 等效机制）：存储操作函数（如调用函数、复制、销毁等）的地址。 其类型擦除通过接口表的方式实现，类似于虚函数机制，但它通常采用静态接口表和手动的动态分配来支持多种类型的可调用对象。\n性能分析 关于std function和lambda function的性能调试 \u0026ndash;法号桑菜。\nAvoiding The Performance Hazzards of std::function。\nThere are two performance implications of using std::function that might surprise you:\nWhen calling a std::function, it does a virtual function call. When assigning a lambda with significant captures to a std::function, it will do a dynamic memory allocation! 一是std::function 会使用虚函数调用，有开销。 二是将 lambda 赋给std::function的时候，如果捕获内容较多，会需要额外的动态内存分配。 第二点其实说的就是：\nstd::function 对小型的可调用对象会使用“小对象优化（Small Object Optimization, SOO）”，避免动态分配堆内存。但如果对象超过了实现中的小对象优化阈值，则会触发堆分配（new 操作）。\n一些可能有用的优化 手动使用模板代替 std::function： 1 2 3 4 template \u0026lt;typename Callable\u0026gt; void invoke(Callable f) { f(); } 直接在编译期确定类型，避免了类型擦除和动态分配。\n缺点就是使用场景受限于编译期类型，灵活性不如 std::function。\n向 std::function 传递 lambda 的时候使用 std::ref() / std::cref() std::ref() and std::cref() return reference wrappers (costant ref wrapper in the cref case) which can hold arbitrary types as references. If you put your large capture lambda into one of these functions and give it to std::function, there’s a std::function constructor which is able to take this reference, and use that instead of allocating more memory.\n1 2 3 4 5 6 array i; auto A = [=]() -\u0026gt; int { return (i[0] + i[1] * i[2] + i[3]) ^ i[4]; }; // no allocation, std::function stores a reference to A instead of A itself function fA(ref(A)); 使用 std::variant： 通过 std::variant 直接存储无类型擦除的函数对象。似乎有点跑题，在此处作用有限。\nstd::variant 的内存是静态分配的：其大小是所有可能存储的类型大小的最大值，避免了堆分配的开销。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct PrintHello { void operator()() const { std::cout \u0026lt;\u0026lt; \u0026#34;Hello, Struct!\u0026#34; \u0026lt;\u0026lt; std::endl; } }; using Callable = std::variant\u0026lt;void(*)(), PrintHello\u0026gt;; void invoke(const Callable\u0026amp; f) { std::visit([](const auto\u0026amp; func) { func(); }, f); } int main() { invoke([]() { std::cout \u0026lt;\u0026lt; \u0026#34;Hello, Function Pointer!\u0026#34; \u0026lt;\u0026lt; std::endl; }); // 函数指针 invoke(PrintHello{}); // 自定义的函数对象 return 0; } Stack Allocation: 给 std::function 一个自定义分配器\u0026hellip;\nLambda 的 +\n一个无捕获的 lambda 表达式不依赖于任何外部状态，可以被隐式转换为函数指针：\n1 2 3 auto lambda = []() { std::cout \u0026lt;\u0026lt; \u0026#34;Hello, Lambda!\u0026#34; \u0026lt;\u0026lt; std::endl; }; void (*funcPtr)() = lambda; // 无需显式转换 funcPtr(); // 调用函数指针，输出 \u0026#34;Hello, Lambda!\u0026#34; 在 lambda 表达式前使用+，会强制将 lambda 转换为函数指针：\n1 2 3 4 5 6 7 void invoke(void (*func)()) { func(); } int main() { invoke(+[]() { std::cout \u0026lt;\u0026lt; \u0026#34;Hello, Lambda!\u0026#34; \u0026lt;\u0026lt; std::endl; }); // 显式转换为函数指针 } All in all:\n","date":"2025-01-15T00:00:00Z","permalink":"/posts/cpp-c++-stdfunction-%E4%B9%8B%E8%84%B1%E8%A3%A4%E5%AD%90%E6%94%BE%E5%B1%81%E7%9A%84%E4%BC%98%E5%8C%96/","title":"C++ std::function 之脱裤子放屁的优化"},{"content":"利用 RAII 和 C++ 的析构函数。\n1 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 #include \u0026lt;iostream\u0026gt; #include \u0026lt;functional\u0026gt; #include \u0026lt;vector\u0026gt; class Defer { public: explicit Defer(std::function\u0026lt;void()\u0026gt; func) : func_(std::move(func)), active_(true) {} // 禁止复制 Defer(const Defer\u0026amp;) = delete; Defer\u0026amp; operator=(const Defer\u0026amp;) = delete; // 允许移动 Defer(Defer\u0026amp;\u0026amp; other) noexcept : func_(std::move(other.func_)), active_(other.active_) { other.active_ = false; } // 析构函数中调用defer的函数 ~Defer() { if (active_ \u0026amp;\u0026amp; func_) { func_(); } } void cancel() { active_ = false; } private: std::function\u0026lt;void()\u0026gt; func_; bool active_; }; #define CONCAT_IMPL(x, y) x##y #define CONCAT(x, y) CONCAT_IMPL(x, y) #define defer(func) Defer CONCAT(_defer_, __LINE__)(func) int main() { std::cout \u0026lt;\u0026lt; \u0026#34;Start of main function\u0026#34; \u0026lt;\u0026lt; std::endl; defer([]() { std::cout \u0026lt;\u0026lt; \u0026#34;Deferred action 1\u0026#34; \u0026lt;\u0026lt; std::endl; }); { defer([]() { std::cout \u0026lt;\u0026lt; \u0026#34;Deferred action in scope\u0026#34; \u0026lt;\u0026lt; std::endl; }); std::cout \u0026lt;\u0026lt; \u0026#34;Inside scope\u0026#34; \u0026lt;\u0026lt; std::endl; } defer([]() { std::cout \u0026lt;\u0026lt; \u0026#34;Deferred action 2\u0026#34; \u0026lt;\u0026lt; std::endl; }); std::cout \u0026lt;\u0026lt; \u0026#34;End of main function\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } 这些宏的目的是为 defer 提供一种易用的语法，同时确保每次使用 defer 都会创建一个唯一的变量名，从而避免变量名冲突。\nCONCAT_IMPL(x, y) 和 CONCAT(x, y) 1 2 #define CONCAT_IMPL(x, y) x##y #define CONCAT(x, y) CONCAT_IMPL(x, y) x##y 是 C++ 预处理器的标记连接运算符，将 x 和 y 拼接成一个标识符。 CONCAT_IMPL 是底层的宏，用于直接连接标识符。 CONCAT 是一个包装宏，它确保在预处理器展开过程中，所有参数被正确解析后再拼接。 例如：\n1 CONCAT(foo, bar) // 展开为 foobar defer(func) 1 #define defer(func) Defer CONCAT(_defer_, __LINE__)(func) __LINE__ 是预处理器的内置宏，表示当前代码所在的行号。 CONCAT(_defer_, __LINE__) 会将 _defer_ 和当前行号拼接，生成一个唯一的变量名。 例如：\n1 defer([]() { std::cout \u0026lt;\u0026lt; \u0026#34;Cleanup!\u0026#34; \u0026lt;\u0026lt; std::endl; }); 假设这段代码位于第 25 行，则展开后为：\n1 Defer _defer_25([]() { std::cout \u0026lt;\u0026lt; \u0026#34;Cleanup!\u0026#34; \u0026lt;\u0026lt; std::endl; }); 这样每次调用 defer 时生成的变量名都唯一，避免了同一作用域中多个 defer 语句导致变量名冲突的问题。\n","date":"2025-01-15T00:00:00Z","permalink":"/posts/cpp-%E7%94%A8-c++-%E5%AE%9E%E7%8E%B0-golang-%E7%9A%84-defer/","title":"在 C++ 里实现 Golang 的 defer"},{"content":"C++的POD以及如何判断是否POD - cheeto的文章 - 知乎。\n在C++11及以后的版本中，POD类型（Plain Old Data）的定义被细化为两个核心概念：\n平凡类型（Trivial Type）和标准布局类型（Standard Layout Type）。当类型为Trivial \u0026amp;\u0026amp; Standard Layout时才能被认为是POD。\n平凡类型（Trivial Type） 满足以下条件：\n默认构造函数：没有用户定义的构造函数，即使用默认构造函数。 默认拷贝构造函数：没有用户定义的拷贝构造函数。 默认析构函数：没有用户定义的析构函数。 默认赋值操作符：没有用户定义的拷贝赋值和移动赋值操作符。 对于平凡类型，编译器会为其提供默认的构造、拷贝和析构行为，无需用户显式定义。\n比如说以下Trivial，即使它有构造函数和析构函数 只要不是用户自定义而是default也可以\n1 2 3 4 5 struct Trivial { int a; Trivial() = default; // 默认构造函数 ~Trivial() = default; // 默认析构函数 }; 标准布局类型（Standard Layout Type） 满足以下条件：\n无虚函数：它没有虚函数。 无虚基类：它没有虚基类。 成员变量顺序：它的成员变量是按声明顺序排列的。 直接用std::is_standard_layout_v判断即可\n1 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 #define Print(x) std::cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39; struct safe { int m; }; struct unsafe_cons { unsafe_cons(unsafe_cons const \u0026amp;) {} }; struct unsafe_vir { virtual void foo(); }; template \u0026lt;typename T\u0026gt; class unsafe_tem { T data; }; struct Trivial { int a; Trivial() = default; // 默认构造函数 ~Trivial() = default; // 默认析构函数 }; struct StandardLayout { char a; // 1 byte int b; // 4 bytes }; // 用于检查是否为 POD 类型 template \u0026lt;typename T\u0026gt; struct is_pod { static constexpr bool value = std::is_trivial\u0026lt;T\u0026gt;::value \u0026amp;\u0026amp; std::is_standard_layout\u0026lt;T\u0026gt;::value \u0026amp;\u0026amp; std::is_trivially_default_constructible\u0026lt;T\u0026gt;::value; }; void test1() { Print(is_pod\u0026lt;int\u0026gt;::value); // 1 Print(is_pod\u0026lt;std::string\u0026gt;::value); // 0 Print(is_pod\u0026lt;Trivial\u0026gt;::value); // 1 Print(is_pod\u0026lt;StandardLayout\u0026gt;::value); // 1 Print(std::is_trivial\u0026lt;Trivial\u0026gt;::value); // 1 Print(std::is_trivial\u0026lt;StandardLayout\u0026gt;::value); // 1 Print(std::is_standard_layout_v\u0026lt;StandardLayout\u0026gt;); // 1 } 这里的标准布局的判定反而没有这么严格\n我说的严格指的是\n1 2 3 4 5 6 7 8 9 10 struct A{ char a; // 1 byte int b; // 4 bytes }; 和 struct B{ int b; // 4 bytes char a; // 1 byte }; 这种\nstruct A 会在 char 后插入填充字节，以满足 int 的对齐要求。 struct B 的内存布局使得结构体末尾需要填充字节，确保结构体的总大小满足 4 字节对齐要求。\n小结 也就是说填充不影响POD的判定 而是成员变量顺序发生了改变才不算POD。\nPOD 类型的定义主要关注是否有特殊的构造、析构或拷贝操作，以及成员变量的顺序是否保持一致。\n如何判断是否POD\n1 2 3 4 5 6 7 // 用于检查是否为 POD 类型 // 使用例is_pod\u0026lt;int\u0026gt;::value template \u0026lt;typename T\u0026gt; struct is_pod { static constexpr bool value = std::is_trivial\u0026lt;T\u0026gt;::value \u0026amp;\u0026amp; std::is_standard_layout\u0026lt;T\u0026gt;::value \u0026amp;\u0026amp; std::is_trivially_default_constructible\u0026lt;T\u0026gt;::value; }; 拓展 对于平凡类型的 class 和 struct，它们在内存布局、对象拷贝、传递给 C 函数等操作中，几乎没有区别。因此，可以像使用 C 语言中的结构体一样使用它们。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct Point { int x, y; }; int main() { Point p = {1, 2}; // 使用 reinterpret_cast 强制转换 void* ptr = \u0026amp;p; // void* 指针指向结构体 // 使用 reinterpret_cast 强制转换为 Point* 类型 Point* p2 = reinterpret_cast\u0026lt;Point*\u0026gt;(ptr); // 访问成员 std::cout \u0026lt;\u0026lt; \u0026#34;x: \u0026#34; \u0026lt;\u0026lt; p2-\u0026gt;x \u0026lt;\u0026lt; \u0026#34;, y: \u0026#34; \u0026lt;\u0026lt; p2-\u0026gt;y \u0026lt;\u0026lt; std::endl; return 0; } ","date":"2025-01-09T00:00:00Z","permalink":"/posts/cpp-%E8%BD%AC%E8%BD%BD-c++%E7%9A%84pod%E4%BB%A5%E5%8F%8A%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6pod/","title":"C++的POD以及如何判断是否POD"},{"content":" 智能指针之shared_ptr易错点05。\n掌握C++ 智能指针的自我引用：深入解析 shared_from_this 和 weak_from_this。\nC++之shared_from_this用法以及类自引用this指针陷阱。\n思考：\n设计一个树的节点的时候，如果使用智能指针：用一个std::vector\u0026lt;shared_ptr\u0026lt;TreeNode\u0026gt;\u0026gt;来存储子节点，为避免循环引用，用weak_ptr\u0026lt;TreeNode\u0026gt;来存储自身的父节点指针。\n那添加子节点的时候，怎么把自身的shared_ptr赋值给子节点存储的父节点指针呢？\n两个错误做法： 使用std::make_shared\u0026lt;TreeNode\u0026lt;T\u0026gt;\u0026gt;(*this)来创建一个新的shared_ptr，然后赋值给子节点存储的父节点指针。 1 2 3 4 5 void addChild(std::shared_ptr\u0026lt;TreeNode\u0026lt;T\u0026gt;\u0026gt; child) { // 使用 make_shared 来创建子节点并设置父节点 child-\u0026gt;setParent(std::make_shared\u0026lt;TreeNode\u0026lt;T\u0026gt;\u0026gt;(*this)); // 错误的做法 children.push_back(child); } 使用 std::make_shared\u0026lt;TreeNode\u0026lt;T\u0026gt;\u0026gt;(*this) 时，实际上是对当前对象的 拷贝构造（调用拷贝构造函数）来创建一个新的 TreeNode\u0026lt;T\u0026gt; 对象。这意味着你将当前节点的状态（但不是智能指针）拷贝到一个新的对象中，而新对象的生命周期由 std::shared_ptr 管理。\n使用 std::shared_ptr\u0026lt;TreeNode\u0026lt;T\u0026gt;\u0026gt; ptr(this) 把裸指针 this 包装为 shared_ptr。 将 this 传递给 std::shared_ptr\u0026lt;TreeNode\u0026lt;T\u0026gt;\u0026gt; 会导致新创建的 shared_ptr 管理一个裸指针，而裸指针的生命周期没有由智能指针控制。\n引出一个常规问题，从裸指针创建 shared_ptr 的隐患：\n当 shared_ptr 的引用计数归零时，它会释放它所管理的对象。如果裸指针在此时继续存在，它仍然会指向原来的内存地址。但这时该内存已被释放，裸指针成为了悬空指针，也就是所谓的野指针。 如果裸指针指向的内存已经被释放（例如，该指针原本由 delete 或 delete[] 释放），然后你用这个裸指针创建 shared_ptr，那么 shared_ptr 仍然会管理这个已经释放的内存区域。这会导致访问已释放内存（悬空指针）或双重释放内存的问题（如果 shared_ptr 销毁时再次释放内存）。 裸指针可能指向一个栈上的对象：如果裸指针指向一个栈上分配的对象，并且你用它创建 shared_ptr，那么 shared_ptr 会试图在引用计数归零时释放这个栈上对象的内存。然而，栈上对象的生命周期由栈帧的销毁来管理，而 shared_ptr 并不清楚这一点。这将导致程序的未定义行为。 案例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class TestB { public: TestB(){ cout \u0026lt;\u0026lt; \u0026#34;TestB create\u0026#34; \u0026lt;\u0026lt; endl; } ~TestB(){ cout \u0026lt;\u0026lt; \u0026#34;TestB destory\u0026#34; \u0026lt;\u0026lt; endl; } shared_ptr\u0026lt;TestB\u0026gt; getSharedFromThis() { return shared_ptr\u0026lt;TestB\u0026gt; (this); } }; int main(){ { shared_ptr\u0026lt;TestB\u0026gt; ptr3(new TestB()); shared_ptr\u0026lt;TestB\u0026gt; ptr4 = ptr3-\u0026gt;getSharedFromThis(); cout \u0026lt;\u0026lt; \u0026#34;ptr2 count: \u0026#34; \u0026lt;\u0026lt; ptr3.use_count() \u0026lt;\u0026lt; \u0026#34; ptr4 count: \u0026#34; \u0026lt;\u0026lt; ptr4.use_count() \u0026lt;\u0026lt; endl; //输出：ptr2 count: 1 ptr4 count: 1 然后会崩溃因为重复释放 } cin.get(); return 0; } 如何会导致shared_ptr指向同一个对象，但是不共享引用计数器？\n是因为裸指针与shared_ptr混用，如果我们用一个裸指针初始化或者赋值给shared_ptr指针时，在shared_ptr内部生成一个计数器，当另外一个shared_ptr不用share_ptr赋值或者初始化的话，再次将一个裸指针赋值给另外一个shared_ptr时，又一次生成一个计数器，两个计数器不共享。\nshared_ptr实现原理： shared_ptr 从 _Ptr_base 继承了 element_type 和 _Ref_count_base 类型的两个成员变量。\n1 2 3 4 5 6 7 8 9 10 template\u0026lt;class _Ty\u0026gt;class _Ptr_base { private: element_type * _Ptr{ ptr }; // 指向资源的指针 _Ref_count_base * _Rep{ ptr }; // 指向资源引用计数的指针 }; _Ref_count_base 中定义了原子类型的变量 _Uses 和 _Weaks，它们分别记录资源的引用个数和资源观察者的个数。\n1 2 3 4 5 6 class __declspec(novtable) _Ref_count_base { private: _Atomic_counter_t _Uses;//记录资源引用个数 _Atomic_counter_t _Weaks;//记录观察者个数 } 从 this 构造智能指针的正确做法 1 2 3 4 5 6 7 8 class MyClass: enable_shared_from_this\u0026lt;MyClass\u0026gt;//必须继承enable_shared_from_this { public: shared_ptr\u0026lt;MyClass\u0026gt; getself() { return shared_from_this(); } }; shared_from_this 是 C++11 中引入的功能，允许对象在继承了 std::enable_shared_from_this 的情况下，安全地生成自身的 std::shared_ptr 实例，而不会创建新的控制块（reference counting block）。这样可以避免悬垂指针的问题，特别是在对象的成员函数中使用时，可以确保对象在使用期间不被销毁。\nstd::enable_shared_from_this\u0026lt;T\u0026gt; 内部维护了一个 std::weak_ptr\u0026lt;T\u0026gt;。当第一个 std::shared_ptr\u0026lt;T\u0026gt; 开始管理该对象时，这个 weak_ptr 被初始化。之后，当 shared_from_this() 被调用时，它将基于这个已经存在的 weak_ptr 返回一个新的 std::shared_ptr\u0026lt;T\u0026gt;，这个新的 shared_ptr 与原有的 shared_ptr 共享对对象的所有权。\n1 2 3 4 5 6 7 8 9 shared_ptr\u0026lt;_Tp\u0026gt; shared_from_this() { return shared_ptr\u0026lt;_Tp\u0026gt;(this-\u0026gt;_M_weak_this); } shared_ptr\u0026lt;const _Tp\u0026gt; shared_from_this() const { return shared_ptr\u0026lt;const _Tp\u0026gt;(this-\u0026gt;_M_weak_this); } mutable weak_ptr\u0026lt;_Tp\u0026gt; _M_weak_this; 实践：\n实现这个 TreeNode 类的时候，shared_from_this 解析不出来(似乎是因为模板导致的 clangd 语法解析失败)。\n改为 this-\u0026gt;shared_from_this() 后报错消失，因为 shared_from_this 实际上是当前父类 enable_shared_from_this 的成员函数。\n最终实现：\n1 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 template \u0026lt;typename T\u0026gt; class TreeNode : public std::enable_shared_from_this\u0026lt;TreeNode\u0026lt;T\u0026gt;\u0026gt; { public: TreeNode(T value) : value(value) {} void setParent(std::weak_ptr\u0026lt;TreeNode\u0026lt;T\u0026gt;\u0026gt; parent) { this-\u0026gt;parent = parent; } void addChild(std::shared_ptr\u0026lt;TreeNode\u0026lt;T\u0026gt;\u0026gt; child) { child-\u0026gt;setParent(this-\u0026gt;shared_from_this()); children.push_back(child); } T getValue() const { return value; } std::shared_ptr\u0026lt;TreeNode\u0026lt;T\u0026gt;\u0026gt; getParent() const { return parent.lock(); } const std::vector\u0026lt;std::shared_ptr\u0026lt;TreeNode\u0026lt;T\u0026gt;\u0026gt;\u0026gt;\u0026amp; getChildren() const { return children; } private: T value; std::vector\u0026lt;std::shared_ptr\u0026lt;TreeNode\u0026lt;T\u0026gt;\u0026gt;\u0026gt; children; std::weak_ptr\u0026lt;TreeNode\u0026lt;T\u0026gt;\u0026gt; parent; }; ","date":"2025-01-09T00:00:00Z","permalink":"/posts/cpp-%E4%BB%8E%E5%9C%BA%E6%99%AF%E8%A7%A3%E6%9E%90-c++-shared_from_this/","title":"从场景解析 C++ shared_from_this"},{"content":"Redis 在 2.8.9 版本添加了 HyperLogLog （HLL）。\nHyperLogLog 是一种高效的基数估算工具，通过概率算法和哈希化技术，在常数空间内提供了基数的估算。\neg.\n统计一个网站的独立用户数。 统计一个日志中的独立 IP 数量。 计算一个流中的独立事件数。 传统的做法是将所有元素存储在集合中，然后进行去重、计数。但当集合的元素数量非常大时，这种方法会占用大量内存，甚至无法存储所有数据。\nHyperLogLog 有一定误差，但对于海量数据来说，它的内存开销极低且精度足够高，非常适合用于大数据处理、流量统计、去重计数等场景。\nRedis 中的 HyperLogLog 支持以下操作：\nPFADD：将元素添加到 HyperLogLog 中，Redis 会对元素进行哈希处理，并更新相应的桶。 PFCOUNT：返回一个或多个 HyperLogLog 键的基数估算。 PFMERGE：合并多个 HyperLogLog 键的基数估算。 核心思想 HyperLogLog 使用概率算法，通过哈希化数据并记录哈希值的前导零数量来估算基数。\n1. 哈希函数与二进制表示 为了将集合中的元素映射为哈希值，HyperLogLog 使用了 哈希函数。假设我们使用一个 m-bit 的哈希函数，它会把输入数据映射到一个包含 m 位二进制数字的哈希值。\n例如：\n假设我们将一个元素哈希成 10111001101100010101010101101010 这样的 32 位二进制数字。 这个哈希值的前导零数量就是我们关心的指标。对于这个例子，假设前导零的数量为 3。 2. 关键点：记录前导零的数量 HyperLogLog 并不直接存储每个哈希值，而是计算每个哈希值的前导零的数量，把这个值保存在一个桶（通过哈希值的某些位进行映射）中。\n3. 桶与桶编号 为了优化空间，HyperLogLog 使用多个桶来存储不同的哈希值。每个桶的索引是由哈希值的某些位生成的。假设我们有一个桶数量为 b 的 HyperLogLog。我们将哈希值的前 log2(b) 位作为桶的索引，其余的位用于计算前导零数量。\n例如，如果我们有 16 个桶（b = 16），则桶的索引由哈希值的前 4 位决定（因为 log2(16) = 4）。如果哈希值的前 4 位为 1100，那么该哈希值将被映射到第 12 号桶（因为 1100 二进制对应 12）。 4. 计算基数估算 HyperLogLog 会计算所有桶中记录的前导零最大值的平均值。然后根据这个平均值来估算整个数据集的基数。\n其数学公式如下：\n[ \\text{Estimate} = \\alpha \\times m^2 \\times 2^{\\bar{R}} ]\n其中：\n( \\alpha ) 是一个常数，通常为 0.7213 / (1 + 1.079 / m)，用来调整估算的误差。 ( m ) 是桶的数量。 ( \\bar{R} ) 是所有桶中的前导零数量的平均值。 5. 桶的数量和误差 桶的数量 ( m ) 直接影响 HyperLogLog 的估算精度。通常，桶的数量越多精度越高，但内存消耗也会相应增加。Redis 默认使用 14 个字节来存储 HyperLogLog，这大约可以提供 0.81% 的误差。\n详细讲解：Redis中的HyperLogLog以及HyperLogLog原理。\n","date":"2025-01-08T00:00:00Z","permalink":"/posts/redis-redis-%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B9%8B%E8%B6%85%E6%97%A5%E5%BF%97-hyperloglog/","title":"Redis 数据结构之超日志 HyperLogLog"},{"content":"(考察linux网络编程、系统编程、网络协议、网络传输协议等知识)\n问：局域网内有A、B、C三台主机，A与B不知道相互之间的IP。A要向B传输一个1G的文件，怎么做？\n大文件传输的优化： 分块传输：将大文件分成多个小块（如4KB、8KB等），每次传输一块，避免占用过多内存。 校验和（Checksum）：在每一块传输后进行数据校验，确保数据的完整性。 带宽控制：通过控制每次发送的数据量来避免一次性传输过多数据，控制网络负载。 断点续传的实现： 记录传输进度：客户端和服务器都需要记录已经成功传输的数据块或字节的位置。 支持断点请求：客户端在恢复传输时，应该告知服务器从哪个位置开始传输。 校验和和确认机制：每次传输数据块后，都应该进行确认，确保数据正确传送。 步骤：\n使用局域网广播发现B的IP地址 由于A和B的IP地址不直接已知，A可以通过局域网广播来找到B的IP地址。A可以向网络中的所有主机发送一个UDP广播消息，所有主机都会接收到这个消息，B在接收到这个广播后，可以回复A，告知自己的IP地址。 UDP广播：A可以通过发送一个UDP广播包到特定的端口，让局域网中的所有主机收到该消息。B可以通过监听这个端口，收到消息后回应自己的IP地址。 使用TCP协议进行文件传输 一旦A得到了B的IP地址，就可以使用TCP协议进行文件传输。A通过TCP连接到B，建立数据通道，开始发送1GB的文件。 1. UDP广播发现B的IP地址 A使用UDP广播向局域网中的所有主机发送请求，B收到请求后会通过UDP回应自己的IP地址。\nA端：发送UDP广播请求 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 #include \u0026lt;iostream\u0026gt; #include \u0026lt;cstring\u0026gt; #include \u0026lt;arpa/inet.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #define BROADCAST_PORT 12345 void send_broadcast_message() { int sockfd; struct sockaddr_in broadcast_addr; int broadcast_enable = 1; const char* message = \u0026#34;Are you there, B? Please reply with your IP address.\u0026#34;; // 创建UDP套接字 sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd \u0026lt; 0) { perror(\u0026#34;Socket creation failed\u0026#34;); return; } // 允许广播 if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, \u0026amp;broadcast_enable, sizeof(broadcast_enable)) \u0026lt; 0) { perror(\u0026#34;Setting broadcast option failed\u0026#34;); close(sockfd); return; } memset(\u0026amp;broadcast_addr, 0, sizeof(broadcast_addr)); broadcast_addr.sin_family = AF_INET; broadcast_addr.sin_port = htons(BROADCAST_PORT); broadcast_addr.sin_addr.s_addr = htonl(INADDR_BROADCAST); // 发送广播消息 if (sendto(sockfd, message, strlen(message), 0, (struct sockaddr*)\u0026amp;broadcast_addr, sizeof(broadcast_addr)) \u0026lt; 0) { perror(\u0026#34;Broadcast failed\u0026#34;); close(sockfd); return; } std::cout \u0026lt;\u0026lt; \u0026#34;Broadcast message sent!\u0026#34; \u0026lt;\u0026lt; std::endl; close(sockfd); } int main() { send_broadcast_message(); return 0; } B端：接收UDP广播并回应自己的IP 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 #include \u0026lt;iostream\u0026gt; #include \u0026lt;cstring\u0026gt; #include \u0026lt;arpa/inet.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #define BROADCAST_PORT 12345 #define RESPONSE_PORT 12346 void listen_for_broadcasts() { int sockfd; struct sockaddr_in server_addr, client_addr; socklen_t client_len = sizeof(client_addr); char buffer[1024]; // 创建UDP套接字 sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd \u0026lt; 0) { perror(\u0026#34;Socket creation failed\u0026#34;); return; } memset(\u0026amp;server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(BROADCAST_PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定UDP套接字 if (bind(sockfd, (struct sockaddr*)\u0026amp;server_addr, sizeof(server_addr)) \u0026lt; 0) { perror(\u0026#34;Bind failed\u0026#34;); close(sockfd); return; } // 接收广播消息 while (true) { int recv_len = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)\u0026amp;client_addr, \u0026amp;client_len); if (recv_len \u0026lt; 0) { perror(\u0026#34;Failed to receive message\u0026#34;); continue; } buffer[recv_len] = \u0026#39;\\0\u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#34;Received message: \u0026#34; \u0026lt;\u0026lt; buffer \u0026lt;\u0026lt; std::endl; // 如果消息包含特定请求，可以回复自己的IP地址 std::string response = \u0026#34;IP Address of B: \u0026#34;; response += inet_ntoa(client_addr.sin_addr); sendto(sockfd, response.c_str(), response.length(), 0, (struct sockaddr*)\u0026amp;client_addr, client_len); std::cout \u0026lt;\u0026lt; \u0026#34;Sent response with IP address: \u0026#34; \u0026lt;\u0026lt; inet_ntoa(client_addr.sin_addr) \u0026lt;\u0026lt; std::endl; } close(sockfd); } int main() { listen_for_broadcasts(); return 0; } 2. 使用TCP协议传输文件 一旦A得到了B的IP地址，A就可以通过TCP连接与B进行文件传输。\nA端：通过TCP向B发送文件 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 #include \u0026lt;iostream\u0026gt; #include \u0026lt;fstream\u0026gt; #include \u0026lt;cstring\u0026gt; #include \u0026lt;arpa/inet.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #define SERVER_PORT 8080 #define CHUNK_SIZE 4096 // 4KB per chunk void send_file(const std::string \u0026amp;filename, const std::string \u0026amp;ip_address) { int sockfd; struct sockaddr_in server_addr; char buffer[CHUNK_SIZE]; std::ifstream file(filename, std::ios::binary); off_t offset = 0; // 记录已经发送的文件偏移量 // 创建TCP套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd \u0026lt; 0) { perror(\u0026#34;Socket creation failed\u0026#34;); return; } memset(\u0026amp;server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = inet_addr(ip_address.c_str()); // 连接到B if (connect(sockfd, (struct sockaddr*)\u0026amp;server_addr, sizeof(server_addr)) \u0026lt; 0) { perror(\u0026#34;Connection failed\u0026#34;); close(sockfd); return; } // 发送文件的分块数据 while (file.read(buffer, CHUNK_SIZE)) { // 发送文件块数据 ssize_t bytes_sent = send(sockfd, buffer, file.gcount(), 0); if (bytes_sent \u0026lt; 0) { perror(\u0026#34;Send failed\u0026#34;); break; } offset += bytes_sent; // 发送每个块后，需要确认接收进度 // 可以通过协议的方式要求B确认 std::cout \u0026lt;\u0026lt; \u0026#34;Sent chunk, current offset: \u0026#34; \u0026lt;\u0026lt; offset \u0026lt;\u0026lt; std::endl; } // 发送文件剩余的数据（如果有） if (file.gcount() \u0026gt; 0) { send(sockfd, buffer, file.gcount(), 0); offset += file.gcount(); } std::cout \u0026lt;\u0026lt; \u0026#34;File sent successfully! Total bytes sent: \u0026#34; \u0026lt;\u0026lt; offset \u0026lt;\u0026lt; std::endl; close(sockfd); } int main() { send_file(\u0026#34;large_file.bin\u0026#34;, \u0026#34;192.168.1.2\u0026#34;); return 0; } B端：接收文件并保存 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 #include \u0026lt;iostream\u0026gt; #include \u0026lt;fstream\u0026gt; #include \u0026lt;cstring\u0026gt; #include \u0026lt;arpa/inet.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #define SERVER_PORT 8080 #define CHUNK_SIZE 4096 // 4KB per chunk void receive_file(const std::string \u0026amp;filename) { int sockfd, newsockfd; struct sockaddr_in server_addr, client_addr; socklen_t client_len = sizeof(client_addr); char buffer[CHUNK_SIZE]; std::ofstream file(filename, std::ios::binary | std::ios::app); // 以追加方式打开文件 off_t offset = file.tellp(); // 获取文件当前的偏移量（即已经接收的字节数） // 创建TCP套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd \u0026lt; 0) { perror(\u0026#34;Socket creation failed\u0026#34;); return; } memset(\u0026amp;server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定套接字 if (bind(sockfd, (struct sockaddr*)\u0026amp;server_addr, sizeof(server_addr)) \u0026lt; 0) { perror(\u0026#34;Bind failed\u0026#34;); close(sockfd); return; } // 监听 listen(sockfd, 1); std::cout \u0026lt;\u0026lt; \u0026#34;Waiting for connection...\u0026#34; \u0026lt;\u0026lt; std::endl; // 接受连接 newsockfd = accept(sockfd, (struct sockaddr*)\u0026amp;client_addr, \u0026amp;client_len); if (newsockfd \u0026lt; 0) { perror(\u0026#34;Accept failed\u0026#34;); close(sockfd); return; } // 接收文件 while (true) { int recv_len = recv(newsockfd, buffer, CHUNK_SIZE, 0); if (recv_len \u0026lt;= 0) break; // 写入文件（追加模式） file.write(buffer, recv_len); offset += recv_len; std::cout \u0026lt;\u0026lt; \u0026#34;Received chunk, current offset: \u0026#34; \u0026lt;\u0026lt; offset \u0026lt;\u0026lt; std::endl; } std::cout \u0026lt;\u0026lt; \u0026#34;File received successfully! Total bytes received: \u0026#34; \u0026lt;\u0026lt; offset \u0026lt;\u0026lt; std::endl; close(newsockfd); close(sockfd); } int main() { receive_file(\u0026#34;received_large_file.bin\u0026#34;); return 0; } 断点续传实现说明：\nA端：每发送完一个块，A会更新文件的偏移量（即offset），并传递该偏移量的信息。如果传输过程中发生中断，A可以记录上次发送的偏移量，从该位置开始重新传输。 B端：B端会在接收每个块时，记录接收到的字节数（即offset）。B端可以通过检查文件的大小来判断是否需要继续接收文件。如果B端关闭了连接，下次启动时会从文件尾部继续接收。 拓展： MD5（Message Digest Algorithm 5）是一种广泛使用的加密哈希函数，它产生一个128位（16字节）的哈希值，通常用32个十六进制字符表示。MD5被设计用来接收任意长度的数据（通常是文件或消息）并生成一个固定长度的“摘要”或“指纹”，这个摘要用于验证数据的完整性。\n","date":"2025-01-07T00:00:00Z","permalink":"/posts/linux-linux-%E5%A4%A7%E6%96%87%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%9C%BA%E6%99%AF%E9%A2%98/","title":"Linux 大文件传输场景题"},{"content":"分段锁（Segmented Locking）是一种用于优化多线程访问共享资源时锁粒度的技术。它通过将资源分成多个小段，并为每段分配独立的锁，来减少锁的争用，从而提升并发性能。\n分段锁通过减少锁粒度，让多个线程可以同时访问不同的段，从而显著提高性能。这种方法常见于 哈希表、数据库索引 和其他高并发系统中。\n基本原理 划分资源： 将容器划分为多个独立的段（segment），每段可以包含一部分数据。例如，一个哈希表可以按哈希值将数据分配到多个桶（bucket），每个桶代表一个段。\n独立加锁： 每个段都有一个独立的锁（如 std::mutex 或 std::shared_mutex），对该段的数据操作时，只需要锁定对应的段即可，其他段不受影响。\n映射规则： 通过某种映射规则（如哈希函数）将操作定位到特定的段。这种映射规则应尽可能均匀，以避免热点问题（即某些段过于频繁被访问，导致锁竞争）。\n适用场景 高并发读写：如多线程访问的大型哈希表、数据库索引。 热点数据分散：通过分段减少单点锁的争用，提升性能。 读多写少：可以结合 std::shared_mutex 提供共享锁和独占锁，进一步优化读性能。 注：负载不均风险：如果映射规则不合理，可能导致某些段成为热点(eg. 热点桶)，影响性能。\n下面通过一个线程安全的哈希表（ThreadSafeHashMap）来展示分段锁的实现(用std::vector简单模拟)。\n将哈希表分为多个桶（bucket），每个桶独立管理其数据。 使用哈希函数将键映射到对应的桶。 为每个桶分配一个 std::mutex 来保护数据。 对于读操作，只锁定对应的桶，支持并行读取。 对于写操作，也只锁定对应的桶，减少锁的范围。 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 #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;mutex\u0026gt; #include \u0026lt;shared_mutex\u0026gt; #include \u0026lt;thread\u0026gt; #include \u0026lt;functional\u0026gt; template \u0026lt;typename Key, typename Value\u0026gt; class ThreadSafeHashMap { private: struct Bucket { std::shared_mutex mtx; // 每个桶的独立锁 std::vector\u0026lt;std::pair\u0026lt;Key, Value\u0026gt;\u0026gt; data; }; std::vector\u0026lt;Bucket\u0026gt; buckets; size_t num_buckets; // 哈希函数，将键映射到对应的桶 size_t hash(const Key\u0026amp; key) const { return std::hash\u0026lt;Key\u0026gt;{}(key) % num_buckets; } public: ThreadSafeHashMap(size_t num_buckets = 16) : num_buckets(num_buckets) { buckets.resize(num_buckets); } // 插入操作，按桶分段加锁 void insert(const Key\u0026amp; key, const Value\u0026amp; value) { size_t index = hash(key); std::unique_lock\u0026lt;std::shared_mutex\u0026gt; lock(buckets[index].mtx); buckets[index].data.push_back({key, value}); } // 查找操作，按桶分段加锁 bool find(const Key\u0026amp; key, Value\u0026amp; value) { size_t index = hash(key); std::shared_lock\u0026lt;std::shared_mutex\u0026gt; lock(buckets[index].mtx); for (const auto\u0026amp; pair : buckets[index].data) { if (pair.first == key) { value = pair.second; return true; } } return false; } // 删除操作，按桶分段加锁 bool erase(const Key\u0026amp; key) { size_t index = hash(key); std::unique_lock\u0026lt;std::shared_mutex\u0026gt; lock(buckets[index].mtx); auto\u0026amp; bucket = buckets[index].data; for (auto it = bucket.begin(); it != bucket.end(); ++it) { if (it-\u0026gt;first == key) { bucket.erase(it); return true; } } return false; } }; int main() { ThreadSafeHashMap\u0026lt;int, std::string\u0026gt; map; // 多线程插入数据 std::thread t1([\u0026amp;]() { map.insert(1, \u0026#34;one\u0026#34;); }); std::thread t2([\u0026amp;]() { map.insert(2, \u0026#34;two\u0026#34;); }); std::thread t3([\u0026amp;]() { map.insert(3, \u0026#34;three\u0026#34;); }); t1.join(); t2.join(); t3.join(); // 查找数据 std::string value; if (map.find(2, value)) { std::cout \u0026lt;\u0026lt; \u0026#34;Found: \u0026#34; \u0026lt;\u0026lt; value \u0026lt;\u0026lt; std::endl; } // 删除数据 map.erase(2); return 0; } 像 Intel TBB 等并发库提供了更加高效的线程安全容器。\nTBB(Thread Building Blocks)是英特尔发布的一个库，全称为 Threading Building Blocks。TBB 获得过 17 届 Jolt Productivity Awards，是一套 C++ 模板库。\n1 2 3 4 5 6 sudo apt-get install libtbb-dev # Ubuntu/Debian sudo yum install tbb-devel # CentOS/Red Hat ./vcpkg install tbb conan install tbb brew install tbb ","date":"2025-01-01T00:00:00Z","permalink":"/posts/cpp-%E5%88%86%E6%AE%B5%E9%94%81%E6%8A%80%E6%9C%AF%E8%AF%A6%E8%A7%A3%E5%8F%8A-c++-%E5%AE%9E%E7%8E%B0/","title":"分段锁技术详解及 C++ 实现"},{"content":" 哈希冲突是指 不同的输入（通常是不同的键）通过哈希函数计算后，得到相同的哈希值并被映射到相同的桶或位置。这是哈希算法的一个固有问题，通常发生在哈希表中。\n为什么会有哈希冲突？ 有限的哈希空间：\n假设哈希函数将键映射到一个固定大小的数组中，哈希表的桶数有限，而键的数量可能很大（例如百万个不同的键），那么无论哈希函数设计得多么巧妙，都可能有多个键映射到同一个桶。\n哈希函数的碰撞：\n哈希函数的设计决定了如何将键映射到哈希表的桶中。如果哈希函数不足够“分散”键值，导致多个键的哈希值相同，就会产生冲突。即使两个键的实际值不同，它们也可能因为哈希函数的限制而得到相同的哈希值。\n键的分布不均匀：\n如果数据（即键）在哈希表中的分布不均匀，某些桶可能会有大量的键，而其他桶则几乎没有。这通常是由于选择的哈希函数无法均匀地分布键值，导致哈希冲突在某些桶中更加集中。\n哈希冲突的发生是不可避免的，因为：\n有限的输出空间： 哈希函数的输出通常是固定长度的（比如 32 位、64 位或更高），而实际的输入数据可以非常庞大。例如，输入可能是所有的整数、字符串或者更复杂的数据结构，数量远远超过了哈希值的种类。因此，总会有两个不同的输入数据被映射到相同的哈希值（即哈希冲突）。\n抽象数据类型： 对于复杂的数据类型（如对象、结构体、字符串等），设计一个完美的哈希函数是非常困难的。在某些情况下，即使设计了高效的哈希算法，也很难保证哈希值的完全均匀分布，因此冲突不可避免。\n如何处理哈希冲突？ 尽管哈希冲突不可避免，但我们可以采用多种方法来解决或减少冲突的影响：\n1. 链表法（Separate Chaining） 每个桶（哈希表的一个位置）存储一个链表，所有映射到相同哈希值的元素都放在这个链表中。虽然哈希冲突发生，但可以通过遍历链表来解决。\n优点：简单易懂，适用于动态扩容。 缺点：性能取决于链表的长度，如果链表较长，查找、插入、删除的时间复杂度会退化为 O(n)。 2. 开放地址法（Open Addressing） 在这种方法中，当哈希冲突发生时，程序会尝试在表中寻找另一个空的位置来存储数据。常见的解决方式包括：\n线性探测：检查当前位置之后的下一个位置，直到找到空位。\n二次探测：尝试检查当前位置之后的平方距离的其他位置，避免线性探测中可能出现的聚集问题。\n双重哈希：使用第二个哈希函数来决定探测的步长。\n优点：避免了链表法的额外内存开销。\n缺点：当哈希表装载过高时，查找效率会下降。\n3. 再哈希（Rehashing） 再哈希是通过扩展哈希表的大小并重新计算每个元素的哈希值来解决冲突。当哈希表装载因子过高时（即元素数量接近桶的数量），会触发再哈希过程。\n优点：能够有效减少冲突，提高性能。 缺点：再哈希时会涉及到大量的重新计算和内存分配，可能导致性能下降。 4. 使用平衡树（如红黑树） 在哈希表中，如果某个桶的冲突过多，可以使用红黑树（或者其他平衡二叉树）来存储冲突的元素，这样可以在每个桶内保持较好的查找、插入性能。红黑树的查找、插入、删除时间复杂度为 O(log N)。\n优点：比链表法更高效，能够提供对数时间的操作。 缺点：相比链表法，维护平衡树需要更多的时间和内存。 ","date":"2025-01-01T00:00:00Z","permalink":"/posts/cpp-%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%93%88%E5%B8%8C%E5%86%B2%E7%AA%81/","title":"简述如何解决哈希冲突？"},{"content":"在 C++ 中，当函数返回一个对象时，编译器通常需要进行对象的拷贝或移动操作。例如：\n1 2 3 4 5 SomeClass createObject() { SomeClass obj; // 设置 obj 的一些成员 return obj; // 返回一个对象 } 在没有优化的情况下，obj 被返回时，编译器可能会执行一次拷贝构造或移动构造操作，甚至可能是两次（先拷贝到临时对象，再从临时对象拷贝到目标变量）。这些额外的拷贝或移动操作会导致性能下降。\n为了减少这种不必要的开销，现代 C++ 编译器通常会进行优化，减少返回值时的拷贝或移动，使用如 RVO 和 NRVO 的优化策略。\n1. RVO（Return Value Optimization，返回值优化） RVO（Return Value Optimization，返回值优化），编译器可以直接在目标变量的位置构造返回值，减少不必要的对象拷贝和内存开销。\n1 2 3 4 SomeClass createObject() { SomeClass obj; // 局部对象 return obj; // 返回该对象 } 在没有优化的情况下，obj 被返回时，编译器可能会做两次操作：\n将 obj 拷贝或移动到一个临时对象中。 将临时对象拷贝或移动到调用者的目标变量。 RVO 的核心思想是，在函数返回临时对象时，编译器可以直接将返回值构造到调用者的接收变量中，而无需通过中间的临时对象进行拷贝或移动。\n1 2 3 int main() { SomeClass obj = createObject(); // RVO 优化将直接构造在 obj 中 } RVO 只适用于临时对象返回的场景，对于具名对象（有名称的局部对象），编译器一般不能直接应用 RVO。返回具名对象时，编译器会尝试应用 NRVO（Named Return Value Optimization，命名返回值优化），以减少不必要的拷贝或移动。\n1 2 3 4 SomeClass createObject() { SomeClass obj; // 具名局部变量 return obj; // 这里不能使用 RVO } 编译器行为：\nGCC/Clang：启用优化选项（如 -O2 或 -O3）时，编译器会自动应用 RVO 来优化返回临时对象的代码。 MSVC：在 Visual Studio 中，编译器会自动应用 RVO，并且它通常比 GCC 和 Clang 更早地进行这种优化。 2. NRVO（Named Return Value Optimization，命名返回值优化） NRVO 可以看作是 RVO 的一种扩展。\n它仅在返回的是具名对象时有效。具体来说，当函数返回一个具名的局部变量时，NRVO 允许编译器直接将该局部变量的位置“转移”到调用者的接收变量中，而不需要进行拷贝或移动。\n拓展 从函数中返回stl容器开销很大吗？\n禁用 NRVO 优化的情况下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct X { X() { puts(\u0026#34;X()\u0026#34;); } X(const X\u0026amp;) { puts(\u0026#34;X(const X\u0026amp;)\u0026#34;); } X(X\u0026amp;\u0026amp;)noexcept { puts(\u0026#34;X(X\u0026amp;\u0026amp;)\u0026#34;); } ~X() {puts(\u0026#34;~X()\u0026#34;);} }; X func() { X x; puts(\u0026#34;-----------\u0026#34;); return x; } int main() { auto result = func(); } 输出：\n1 2 3 4 5 6 7 X() ----------- X(X\u0026amp;\u0026amp;) ~X() X(X\u0026amp;\u0026amp;) ~X() ~X() 如果启用了 命名返回值优化（NRVO），编译器可以直接将 x 移动到返回值位置，而无需额外的构造操作。\n由于 x 是一个左值，标准情况下会调用拷贝构造函数，但在返回时，由于是函数返回值（即返回局部变量），且需要将返回值传递给 result（通过移动语义优化），C++ 编译器通常会选择移动构造。\n","date":"2024-12-23T00:00:00Z","permalink":"/posts/cpp-c++-%E7%BC%96%E8%AF%91%E5%99%A8%E8%BF%94%E5%9B%9E%E5%80%BC%E4%BC%98%E5%8C%96/","title":"C++ 编译器返回值优化"},{"content":" 数据结构 std::unordered_map：用于快速查找键值对。 双向链表：与哈希表结合实现 LRU 缓存。 淘汰策略 LRU (Least Recently Used)：删除最近最少使用的元素。 LFU (Least Frequently Used)：删除使用频率最低的元素。 FIFO (First In First Out)：删除最早进入缓存的元素。 并发处理 读写锁（如std::shared_mutex）：允许多个读者或一个写者。 线程安全容器：可以使用库（如 TBB 或 Folly）提供的线程安全容器。 高级优化 分片缓存：使用分片（sharding）将缓存划分成多个独立的部分，以减少锁争用。例如，使用键的哈希值对分片数量取模。 持久化：将缓存数据保存到磁盘（如使用 RocksDB 或 Redis），以便服务重启后恢复。 预加载：在服务启动时，预加载常用的数据到缓存中，减少冷启动时间。 分布式缓存：如果单机缓存不足，可以使用分布式缓存（如 Memcached 或 Redis）来扩展容量。 监控和调试：添加缓存命中率统计、日志记录和监控接口，以便分析性能和优化缓存策略。 ","date":"2024-12-21T00:00:00Z","permalink":"/posts/cpp-%E7%BC%93%E5%AD%98%E7%9A%84%E8%AE%BE%E8%AE%A1/","title":"缓存的设计"},{"content":" 数据类型 描述 存储范围/格式 示例 INT（整数型） 存储整数，有不同的字节大小来适应不同范围的整数 有TINYINT（1字节，范围 - 128到127）、SMALLINT（2字节，范围 - 32768到32767）、MEDIUMINT（3字节）、INT（4字节，范围 - 2147483648到2147483647）、BIGINT（8字节） age INT;，可以存储像25这样的年龄值 FLOAT和DOUBLE（浮点型） 用于存储带有小数部分的数值，FLOAT精度较低，DOUBLE精度较高 FLOAT单精度浮点数，大约7位有效数字；DOUBLE双精度浮点数，大约15位有效数字 price FLOAT;可以存储像9.99这样的价格值，对于更高精度的科学计算可能使用measurement DOUBLE; DECIMAL 精确的小数值存储，常用于金融等对精度要求极高的领域 格式为DECIMAL(M,D)，M是数字总位数，D是小数点后的位数 amount DECIMAL(10,2);可以精确存储像12345.67这样的金额，其中总共可以存储10位数字，小数点后2位 CHAR 定长字符串，存储固定长度的字符序列 定义时指定长度，如CHAR(10)，最多存储10个字符，不足部分用空格填充 code CHAR(5);可以存储像\u0026rsquo;ABCD \u0026lsquo;（注意后面有空格）这样的字符串 VARCHAR 可变长字符串，根据实际存储的字符长度占用空间 定义最大长度，如VARCHAR(255)，实际存储多长就占用多少空间加上1 - 2字节用于记录长度 name VARCHAR(50);可以存储像\u0026rsquo;John Doe\u0026rsquo;这样的名字，长度小于等于50个字符 TEXT 用于存储大量文本内容 有TINYTEXT、TEXT、MEDIUMTEXT和LONGTEXT，存储大小逐渐增大 description TEXT;可以存储一篇短文或者产品描述 BLOB 存储二进制大型对象，如图像、音频等 有TINYBLOB、BLOB、MEDIUMBLOB和LONGBLOB，存储大小逐渐增大 image BLOB;可以存储一张照片的二进制数据 DATE 存储日期，格式为YYYY - MM - DD 从1000 - 01 - 01到9999 - 12 - 31 birth_date DATE;可以存储像'2000 - 01 - 01\u0026rsquo;这样的出生日期 TIME 存储时间，格式为HH:MM:SS - start_time TIME;可以存储像'09:00:00\u0026rsquo;这样的开始时间 DATETIME 存储日期和时间，格式为YYYY - MM - DD HH:MM:SS 从1000 - 01 - 01 00:00:00到9999 - 12 - 31 23:59:59 order_time DATETIME;可以存储像'2024 - 01 - 01 10:30:00\u0026rsquo;这样的订单时间 TIMESTAMP 存储日期和时间戳，会受到时区影响 从1970 - 01 - 01 00:00:00 UTC到2038 - 01 - 19 03:14:07 UTC update_time TIMESTAMP;用于记录更新时间，在不同时区设置下可能会有变化 ","date":"2024-11-21T00:00:00Z","permalink":"/posts/database-%E8%A1%A8%E6%A0%BC%E5%B1%95%E7%A4%BA-mysql-%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/","title":"表格展示 MySQL 基础数据类型"},{"content":"图解Linux进程优先级 实时优先级用于实时应用程序，如硬实时任务和实时控制系统，而普通优先级用于非实时应用程序。\n实时进程：动态优先级为0-99的进程，采用实时调度算法调度。 普通进程：动态优先级为100-139的进程，采用完全公平调度算法调度。 Linux进程调度之完全公平调度（压箱底的干货分享）。完全公平调度，CFS (Completely Fair Scheduler) 。\nnice值：是用于调整普通进程优先级的参数。范围：-20-19。\n1 2 3 4 5 6 7 task_struct { ...... int prio; // prio（动态优先级） int static_prio;\t// static_prio（静态优先级） int normal_prio;\t// normal_prio（归一化优先级） unsigned int rt_priority; // rt_priority（实时优先级） }; prio（动态优先级）\n动态优先级，有效优先级，调度器最终使用的优先级数值，范围0-139，值越小，优先级越高。 static_prio（静态优先级）\n静态优先级，采用SCHED_NORMAL和SCHED_BATCH调度策略的进程（即普通进程）用于计算动态优先级的，范围100-139。 prio = static_prio = nice + DEFAULT_PRIO = nice + 120 normal_prio（归一化优先级）\n用于计算prio的中间变量，不需要太关心。 rt_priority（实时优先级）\n实时优先级，采用SCHED_FIFO和SCHED_RR调度策略进程（即实时进程）用于计算动态优先级，范围0-99。 prio = MAX_RT_PRIO - 1 - rt_prio = 100 - 1 - rt_priority; 实时优先级数值越大，得到的动态优先级数值越小，优先级越高。\nps -elf命令查看进程优先级。PRI：进程优先级，数值越小，优先级越高。（并非动态优先级）NI：nice值。\nSCHED_FIFO（先进先出调度）和SCHED_RR（时间片轮转调度），这些策略可以通过sched_setscheduler()系统调用（头文件\u0026lt;sched.h\u0026gt;）来设置：\n1 2 3 4 5 6 7 8 struct sched_param param; // 设置优先级为最高优先级 param.sched_priority = sched_get_priority_max(SCHED_FIFO); // 设置调度策略为SCHED_FIFO if (sched_setscheduler(getpid(), SCHED_FIFO, \u0026amp;param) == -1) { std::cerr \u0026lt;\u0026lt; \u0026#34;无法设置实时调度策略\u0026#34; \u0026lt;\u0026lt; std::endl; return 1; } ","date":"2024-11-04T00:00:00Z","permalink":"/posts/linux-linux-%E8%BF%9B%E7%A8%8B%E4%BC%98%E5%85%88%E7%BA%A7/","title":"Linux 进程优先级"},{"content":"Linux 文件系统 文件描述符(File Descriptor，FD)(win里一般称为文件句柄)是操作系统中用于标识和管理已打开文件或I/O资源的整数值。\n当进程请求打开一个文件或资源时，操作系统为该资源分配一个文件描述符，并将其返回给进程。进程随后使用该文件描述符来进行读写操作。 是一个进程级别的概念。\n继承：在创建子进程时，文件描述符可以在父子进程之间共享（例如通过 fork()）。 复制：通过系统调用 dup() 或 dup2() 可以复制文件描述符，使它们引用同一个文件或资源。 文件描述符不仅用于操作文件，还可以指向：\n管道（Pipes）：用于进程间通信。 套接字（Sockets）：用于网络通信。 设备文件：例如硬盘、串口等设备。 文件分配表(File Allocation Table, FAT)。\n何为文件索引节点?文件与磁盘的爱恨情仇。。\n阮一峰的个人网站。\ninode（索引节点）。理解inode。\n文件储存在硬盘上，硬盘的最小存储单位叫做\u0026quot;扇区\u0026quot;（Sector）。每个扇区储存512字节（相当于0.5KB）。 操作系统读取硬盘的时候，不会一个个扇区地读取，这样效率太低，而是一次性连续读取多个扇区，即一次性读取一个\u0026quot;块\u0026quot;（block）。这种由多个扇区组成的\u0026quot;块\u0026quot;，是文件存取的最小单位。\u0026ldquo;块\u0026quot;的大小，最常见的是4KB，即连续八个 sector组成一个 block。 文件数据都储存在\u0026quot;块\u0026quot;中，那么很显然，我们还必须找到一个地方储存文件的元信息，比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode，中文译名为\u0026quot;索引节点\u0026rdquo;。\n可以用stat命令，查看某个文件的inode信息。 inode也会消耗硬盘空间，所以硬盘格式化的时候，操作系统自动将硬盘分成两个区域。一个是数据区，存放文件数据；另一个是inode区（inode table），存放inode所包含的信息。 Unix/Linux系统内部不使用文件名，而使用inode号码来识别文件。\n超级块（Superblock）位于每个文件系统的开头，提供了操作系统如何解释和管理该文件系统的元数据。(记录各个inode?)\n软链接和硬链接：\n软链接是一个独立的文件，它包含了另一个文件或目录的路径。它类似于 Windows 系统中的快捷方式。 硬链接是文件的一个直接引用（或者说是指针），它与原文件共享相同的inode。删除原文件后，硬链接仍然有效，因为它直接引用了文件的数据；文件只有当所有硬链接都被删除后，数据才会被清除。inode信息中有一项叫做\u0026quot;链接数\u0026quot;，记录指向该inode的文件名总数。 linux软件更新过程：软件更新变得简单：系统通过inode号码，识别运行中的文件，不通过文件名。更新的时候，新版文件以同样的文件名，生成一个新的inode，不会影响到运行中的文件。等到下一次运行这个软件的时候，文件名就自动指向新版文件，旧版文件的inode则被回收。\nLinux 虚拟文件系统 linux I/O原理、监控、和调优思路。\n图解Linux虚拟文件系统(VFS)之关系篇。\n虚拟文件系统（Virtual File System，VFS）：它提供了一个统一的接口，使得用户和应用程序可以通过相同的方式访问不同类型的文件系统。\n通过VFS用户可以使用相同的系统调用（如open、read、write等）来访问不同类型的文件系统，包括本地文件系统（如ext4、XFS等）、网络文件系统（如NFS、CIFS等）以及虚拟文件系统（如procfs、sysfs等）。\nVFS由以下几个主要组件组成：\n虚拟文件系统接口：VFS定义了一组通用的文件系统操作接口。 超级块(super_block)：每个文件系统都有一个超级块，它包含了文件系统的元数据信息，如文件系统类型、块大小、inode表等，超级块提供了对文件系统的整体描述和管理。 目录项(dentry)：Directory Entry，用于表示文件系统中的目录和文件，dentry包含了目录和文件对应的inode指针、层级关系(parent)等。 dentry结构体的主要作用是提供文件系统层次结构的表示，它们通过形成一个树状结构来组织目录和文件，每个dentry都有一个唯一的路径名，可以通过遍历dentry树来找到特定文件或目录。\n1 2 3 4 5 6 7 8 9 10 11 struct dentry { struct dentry *d_parent; struct qstr d_name; struct inode *d_inode; const struct dentry_operations *d_op; struct super_block *d_sb; struct list_head d_child; struct list_head d_subdirs; .... }; 文件节点(inode)：inode是文件系统中的一个数据结构，用于存储文件或目录的元数据信息，如文件大小、权限、所有者等，每个文件或目录都对应一个唯一的inode。 文件对象(file)：file是表示打开文件的数据结构，它包含了对应的inode指针、当前读写位置等信息，通过file可以进行文件的读写操作。 索引节点(index node, inode)，用来记录文件的元数据，比如 inode 编号、文件大小、访问权限、修改日期、数据的位置等。索引节点和文件一一对应，它跟文件内容一样，都会被持久化存储到磁盘中。所以记住，索引节点同样占用磁盘空间。 目录项(directory entry, dentry)，用来记录文件的名字、索引节点指针以及与其他目录项的关联关系。多个关联的目录项，就构成了文件系统的目录结构。不过，不同于索引节点，目录项是由内核维护的一个内存数据结构，所以通常也被叫做目录项缓存。 ramfs是一种基于内存的文件系统。它将所有的文件数据存储在内存（RAM）中，而不是像传统的文件系统那样存储在磁盘等外部存储设备上。\n定义好文件系统后，通过register_filesystem函数将文件系统注册至Linux系统，注册成功的文件系统会插入全局文件系统链表，已注册的文件系统能够用来创建超级块（super block）。\n文件系统挂载：新文件系统生成一个挂载实例（struct mount），让新挂载实例和父文件系统的挂载实例建立父子关系。每个文件系统都有一个根目录，当索引一个文件路径进入到一个新的文件系统后，会从新的文件系统根目录开始索引。\nMemory Cgroup（内存控制组） Memory Cgroup 是 cgroup（控制组）的一个子系统，用于控制和限制进程组或任务组的内存使用。它是 Linux 内核提供的一种资源管理机制，主要目的是隔离和限制内存资源的使用，确保系统的稳定性和公平性。\n可以通过配置参数来设置内存使用的硬限制（memory.limit_in_bytes）。当进程组使用的内存达到这个硬限制时，会触发内存回收机制，如通过OOM - Killer（内存不足杀手）选择并终止进程组中的某些进程来释放内存。 会收集进程组的各种内存使用统计信息，包括rss（ Resident Set Size，实际驻留在内存中的内存大小）、cache（缓存内存大小）等。 ","date":"2024-11-04T00:00:00Z","permalink":"/posts/linux-%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E4%B8%8E%E8%99%9A%E6%8B%9F%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F/","title":"Linux 文件系统与虚拟文件系统"},{"content":"事务（Transaction）是数据库管理系统执行过程中的一个逻辑单位，它由一个或多个数据库操作组成，这些操作要么全部执行成功，要么全部不执行，以保证数据的一致性和完整性。\nACID 是事务的四个重要特性：\n原子性（Atomicity）;\n一致性（Consistency）;\n隔离性（Isolation）；不同的隔离级别：\n读未提交（Read Uncommitted）：最低的隔离级别。一个事务可以读取另一个未提交事务的数据。 可能会出现脏读（Dirty Read）的情况。 脏读是指一个事务读取了另一个尚未提交的事务修改的数据。eg.事务 T1 修改了一条记录但尚未提交，事务 T2 在这个时候读取了这条被修改的记录。如果 T1 后来回滚了，那么 T2 读取的数据就是无效的、“脏” 的数据。\n读已提交（Read Committed）： 可能会出现不可重复读（Non - Repeatable Read）的问题。 不可重复读是指在一个事务内，多次读取同一数据，由于其他事务的修改操作，导致每次读取的结果不同。\n可重复读（Repeatable Read）： 可能会出现幻读（Phantom Read）的情况。 当一个事务（T1）按照一定的条件进行数据读取操作时，第一次读取没有发现满足条件的某些行（记录）。但是在这个事务还没有结束的时候，另一个事务（T2）插入（或删除）了一些满足（或原本满足）T1 查询条件的行。当 T1 再次按照相同的条件进行读取时，就会发现比第一次读取时更多（或更少）的符合条件的行，这些 “凭空出现” 或 “突然消失” 的行就像 “幻觉” 一样，所以被称为幻读。\n串行化（Serializable）：最高的隔离级别。牺牲了数据库的并发性能。 持久性（Durability）。\n","date":"2024-11-04T00:00:00Z","permalink":"/posts/database-%E4%BA%8B%E5%8A%A1transaction%E7%9A%84%E5%9F%BA%E7%A1%80%E7%89%B9%E6%80%A7/","title":"事务 Transaction 的基础特性"},{"content":"现代C++的内存模型。\u0026ndash;神文\n自底向上理解memory_order。\n大白话C++之：一文搞懂C++多线程内存模型(Memory Order)。\n时钟周期也称为振荡周期，定义为时钟频率的倒数。时钟周期是计算机中最基本的、最小的时间单位。在一个时钟周期内，CPU仅完成一个最基本的动作。时钟周期表示了SDRAM所能运行的最高频率。\n如果没有Cache，CPU每执行一条指令，都要去内存取下一条，而执行一条指令也就几个时钟周期（几ns），而取指令却要上百个时钟周期，这将导致CPU大部分时间都在等待状态，进而导致执行效率低下。\nC++ 内存模型（Memory Model）定义了程序在多线程环境中如何访问和共享内存，它为程序的正确性、并发性和可移植性提供了保证。C++ 内存模型主要通过原子操作、内存序列（Memory Ordering）、同步和锁等机制来规范线程之间的内存访问行为。\nCPU 的五级流水线 CPU 将指令执行分解成5个部分，分别是：IF 取指令，ID 译码，EX 执行，MEM 访问内存，WB 写回。\n内存顺序模型 描述 memory_order_seq_cst 顺序一致(sequentially consistent ordering)，只有该值满足sC顺序一致性，原子操作默认使用该值。 memory_order_relaxed 松散(relaxed ordering) memory_order_consume 获取发布(acquire-release ordering) memory_order_acquire 获取发布(acquire-release ordering) memory_order_release 获取发布(acquire-release ordering) memory_order_acq_rel 获取发布(acquire-release ordering) 与编译器优化有关：\n1 2 3 4 5 6 7 //reordering 重排示例代码 int A = 0, B = 0; void foo() { A = B + 1; //(1) B = 1; //(2) } 1 2 3 4 5 6 7 8 // g++ -std=c++11 -O2 -S test.cpp // 编译器重排后的代码 // 注意第一句汇编，已经将B最初的值存到了 // 寄存器eax，而后将该eax的值加1，再赋给A movl B(%rip), %eax movl $1, B(%rip) // B = 1 addl $1, %eax // A = B + 1 movl %eax, A(%rip) 1 2 3 4 5 6 7 8 9 // Invention示例代码 // 原始代码 if( cond ) x = 42; // 优化后代码 r1 = x;// read what\u0026#39;s there x = 42;// oops: optimistic write is not conditional if( !cond)// check if we guessed wrong x = r1;// oops: back-out write is not SC 对于内存读写来说，读写顺序需要严格按照代码顺序，即要求如下（符号\u0026lt;p表示程序代码顺序，符号\u0026lt;m表示内存的读写顺序）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 顺序一致的要求 /* Load→Load */ /*若按代码顺序，a变量的读取先于b变量， 则内存顺序也需要先读a再读b 后面的规则同理。*/ if L(a) \u0026lt;p L(b) ⇒ L(a) \u0026lt;m L(b) /* Load→Store */ if L(a) \u0026lt;p S(b) ⇒ L(a) \u0026lt;m S(b) /* Store→Store */ if S(a) \u0026lt;p S(b) ⇒ S(a) \u0026lt;m S(b) /* Store→Load */ if S(a) \u0026lt;p L(b) ⇒ S(a) \u0026lt;m L(b) 顺序一致这么严格，其显然会限制编译器和CPU的优化，所以业界提出了很多宽松的模型，例如在X86中使用的TSO（Total Store Order）便允许某些条件下的重排。\nmemory_order_acquire：对于使用该枚举值的load操作，不允许该load之后的操作重排到load之前。\nmemory_order_release：使用该枚举值的store操作，不允许store之前的操作重排到store之后。\n现代C++（包括Java）都是使用了SC-DRF(Sequential consistency for data race free)。在SC-DRF模型下，程序员只要不写出Race Condition的代码，编译器和CPU便能保证程序的执行结果与顺序一致相同。因而，内存模型就如同程序员与编译器/CPU之间的契约，需要彼此遵守承诺。C++的内存模型默认为SC-DRF，此外还支持更宽松的非SC-DRF的模型。\nC++内存模型借鉴lock/unlock，引入了两个等效的概念，Acquire（类似lock）和Release（类似unlock），这两个都是单方向的屏障（One-way Barriers: acquire barrier, release barrier）。\n","date":"2024-10-09T00:00:00Z","permalink":"/posts/system-cpu-%E7%9A%84%E4%BA%94%E7%BA%A7%E6%B5%81%E6%B0%B4%E7%BA%BF/","title":"CPU 的五级流水线"},{"content":"思考了一下reinterpret_cast和强转的区别？这段非常易懂：\nC 语言的类型转换实际上包含各种转换方式，是 static_cast 跟 reinterpret_cast 等的父操作。\n一类是从逻辑意义上读取原有的值，然后到新的变量类型生成一个新值。（可以称为显式类型转换，简称显转）\n一类是完全保持原有值的内存表达方式，用新的变量类型来解读这段内存区域。（可以称为强制类型转换，简称强转）\n这两个用法实际的动作完全不同，但在 C 语言中是同一种写法。所以到了C++，就把前一种写法写成 static_cast，后一种写法写成 reinterpret_cast。\nreinterpret_cast 仅作用于编译时，可以保证不改变内存区域的内容。\ndynamic_cast：这是 C 里面不存在的转型方式，用来在带有虚函数的“动态对象”继承树里进行指针或引用的类型转换。比如，假设我们有对象基类 Shape 和派生类 Circle 和 Rectangle：如果有 Shape 指针 ptr，我们可以使用 dynamic_cast\u0026lt;Circle*\u0026gt;(ptr) 尝试把它转型成 Circle*。系统会进行需要的类型检查，并在转型成功时返回一个非空指针，返回空指针则表示失败（如当 ptr 实际指向的不是 Circle，而是 Rectangle）。\nstatic_cast：这是一种在很多认为较安全的场景下的“静态”转型方式。你可以使用它在不同的数值类型之间进行转换，如从 long 到 int，或者从 long long 到 double——当转换有可能有精度损失时，就不能使用隐式类型转换，而得明确使用转型了。你也可以使用它把一个 void* 转成一个实际类型的指针（如 int*）。你还可以用它把基类的指针转成派生类的指针，前提条件是你能确认这个基类的指针确实指向一个派生类的对象。显然，对于这最后一种场景 static_cast 不如 dynamic_cast 安全，但由于不需要进行运行期的检查，它的性能比 dynamic_cast 要高，在很多情况下是个空操作。\nconst_cast：这种转型方式潜在就不那么安全了。它的目的是去掉一个指针或引用类型的 const 或 volatile 修饰，如从 const char* 转换到 char*。这种转型的一种常见用途是把一个 C++ 的指针传递到一个 const 不正确的 C 接口里去，比如 C 接口在该用 const char* 时使用了 char*。注意这种转型只是为了“欺骗”类型系统，让代码能通过编译。如果你通过 const_cast 操作指针或引用去修改一个 const 对象，这仍然是错误的，是未定义行为，可能会导致奇怪的意外结果。\nreinterpret_cast：这是最不安全的对数据进行“重新解释”的转型方式，用来在不相关的类型之间进行类型转换，如把指针转换成 uintptr_t。这种转换有可能得到错误的结果，比如，在存在多继承的情况下，如要把基类指针转成派生类指针，使用 static_cast 和使用 reinterpret_cast 可能会得到不同的结果：前者会进行偏移量的调整，而后者真的只是简单粗暴的硬转而已，因此结果通常是错的。又如，根据 C++ 的严格别名规则，如果你用 char 或 byte 之外类型的指针访问并非该类型的对象（如通过 int* 访问 double 对象），会导致未定义行为。\ndynamic_cast 和 static_cast 都能用于继承的情况下，比较容易混淆：\ndynamic_cast 仅适用于多态类型（即具有虚函数的类）的转换。\n用途\n用于类继承层次间的安全向下转型（从基类指针/引用转换为派生类指针/引用）。 支持交叉转换（同一继承体系中不同分支的类之间的转换，如兄弟类转换）。 运行时检查类型安全性，若转换失败： 对指针返回nullptr； 对引用抛出std::bad_cast异常。 1 2 3 4 5 6 7 8 class Base { virtual void foo() {} }; class Derived : public Base {}; Base* pb = new Derived; Derived* pd = dynamic_cast\u0026lt;Derived*\u0026gt;(pb); // 安全转换，返回有效指针 Base* pb2 = new Base; Derived* pd2 = dynamic_cast\u0026lt;Derived*\u0026gt;(pb2); // 失败，返回nullptr static_cast 用途\n非多态类型转换：如基本数据类型转换（int→double）。\n上行转换（派生类→基类），效果与隐式转换相同。\n显式强制转换（如void*→具体类型指针）。\n不进行运行时检查。\n向下转型时若实际对象类型不匹配，可能导致未定义行为（如访问非法内存）。\n不支持交叉转换（编译报错）。\n1 2 3 4 5 int a = 5; double b = static_cast\u0026lt;double\u0026gt;(a); // 基本类型转换 Base* base_ptr = new Derived; Derived* derived_ptr = static_cast\u0026lt;Derived*\u0026gt;(base_ptr); // 不安全，假设base_ptr实际指向Derived对象 特性 dynamic_cast static_cast 安全性 运行时类型检查，失败返回nullptr或异常 无运行时检查，依赖程序员判断 适用场景 多态类的向下转型、交叉转换 非多态转换、上行转换、基本类型转换 虚函数要求 必须存在虚函数（RTTI依赖） 无要求 性能开销 较高（运行时类型查询） 无额外开销（编译时完成） 转换失败处理 指针返回nullptr，引用抛出异常 未定义行为（可能崩溃或数据损坏） 优先使用static_cast的场景\n类型转换明确安全（如上行转换或数值类型转换）。 需要高性能且能确保转换正确性时（如游戏开发中的内联优化）。 必须使用dynamic_cast的场景\n多态类的向下转型，尤其是无法确定基类指针实际指向的对象类型时。 需要避免因类型错误导致程序崩溃（如框架中动态加载插件）。 const_cast与reinterpret_cast： const_cast用于移除或添加const限定符。 reinterpret_cast用于无关类型之间的危险转换（如指针→整数），慎用。 ","date":"2024-09-28T00:00:00Z","permalink":"/posts/cpp-c++-%E7%9A%84%E5%9B%9B%E7%A7%8D%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2/","title":"C++ 的四种类型转换"},{"content":"tuple本身就是一种结构体，但是是一个模板类。利用形参包(Parameter pack)。C++ std::tuple的原理及简易实现，靠着模板元的递归实现的，相当抽象。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 template\u0026lt;typename...Args\u0026gt; struct tuple; // 当元组中没有元素时，递归结束 template\u0026lt;\u0026gt; struct tuple\u0026lt;\u0026gt; { constexpr tuple() noexcept = default; constexpr tuple(const tuple \u0026amp;) noexcept {}; constexpr tuple \u0026amp;operator=(const tuple \u0026amp;) = default; }; // 当元组中有一个或多个元素时，将第一个元素的类型分离出来，并通过继承，将剩下的元素作为另一个元组处理。 template\u0026lt;typename head, typename...Args\u0026gt; struct tuple\u0026lt;head, Args...\u0026gt; : tuple\u0026lt;Args...\u0026gt; { using base_ = tuple\u0026lt;Args...\u0026gt;; template\u0026lt;typename head_, typename...Args_\u0026gt; constexpr tuple(head_ \u0026amp;\u0026amp;val, Args_ \u0026amp;\u0026amp;...args) : head_val_(std::forward\u0026lt;head_\u0026gt;(val)), base_(std::forward\u0026lt;Args_\u0026gt;(args)...) {} tuple_val_\u0026lt;head\u0026gt; head_val_; }; 类模板实参推导（CTAD）(C++17 起)。\n","date":"2024-09-28T00:00:00Z","permalink":"/posts/cpp-c++-%E4%B8%AD-tuple-%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84/","title":"C++ 中 tuple 是如何实现的？"},{"content":"左值右值、函数传参： 传值（按值传递） 如果函数的参数是通过按值传递的，传入一个右值时，编译器会生成一个临时对象，并将该临时对象复制或移动到函数内部的局部变量中。 复制：对于不可移动的类型（例如基础类型 int），右值会被复制。 移动：对于可以移动的类型（例如拥有移动构造函数的类），右值将会被移动，从而避免复制的开销。移动操作是一个高效的浅拷贝操作，将资源的所有权从右值转移到函数内部的局部变量中。 传引用（按引用传递）\n2.1 传左值引用 void foo(const std::string\u0026amp; s); 当函数接受一个const 左值引用时，如果传入一个右值，编译器会生成一个临时对象并将它绑定到左值引用上。这时不会发生复制或移动，函数内部会直接使用右值的临时对象。这个临时对象的生命周期会被延长到函数结束。 2.2 传右值引用 void foo(std::string\u0026amp;\u0026amp; s); 当函数接受一个右值引用时，右值引用参数可以直接绑定到右值，因此不会发生复制。通常情况下，右值引用用于转移资源的所有权，函数内部可以自由地操作该右值引用的内存内容。 std::move 与 智能指针 std::move() 本身并不会移动数据，它只是将对象的左值强制转换为右值引用，从而允许对象使用移动构造函数或移动赋值运算符。实际的“移动”行为是在这些函数中实现的。\nstd::move() 可以传入普通指针（如 int*）。传入指针时并不会产生任何有实际意义的“移动”行为，由于指针只是指向某个内存地址的变量(而不负责管理资源)，所以“移动”一个指针只是简单地转移其地址值，并没有实际涉及资源的所有权转移。\n当 std::move 传入智能指针（如 std::unique_ptr 或 std::shared_ptr）时，与传入普通指针相比，它会产生实际的资源转移，这是智能指针的移动语义带来的结果。\nstd::unique_ptr 是独占所有权的智能指针，意味着它独自管理动态分配的对象。不能复制 std::unique_ptr，但可以通过移动将所有权转移给另一个 std::unique_ptr。被移动的 std::unique_ptr 会变为 nullptr。 std::move + std::shared_ptr：将 std::shared_ptr 的引用计数和所有权从一个对象转移到另一个。被移动的 shared_ptr 变为 nullptr，但原来共享的资源只会在最后一个 shared_ptr 销毁时释放。 1 2 3 4 5 void process(std::unique_ptr\u0026lt;int\u0026gt;\u0026amp;\u0026amp; ptr) { std::cout \u0026lt;\u0026lt; \u0026#34;Value: \u0026#34; \u0026lt;\u0026lt; *ptr \u0026lt;\u0026lt; std::endl; } std::unique_ptr\u0026lt;int\u0026gt; p1 = std::make_unique\u0026lt;int\u0026gt;(42); process(std::move(p1)); // 使用 std::move 将所有权转移给 process 函数 当 std::unique_ptr 作为参数传入函数时：\n如果 unique_ptr 是左值并传递到需要按值接收 unique_ptr 的函数，会导致编译错误，因为 unique_ptr 不支持拷贝。 按左值引用传递不会转移所有权，传入的 unique_ptr 仍保持有效。 如果需要转移所有权，通常使用右值引用或者 std::move 将其显式地转换为右值。 之前把函数对象/仿函数当做是麻烦版本的lamada，看书里的用法才知道其灵活性。相当于一个方便管理变量的函数。\n","date":"2024-09-19T00:00:00Z","permalink":"/posts/cpp-%E5%AF%B9-c++-%E5%B7%A6%E5%80%BC%E5%8F%B3%E5%80%BC%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88%E7%9A%84%E6%80%9D%E8%80%83/","title":"对 C++ 左值、右值、智能指针的思考"},{"content":"一篇文章学完 Effective Modern C++：条款 \u0026amp; 实践：\n条款1： 模板参数类型推导，引用折叠 1 2 3 4 5 6 7 8 9 10 11 12 13 14 template\u0026lt;typename T\u0026gt; void f(T\u0026amp;\u0026amp; param); int x = 27; const int cx = x; const int\u0026amp; rx = x; // 左值的情况 f(x); // T 的类型为 int\u0026amp;, paramType 为 int\u0026amp; f(cx); // T 的类型为 const int\u0026amp;, paramType 为 const int\u0026amp; f(rx); // T 的类型为 const int\u0026amp;, paramType 为 const int\u0026amp; // 右值的情况 f(27) // T 的类型为 int, paramType 为 int\u0026amp;\u0026amp; 对于指向 const 对象的 const 指针的传递，仅有指针本身的常量性会被忽略：\n1 2 3 4 5 6 template\u0026lt;typename T\u0026gt; void f(T param); const char* const ptr = \u0026#34;Fun with pointers\u0026#34;; f(ptr); // T 和 param 的类型均为 const char* 按值传递给函数模板的数组类型将退化为指针类型，但按引用传递却能推导出真正的数组类型：\n1 2 3 4 5 6 template\u0026lt;typename T\u0026gt; void f(T\u0026amp; param); const char name[] = \u0026#34;J. P. Briggs\u0026#34;; f(name); // T 的类型为 const char[13], paramType 为 const char (\u0026amp;)[13] 利用声明数组引用这一能力可以创造出一个模板，用来推导出数组含有的元素个数：\n1 2 3 4 5 6 7 template\u0026lt;typename T, std::size_t N\u0026gt; constexpr std::size_t arraySize(T (\u0026amp;)[N]) noexcept { return N; } // constexpr 函数，表示这个函数可以在编译时计算结果 int arr[10]; std::size_t size = arraySize(arr); // size 的值是 10 函数类型同样也会退化成函数指针，并且和数组类型的规则类似：\n1 2 3 4 5 6 7 8 9 10 void someFunc(int, double); template\u0026lt;typename T\u0026gt; void f1(T param); template\u0026lt;typename T\u0026gt; void f2(T\u0026amp; param); f1(someFunc); // param 被推导为函数指针，具体类型为 void (*)(int, double) f2(someFunc); // param 被推导为函数引用，具体类型为 void (\u0026amp;)(int, double) 条款2： auto类型推导 条款3：理解 decltype 在 C++11 中，decltype的主要用途是声明返回值类型依赖于形参类型的函数模板，这需要用到返回值类型尾置语法(trailing return type syntax)：\n1 2 3 4 5 template\u0026lt;typename Container, typename Index\u0026gt; auto authAndAccess(Container\u0026amp; c, Index i) -\u0026gt; decltype(c[i]) { authenticateUser(); return c[i]; } C++11 允许对单表达式的 lambda 的返回值实施类型推导，而 C++14 将这个允许范围扩张到了一切函数和一切 lambda，包括那些多表达式的。这就意味着在 C++14 中可以去掉返回值类型尾置语法，仅保留前导auto。\n但编译器会为auto指定为返回值类型的函数实施模板类型推导，这样就会留下隐患（例如忽略初始化表达的引用性），使用decltype(auto)来说明我们采用的是decltype的规则，就可以解决这个问题：\n1 2 3 4 5 template\u0026lt;typename Container, typename Index\u0026gt; decltype(auto) authAndAccess(Container\u0026amp; c, Index i) { authenticateUser(); return c[i]; } 在初始化表达式处也可以应用decltype类型推导规则：\n1 2 3 4 Widget w; const Widget\u0026amp; cw = w; auto myWidget1 = cw; // auto 推导出类型为 Widget decltype(auto) myWidget2 = cw; // decltype 推导出类型为 const Widget\u0026amp; 在上述情形中，我们无法向函数传递右值容器，若想要采用一种既能绑定到左值也能绑定到右值的引用形参，就需要借助万能引用，并应用std::forward（参考条款 25）：\n1 2 3 4 5 template\u0026lt;typename Container, typename Index\u0026gt; decltype(auto) authAndAccess(Container\u0026amp;\u0026amp; c, Index i) { authenticateUser(); return std::forward\u0026lt;Container\u0026gt;(c)[i]; } ","date":"2024-09-01T00:00:00Z","permalink":"/posts/cpp-morden-c++-%E6%A8%A1%E6%9D%BF%E7%B1%BB%E5%9E%8B%E6%8E%A8%E5%AF%BC/","title":"C++ 模板类型推导"},{"content":"三五法则（Rule of Three/Five/Zero）。\n“三法则”主要适用于 C++98/03 标准下的资源管理。在使用动态内存或其他资源时，如果类需要显式地管理资源，通常需要实现以下三个特殊成员函数：\n拷贝构造函数（Copy Constructor）：用于复制对象时分配新资源。 拷贝赋值运算符（Copy Assignment Operator）：用于对象赋值时释放旧资源并分配新资源。 析构函数（Destructor）：用于对象销毁时释放资源。 随着 C++11 引入了移动语义和右值引用，\u0026ldquo;五法则\u0026quot;扩展了“三法则”，增加了两个新的特殊成员函数：\n移动构造函数（Move Constructor）：用于移动对象时“窃取”资源，而不是复制。 移动赋值运算符（Move Assignment Operator）：用于对象赋值时“窃取”资源，而不是复制。 ","date":"2024-08-26T00:00:00Z","permalink":"/posts/cpp-c++-%E7%9A%84%E4%B8%89%E4%BA%94%E6%B3%95%E5%88%99%E6%98%AF%E4%BB%80%E4%B9%88/","title":"C++ 的三五法则是什么？"},{"content":"C++17编译期if：constexpr。\n用例：不加constexpr会编译出错，因为必有一种情况是语法错误的。如果T为X类型，则内部没有y_func()。\n1 2 3 4 5 6 7 8 9 template\u0026lt;typename T\u0026gt; void f(T t) { // 判断类型 if constexpr (std::is_same_v\u0026lt;T, X\u0026gt;) { t.x_func(); } else { // 此处若为 \u0026#34;舍弃语句\u0026#34;，不会参加编译。但会检查语法错误(但不会检查模板的实例化)。而预处理器if(#if)如果舍弃，完全不检查。 t.y_func(); } } 返回类型推导：C++14后可以用auto作为函数返回值，但所有表达式必须推导出相同的返回类型(不能在不同情况下返回不同的类型，例如int和float)。但如果在判断的地方使用constexpr，能通过编译(因为是在编译期判断的)。\n1 2 3 4 5 6 7 auto func() { if constexpr (...) { return 1.0f; } else { return 0; } } ","date":"2024-08-10T00:00:00Z","permalink":"/posts/cpp-c++-17-%E7%BC%96%E8%AF%91%E6%9C%9F-if/","title":"C++ 17 编译期 if"},{"content":"\n这两个月学习了一下OpenGL。从Cherno的教学视频开始学习，看完后开始看LearnOpenGL，应该是很常见的学习路径。\n在此以新手视角，记录一下学习中在工程方面遇到的一些坑（数学和底层方面就不打算开口丢人了）。\nCherno主页 | LearnOpenGL\n1. 直接选择 64 位 Cherno视频是2017及之前的，为了兼容性，教程里32位。而LearnOpenGL写到后面是64位，还要用Assimp库，默认是编译成64位。建议直接x64，像我这样闷头跟着写的话要把 GLEW 和 GLFW 的静态库全换一遍，或者去折腾CMake。\n2. GLEW, GLAD, GLFW 这三个比较常用。两个教程的选择都是 GLEW + GLFW，其中 GLEW 和 GLAD 定位相似，都是用于访问OpenGL函数。可以先看看自己喜欢哪一个，免得后面想换再费功夫。\n3. Texture 的实现 \u0026ndash; 小心析构函数 LearnOpenGL中的Texture只是一个存储数据的结构体：\n1 2 3 4 5 struct Texture { GLuint id; string type; aiString path; }; 而Cherno将Texture创建为类，构造函数中直接完成加载图片的操作，并且在析构函数里调用glDeleteTextures。\n如果无脑缝代码就完蛋了，因为LearnOpenGL在Model::loadMaterialTextures函数中创建了Texture的临时对象并返回，会调用析构函数：\n1 2 3 4 5 6 vector\u0026lt;Texture\u0026gt; loadMaterialTextures(aiMaterial* mat, aiTextureType type, string typeName) { vector\u0026lt;Texture\u0026gt; textures; // ... return textures; } 可以选择：\n修改Texture类的实现（比如把）glDeleteTextures单独调用； 修改Model类中加载纹理的实现，例如传入Texture的引用； 使用指针。我选择了使用智能指针（相对应的地方全要改）： 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 // 顺便把参数改成 `aiTextureType`(Assimp定义的用于表示Texture不同类型的枚举) // 优化掉LearnOpenGL里那个丑陋的字符串处理 std::vector\u0026lt;std::shared_ptr\u0026lt;Texture\u0026gt;\u0026gt; Model::loadMaterialTextures(aiMaterial* mat, aiTextureType type) { std::vector\u0026lt;std::shared_ptr\u0026lt;Texture\u0026gt;\u0026gt; textures; for (GLuint i = 0; i \u0026lt; mat-\u0026gt;GetTextureCount(type); i++) { aiString str; mat-\u0026gt;GetTexture(type, i, \u0026amp;str); bool canSkip = false; for (int j = 0; j \u0026lt; this-\u0026gt;textures_loaded.size(); j++) { if (textures_loaded[j]-\u0026gt;path == str) { textures.push_back(textures_loaded[j]); canSkip = true; break; } } if (!canSkip) { std::string filename = std::string(str.C_Str()); filename = directory + \u0026#39;/\u0026#39; + filename; std::shared_ptr\u0026lt;Texture\u0026gt; texture = std::make_shared\u0026lt;Texture\u0026gt;(filename); // 教程里此处调用了TextureFromFile()来初始化texture，但可以用Texture的构造函数 texture-\u0026gt;type = type; texture-\u0026gt;path = str; textures.push_back(texture); this-\u0026gt;textures_loaded.push_back(texture); } } return textures; } 同理，小心其他类里的析构函数（例如Shader类可能会在析构里调用glDeleteProgram）。\n","date":"2024-07-29T00:00:00Z","permalink":"/posts/opengl-%E8%8F%9C-opengl-%E5%88%9D%E5%AD%A6%E7%AC%94%E8%AE%B0--cherno-+-learnopengl/","title":"OpenGL 初学笔记 -- Cherno + LearnOpenGL"},{"content":" C++ 一篇文章学完 Effective Modern C++：条款 \u0026amp; 实践。\n深入理解C++内存管理：指针、引用和内存分配。\nCPU的核心数和线程数量是什么关系？ - texttime vage的回答 - 知乎。\n合集·现代 C++ 语言核心特性解析。\n现代 C++ 教程：高速上手 C++ 11/14/17/20。\n小鹏的教程：合集·现代C++项目实战。\n校招C++大概学习到什么程度？ - 程序员内功修炼的回答 - 知乎。八股文组合拳。\n陈皓（左耳朵耗子）的一些博客：\nC++ 虚函数表解析。\nC++ 对象的内存布局（上）。\n跟我一起写 Makefile（一）。\nTCP 的那些事儿（上）。\nC++那些事 CPlusPlusThings。\nC++那些事 中文。\n现代C++教程 2023。\nCUDA C++ Programming Guide。\n如何成为linux服务端C++开发专家? - 不谙世事的吴同学的回答 - 知乎。\nc++17 多态内存资源(PMR)。\n内存管理：设计Arena。\n这本书加深了我对C++的理解 | C++ Core Guidelines解析。\n深入理解Asan:内存错误检测工具与实践。\n为什么C/C++等少数编程语言要区分左右值？ - 腾讯技术工程的回答 - 知乎。非常牛逼。讲得很深，从C语言的发展、寄存器讲起。\n13.纤程（Fiber）与协程（Coroutine）。\n深入并发之线程、进程、纤程、协程、管程与死锁、活锁｜详解。\n从操作系统内存管理来说，malloc申请一块内存的背后原理是什么？ - 编程指北的回答 - 知乎。\n深入理解程序的结构。\n快速理解 .bss、.data和.rodata。 bss,data,text,rodata,堆,栈,常量段。\n有哪些优秀的 C/C++ 开源代码框架？这些框架的设计思路是怎样的？ - 南山烟雨珠江潮的回答 - 知乎。\nC++内存模型：从C++11到C++23 - Alex Dathskovsky - CppCon 2023。\nC++设计模式入门。\nLinux系统调试篇——核心转储(core dump)。\n编译过程-动画演示。\n项目 / 库 webserver面试题汇总。\n从面试角度重新看c++11的Webserver。\nlinux服务器编程：从epoll升级到io_uring。 Swoole v6 将引入 Linux io_uring ，并发读写文件性能提升了 5 倍。\n知名开源小线程池：thread-pool。\n基于C++11实现线程池 - Skykey的文章 - 知乎。\nC++项目：如何找合适的C++项目给自己的简历加分？ - 大糖的回答 - 知乎。\n游双《Linux高性能服务器》springsnail（一个简易的四层负载均衡服务器，只提供least connection最小连接数算法，以进程池提供原动力）、LVS（linux virtual server，一个成熟的四层负载均衡服务器，提供了3种工作模式和10种负载均衡算法等等）\n深入探索libevent网络库的内部实现机制。\nC++Linux进阶项目分析-仿写Redis之Qedis，深入掌握C++Linux必备的Redis技术栈。\nvectorDB：有什么高质量c++练手项目推荐嘛？ - zlatan的回答 - 知乎。\n合集·CheatEngine源码探究。\n雅兰亭库, 是一个非常现代的c++库(阿里写的), 除了反射 还有协程库、RPC库\u0026hellip;\nLinux C++项目推荐：WebFileServer文件服务器+如何快速上手C++大项目。\n小而美的C++项目推荐：缓存系统。\n\u0026ldquo;全球最强\u0026rdquo; | C++ 动态反射库。\nC++后台开发实战-高性能异步RPC框架。\n三个比较小的C++项目与简历内容 - 严格鸽的文章 - 知乎。json解析器，跳表，线程池。\nworkspace是基于C++11的轻量级异步执行框架。一个开源的线程池。\n这是前身的教程：01 Hipe_C++线程池框架_简介（更新）。\nnetty源码看不懂？试着写一个吧。\n深入了解QT消息循环及线程相关性Froser。\nspdlog库笔记汇总。\n黑马C++项目之分布式服务器编程。\n面经 百度面经。C++ 找工作校招需要掌握到什么程度？ - 阿biu的回答 - 知乎。\nC/C++高频面试题：内存泄漏的原因、检测、解决方案。\n腾讯天美C++后端三次面试。\n一个失败者的秋招面经。\n腾讯一面：malloc是如何分配内存的，free怎么知道该释放多少内存？。\n字节跳动C++二面：手撕shared_ptr。\n华为海思C++一面：手撕线程池~源码分享。\n腾讯、百度C++二面：手撕定时器实现、附实现源码~\nmomenta内推momenta面经 Go。\n从面试角度重新看c++11的Webserver。\n大厂面试系统设计题：如何设计一个红包雨系统？\n小林coding图解系统。\n字节一面：TCP 和 UDP 可以使用同一个端口吗？ - 小林coding的回答 - 知乎。\n字节一面：TCP 和 UDP 可以使用同一个端口吗？ - 车小胖的回答 - 知乎。一些非常刁钻的面试题。答案是可以，有点意外。\n腾讯面试：那些腾讯面试过的MySQL场景问题 - 王中阳讲编程的文章 - 知乎。\n美团C++面经 - 泸沽寻梦的文章 - 知乎。\n内存泄漏的通用排查方法。\n天美一面 后台开发（凉） - 牛客面经的文章 - 知乎。\nC++面试题个人总结（2023-5）。里面有一些听说不准确。\nLinux系统面试题汇总，纯八股文~\n几千HC下的大厂暑期实习锐评（一）。\n数学/算法 【官方双语/合集】线性代数的本质 - 系列合集。\n【从入门到放弃】线性回归。\n《数值分析》| 华科 | 研究生基础课。\nPaul\u0026rsquo;s Online Math Notes。\n你还不懂ZIP压缩的原理？一条视频讲清楚ZIP算法中的LZ77编码。\n哈夫曼编码很难懂？一条视频讲清楚。\nJPEG 有损压缩 离散余弦变换 DCT 一条视频讲清楚。\n模型分割后处理 加速 极致优化 4 矩阵乘法优化。\n图解世界上最快的排序算法：Timsort。\n底层 / 四大件 深入GPU硬件架构及运行机制。\nHPC(高性能计算第一篇):一文彻底搞懂并发编程与内存屏障。\n基础软件开发新坑 \u0026ndash; 神秘的MESI和坑爹的LockFree（一）。\nMIT 6.033 Spring 2021: Computer System 计算机系统。\n讲得很清晰的计网概述：详细剖析分布式微服务架构下网络通信的底层实现原理（图解）。\n合集·从零开始自制操作系统。\n【合集】MIT 6.828: Operating System Engineering [Fall 2014] (无字幕)。\n面试必考题：笨叔总结的DMA Cache用法1。\n我想写一个demo级别的编译器，我是该用C语言实现还是用nodejs实现？ - 南山烟雨珠江潮的回答 - 知乎。南山对编译器入门的推荐。\n深入理解Linux的TCP三次握手。\n从Linux系统函数角度，讲得比较深。\n有哪些讲源码的书籍？ - 南山烟雨珠江潮的回答 - 知乎。\n一文看懂 | 什么是页缓存（Page Cache）。\n一文读懂Linux内存管理。\n一口气搞懂【Linux内存管理】，就靠这60张图、59个问题了。\nGo uber出的go的指南guide。\nGo语言圣经（中文版）。\nGo Developer Roadmap。\n【Go手写RPC框架】。\n如何在go语言中实现高并发的RPC框架。\nGo 语言中的零拷贝优化。\n有没有推荐的golang的练手项目？ - 极客兔兔的回答 - 知乎。\n字节跳动 Go RPC 框架 KiteX 性能优化实践。\nleveldb的优秀博客。\n使用Golang实现Tcp反向代理服务器。\n使用Golang实现内网端口映射。\n合集·大厂Go面经系列。\n01 | IAM系统概述：我们要实现什么样的 Go 项目？\nGo 组件：context 学习笔记。\ngodis。Go 语言实现的 Redis 服务器和分布式集群。\n基于Golang所开发的大模型API高性能调度平台。\nnes，这是一个用Golang编写的NES模拟器。可以看到如何用Golang编写一个模拟器，以及如何用Go模拟CPU和GPU。\n面试别再商城项目博客项目啦！双非学员用【分布式AI微服务Golang项目】已经上岸了！。\n数据库 面试被经常问的SQL窗口函数！\n课程：Redis - 大厂程序员是怎么用的。掘金的字节课。\n为什么分布式一定要有redis?\nGo 实战项目 rosedb 源码剖析 1—架构原理。\nrosedb github。\n2024年吃透MySQL数据库（MySQL高级优化+索引调优+SQL调优+经典面试题一站式掌握）。\n2024吃透数据库MySQL+Redis缓存+分库分表实战，1000分钟数据库面试高质量教程！\n一些想看的场景题。\n分布式数据库与集中式数据库区别详解！\nweb 通识 25 | 认证机制：应用程序如何进行访问认证？讲得非常好。\ncookie、localStorage和sessionStorage三者的区别。\n微服务 / 分布式 / rpc / 流计算 / 云计算 5种微服务注册中心如何选型？从原理给你解读！\n解析消息队列（Kafka版本）。\n全B站最好懂的云计算入门课（Azure）\n分布式 ID 详解 CSDN。 一文读懂“Snowflake（雪花）”算法 腾讯云。\n‘分布式事务‘ 圣经：从入门到精通，架构师尼恩最新、最全详解 (50+图文4万字全面总结 )。\n云原生灰度更新实践。\nk8s灰度更新_k8s实现灰度发布。\n未读。\n深入浅出讲解 MapReduce。\nHDFS原理 | 一文读懂HDFS架构与设计。\n分布式系统基础：\nCAP定理与BASE理论（理解分布式系统设计的核心约束） 共识算法（Raft/Paxos → 实现自己的简易版本） 分布式事务（2PC/TCC/SAGA → 结合具体框架如Seata） 服务发现与负载均衡（ZooKeeper/etcd → 实现服务注册中心） 分布式锁与时钟同步（Redlock算法与NTP协议） 云计算核心：\n虚拟化技术（KVM/QEMU → 尝试手动创建虚拟机） 容器化演进（Docker原理 → 手动构建镜像并分析层级结构） 编排系统（Kubernetes架构 → 重点掌握Pod调度策略） Serverless范式（冷启动问题与函数计算优化） 云原生生态（Service Mesh/Istio的sidecar模式） 架构 性能追击：30+图详解8大主流服务器程序线程模型展示。\n字节跳动开源 Go HTTP 框架 Hertz 设计实践。\n大厂面试系统设计题：如何设计一个红包雨系统？\n从0到10亿，微信后台架构及基础设施设计与实践！\nTA / 引擎 / 游戏 转行技术美术（TA）的分享。\nTA技术美术（偏T)学习规划。\n我是一名前端，部门想让我转webgl方向，要不要考虑一下？ - Jhohkkk的回答 - 知乎 \u0026ndash; 图程路线。\n光线追踪无痛入门。\nRay Tracing: The Next Week V3.0中文翻译（上）。\nRay Tracing: The Next Week V3.0中文翻译（下）。\n游戏研发秋招 经验信息分享帖。\naudiokinetic。音频开发。\n在shader中实现五种描边方法。\nunreal engine Mesh Drawing Pipeline。\n游戏制作的窄门：构建mini游戏引擎 - 1 - Third Party Libs。\n音频库FMOD。\nFMOD。远不仅是音频库，还有引擎内核的。\n花了一年半时间写的玩具离线渲染器，用来学习并实现各种渲染算法。\n【搬运】Hazel 3D游戏引擎开发教程#001。\n用C++打造超强物理引擎 - 模拟机械运动。\n导出 3D 刚体物理并用 C/C++ 实现：Deriving 3D Rigid Body Physics and implementing it in C/C++ (with intuitions)。\n数字孪生常见特效Shader实现4 直线 PolyLine。\n游戏引擎开发新感觉！(6) c++17内存管理。\nOpenGL 面试题 - Leslie的文章 - 知乎。\n《游戏设计模式》(游戏编程模式)全书笔记+Unity实现。\nUnreal从0到1专栏概述。\n对UE5神经网络引擎的一些理解和看法。\n【游戏开发】动画技术文章合集。\n图形学： 入门：GAMES-101 实时渲染：GAMES-202、GAMES-104、RTR4(https://github.com/Morakito/Real-Time-Rendering-4th-CN) 离线渲染： 简单的软光追：Ray Tracing in One Weekend三部曲(https://raytracing.github.io) 进阶：PBRT-V4 (https://pbr-book.org/4ed/contents) 图形API： OpenGL：https://learnopengl-cn.github.io Vulkan：渐进式教程：https://vulkan-tutorial.com Example：https://github.com/SaschaWillems/Vulkan 将Vulkan封装成RHI，写出小引擎：https://github.com/BoomingTech/Piccolo 引擎原理: GAMES-104, 课程附带的 Piccolo源码值得一读 (https://github.com/BoomingTech/Piccolo) Godot (https://github.com/godotengine/godot) 以及虚幻5源码\n引擎使用: Unity：https://catlikecoding.com/unity/tutorials/\n编程: C#：《C#图解教程》 Shader：《Unity Shader 入门精要》 C++：《C++ Primer》《Effective Modern C++》《深度探索C++对象模型》《C++并发编程实战》 Lua：《Lua程序设计》 以及设计模式 其他: GDC SIGGRAPH\nUnity开发，手机sdk接入\n搞懂Unity在Android上C#，Java，C++的互通。\nAI / 机器学习 B站最全智能优化算法课程，模拟退火算法，粒子群算法，遗传算法等16种优化算法_机器学习_深度学习_人工智能。\nPython 一个标星144.4k⭐Python项目 100 天从新手到大师 :Python-100-Days。\nPython-100-Days。\n通过一个项目全面了解FastAPI（绝无废话！）\n13分钟解释每个Python库/模块 ｜ Python 常用库。\n其他 【大数据】什么是数据融合（Data Fusion)?\n【情报修考】形式语言与自动机 基础知识。\njava 手写一个WEB应用服务，彻底搞懂Tomcat。\n训练营、证书、比赛、GameJam、实习 PAT考试\nOSPP开源之夏\n字节青训营\n七牛云1024创作节\ncode forces\nGitLink“确实开源”编程夏令营(GLCC)\n","date":"2024-06-02T00:00:00Z","permalink":"/posts/means/","title":"C++ / Golang / 游戏开发 / TA 学习路线汇总"}]