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

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

本作品采用《CC 协议》,转载必须注明作者和本文链接
Warren Ren
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!