本文介绍一种内存池管理技术。
在m公司工作了4年多,一直负责内存池模块问题的处理,比如内存越界,data abort 系统异常的处理,本文加以总结,以便后续参考。
读本文之前,先有个约定,本文中提到的pool指的就是内存池,buffer就是内存池中的一个存储单元,一个pool包含多个buffer。
1. 内存池整体规划
首先介绍下内存池的布局,pool共12个,pool[0]包含930个buffer,每个buffer的大小是 32 bytes, 12个pool的详细信息见下图:
2. buffer 的布局
2.1 buffer示意图
以pool[0] 为例介绍一下buffer的布局,示意图(将来画一个漂亮的图替代之)如下:
上图左半部分: pool[0]包含930个buffer,每个buffer 的大小是32bytes.
上图右半部分:是buffer[3]的内存布局,可见除了用户申请的32个bytes之外,还有20个bytes的额外开销(管理成本),这20个bytes的额外开销包含5个4bytes: 4x4bytes 位于分配的内存之前(图中用1,2,3,4表示),1x4bytes位于分配的内存之后。
头部的4x4bytes | 作用 | 解释 |
---|---|---|
1(POOL_NEXT) | Next available buffer address | 如果当前buffer的状态为free,指的是本pool中下一个可用buffer的地址.如果当前状态的状态是allocated, 为magic number:0x50555345 |
2(POOL_ID) | 指向当前buffer所属pool 的info | 当前pool中的所有buffer 中的这一地址存储一样的地址。(后面会详细介绍pool info) |
3(HEAD) | 固定魔数: 0XF1F1F1F1 | Header |
4(TASK) | 当前buffer所属的owner | 当前buffer是被那个task申请的 |
尾部的1x4bytes | 作用 | 解释 |
---|---|---|
1 | Buffer的footer | 后2个buffer固定0xF2F2,前2个buffer位当前buffer所在pool的index.上图为:0x03F2F2 |
要注意一点如果用户申请的内存小于32个bytes, 我们也会分配一个32个bytes的buffer给用户。 用户申请n个bytes, n<32, 系统会将[n+1,n+4]这4个bytes设置为0xF2F2F2F2, 我们把这个魔数叫做Footer. 上图中用户实际申请16个bytes,内存系统会将17直20这4个bytes设置为0xF2F2F2F2.
2.2 Trace32 恢复出的buffer布局
0x02F1F2F2: 是第0x02F1个buffer
接下来的52个bytes[0x649AA870, 0x649AA8A4]为第0x02F2个buffer的地址范围,可以对照上面的解释详细分析下这52个bytes:
0x50555345: 这个buffer的状态为allocated.
0x649A0EFC: 这个地址存储的是此buffer所属内存池的pool info.
0xF1F1F1F1: Header,
0x64B1B448: 这个buffer属于哪个task分配的。
[0x649A A880, 0x649A A89F]:这32个bytes 为实际分配的内存,其中有2个0xF2F2F2F2, 所以可以推测出用户实际申请的real size 为16 bytes 或者 24 bytes, 但是从目前的信息无法确实是16还是24. (参考第六节:内存系统的不足)
0x02F2F2F2:指示这是第0x02F2个buffer.
2.3 申请和释放buffer时的错误检查
通过以上分析,每个buffer有20 bytes的额外开销,这些开销对后续定位内存越界有很大的帮助。内存管理系统在用户申请/释放buffer时,会对buffer的布局进行检查。
Pool初始化完后内部buffer如下。在分配内存时会检查 PM_NEXT/POOL_ID/HEAD信息是否正确,如果存在问题,会报异常出来,我们称简称这种错误为HEAD被踩.
Buffer被分配出去(Allocated)后的布局如下,在释放buffer时除了检查上述的HEAD被踩的情况,还需要检查是否FOOTER被踩
被踩类型 | 原因 |
---|---|
HEAD 被踩 | 前一个buffer发生越界,需要检查前一个buffer的内存布局是否正确。如果前一个buffer的HEAD也被踩,需要继续向前查前一个buffer. |
FOOTER被踩 | (HEAD未被踩) 当前buffer越界,需要检查当前buffer所属task的上下文。 |
3. pool info介绍
第2节中有介绍每个buffer的头部的4*4bytes中,有4个bytes(POOL_ID)存储的是当前buffer所属pool 的信息,pool结构体细节如下:
4. buffer statistics
第3节中提到了在pool的信息中存储了buffer的统计信息,也就是buffer的history,存储了3个数组。这3个数组中也包含了当前buffer的使用信息,这3个数组可以理解为一个环形队列,由于buffer只有2中状态:ALLOCATED/FREE, 所以这3个数组中的buffer_status依次只有如下可能:ALLOCATED/FREE/ ALLOCATED 或者FREE/ALLOCATED/FREE, 根据owner_task或buffer_status就可以推导出那个node 属于当前buffer的信息。
下图为buffer[860]对应的buf_statistics中的3个历史节点,通过以上分析,可知node[2]为当前buffer 对应的节点。
如果这个队列中出现了2个ALLOCATED或者FREE依次出现,就表明程序存在bug。举例:History node[0].buffer_status等于ALLOCATED,History node[1].buffer_status也等于ALLOCATED, 则程序存在bug.
5. pool中可用的buffer链表
第3节中提到了在pool的信息中存储了可用buffer的链表available_list,另外在第2节中提到的buffer头中的POOL_NEXT,这2者之间存在关联,我们摘取Pool中的buffer初始化状态:
user_ptr是buffer中实际内存的地址(不包含buffer头部的16 bytes (0x10)的额外开销),PM_NEXT是buffer中开头地址(包含buffer头部的16bytes的额外开销)
buffer[n]中的PM_NEXT+0x10 指向buffer[n-1]中user_ptr
比如,buf[8]中的PM_NEXT(0x635E 3824 + 0x10)指向buf[7]中的user_ptr(0x635E 3834)
通过上图可以推测,以pool[0]为例(930个buffer), pool[0]初始化时,pool info中的available_list存储的是buffer[929]中的user_ptr, 当用户第1次申请buffer(小于等于32 bytes)时, 会将buffer[929]中PM_NEXT+0x10赋值给available_list,依次类推….,所以pool中的buffer是从后往前依次使用的。
扩展:释放内存时,内存系统怎么处理呢?
一种可能是:假设buffer[x]要释放,当前available_list.head 指向的是buffer[800], 伪代码如下:
buffer[x].PM_NEXT = buffer[800].user_ptr – 0x10;
avaialble_list.head = buffer[x].user_ptr;
这种做法的特点是:被释放的内存会很快被再次分配出去
还有一种可能是:假设buffer[x]要释放, available_list.tail 指向的是buffer[0], 伪代码如下:
buffer[0].PM_NEXT = buffer[x].user_ptr – 0x10;
buffer[x].PM_NEXT = NULL;
这种做法的特点是:被释放的内存会最后被分配出去,符合FIFO. 还记得pool_info.pm_fifo_suspend吗?推测和内存释放的策略有关系。
6. 内存系统中的不足和可改善之处
第5章提到的available_list,其实可以不用链表,如果为了节省空间的话,只需要存储一个地址即可:指向下一个可用的buffer的user_ptr. 但是这样做的缺点时,如果buffer中的PM_NEXT由于内存越界被踩的话,可用buffer链表可能会断掉。
此内存系统在free buffer时,没有clean buffer. 从已经分配buffer中存在多个0xF2F2F2F2就证明了这一点。 难道是为了提高程序的运行效率?
尽可能少的报异常出来
如果只是踩了buffer自己的Footer(0xF2F2F2F2),是不是可以考虑不报异常出来,尽可能让程序运行下去呢?提高用户体验。
第1节提到的每个pool中的buffer个数是固定的,所以每一代产品都需要评估这个数量是否够用。如果buffer被申请完了,再申请的话,就会报错(pool_info.task_waiting推测与此有关,如果申请不到,task是否需要等待,此内存池系统无内存回收机制)。另外,pool_info.buf_statistic.time_stamp存储了buffer申请的时间,是不是可以考虑,如果某个buffer长久不释放的话,需要让buffer申请者检查下程序是否存在bug, 从而控制buffer的无限制扩大。
7. 扩展
use_after_free检查
buffer头中的PM_NEXT,如果不是0x50555345,则表示buffer为free状态,如果这时要访问此buffer,则发生use_after_free.
可以考虑在debug版本下打开此功能。release版本下打开会影响性能。