实验七:更好的内存管理¶
在执行每一条命令前,请你对将要进行的操作进行思考
为了你的数据安全和不必要的麻烦,请谨慎使用 sudo
,并确保你了解每一条指令的含义。
1. 实验文档给出的命令不需要全部执行
2. 不是所有的命令都可以无条件执行
3. 不要直接复制粘贴命令执行
合并实验代码¶
如何使用本次参考代码
本次给出的参考代码为增量补充,即在上一次实验的基础上进行修改和补充。因此,你需要将本次参考代码与上一次实验的代码进行合并。
合并后的代码并不能直接运行,你需要基于合并后的代码、按照文档进行修改补充,才能逐步实现本次实验的功能。
本次实验代码量较小,给出的代码集中于 pkg/kernel/src/proc/vm
目录下。
heap.rs
:添加了Heap
结构体,用于管理堆内存。mod.rs
:除栈外,添加了堆内存、ELF 文件映射的初始化和清理函数。
关于 ProcessVm
的角色
在本实验设计中,ProcessVm
结构体用于记录用户程序的内存布局和页表,在调用下级函数前对页表、帧分配器进行获取,从而统一调用 mapper
或者 get_frame_alloc_for_sure
的时机。
帧分配器的内存回收¶
在 Lab 4 的加分项中,提到了尝试实现帧分配器的内存回收。在本次实验中将进一步完善这一功能。
在进行帧分配器初始化的过程中,内核从 bootloader 获取到了一个 MemoryMap
数组,其中包含了所有可用的物理内存区域,并且内核使用 into_iter()
将这一数据结构的所有权交给了一个迭代器,你可以在 pkg/kernel/src/memory/frames.rs
中了解到相关类型和实现。
迭代器是懒惰的,只有在需要时才会进行计算,因此在进行逐帧分配时,并没有额外的内存开销。但是,当需要进行内存回收时,就需要额外的数据结构来记录已经分配的帧,以便进行再次分配。
相对于真实的操作系统,本实验中的内存回收是很激进的:即能回收时就回收,不考虑回收对性能的影响。在实际的操作系统中,内存回收是一个复杂的问题,需要考虑到内存的碎片化、内存的使用情况、页面的大小等细节;进而使用标记清除、分段等策略来减少内存回收的频率和碎片化。
因此对于本实验的帧分配器来说,内存回收的操作是非常简单的,只需要将已经分配的帧重新加入到可用帧的集合中即可。
为了减少内存占用,这一操作通常使用位图来实现,即使用一个位图来记录每一帧的分配情况。由于 Rust 的标准库中并没有提供位图的实现,因此你可以简单地使用一个 Vec<PhysFrame>
作为已经回收的帧的集合。
内存占用
使用 Vec<PhysFrame>
进行最简单的内存回收记录时,每一页需要使用 8 字节的内存来记录。相对于直接使用位图,这种方法会占用更多的内存(比位图多 64 倍)。
下面进行具体的实现:
1 2 3 4 5 6 |
|
之后实现分配和回收的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
想要使用位图?
如果你想要使用位图来实现页面回收,可以尝试使用 Roaring Bitmap,在笔者 2024 年 2 月的 PR 之后,它可以支持在 no_std
环境下使用。
但它只支持 u32
类型的位图,如何让它在合理范围内记录页面的分配?
或许你可以假定物理内存的最大大小不会超过一个合理的值……
用户程序的内存统计¶
在进行实际内存统计前,先来了解一下在 Linux 下的用户程序内存布局和管理:
Linux 的进程内存¶
在 Linux 中,进程的内存区域可以由下属示意图来简单展示:
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 |
|
你可以在 Linux 中通过 cat /proc/<pid>/maps
查看进程的内存映射情况,笔者以 cat /proc/self/maps
为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
其中所示的内存区域:
/usr/bin/cat
:ELF 文件映射的内存区域,这里包含了cat
程序的.text
、.data
、.bss
等段。[heap]
:堆区的内存区域,这里包含了cat
程序的堆内存,使用brk
系统调用进行分配。/usr/lib/x86_64-linux-gnu/*.so.*
:动态链接库的内存区域,这里包含了程序使用的动态链接库的.text
、.data
、.bss
等段。[stack]
:栈区的内存区域,这里包含了程序的栈内存,在达到栈区的最大大小前,栈区会自动增长。[vvar]
、[vdso]
、[vsyscall]
:内核的内存区域,这里包含了内核相关的一些数据结构,如vvar
包含了一些内核和用户空间之间共享的变量;vdso
是虚拟的动态共享对象 (Virtual Dynamic Shared Object) 区域,用于在用户空间和内核空间之间提供一些系统调用的快速访问。
你也可以查看程序的内存使用情况,使用 cat /proc/<pid>/status
,依旧以 cat
程序为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
其中有几个需要注意的字段:
VmPeak
/VmSize
:进程的峰值虚拟内存大小和当前虚拟内存大小,指的是整个虚拟内存空间的大小。VmHWM
/VmRSS
:进程的峰值物理内存大小和当前物理内存大小,指的是进程实际使用的物理内存大小。RssAnon
/RssFile
/RssShmem
:进程的匿名内存、文件映射内存和共享内存的大小。VmData
/VmStk
/VmExe
:进程的数据段、栈段、代码段的大小。VmLib
:进程的动态链接库的大小,对于cat
程序来说,这里主要是libc
的占用,但这部分内存可以被多个进程很好地共享。VmPTE
:进程的页表项的大小。
当使用 ps aux
查看进程时,你可以看到更多的信息,如进程的 CPU 占用、内存占用、进程的状态等。
1 2 3 4 |
|
其中 VSZ 和 RSS 就对应了上述的 VmSize
和 VmRSS
,%MEM
表示了 RSS (Resident Set Size) 占总物理内存的百分比。
总之,在 Linux 中进程的虚拟内存大小会比实际使用的物理内存大小要大很多,进程大部分的虚拟内存空间(尤其是文件映射部分)通常被标记为不存在,只有在访问时才会被加载到物理内存中。结合动态链接库的共享,Linux 可以很好地将物理内存物尽其用。
内存统计的实现¶
在目前的实现(Lab 3)中,用户程序在进程结构体中记录的内存区域只有栈区,堆区由内核进行代劳,同时 ELF 文件映射的内存区域也从来没有被释放过,无法被其他程序复用。
而相较于 Linux,本实验并没有将内存管理抽象为具有上述复杂功能的结构:用户程序的内存占用严格等同于其虚拟内存大小,并且所有页面都会被加载到物理内存中,不存在文件映射等概念,只有堆内存和栈内存是可变的。
因此,其内存统计并没有那么多的细节,只需要统计用户程序的栈区和堆区的大小即可。在 Stack
和 Heap
中,已经实现了 memory_usage
函数来获取栈区和堆区的内存占用字节数。
1 2 3 4 5 6 7 8 9 10 11 |
|
堆区的内存管理将在本实验后部分实现,不过此处可以直接将 Heap
先行加入到进程结构体中
那么根据上述讨论,对本实验的内存占用而言,只剩下了 ELF 文件映射的内存区域和页表的内存占用,为实现简单,本部分忽略页表的内存占用,只统计 ELF 文件映射的内存占用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
获取用户程序 ELF 文件映射的内存占用的最好方法是在加载 ELF 文件时记录内存占用,这需要对 elf
模块中的 load_elf
函数进行修改:
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 |
|
之后,在 pkg/kernel/src/proc/vm
中完善 ProcessVm
的 load_elf_code
函数,在加载 ELF 文件时记录内存占用。
为了便于测试和观察,在 pkg/kernel/src/proc/manager.rs
的 print_process_list
和 Process
的 fmt
实现中,添加打印内存占用的功能。
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 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
阶段性成果
使用你实现的 Shell 打印进程列表和他们的内存占用。
使用 fact
阶乘递归程序进行测试,在其中使用 sys_stat
系统调用打印进程信息,尝试观察到内存占用的变化。
用户程序的内存释放¶
在经过上述的讨论和实现后,目前进程的内存管理已经包含了栈区、堆区和 ELF 文件映射三部分,但是在进程退出时,这些内存区域并没有被释放,内存没有被回收,无法被其他程序复用。
不过,在实现了帧分配器的内存回收、进程的内存统计后,进程退出时的内存释放也将得以实现。
页表上下文的伏笔¶
关于本实验的内存模型……
还记得 fork
的实现吗?在 Linux 等真实世界的操作系统中,fork
不会真正复制物理内存,而是使用写时复制 (Copy-On-Write) 的技术,只有在子进程或父进程修改内存时才会进行复制。
但在本实验中,出于并发等实验功能的实现简易性,fork
得到的进程与父进程共享页面和页表,因此在释放内存时要注意不要影响到其他进程。
PageTableContext
是自 Lab 3 就给出的页表上下文结构,如果你注意过它的定义,或许会发现自那时就埋下的伏笔——没印象也无妨,它的定义如下所示:
1 2 3 |
|
笔者打赌绝大部分的读者可能会是第一次打开
proc/paging.rs
文件……
在目前的项目中,你应当能看到它实现的两个不同的方法:clone_level_4
和 fork
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
clone_level_4
用于复制当前页表(仅第四级页表),并将其作为一个新的页表上下文返回,用于spawn
时复制内核页表;fork
则是直接对Arc<Cr3RegValue>
进行clone
操作,使得新的进程与父进程共享页表。
也即,在目前的实现中,对于每一棵独立的“进程树”,它们的页表是独立的,但是在同一棵“进程树”中,它们的页表是共享的。
这里仅是一个简单的表示,本实验并没有真正去记录有关“树”的信息,只是为了方便理解。
既然 Arc
表示“原子引用计数”,也就意味着可以通过它来确定”当前页表被多少个进程共享“,从而在释放内存时,只有在最后一个进程退出时才释放共享的内存。
为 PageTableContext
添加一个 using_count
方法,用于获取当前页表被引用的次数:
1 2 3 |
|
内存释放的实现¶
出于模块化设计,先为 Stack
实现 clean_up
函数,由于栈是一块连续的内存区域,且进程间不共享栈区,因此在进程退出时直接释放栈区的页面即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
接下来重点关注 ProcessVm
的相关实现,位于 pkg/kernel/src/proc/vm/mod.rs
中,首先为它添加 clean_up
函数:
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 |
|
在上述框架之上,按照顺序依次释放栈区、堆区和 ELF 文件映射的内存区域:
- 释放栈区:调用
Stack
的clean_up
函数; - 如果当前页表被引用次数为 1,则进行共享内存的释放,否则跳过至第 7 步;
- 释放堆区:调用
Heap
的clean_up
函数(后续实现); - 释放 ELF 文件映射的内存区域:根据记录的
code
页面范围数组,依次调用elf::unmap_range
函数,并进行页面回收。 - 清理页表:调用
mapper
的clean_up
函数,这将清空全部无页面映射的一至三级页表。 - 清理四级页表:直接回收
PageTableContext
的reg.addr
所指向的页面。 - 统计内存回收情况,并打印调试信息。
对于第 5 和第 6 步,可以参考使用如下代码:
1 2 3 4 5 6 7 |
|
最后,遵守 Rust 的内存管理规则,需要在 Process
的 Drop
实现中调用 ProcessVm
的 clean_up
函数:
1 2 3 4 5 6 7 |
|
在实现了 Drop
之后,你可以在 Process
的 kill
函数中直接使用 take
来释放进程的内存:
1 2 |
|
阶段性成果
使用你实现的 Shell 运行 fact
阶乘递归程序,观察进程的内存占用和释放情况。
在 fact
程序中,尝试使用 sys_stat
系统调用打印进程信息,观察到内存占用的变化。
你的页面被成功回收了吗?
内核的内存统计¶
至此,用户程序的内存管理已经得到了较好的实现,但是内核占用了多少内存呢?
类似于用户进程的加载过程,可以通过在内核加载时记录内存占用来实现内核的初步内存统计,即在 bootloader 中实现这一功能。
首先,在 pkg/boot/src/lib.rs
中,定义一个 KernelPages
类型,用于传递内核的内存占用信息,并将其添加到 BootInfo
结构体的定义中:
1 2 3 4 5 6 7 8 |
|
并在 pkg/boot/src/main.rs
中,将 load_elf
函数返回的内存占用信息传递至 BootInfo
结构体中:
使用了其他函数加载内核?
如果你跟着实验指南一步一步实现,那么你的内核加载函数应当是 load_elf
,它通过分配新的帧、映射它们、复制数据的顺序来进行加载。
如果你使用了参考实现提供的代码,这里可能会有所不同:参考实现中使用 map_elf
来实现内核页面的映射它不再新分配帧,而是对 ELF 文件中的页面直接映射,因此你需要根据实际情况来获取内核被加载的页面信息。
作为参考,可以使用如下代码直接从 ElfFile
中获取内核被加载的页面信息:
1 2 3 4 5 6 |
|
成功加载映射信息后,将其作为 ProcessManager
的初始化参数,用于构建 kernel
进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
其中,为 ProcessVm
添加 init_kernel_vm
函数,用于初始化内核的内存布局:
1 2 3 4 5 6 7 8 9 10 |
|
在进行后续实验的过程中,将会继续对 ksatck
函数进行修改,这里可以直接使用配置文件中指定的常量来初始化,或者先行忽略。
阶段性成果
试使用 sys_stat
系统调用打印进程信息,观察内核内存的占用情况。
内核栈的自动增长¶
在 Lab 3 中简单实现了用户进程的栈区自动增长,但是内核的栈区并没有进行相应的处理,这将导致内核栈溢出时无法进行自动增长,从而导致内核崩溃。
为了在之前的实验中避免这种情况,实验通过 bootloader 直接为内核分配了 512 * 4 KiB = 2 MiB 的栈区来避免可能的栈溢出问题。但这明显是不合理的,因为内核的栈区并不需要这么大的空间。
与其分配一个固定大小的栈区,不如在缺页中断的基础上实现一个简单的栈区自动增长机制,当栈区溢出时,自动为其分配新的页面。
需要用到的配置项在 Lab 1 中已经给出,即 kernel_stack_auto_grow
,对它的行为进行如下约定:
- 默认为
0
,这时内核栈区所需的全部页面(页面数量为kernel_stack_size
)将会在内核加载时一次性分配。 - 当这一参数为非零值时,表示内核栈区的初始化页面数量,从栈顶开始向下分配这一数量的初始化页面,并交由内核进行自己的栈区管理。
1 2 3 4 5 6 7 8 |
|
与用户态栈类似,你可以在 pkg/kernel/src/proc/vm/stack.rs
中将这些信息定义为常量,并在 Stack
的 kstack
函数中使用这些常量来初始化内核栈区:
1 2 3 4 5 6 7 8 9 |
|
别忘了修改配置文件使其描述的区域一致!
对于上述的常量,你应当在配置文件中这样修改,其中 kernel_stack_auto_grow
的取值视实现可能有所不同:
1 2 3 4 5 |
|
最后,在缺页中断的处理过程中,对权限、区域进行判断。如果发生缺页中断的进程是内核进程则不要设置用户权限标志位,并进行日志记录:
1 |
|
最后,为了测试你的栈扩容成果,可以用如下代码在 pkg/kernel/src/lib.rs
中进行测试:
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 |
|
阶段性成果
尝试能使你的内核启动的最小的 kernel_stack_auto_grow
值,观察内核栈的自动增长情况。
并尝试回答思考题 3,它或许会对你的理解有所帮助。
用户态堆¶
最后,为了提供给用户程序更多的内存管理能力,还需要实现一个系统调用:sys_brk
,用于调整用户程序的堆区大小。
关于 brk
系统调用……
brk
系统调用是一个古老的系统调用,本意为调整 Program Break(程序断点)指针的位置,该指针最初指进程的数据段末尾,但这一断点可以向上增长,进而留出灵活可控的空间作为“堆内存”。
那句老话:“堆向高地址增长,栈向低地址增长”。你可以在本实验开头的 “Linux 进程内存” 部分中找到它。
而 brk
系统调用则是用于调整这一断点的位置,从而调整堆区的大小。在开启地址随机化后,它在初始化时会被加上一个随机的偏移量,从而使得堆区的地址不再是固定的。
在 C 中,提供了 brk
和 sbrk
两个函数来调用这一系统调用,在现代的 Linux 中,brk
系统调用的功能已经逐渐被更灵活的 mmap
系统调用所取代。
但是在本实验中,为了简化内存管理的实现,仍然使用 brk
系统调用来调整用户程序的堆区大小,进而为后续可能的实验提供基础。
首先,参考给出代码中的 pkg/kernel/src/proc/vm/heap.rs
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
在 Heap
中,base
表示堆区的起始地址,end
表示堆区的结束地址。end
是一个 Arc<AtomicU64>
类型的可被共享的原子变量,它可以在多个进程的操作中被并发访问。
也就是说,用户程序的堆区是在父子进程之间共享的,
fork
时不需要复制堆区内容,只需要复制Heap
结构体即可。
在本实验设计中,堆区的最大大小固定、起始地址固定,堆区的大小由 end
变量来控制。当用户程序调用 brk
系统调用时,内核会根据用户程序传入的参数来调整 end
的值,并进行相应的页面映射,从而调整堆区的大小。
如果你还是想和 Linux 对齐,
brk
系统调用的调用号为 12。
下面对 brk
系统调用的参数和行为进行简单的约定。
在用户态中,考虑下列系统调用函数封装:brk
系统调用的参数是一个可为 None
的“指针”,表示用户程序希望调整的堆区结束地址,用户参数采用 0
表示 None
,返回值采用 -1
表示操作失败。
1 2 3 4 5 6 7 8 |
|
在内核中,brk
系统调用的处理函数如下:将用户传入的参数转换为内核的 Option<VirtAddr>
类型进行传递,并使用相同类型作为返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
最终,你需要在 pkg/kernel/src/proc/vm/heap.rs
中为 Heap
结构体实现 brk
函数:
- 如果参数为
None
,则表示用户程序希望获取当前的堆区结束地址,即返回end
的值; - 如果参数不为
None
,则检查用户传入的目标地址是否合法,即是否在[HEAP_START, HEAP_END]
区间内,如果不合法,直接返回None
。
如果目标地址合法,则按照以下约定处理:
- 初始化堆区时,
base
和end
的值均为HEAP_START
; - 用户希望释放整个堆区:目标地址为
base
,释放所有页面,end
重置为base
; - 用户希望缩小堆区:目标地址比当前
end
小,先将目标地址向上对齐到页边界,然后释放多余的页面; - 用户希望扩大堆区:目标地址比当前
end
大,先将目标地址向上对齐到页边界,然后分配的页面; - 若调整成功,返回新的堆区终止地址;否则返回
None
。
用户程序中,一段典型的调整堆的系统调用过程,可以参考如下代码:
1 2 3 4 5 6 |
|
最后,别忘了为 Heap
实现 clean_up
函数,用于释放堆区的页面,对于连续的堆区页面释放可以参考 Stack
进行实现,这里不再赘述。
在实现了 sys_brk
系统调用后,你可以在用户程序中使用 brk
系统调用来调整堆区的大小,从而实现用户程序的内存管理。
如果直接替换现有的用户态堆分配,则很难找出可能存在的问题,因此下面给出一个测试和实现流程作为参考:
-
新建一个用户程序,参考上述代码,尝试在其中使用
brk
系统调用来调整堆区的大小,并进行写入和读取操作; -
若上述操作没有问题,则可以在
lib
中实现可选的第二个内存分配器(代码已在pkg/lib/src/allocator/brk.rs
给出);内存分配器的自主实现不是本次实验的内容,因此这里直接使用
linked_list_allocator
进行代劳。 -
尝试使用新的内存分配器,可以按如下方式修改用户程序的
Cargo.toml
:1 2 3 4 5 6 7 8
[dependencies] - lib = { package = "yslib", path = "../../lib" } + [dependencies.lib] + package = "yslib" + path = "../../lib" + default-features = false + features = ["brk_alloc"]
-
在你测试通过后,可以修改
pkg/lib/Cargo.toml
,将其作为用户程序默认的内存分配器:1 2 3
[features] - default = ["kernel_alloc"] + default = ["brk_alloc"]
在后续的实验中,如果你想要自行实现内存管理算法,可以参考上述过程,通过添加 feature
对代码进行隔离,以便于测试和调试。
如果想要自主测试其他内存管理操作,可以修改自定义的用户程序,或者直接将其实现为接受用户输入的 Shell 命令,进一步测试并记录你的 brk
系统调用的行为。
阶段性成果
你应该能够使用新的内存分配器来让之前的每个用户程序正常执行了。
思考题¶
-
当在 Linux 中运行程序的时候删除程序在文件系统中对应的文件,会发生什么?程序能否继续运行?遇到未被映射的内存会发生什么?
-
为什么要通过
Arc::strong_count
来获取Arc
的引用计数?查看它的定义,它和一般使用&self
的方法有什么不同?出于什么考虑不能直接通过&self
来进行这一操作? -
bootloader 加载内核并为其分配初始栈区时,至少需要多少页内存才能保证内核正常运行?
尝试逐渐增大内核的栈区大小,观察内核的运行情况,对于不能正常启动的情况,尝试分析可能的原因。
提示:内核实现缺页中断的处理时,依赖于哪些子系统?报错是什么?什么子系统可能会导致对应的问题?
-
尝试查找资料,了解
mmap
、munmap
和mprotect
系统调用的功能和用法,回答下列问题:-
mmap
的主要功能是什么?它可以实现哪些常见的内存管理操作? -
munmap
的主要功能是什么?什么时候需要使用munmap
? -
mprotect
的主要功能是什么?使用mprotect
可以实现哪些内存保护操作? -
编写 C 程序,使用
mmap
将一个文件映射到内存中,并读写该文件的内容。思考:文件内容什么时候会被写入到磁盘?
-
综合考虑有关内存、文件、I/O 等方面的知识,讨论为什么
mmap
系统调用在现代操作系统中越来越受欢迎,它具有哪些优势?
-
加分项¶
-
😋 尝试借助
brk
为用户态堆实现自动扩容:LockedHeap
支持extend
方法,可以在堆区不足时扩容大小,但是需要用户程序分配好所需的空间;-
自定义数据结构
BrkAllocator
,并为其实现GlobalAlloc
trait: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
#[global_allocator] static ALLOCATOR: BrkAllocator = BrkAllocator::empty(); struct BrkAllocator { allocator: LockedHeap, } pub fn init() { ALLOCATOR.init(); } impl BrkAllocator { pub const fn empty() -> Self { Self { allocator: LockedHeap::empty(), } } pub fn init(&self) { // FIXME: init heap to initial size with `brk` system call } pub unsafe fn extend(&self /* maybe add params you need */) -> bool { // FIXME: extend heap size with `brk` system call // return false if failed or reach the max size (8 MiB suggested) } } unsafe impl GlobalAlloc for BrkAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { let mut ptr = self.allocator.alloc(layout); // FIXME: if alloc failed, ptr is null // FIXME: try to extend heap size, then alloc again ptr } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { self.allocator.dealloc(ptr, layout) } }