动态内存分配

我们之前在 C/C++ 语言中使用过 new, malloc 等动态内存分配方法,与在编译期就已完成的静态内存分配相比,动态内存分配可以根据程序运行时状态修改内存申请的时机及大小,显得更为灵活,但是这是需要操作系统的支持的,会带来一些开销。

我们的内核中也需要动态内存分配。典型的应用场景有:

  • Box<T> ,你可以理解为它和 new, malloc 有着相同的功能;
  • 引用计数 Rc<T> , 原子引用计数 Arc<T> ,主要用于在引用计数清零,即某对象不再被引用时,对该对象进行自动回收;
  • 一些数据结构,如 Vec, HashMap 等。

为了在我们的内核中支持动态内存分配,在 Rust 语言中,我们需要实现 Trait GlobalAlloc ,将这个类实例化,并使用语义项 #[global_allocator] 进行标记。这样的话,编译器就会知道如何进行动态内存分配。

为了实现 Trait GlobalAlloc ,我们需要支持这么两个函数:

  1. unsafe fn alloc(&self, layout: Layout) -> *mut u8;
  2. unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout);

可见我们要分配/回收一块虚拟内存。

那么这里面的 Layout 又是什么呢?从文档中可以找到,它有两个字段: size 表示要分配的字节数,align 则表示分配的虚拟地址的最小对齐要求,即分配的地址要求是 align 的倍数。这里的 align 必须是

动态内存分配 - 图1

的幂次。

也就表示,我们的需求是分配一块连续的、大小至少为 size 字节的虚拟内存,且对齐要求为 align

连续内存分配算法

假设我们已经有一整块虚拟内存用来分配,那么如何进行分配呢?

我们可能会想到一些简单粗暴的方法,比如对于一个分配任务,贪心地将其分配到可行的最小地址去。这样一直分配下去的话,我们分配出去的内存都是连续的,看上去很合理的利用了内存。

但是一旦涉及到回收的话,设想我们在连续分配出去的很多块内存中间突然回收掉一块,它虽然是可用的,但是由于上下两边都已经被分配出去,它就只有这么大而不能再被拓展了,这种可用的内存我们称之为外碎片

随着不断回收会产生越来越多的碎片,某个时刻我们可能会发现,需要分配一块较大的内存,几个碎片加起来大小是足够的,但是单个碎片是不够的。我们会想到通过碎片整理将几个碎片合并起来。但是这个过程的开销极大。

* buddy system 算法简介

这一节将介绍连续内存分配算法 buddy system 的实现细节与讨论,不感兴趣的读者可以跳过这一节。

假设这一整块虚拟内存的大小是

动态内存分配 - 图2

的幂次,我们可以使用一种叫做 buddy system 的连续内存分配算法。其本质在于,每次分配的时候都恰好分配一块大小是

动态内存分配 - 图3

的幂次的内存,且要保证内存的开头地址需要是对齐的,也就是内存的开头地址需要是这块内存大小的倍数。

只分配大小为

动态内存分配 - 图4

的幂次的内存,意味着如果需要一块大小为

动态内存分配 - 图5

内存,我们都只能给它分配一块

动态内存分配 - 图6

的内存,这其中有

动态内存分配 - 图7

我们没有使用但又没法再被分配出去,这种我们称之为内碎片。虽然也会产生一定的浪费,但是相比外碎片,它是可控且易于管理的。

伙伴分配器的一个极简实现所说,我们可以使用一颗线段树很容易地实现这个算法。我们只需在每个线段树节点上存当前区间上所能够分配的最大

动态内存分配 - 图8

的幂次的内存大小

动态内存分配 - 图9

  • 分配时,为了尽可能满足分配的对齐需求,我们先尝试右子树,再尝试左子树,直到找到一个节点满足这个区间整体未分配,且它的左右子区间都不够分配,就将这个区间整体分配出去,将当前区间的

    动态内存分配 - 图10

    值改为

    动态内存分配 - 图11

  • 之后自下而上进行

    动态内存分配 - 图12

    值的更新,

    动态内存分配 - 图13

    。但有一个特例,如果左右区间均完全没有被分配,则

    动态内存分配 - 图14

    , 即将两个区间合并成一个更大的区间以供分配。

  • 回收时只需找到分配时的那个节点,将其

    动态内存分配 - 图15

    值改回去,同时同样自下而上进行

    动态内存分配 - 图16

    值更新即可。从更新逻辑可以看出,我们实现了对于回收内存进行合并。

这样的实现虽然比较简单,但是内存消耗较大。为了减少内存消耗,我们不存

动态内存分配 - 图17

,而用一个 u8

动态内存分配 - 图18

,但是整颗线段树仍需要消耗虚拟内存大小

动态内存分配 - 图19

倍的内存!因此,等于要占用

动态内存分配 - 图20

倍的内存,才能有一块虚拟内存用来连续分配,这会导致我们的内核及其臃肿。

有些实现规定了最小分配块大小,比如说是

动态内存分配 - 图21

字节 ,这样我们只需总共占用

动态内存分配 - 图22

倍的内存就能有一块虚拟内存用于分配了!在我们

动态内存分配 - 图23

位的环境下,哪怕分配一个智能指针也需要

动态内存分配 - 图24

字节,看上去挺合理的。还有一些其他方法,比如把底层换成 Bitmap 等卡常数手段…

简单的思考一下,实现简便与内存节约不可兼得啊…

支持动态内存分配

我们这里直接用学长写好的 buddy system allocator。

  1. // Cargo.toml
  2. [dependencies]
  3. buddy_system_allocator = "0.3"
  1. // src/lib.rs
  2. #![feature(alloc_error_handler)]
  3. extern crate alloc;
  1. // src/consts.rs
  2. // 内核堆大小为8MiB
  3. pub const KERNEL_HEAP_SIZE: usize = 0x800000;
  1. // src/memory/mod.rs
  2. use crate::consts::*;
  3. use buddy_system_allocator::LockedHeap;
  4. pub fn init(l: usize, r: usize) {
  5. FRAME_ALLOCATOR.lock().init(l, r);
  6. init_heap();
  7. println!("++++ setup memory! ++++");
  8. }
  9. fn init_heap() {
  10. // 同样是在内核中开一块静态内存供 buddy system allocator 使用
  11. static mut HEAP: [u8; KERNEL_HEAP_SIZE] = [0; KERNEL_HEAP_SIZE];
  12. unsafe {
  13. // 这里我们也需要先开锁,才能进行操作
  14. DYNAMIC_ALLOCATOR
  15. .lock()
  16. .init(HEAP.as_ptr() as usize, KERNEL_HEAP_SIZE);
  17. }
  18. }
  19. #[global_allocator]
  20. static DYNAMIC_ALLOCATOR: LockedHeap = LockedHeap::empty();
  21. #[alloc_error_handler]
  22. fn alloc_error_handler(_: core::alloc::Layout) -> ! {
  23. panic!("alloc_error_handler do nothing but panic!");
  24. }

动态内存分配测试

现在我们来测试一下动态内存分配是否有效,分别动态分配一个整数和一个数组:

  1. // src/init.rs
  2. fn dynamic_allocating_test() {
  3. use alloc::vec::Vec;
  4. use alloc::boxed::Box;
  5. extern "C" {
  6. fn sbss();
  7. fn ebss();
  8. }
  9. let lbss = sbss as usize;
  10. let rbss = ebss as usize;
  11. let heap_value = Box::new(5);
  12. assert!(*heap_value == 5);
  13. println!("heap_value assertion successfully!");
  14. println!("heap_value is at {:p}", heap_value);
  15. let heap_value_addr = &*heap_value as *const _ as usize;
  16. assert!(heap_value_addr >= lbss && heap_value_addr < rbss);
  17. println!("heap_value is in section .bss!");
  18. let mut vec = Vec::new();
  19. for i in 0..500 {
  20. vec.push(i);
  21. }
  22. for i in 0..500 {
  23. assert!(vec[i] == i);
  24. }
  25. println!("vec assertion successfully!");
  26. println!("vec is at {:p}", vec.as_slice());
  27. let vec_addr = vec.as_ptr() as usize;
  28. assert!(vec_addr >= lbss && vec_addr < rbss);
  29. println!("vec is in section .bss!");
  30. }

make run 看一下结果:

动态内存分配测试

  1. heap_value assertion successfully!
  2. heap_value is at 0x80a10000
  3. heap_value is in section .bss!
  4. vec assertion successfully!
  5. vec is at 0x80211000
  6. vec is in section .bss!

我们可以发现这些动态分配的变量可以使用了。而且通过查看它们的地址我们发现它们都在

动态内存分配 - 图25

段里面。这是因为提供给动态内存分配器的那块内存就在

动态内存分配 - 图26

段里面啊。

如果结果不太对劲,可以在这里查看现有的代码。