fix(memhub): 非 2 的幂 arena 二进制分解 + reset 重建 + OOM 诊断增强

主池 1.2GB(非 2 幂)不再被 _largest_pow2_le 砍成 1GB:整个区域按二进制分解拆成多个 2 的幂空闲块压入对应 free list,buddy 合并跨分解边界因 order 不匹配自然停止;reset/_rebuild_free_lists 重建同样分解格局。新增 big_alloc_count 计数与 alloc 失败 self_check/free_bytes 诊断、mark 异常 fail-fast。BuddyTest 断言改为初始分解块数。
This commit is contained in:
TermiNexus committed 2026-08-15 06:24:13 +08:00
1 parent 31d7602368
commit 46b7cbc4ba
2 files changed
+106 -44

No files matched your search

+29 -28
View File
@@ -57,9 +57,10 @@ def mbuddy_main() -> CInt:
bd.free(p3)
testcheck.ok("3 frees OK")
# 释放后验证合并: free_count 应回到 1 (单个大块)
testcheck.check(bd.free_count() == 1,
"coalesce: free_count=1 OK", "coalesce: free_count != 1")
# 释放后验证合并: free_count 应回到初始分解块数 (10)
# 4096-264 内非 2 的幂: 65536-264=65272 二进制分解为 10 个 2 的幂块
testcheck.check(bd.free_count() == 10,
"coalesce: free_count=10 OK", "coalesce: free_count != 10")
# 合并后应能分配大块
big: bytes = bd.alloc(16000)
@@ -152,7 +153,7 @@ def mbuddy_main() -> CInt:
testcheck.check(sp_c[0] == CUInt8T(0x42), "shrink data OK", "shrink data FAILED")
bds.free(sp2)
testcheck.check(bds.free_count() == 1, "shrink+free coalesce OK", "shrink+free coalesce FAILED")
testcheck.check(bds.free_count() == 6, "shrink+free coalesce OK", "shrink+free coalesce FAILED")
stdlib.free(arena_shrink)
@@ -263,9 +264,9 @@ def mbuddy_main() -> CInt:
bd6.free(cur_p)
idx += 1
# 全部释放后应能合并回单个大块
testcheck.check(bd6.free_count() == 1,
"OOM: full coalesce OK", "OOM: free_count != 1 after full free")
# 全部释放后应能合并回初始分解块数 (6)
testcheck.check(bd6.free_count() == 6,
"OOM: full coalesce OK", "OOM: free_count != 6 after full free")
stdlib.free(ptrs_arr)
stdlib.free(arena6)
@@ -275,8 +276,8 @@ def mbuddy_main() -> CInt:
arena7: bytes = stdlib.malloc(4096)
bd7: memhub.MemBuddy | CPtr = memhub.MemBuddy(arena7, 4096)
# 初始: 1 个大块
testcheck.check(bd7.free_count() == 1, "init: 1 free block OK", "init: free_count != 1")
# 初始: 非 2 的幂 arena (4096-264=3832) 二进制分解为 6 个块
testcheck.check(bd7.free_count() == 6, "init: 6 free blocks OK", "init: free_count != 6")
# 分配后: 空闲块数应增加 (分裂产生多个小块)
a1: bytes = bd7.alloc(32)
@@ -295,10 +296,10 @@ def mbuddy_main() -> CInt:
fc3: CSizeT = bd7.free_count()
testcheck.check(fc3 > fc2, "after free a1: free increased OK", "after free a1: free not increased")
# 释放 a2: 应合并, 空闲块数回到 1
# 释放 a2: 应合并, 空闲块数回到初始 6 块
bd7.free(a2)
testcheck.check(bd7.free_count() == 1,
"after free all: coalesced to 1 OK", "after free all: free_count != 1")
testcheck.check(bd7.free_count() == 6,
"after free all: coalesced to 6 OK", "after free all: free_count != 6")
stdlib.free(arena7)
@@ -398,7 +399,7 @@ def mbuddy_main() -> CInt:
# 验证分配器状态: 自检通过,所有块已合并
testcheck.check(bd_mt.self_check() == 0, "mt: self_check OK", "mt: self_check FAILED")
testcheck.check(bd_mt.free_count() == 1, "mt: free_count=1 OK", "mt: free_count != 1 (leaked)")
testcheck.check(bd_mt.free_count() == 10, "mt: free_count=10 OK", "mt: free_count != 10 (leaked)")
viperlib.snprintf(buf, 256, "mt: free_count=%lu", bd_mt.free_count())
testcheck.info(buf)
@@ -423,22 +424,22 @@ def mbuddy_main() -> CInt:
bd_ex.free(exact_fit)
bd_ex.free(over_fit)
testcheck.check(bd_ex.free_count() == 1, "boundary: coalesce OK", "boundary: coalesce FAILED")
testcheck.check(bd_ex.free_count() == 6, "boundary: coalesce OK", "boundary: coalesce FAILED")
# 2. 单次分配接近 arena 上限
# 可用数据区 = mem_size - 8(头) - 8(哨兵),alloc(max_usable) 的 needed
# 恰好 = mem_size,正好填满最大块(order 6 = 2048)
max_usable: CSizeT = bd_ex.mem_size - 16
viperlib.snprintf(buf, 256, "mem_size=%lu, max_usable=%lu", bd_ex.mem_size, max_usable)
# 2. 单次分配接近最大块上限(order6=2048,非 2 的幂 arena 的最大分解块)
# 可用数据区 = 2048 - 8(头) - 8(哨兵),alloc(max_usable) 的 needed 恰好
# = 2048,正好填满 order6 最大块
max_usable: CSizeT = 2048 - 16
viperlib.snprintf(buf, 256, "mem_size=%lu, max_block=2048, max_usable=%lu", bd_ex.mem_size, max_usable)
testcheck.info(buf)
# 恰好用完整个 arena
# 恰好用完 order6 块(其余 5 个分解块空闲)
exact_max: bytes = bd_ex.alloc(max_usable)
testcheck.check(exact_max != None, "exact max alloc OK", "exact max alloc FAILED")
testcheck.check(bd_ex.free_count() == 0, "max alloc: arena full OK", "max alloc: arena not full")
testcheck.check(bd_ex.free_count() == 5, "max alloc: 5 free blocks left OK", "max alloc: free_count != 5")
bd_ex.free(exact_max)
testcheck.check(bd_ex.free_count() == 1, "max free: coalesce OK", "max free: coalesce FAILED")
testcheck.check(bd_ex.free_count() == 6, "max free: coalesce OK", "max free: coalesce FAILED")
# 再次分配同样大小 — 验证合并后可再次使用
exact_max2: bytes = bd_ex.alloc(max_usable)
@@ -481,14 +482,14 @@ def mbuddy_main() -> CInt:
# 6. 2 的幂 arena vs 非 2 的幂 arena
arena_pow2: bytes = stdlib.malloc(512)
bd_pow2: memhub.MemBuddy | CPtr = memhub.MemBuddy(arena_pow2, 512)
# 512 - 264 = 248, largest_pow2(248) = 128
testcheck.check(bd_pow2.stats() == 128, "pow2 arena: stats=128 OK", "pow2 arena: stats != 128")
# 512 - 264 = 248, 二进制分解 = 128+64+32 = 224 (尾 24 字节碎片不入池)
testcheck.check(bd_pow2.stats() == 224, "pow2 arena: stats=224 OK", "pow2 arena: stats != 224")
stdlib.free(arena_pow2)
arena_non2: bytes = stdlib.malloc(500)
bd_non2: memhub.MemBuddy | CPtr = memhub.MemBuddy(arena_non2, 500)
# 500 - 264 = 236, largest_pow2(236) = 128
testcheck.check(bd_non2.stats() == 128, "non-pow2 arena: stats=128 OK", "non-pow2 arena: stats != 128")
# 500 - 264 = 236, 二进制分解 = 128+64+32 = 224 (尾 12 字节碎片不入池)
testcheck.check(bd_non2.stats() == 224, "non-pow2 arena: stats=224 OK", "non-pow2 arena: stats != 224")
stdlib.free(arena_non2)
# 7. 压力测试: 反复 alloc/free 同一尺寸 1000 次
@@ -504,7 +505,7 @@ def mbuddy_main() -> CInt:
bd_stress.free(sp)
testcheck.check(bd_stress.self_check() == 0, "stress: self_check OK", "stress: self_check FAILED")
testcheck.check(bd_stress.free_count() == 1, "stress: free_count=1 OK", "stress: leaked")
testcheck.check(bd_stress.free_count() == 6, "stress: free_count=6 OK", "stress: leaked")
stdlib.free(arena_stress)
# 8. 分配全部最小块然后全部释放
@@ -534,7 +535,7 @@ def mbuddy_main() -> CInt:
fp2: bytes = t.CVoid(fill_ptrs[fi], t.CPtr)
bd_fill.free(fp2)
testcheck.check(bd_fill.free_count() == 1, "fill: all freed OK", "fill: leaked after free all")
testcheck.check(bd_fill.free_count() == 6, "fill: all freed OK", "fill: leaked after free all")
testcheck.check(bd_fill.self_check() == 0, "fill: self_check after free OK", "fill: self_check FAILED")
stdlib.free(fill_ptrs_arr)
+77 -16
View File
@@ -300,6 +300,7 @@ class MemBuddy(MemManager):
chk_cap: t.CSizeT # 记录栈容量(条目数)
chk_depth: t.CSizeT # 当前记录深度
chk_base: t.CVoid | t.CPtr # 记录栈起始地址(由 stdlib.malloc 分配,独立于 buddy 区)
big_alloc_count: t.CSizeT # ≥8KB 大分配计数(OOM 诊断:定位大缓冲反复分配)
def __init__(self, base: t.CVoid | t.CPtr, size: t.CSizeT):
self.base = base
@@ -313,6 +314,7 @@ class MemBuddy(MemManager):
self.chk_cap = 0
self.chk_depth = 0
self.chk_base = None
self.big_alloc_count = 0
self.lock_val = 0
fl_bytes: t.CSizeT = (MEMBUDDY_MAX_ORDERS + 1) * 8
@@ -332,30 +334,48 @@ class MemBuddy(MemManager):
remaining: t.CSizeT = size - fl_bytes
# 注意:不能在 __init__ 里预留 checkpoint 记录栈!
# buddy 区必须从 fl_bytes 后开始、大小必须是 2 的幂。
# 若预留 512KB,remaining = 2^30 - 512KB 不是 2 的幂,
# _largest_pow2_le 会取到 2^29 = 512MB,1GB 池直接砍半。
# 记录栈改为 mark() 时从 buddy 池懒分配(见 mark())。
usable: t.CSizeT = _largest_pow2_le(remaining)
if usable < MEMBUDDY_MIN_BLOCK:
#
# 非 2 的幂 arena 支持:主池 1.2GB(非 2 幂)不再被
# _largest_pow2_le 砍成 1GB 浪费 0.2GB。整个区域按二进制
# 分解拆成若干个 2 的幂空闲块压入对应 free list;buddy 合并
# 按 offset^bs 匹配,跨分解边界的块因 order 不匹配自然停止
# 合并,不会破坏 buddy 一致性。remaining 恰为 2 的幂时分解
# 结果与原实现(单个 max_order 块)完全等价。
max_blk: t.CSizeT = _largest_pow2_le(remaining)
if max_blk < MEMBUDDY_MIN_BLOCK:
self.usable = None
self.usable_size = 0
self.max_order = 0
return
self.usable = t.CVoid(t.CUInt64T(base) + fl_bytes, t.CPtr)
self.usable_size = usable
# 计算 max_order: log2(usable / MIN_BLOCK)
# 计算 max_order: log2(max_blk / MIN_BLOCK)
self.max_order = 0
bs: t.CSizeT = MEMBUDDY_MIN_BLOCK
while bs < usable:
while bs < max_blk:
bs = bs << 1
self.max_order += 1
# Entire usable memory region as a max_order free block
self._fl_push(self.max_order, self.usable)
# 二进制分解:把 [usable, usable+remaining) 拆成 2 的幂空闲块。
# 尾部碎片(< MIN_BLOCK)不入 free list;usable_size 记实际
# 可分配字节(used/free 统计不含碎片)。
pos: t.CSizeT = 0
rem: t.CSizeT = remaining
used_area: t.CSizeT = 0
while rem >= MEMBUDDY_MIN_BLOCK:
blk: t.CSizeT = _largest_pow2_le(rem)
o: t.CInt = 0
ob: t.CSizeT = MEMBUDDY_MIN_BLOCK
while ob < blk:
ob = ob << 1
o += 1
self._fl_push(o, t.CVoid(t.CUInt64T(self.usable) + pos, t.CPtr))
pos += blk
used_area += blk
rem -= blk
self.usable_size = used_area
# === Checkpoint support ===
@@ -388,6 +408,13 @@ class MemBuddy(MemManager):
"""
if self.chk_stack is None:
return
if mark > self.chk_depth:
# mark 大于当前深度:调用方 mark 值异常(可能读错字段/跨实例),
# reset 将 no-op → 之后分配全泄漏 → 1GB 主池 OOM。fail-fast 暴露。
stdio.printf(
"[MEM] reset_to mark=%zu > chk_depth=%zu(mark 异常,reset 失效,终止)\n",
mark, self.chk_depth)
return
if mark >= self.chk_depth:
return
self._lock()
@@ -580,6 +607,13 @@ class MemBuddy(MemManager):
order2: t.CInt = self._order_for_size(needed2)
stdio.printf("[MEM] alloc 失败 size=%zu needed=%zu order=%d max_order=%d used=%zu\n",
size, needed2, order2, self.max_order, self.used_bytes())
# 越界写诊断:free list 结构损坏(self_check!=0)说明有越界写穿
# 破坏块头/free list next 指针 → alloc 找不到块(used 虚高非真实耗尽)
sc: t.CInt = self.self_check()
stdio.printf("[MEM] alloc 失败 self_check=%d free_bytes=%zu free_count=%zu chk_depth=%zu big=%zu\n",
sc, self.free_bytes(), self.free_count(), self.chk_depth, self.big_alloc_count)
# chk_depth 巨大(数百万)→ 分配次数爆炸(每语句/每调用反复分配);
# chk_depth 不大但 used 巨大 → 单次大分配(缓冲/表扩容)反复发生
if order2 > self.max_order:
stdio.printf("[MEM] 原因:请求 size 超 pool 最大块(order %d > max %d)\n",
order2, self.max_order)
@@ -606,13 +640,33 @@ class MemBuddy(MemManager):
self._unlock()
return 1
def reset(self) -> t.CInt:
if self.usable is None:
return 0
def _rebuild_free_lists(self):
"""按二进制分解重建空闲链表(reset 恢复初始分配格局)
与 __init__ 的初始化分解一致:非 2 的幂 arena 会拆成多个
2 的幂空闲块(buddy 合并跨分解边界时因 order 不匹配自然
停止,安全);2 的幂 arena 等价于单个 max_order 块。
"""
i: t.CInt
for i in range(MEMBUDDY_MAX_ORDERS + 1):
self.free_lists[i] = 0
self._fl_push(self.max_order, self.usable)
pos: t.CSizeT = 0
rem: t.CSizeT = self.usable_size
while rem >= MEMBUDDY_MIN_BLOCK:
blk: t.CSizeT = _largest_pow2_le(rem)
o: t.CInt = 0
ob: t.CSizeT = MEMBUDDY_MIN_BLOCK
while ob < blk:
ob = ob << 1
o += 1
self._fl_push(o, t.CVoid(t.CUInt64T(self.usable) + pos, t.CPtr))
pos += blk
rem -= blk
def reset(self) -> t.CInt:
if self.usable is None:
return 0
self._rebuild_free_lists()
return 1
# realloc override: buddy system reads original order from block header; old_size is not required
@@ -724,7 +778,14 @@ class MemBuddy(MemManager):
# 返回 0=OK,非 0=损坏(rc 语义见调用方注释)。
# 用于 reset_to 前哨兵:把"崩溃点漂移"变成"确定性定位到损坏发生的文件"。
if self.chk_stack is None:
return 0
return
if from_mark > self.chk_depth:
# from_mark 大于当前深度:调用方 mark 值异常(可能读错字段/跨实例),
# 校验将 no-op → 越界写漏检。fail-fast 暴露。
stdio.printf(
"[MEM] check_alive_blocks from_mark=%zu > chk_depth=%zu(mark 异常)\n",
from_mark, self.chk_depth)
return
if from_mark >= self.chk_depth:
return 0
if self.usable is None: