news 2026/7/13 5:57:21

高并发内存池 - 申请内存过程联调

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
高并发内存池 - 申请内存过程联调

高并发内存池 - 申请内存过程联调

到此我们已经将内存池中 “申请内存” 功能的逻辑实现的七七八八了,接下来我们将设计一个测试函数,看看目前的功能是否能跑通。

UintTest.cpp

#include"Comm.h"#include"ConcurrentAlloc.h"voidAlloc1(){void*ptr1=ConcurrentAlloc(1);void*ptr2=ConcurrentAlloc(2);void*ptr3=ConcurrentAlloc(3);void*ptr4=ConcurrentAlloc(4);void*ptr5=ConcurrentAlloc(5);void*ptr6=ConcurrentAlloc(6);void*ptr7=ConcurrentAlloc(7);void*ptr8=ConcurrentAlloc(8);}voidAlloc2(){for(inti=0;i<5;i++)void*ptr=ConcurrentAlloc(7);}intmain(){Alloc1();Alloc2();std::cout<<"第一阶段测试成功"<<std::endl;return0;}

下面讲讲我在此次测试中,其中出现的 BUG,以及错误原因是什么,如何调试的?

1. 野指针问题

在 Page Cache 的实现中,出现了野指针问题:

Span*spanN=_spanLists[i].PopFront();Span*spanK;//踩的第一个坑,没有申请空间,野指针直接使用spanK->_pageId=spanN->_pageId;spanK->_n=k;spanN->_pageId+=k;spanN->_n-=k;_spanLists[spanN->_n].PushFront(spanN);//踩的第三个坑,把 spanK 挂上去了returnspanK;

可以看到我在Span* spanK后并没有 new 创建一个对象,这导致了野指针问题,程序直接发生了崩溃。

对于 bus error 的错误我是第一次遇见,在此之前的 BUG 基本靠着调试都可以找到崩溃那一步,从而去向上推演,但这次不一样,一步一步调试无法在给我任何信息。

并没有跳到崩溃的那一步去,所以一步一步调试已经无法解决这个问题,在询问过 AI 后,我开始使用 lldb 去解决这个问题。

此时使用 run 指令,观察指针就会发现,指针地址为 0x0000000000000001,很明显出现了野指针问题,0x0000000000000001 指针不会是正常开辟出来的,因为实在是太小了,直接指向核心区低地址,所以一定是指针没有初始化,造成了野指针问题,我开始一步一步向上排查。

在我尝试 bt 后,发现并没有本应呈现的调试信息并没有展现,甚至是在我编译已经加了 -g 的情况下,此时说明栈已经被彻底破坏了,到此基本就可以判定一定是野指针问题,指针未被初始化就直接使用。

但是本次调试最难的点就在于栈已经被破坏。这意味着无法再输出更多的调试信息,

栈帧的结构为这样:

高地址 +------------------+ | 返回地址 (RA) | ← 函数执行完后,CPU 要跳回哪里继续执行 | 旧帧指针 (FP) | ← 上一个函数的栈帧起点,用于回溯调用链 | 局部变量 | ← 你定义的数组、结构体、临时变量 | 参数区 | ← 传给下一个函数的参数 +------------------+ 低地址 ← sp (栈指针)

当代码中出现缓冲区溢出、野指针写栈、链表越界写时,写入的数据可能冲破了局部变量的边界,直接覆盖了返回地址或帧指针:

+------------------+ | 返回地址 (RA) | ← 原本存的是 0x100005678 (main+24) | 旧帧指针 (FP) | ← 原本存的是 0x16fdfd000 +------------------+ | 局部变量 buf[8] | ← 你的代码写了 buf[10] = 0x1 +------------------+

buf[10] 写到了返回地址的位置,把 0x100005678 改成了 0x1。

当函数执行到 return 时,CPU 从栈上取出返回地址,准备跳回去。但它取到的是被覆盖后的 0x1,于是:

  • CPU 试图在地址 0x1 执行指令
  • 0x1 不是有效的代码地址,触发 EXC_BAD_ACCESS
  • 调试器问栈:“你是谁调用的?” 栈说:“我不知道,我的帧指针和返回地址都被人改了。”
  • 于是 bt 只能显示当前这一帧 0x1,上面一片空白

这就是 “栈被彻底破坏”:不是数据错了,而是"现场记录"本身被销毁了。

所以这也是这个 BUG 最难排查的点,不过我尝试从 Page Cache 层向上排查定义指针变量的位置,然后逐一寻找,最后还是锁定了出错位置,解决了这个问题。

当然这其中有很多的调试工具可以使用例如 ASan、watchpoint 等,但我对这些了解也不是深,所以没有办法讲解如何依靠这些工具进行调试,后续的学习中我会将这方面能力补上。

2. 内存垃圾值问题

在 Thread Cache 中,我手滑将没有调整的空间字节大小传入,造成了第二个很麻烦的问题。

void*ThreadCache::Allocate(size_t size){assert(size<MAX_BYTES);size_t alignsize=SizeClass::RoundUp(size);size_t index=SizeClass::Index(size);if(!_freeList[index].Empty()){return_freeList[index].Pop();}else{//从中心缓存获取数据returnFetchFromCentralCache(index,size);//踩坑,一开始传入的不是alignsize,导致读出了垃圾值}}

所以出现了下面的报错情况:

这次产生的是段错误,这里有了上面的经验,我开始使用 lldb 调试。

这里出现了调试信息,代表可能并非野指针问题,但是这里的地址值很有趣 0x807060504030201,是一个递减的数列,我的直觉告诉我,程序崩溃的秘密就藏在这串地址中,于是我去询问了 AI 这串地址的具体含义,这种地址叫做内存垃圾值

下面我们讲解一下什么叫做内存垃圾值

内存中的垃圾值不是"一团乱码",它们有明确的来源和可识别的模式

来源典型值(64位)含义识别特征
未初始化栈变量0xcdcdcdcdcdcdcdcdMSVC/clang Debug 填充连续CD
已释放堆内存0xddddddddddddddddMSVC “Dead Memory”连续DD
已释放栈内存0xccccccccccccccccMSVC 栈释放标记连续CC
mmap 新分配0x0000000000000000操作系统清零全零
越界读取拼接0x807060504030201你遇到的!有规律但不统一
安全金丝雀0xfd7a3e9c2b1d405f栈保护随机值完全随机

关键洞察:0x807060504030201 属于"越界读取拼接值"——它不像 CDCDCDCD 那样来自编译器填充,而是代码自己把相邻内存的数据块拼成了一个 64 位整数

假设 mmap 分配了一块 8KB 的内存,起始地址是 0x100000000(简化假设)。系统出于安全考虑,会把匿名映射内存清零,所以初始状态是:

地址 内存内容 0x100000000: 00 00 00 00 00 00 00 00 0x100000008: 00 00 00 00 00 00 00 00 0x100000010: 00 00 00 00 00 00 00 00 ...

现在 GetOneSpan 用 size=2 构建自由链表:

start+=size;// start = 0x100000002void*tail=newSpan->_freelist;// tail = 0x100000000while(start<end){ObjNext(tail)=start;// 向 tail 写入 8 字节指针!tail=ObjNext(tail);// 从 tail 读取 8 字节指针!start+=size;// start += 2}

第一次循环

  • tail = 0x100000000
  • ObjNext(tail) = 0x100000002:向地址 0x100000000 写入 8 字节
  • 小端序下,内存变成:02 00 00 00 01 00 00 00
  • tail = ObjNext(tail):读取出来是 0x100000002

第二次循环

  • tail = 0x100000002
  • ObjNext(tail) = 0x100000004:向地址 0x100000002 写入 8 字节
  • 这会覆盖 0x100000002 到 0x100000009 的内存!

灾难发生
第一次写入在 0x100000000 的数据是:

地址: 0 1 2 3 4 5 6 7 内存: [02 00 00 00 01 00 00 00]

第二次写入在 0x100000002 覆盖后:

地址: 0 1 2 3 4 5 6 7 内存: [02 00 04 00 00 00 01 00] // ↑ 从这里开始被覆盖!

链表已经彻底混乱了。 每个"节点"只有 2 字节,但写入是 8 字节,导致每个写入都在覆盖前面节点的数据

当 FetchRangeObj 来读取时

end=start;// end = 0x100000000ObjNext(end);// 从 0x100000000 读取 8 字节

此时 0x100000000 处的内存已经被多次覆盖,可能变成了:

内存字节: 01 20 30 40 50 60 70 80

在小端序(ARM64/macOS)下,CPU 读取这 8 个字节为一个 64 位指针时:

  • 字节 0 (01) 是最低有效字节
  • 字节 7 (80) 是最高有效字节

组合结果:0x807060504030201

所以就造成了地址的递减情况,幸运的是 0x807060504030201 并不指向合法地址处,所以产生了崩溃,使我们可以观察到问题,如果恰好指向合法地址处,那么恐怕会隐藏的更深。

继续调试

现在通过 bt,我们可以观察到调用的堆栈结构,接着逐一向上排查

网上排查了一层,观察一下变量的值,逐一去查看他们是否正确,其实到这一步就已经初见端倪了,size 的值应该是 8,但是却为 2,所以排查出了是 size 的问题,最终也成功解决这个 BUG。

其实中途还解决了一些 BUG,不过都是些很简单的错误,没有什么有趣的现象和值得深度挖掘的东西,就不在这里讲述了,我们下篇文章再见👋。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/13 5:55:09

模板驱动文档自动化:从Word手工操作到结构化生成

1. 项目概述&#xff1a;当文档生成从“复制粘贴”升级为“模板引擎驱动”你有没有经历过这样的场景&#xff1a;每周一早上&#xff0c;打开Excel整理上一周的销售数据&#xff0c;再切到Word里&#xff0c;把数字填进一份固定格式的周报模板——标题字号、公司Logo位置、页眉…

作者头像 李华
网站建设 2026/7/13 5:54:17

蒸馏不是“学生抄老师”,我靠“分层偷师”让小模型逆袭了大模型

去年我做过一个特别有意思的项目&#xff0c;要用一个7B的小模型&#xff0c;去复刻一个70B大模型的代码生成效果。一开始我以为蒸馏很简单&#xff0c;不就是让大模型生成一堆样本&#xff0c;然后喂给小模型学吗&#xff1f;结果跑了两周&#xff0c;出来的小模型写代码还是漏…

作者头像 李华
网站建设 2026/7/13 5:53:27

Windows 11专业工作站版一键重装指南:从官方工具到U盘启动

这次我们来详细聊聊如何在普通电脑上自己动手重装 Windows 11 专业工作站版系统。对于很多用户来说&#xff0c;系统重装一直是个技术活&#xff0c;要么花钱找维修师傅&#xff0c;要么冒着风险使用来路不明的重装工具。其实只要掌握正确方法&#xff0c;自己重装系统可以很简…

作者头像 李华
网站建设 2026/7/13 5:53:21

VSCode EIDE 插件 v3.27.2 配置:从 Keil MDK 到 STM32 项目的 3 步迁移指南

VSCode EIDE 插件 v3.27.2 配置&#xff1a;从 Keil MDK 到 STM32 项目的 3 步迁移指南对于习惯了 Keil MDK 开发环境的嵌入式工程师来说&#xff0c;迁移到 VSCode 可能是一个令人望而生畏的过程。然而&#xff0c;借助 EIDE 插件&#xff0c;这一过程可以变得异常简单。本文将…

作者头像 李华
网站建设 2026/7/13 5:51:03

Eino 框架编排讲解:Chain和Graph

在AI应用与Agent开发快速迭代的当下&#xff0c;简单的线性代码拼接已经无法满足复杂大模型业务场景的需求。传统开发模式存在组件耦合严重、流程难以复用、调试排查困难、动态逻辑无法实现等痛点&#xff0c;而编排能力正是解决这些问题的核心关键。 Eino 作为字节开源的Go语言…

作者头像 李华