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]的四个方法自动转化为以下的函数
#[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完成较复杂的任务。