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

本文摘自《深入 rust 标准库》一书 ,已经全网发售,恳请支持

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 完成较复杂的任务。

本作品采用《CC 协议》,转载必须注明作者和本文链接
Warren Ren