RUST堆内存申请与释放接口
资深的C/C++程序员都了解,在大型系统开发时,往往需要自行实现内存管理模块,以根据系统的特点优化内存使用及性能,并作出内存跟踪。
对于操作系统,内存管理模块更是核心功能。
对于C/C++小型系统,没有内存管理,仅仅是调用操作系统的内存系统调用,内存管理交给操作系统负责。操作系统内存管理模块接口是内存申请及内存释放的系统调用
对于GC语言,内存管理由虚拟机或语言运行时负责,利用语言提供的new来完成类型结构内存获取。
RUST的内存管理分成了三个界面:
- 由智能指针类型提供的类型创建函数,一般有new, 与其他的GC类语言相同,同时增加了一些更直观的函数。
- 智能指针使用实现Allocator Trait的类型做内存申请及释放。Allocator使用编译器提供的函数名申请及释放内存。
- 实现了GlobalAlloc Trait的类型来完成独立的内存管理模块,并用#[global_allocator]注册入编译器,替代编译器默认的内存申请及释放函数。
这样,RUST达到了:
- 对于小规模的程序,拥有与GC语言相类似的内存获取机制
- 对于大型程序和操作系统内核,从语言层面提供了独立的内存管理模块接口,达成了将现代语法与内存管理模块共同存在,相互配合的目的。
但因为所有权概念的存在,从内存申请到转换为类型系统仍然还存在复杂的工作。
堆内存申请和释放的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]的四个方法自动转化为以下的函数 | |
fn __rust_alloc(size: usize, align: usize) -> *mut u8; | |
fn __rust_dealloc(ptr: *mut u8, size: usize, align: usize); | |
fn __rust_realloc(ptr: *mut u8, old_size: usize, align: usize, new_size: usize) -> *mut u8; | |
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完成较复杂的任务。