深入RUST标准库内核—内存之堆内存申请及释放

Rust
317
0
0
2022-09-05

本文摘自 inside-rust-std-library

RUST堆内存申请与释放接口

资深的C/C++程序员都了解,在大型系统开发时,往往需要自行实现内存管理模块,以根据系统的特点优化内存使用及性能,并作出内存跟踪。

对于操作系统,内存管理模块更是核心功能。

对于C/C++小型系统,没有内存管理,仅仅是调用操作系统的内存系统调用,内存管理交给操作系统负责。操作系统内存管理模块接口是内存申请及内存释放的系统调用

对于GC语言,内存管理由虚拟机或语言运行时负责,利用语言提供的new来完成类型结构内存获取。

RUST的内存管理分成了三个界面:

  1. 由智能指针类型提供的类型创建函数,一般有new, 与其他的GC类语言相同,同时增加了一些更直观的函数。
  2. 智能指针使用实现Allocator Trait的类型做内存申请及释放。Allocator使用编译器提供的函数名申请及释放内存。
  3. 实现了GlobalAlloc Trait的类型来完成独立的内存管理模块,并用#[global_allocator]注册入编译器,替代编译器默认的内存申请及释放函数。

这样,RUST达到了:

  1. 对于小规模的程序,拥有与GC语言相类似的内存获取机制
  2. 对于大型程序和操作系统内核,从语言层面提供了独立的内存管理模块接口,达成了将现代语法与内存管理模块共同存在,相互配合的目的。

但因为所有权概念的存在,从内存申请到转换为类型系统仍然还存在复杂的工作。

堆内存申请和释放的Trait GlobalAlloc定义如下:

pub unsafe trait GlobalAlloc {
    //申请内存,因为Layout中内存大小不为0,所以,alloc不会申请大小为0的内存 
    unsafe fn alloc(&self, layout: Layout) -> *mut u8;
    //释放内存 
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout);
    //申请后的内存应初始化为0 
    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
        let size = layout.size();
        let ptr = unsafe { self.alloc(layout) };
        if !ptr.is_null() {
            // 此处必须使用write_bytes,确保每个字节都清零 
            unsafe { ptr::write_bytes(ptr, 0, size) };
        }
        ptr
    }

    //其他方法
    ...
    ...
}

在内核编程或大的框架系统编程中,开发人员通常开发自定义的堆内存管理模块,模块实现GlobalAlloc Trait并添加#[global_allocator]标识。对于用户态,RUST标准库有默认的GlobalAlloc实现。

extern "Rust" {
    // 编译器会将实现了GlobalAlloc Trait,并标记 #[global_allocator]的四个方法自动转化为以下的函数 
    #[rustc_allocator] 
    #[rustc_allocator_nounwind] 
    fn __rust_alloc(size: usize, align: usize) -> *mut u8;
    #[rustc_allocator_nounwind] 
    fn __rust_dealloc(ptr: *mut u8, size: usize, align: usize);
    #[rustc_allocator_nounwind] 
    fn __rust_realloc(ptr: *mut u8, old_size: usize, align: usize, new_size: usize) -> *mut u8;
    #[rustc_allocator_nounwind] 
    fn __rust_alloc_zeroed(size: usize, align: usize) -> *mut u8;
}

//对__rust_xxxxx_再次封装
pub unsafe fn alloc(layout: Layout) -> *mut u8 {
    unsafe { __rust_alloc(layout.size(), layout.align()) }
}

pub unsafe fn dealloc(ptr: *mut u8, layout: Layout) {
    unsafe { __rust_dealloc(ptr, layout.size(), layout.align()) }
}

pub unsafe fn realloc(ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
    unsafe { __rust_realloc(ptr, layout.size(), layout.align(), new_size) }
}

pub unsafe fn alloc_zeroed(layout: Layout) -> *mut u8 {
    unsafe { __rust_alloc_zeroed(layout.size(), layout.align()) }
}

再实现Allocator Trait,对以上四个函数做封装处理。作为RUST其他模块对堆内存的申请和释放接口。

pub unsafe trait Allocator {
    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError>;
    fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        let ptr = self.allocate(layout)?;
        // SAFETY: `alloc` returns a valid memory block 
        // 复杂的类型转换,实际是调用 *const u8::write_bytes(0, layout.size_) 
        unsafe { ptr.as_non_null_ptr().as_ptr().write_bytes(0, ptr.len()) }
        Ok(ptr)
    }

    unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout);
    ...

}

Global 实现了 Allocator Trait。Rust大部分alloc库数据结构的实现使用Global作为Allocator。

unsafe impl Allocator for Global {
    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        //上文已经给出alloc_impl的说明 
        self.alloc_impl(layout, false)
    }

    fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        self.alloc_impl(layout, true)
    }

    unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
        if layout.size() != 0 {
            // SAFETY: `layout` is non-zero in size, 
            // other conditions must be upheld by the caller 
            unsafe { dealloc(ptr.as_ptr(), layout) }
        }
    }

    ...
    ...

}

Allocator使用GlobalAlloc接口获取内存,然后将GlobalAlloc申请到的* mut u8转换为确定大小的单一指针NonNull<[u8]>, 并处理申请内存可能出现的不成功。NonNull<[u8]>此时内存布局与 T的内存布局已经相同,后继可以转换为真正需要的T的指针并进一步转化为相关类型的引用,从而符合RUST类型系统安全并进行后继的处理。

以上是堆内存的申请和释放。 基于泛型,RUST也巧妙实现了栈内存的申请和释放机制 mem::MaybeUninit<T>

用Box的内存申请做综合举例:

    //此处A是一个A:Allocator类型 
    pub fn try_new_uninit_in(alloc: A) -> Result<Box<mem::MaybeUninit<T>, A>, AllocError> {
        //实质是T类型的内存Layout 
        let layout = Layout::new::<mem::MaybeUninit<T>>();
        //allocate(layout)?返回NonNull<[u8]>, NonNull<[u8]>::<MaybeUninit<T>>::cast()返回NonNull<MaybeUninit<T>> 
        let ptr = alloc.allocate(layout)?.cast();
        //as_ptr 成为 *mut MaybeUninit<T>类型原生指针 
        unsafe { Ok(Box::from_raw_in(ptr.as_ptr(), alloc)) }
    }

    pub unsafe fn from_raw_in(raw: *mut T, alloc: A) -> Self {
        //使用Unique封装* mut T,并拥有了*mut T指向的变量的所有权 
        Box(unsafe { Unique::new_unchecked(raw) }, alloc)
    }

以上代码可以看到,NonNull<[u8]>可以直接通过cast 转换为NonNull<MaybeUninit>, 这是另一种MaybeUninit的生成方法,直接通过指针类型转换将未初始化的内存转换为MaybeUninit。

所有权转移的底层实现

所有权的转移实际上是两步:1.栈上内存的浅拷贝;2:原先的变量置标志表示所有权已转移。置标志的变量如果没有重新绑定其他变量,则在生命周期结束的时候被drop。 引用及指针自身也是一个isize的值变量,也有所有权,也具备生命周期。

变量调用drop的时机

如下例子:

struct TestPtr {a: i32, b:i32}
impl Drop for TestPtr {
    fn drop(&mut self) {
        println!("{} {}", self.a, self.b);
    }
}

fn main() {
   let test = Box::new(TestPtr{a:1,b:2});
   let test1 = *test;
   let mut test2 = TestPtr{a:2, b:3};
   //此行代码会导致先释放test2拥有所有权的变量,然后再给test2赋值。代码后的输出会给出证据 
   //将test1的所有权转移给test2,无疑代表着test2现有的所有权会在后继无法访问,因此drop被立即调用。
   test2 = test1;
   println!("{:?}", test2);

}

输出:

2 3

TestPtr { a: 1, b: 2 }

1 2

小结

在RUST标准库的ptr, mem,alloc模块提供了RUST内存的底层操作。内存的底层操作是其他RUST库模块的基础设施。不能理解内存的底层操作,就无法驾驭RUST完成较复杂的任务。