跳转至

Rust标准库: alloc

原文: https://zhuanlan.zhihu.com/p/372018328

内存分配

任何程序最基本需求是对 物理内存虚拟内存 的访问。一个分配器负责提供这种访问。你可以把分配器想象成一个服务,接受某种请求,并返回一个(指针)内存块或一些错误。在Rust中,一个请求是一个 Layout,即一些元数据(bytes)是如何在内存上以某种结构排列的。

一个内存布局是由以下部分组成:

  • size (以bytes为单位)
  • alignment (大小以字节为单位并且是2的幂): CPU总是以word的大小(32位系统为4字节,64位系统为8字节)来读取内存。

https://stackoverflow.com/questions/381244/purpose-of-memory-alignment/381368#381368

std::alloc::Layout

#[lang = "alloc_layout"]
pub struct Layout {
    // 请求的内存块的大小,以字节为单位。
    size_: usize,

    // 请求的内存块的排列方式,以字节为单位。
    // 我们确保这始终是2的幂值......
    align_: NonZeroUsize,
}

例如, 要检查一个类型的大小或一个值的对齐方式可以使用以下方法:

println!("type alignment of i32 {}", mem::align_of::<i32>()); // --> 4 bytes
println!("val alignment 1i32 {}", mem::align_of_val(&1i32)); // --> 4 bytes
println!("type size i32 {}", mem::size_of::<i32>()); // --> 4 bytes
println!("val size 1i32 {}", mem::size_of_val(&1i32)); // --> 4 bytes

// 空的结构体
struct A;
let val = A;
println!("type alignment {}", mem::align_of::<A>()); // --> 1 byte
println!("val alignment {}", mem::align_of_val(&val)); // --> 1 byte
println!("type size {}", mem::size_of::<A>()); // --> 0 bytes
println!("val size {}", mem::size_of_val(&val)); // --> 0 bytes

// 还有
println!("Layout of A: {:?}", Layout::new::<A>()); // --> Layout { size_: 0, align_: 1 }

大小与对齐之间是否有关系?

查看 std::alloc::from_size_align 表明在构建具有 给定大小对齐方式 的布局时必须满足以下三个要求 :

  • align 必须非0
  • align 必须2的幂
  • size 当四舍五入到最近的对齐倍数时,不得溢出(即四舍五入的值必须小于usize::MAX)即:

98028-curk1xlda0f.png]

我们可以用以下方法计算四舍五入的大小

29554-euzq6ueu9ya.png

例如: 如果我想创建一个layout大小为6 bytes,并且对齐是8 bytes,那么四舍五入的尺寸将是8 bytes

同样的,对于一个layout大小为10 bytes 并且对齐为 8 bytes 时,四舍五入的大小将是16字节。四舍五入后的大小和给定的大小之间的差异是该对齐方式所需的填充量,分别为2和6。

我们在前面的例子中看到,我们可以用 Layout::new::<T>() 为一个类型T创建一个布局。此外,给定一个引用值 &T,我们可以用std::alloc::for_value创建理想的布局。基本上,它们是

impl<T> Layout<T> {
    pub fn new<T>() -> Self {
        let (size, align) = (mem::size_of::<T>(), mem::align_of::<T>());

        // 请注意,rustc会保证align是2的幂,
        // 并且 size+align的组合被保证适合我们的地址空间。
        // 在这里我们使用了unchecked的构造函数,以避免加入更多的代码。 因为出于教学目地
        // 这段代码没有进行优化,可能会就会出现panic。
        debug_assert!(Layout::from_size_align(size, align).is_ok());
        unsafe {
            Layout::from_size_align_unchecked(size, align)
        }
    }

    pub fn for_value<T: ?Sized>(t: &T) -> Self {
        let (size, align) = (mem::size_of_val(t), mem::align_of_val(t));
        // 请参阅`new`中的基本原理,以了解为什么我们在下面使用unsafe代码块
        debug_assert!(Layout::from_size_align(size, align).is_ok());
        unsafe {
            Layout::from_size_align_unchecked(size, align)
        }
    }
}

到目前为止,我们已经介绍了layout(内存分配器所需的参数)。接下来让我们仔细看看分配器的主要构成部分。

std::alloc::GlobalAlloc

// 注意GlobalAlloc是一个trait,不是struct
pub unsafe trait GlobalAlloc {
    // required methods
    unsafe fn alloc(&self, layout: Layout) -> *mut u8;
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout);
    // provided methods
    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { ... }
    unsafe fn realloc(
        &self, 
        ptr: *mut u8, 
        layout: Layout, 
        new_size: usize
    ) -> *mut u8 { ... }
}

实现这个trait到一个类型上后,可用通过标记 #[global_allocator] 属性注册为标准库默认的默认分配器,alloc方法接受一个layout参数,并返回一个指向内存块的指针,亦或者在 Out-Of-Memory (OOM) 内存不足时返回一个NULL批针,这个分配器也可能啥也不分配(取决于你的控制)。**

在写这个笔记之前, GlobalAlloc 还是 std::alloc::Alloc 的一个拥有更多功能的实验性的变体,请注意,默认分配器是std::alloc::System,在调用System::alloc的情况下,它要么提供一个非空的指针(指向某个内存块),要么提供一些AllocErr。

pub struct System;

// The Alloc impl just forwards to the GlobalAlloc impl, which is in `std::sys::*::alloc`.
#[unstable(feature = "allocator_api", issue = "32838")]
unsafe impl Alloc for System {
    #[inline]
    unsafe fn alloc(&mut self, layout: Layout) -> Result<NonNull<u8>, AllocErr> {
        NonNull::new(GlobalAlloc::alloc(self, layout)).ok_or(AllocErr)
    }

    #[inline]
    unsafe fn alloc_zeroed(&mut self, layout: Layout) -> Result<NonNull<u8>, AllocErr> {
        NonNull::new(GlobalAlloc::alloc_zeroed(self, layout)).ok_or(AllocErr)
    }

    #[inline]
    unsafe fn dealloc(&mut self, ptr: NonNull<u8>, layout: Layout) {
        GlobalAlloc::dealloc(self, ptr.as_ptr(), layout)
    }

    #[inline]
    unsafe fn realloc(&mut self,
                      ptr: NonNull<u8>,
                      layout: Layout,
                      new_size: usize) -> Result<NonNull<u8>, AllocErr> {
        NonNull::new(GlobalAlloc::realloc(self, ptr.as_ptr(), layout, new_size)).ok_or(AllocErr)
    }
}

最新的STD库可能已经变了。具体请参考std::alloc::System

unix系统上, the System 分配器默认使用 libc::malloc

unsafe impl GlobalAlloc for System {
    #[inline]
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        if layout.align() <= MIN_ALIGN && layout.align() <= layout.size() {
            libc::malloc(layout.size()) as *mut u8
        } else {
            #[cfg(target_os = "macos")]
            {
                if layout.align() > (1 << 31) {
                    return ptr::null_mut()
                }
            }
            aligned_malloc(&layout) // --> more or less is: libc::memalign(layout.align(), layout.size()) as *mut u8
        }
    }
    ...

到此为止!我们已经涵盖了std::alloc的大部分基础知识和重要方面。