当前位置:   article > 正文

深入RUST标准库内核(三)NonNull<T>/Unique<T>/mem::replace/Layout/GlobalAlloc_rust nonnull作用

rust nonnull作用

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

文章目录

    • 裸指针模块再分析
    • NonNull<T> 代码分析
      • NonNull<T>创建关联方法
      • NonNull<T>类型转换方法
      • NonNull<[T]> 方法
      • NonNull<T>的使用实例
        • NonNull<T> 与MaybeUninit<T>相关函数
    • Unique<T> 代码分析
    • mem模块函数
      • 泛型类型创建
      • 泛型类型拷贝与替换
        • 所有权转移的底层实现
          • 变量调用drop的时机
      • 其他函数
    • RUST堆内存申请及释放
      • RUST类型系统的内存布局
      • `#[repr(transparent)]`内存布局模式
        • `#[repr(packed)]`内存布局模式
        • `#[repr(align(n))]` 内存布局模式
        • `#[repr(RUST)]`内存布局模式
        • `#[repr(C)]`内存布局模式
      • RUST堆内存申请与释放接口
    • 小结

裸指针模块再分析

有了MaybeUnint做基础后,可以对裸指针其他至关重要的标准库函数做出分析

ptr::read<T>(src: *const T) -> T 此函数在MaybeUninit节中已经给出了代码,ptr::read是对所有类型通用的一种复制方法,需要指出,此函数完成浅拷贝,复制后,src指向的变量的所有权会转移至返回值。所以,调用此函数的代码必须保证src指向的变量生命周期结束后不会被编译器自动调用drop,否则可能导致重复drop,出现UB问题。

ptr::read_unaligned<T>(src: *const T) -> T当数据结构中有未内存对齐的成员变量时,需要用此函数读取内容并转化为内存对齐的变量。否则会引发UB(undefined behaiver) 如下例:

/// 从字节数组中读一个usize的值:

   use std::mem;
  
   fn read_usize(x: &[u8]) -> usize {
       assert!(x.len() >= mem::size_of::<usize>());
      
       let ptr = x.as_ptr() as *const usize;
       //此处必须用ptr::read_unaligned,因为不确定字节是否对齐
       unsafe { ptr.read_unaligned() }
   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

例子中,为了从byte串中读取一个usize,需要用read_unaligned来获取值,不能象C语言那样通过指针类型转换直接获取值。

ptr::write<T>(dst: *mut T, src: T) 代码如下:

pub const unsafe fn write<T>(dst: *mut T, src: T) {
    unsafe {
        //浅拷贝
        copy_nonoverlapping(&src as *const T, dst, 1);
        //必须调用forget,这里所有权已经转移。不允许再对src做drop操作
        intrinsics::forget(src);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

write函数本质上就是一个所有权转移的操作。完成src到dst的浅拷贝,然后调用了forget(src), 这使得src的Drop不再被调用。从而将所有权转移到dst。此函数是mem::replace, mem::transmute_copy的基础。底层由intrisic:: copy_no_overlapping支持。
这个函数中,如果dst已经初始化过,那原dst变量的所有权将被丢失掉,有可能引发内存泄漏。

ptr::write_unaligned<T>(dst: *mut T, src: T) 与read_unaligned相对应。举例如下:

    #[repr(packed, C)]
    struct Packed {
        _padding: u8,
        unaligned: u32,
    }
    
    let mut packed: Packed = unsafe { std::mem::zeroed() };
    
    // Take the address of a 32-bit integer which is not aligned.
    // In contrast to `&packed.unaligned as *mut _`, this has no undefined behavior.
    // 对于结构中字节没有按照2幂次对齐的成员,要用addr_of_mut!宏来获得地址,无法用取引用的方式。
    let unaligned = std::ptr::addr_of_mut!(packed.unaligned);
    
    unsafe { std::ptr::write_unaligned(unaligned, 42) };
    
     assert_eq!({packed.unaligned}, 42); // `{...}` forces copying the field instead of creating a reference.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

ptr::read_volatile<T>(src: *const T) -> T 是intrinsics::volatile_load的封装
ptr::write_volatile<T>(dst: *mut T, src:T) 是intrinsics::volatiel_store的封装

ptr::macro addr_of($place:expr) 因为用&获得引用必须是字节按照2的幂次对齐的地址,所以用这个宏获取非地址对齐的变量地址

pub macro addr_of($place:expr) {
    //关键字是&raw const,这个是RUST的原始引用语义,但目前还没有在官方做公开。
    //区别与&, &要求地址必须满足字节对齐和初始化,&raw 则没有这个问题
    &raw const $place
}
  • 1
  • 2
  • 3
  • 4
  • 5

ptr::macro addr_of_mut($place:expr) 作用同上。

pub macro addr_of_mut($place:expr) {
    &raw mut $place
}
  • 1
  • 2
  • 3

指针的通用函数请参考Rust库函数参考

NonNull 代码分析

结构体定义如下:

#[repr(transparent)]
pub struct NonNull<T: ?Sized> {
    pointer: *const T,
}
  • 1
  • 2
  • 3
  • 4

属性repr(transparent)实际上表示外部的封装结构在内存中等价于内部的变量。NonNull<T>在内存中与*const T完全一致。可以直接转化为* const T
裸指针的值因为可以为0,如果敞开来用,会有很多无法控制的代码隐患。按照RUST的习惯,标准库定义了非0的指针封装结构NonNull,从而可以用Option<NonNull>来对值可能为0的裸指针做出强制安全代码逻辑。不需要Option的则认为裸指针不会取值为0。
NonNull本身是协变(covarient)类型.
RUST中的协变,在RUST中,不同的生命周期被视为不同的类型,对于带有生命周期的类型变量做赋值操作时,仅允许子类型赋给基类型(长周期赋给短周期), 为了从基本类型生成复合类型的子类型和基类型的关系,RUST引入了协变性。从基本类型到复合类型的协变性有 协变(covarient)/逆变(contracovarient)/不变(invarient)三种
程序员分析代码时,可以从基本类型之间的生命周期关系及协变性确定复合类型变量之间的生命周期关系,从而做合适的赋值操作。

因为NonNull实际上是封装* mut T类型,但* mut T 与NonNull的协变性不同,所以程序员如果不能确定需要协变类型,就不要使用NonNull

NonNull创建关联方法

创建一个悬垂(dangling)指针, 保证指针满足类型内存对齐要求。该指针可能指向一个正常的变量,所以不能认为指向的内存是未初始化的。

    pub const fn dangling() -> Self {
        unsafe {
            //取内存对齐地址作为裸指针的地址。调用者应保证不对此内存地址进行读写
            let ptr = mem::align_of::<T>() as *mut T;
            NonNull::new_unchecked(ptr)
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

new函数,由输入的*mut T裸指针创建NonNull。代码如下:

    pub fn new(ptr: *mut T) -> Option<Self> {
        if !ptr.is_null() {
            //ptr的安全性已经检查完毕
            Some(unsafe { Self::new_unchecked(ptr) })
        } else {
            None
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

NonNull::<T>::new_unchecked(* mut T)->Self* mut T生成NonNull,不检查* mut T是否为0,调用者应保证* mut T不为0。
from_raw_parts函数,类似裸指针的from_raw_parts。

    pub const fn from_raw_parts(
        data_address: NonNull<()>,
        metadata: <T as super::Pointee>::Metadata,
    ) -> NonNull<T> {
        unsafe {
            //需要先用from_raw_parts_mut形成* mut T指针
            NonNull::new_unchecked(super::from_raw_parts_mut(data_address.as_ptr(), metadata))
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

由From Trait创建NonNull

impl<T: ?Sized> const From<&mut T> for NonNull<T> {
    fn from(reference: &mut T) -> Self {
        unsafe { NonNull { pointer: reference as *mut T } }
    }
}

impl<T: ?Sized> const From<&T> for NonNull<T> {
    fn from(reference: &T) -> Self {
        //此处说明NonNull也可以接收不可变引用,不能后继将这个变量转换为可变引用
        unsafe { NonNull { pointer: reference as *const T } }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

NonNull类型转换方法

NonNull的方法基本与*const T/* mut T相同,也容易理解,下文仅做罗列和简单说明
NonNull::<T>::as_ptr(self)->* mut T 返回内部的pointer 裸指针
NonNull::<T>::as_ref<`a>(&self)->&`a T返回的引用的生命周期与引用指向的变量生命周期无关,调用者应保证返回的引用的生命周期符合安全性要求
NonNull::<T>::as_mut<`a>(&mut self)->&`a mut T与 as_ref类似,但返回可变引用。
NonNull::<T>::cast<U>(self)->NonNull<U>指针类型转换,程序员应该保证T和U的内存布局相同

NonNull<[T]> 方法

NonNull::<[T]>::slice_from_raw_parts(data: NonNull<T>, len: usize) -> Self将类型指针转化为类型的切片类型指针,实质是ptr::slice_from_raw_parts的一种包装。
NonNull::<[T]>::as_non_null_ptr(self) -> NonNull<T> * const [T]::as_ptr的NonNull版本

NonNull的使用实例

以下的实例展示了 NonNull在动态申请堆内存的使用:

    impl Global {
        fn alloc_impl(&self, layout: Layout, zeroed: bool) -> Result<NonNull<[u8]>, AllocError> {
            match layout.size() {
                0 => Ok(NonNull::slice_from_raw_parts(layout.dangling(), 0)),
                // SAFETY: `layout` is non-zero in size,
                size => unsafe {
                    //raw_ptr是 *const u8类型
                    let raw_ptr = if zeroed { alloc_zeroed(layout) } else { alloc(layout) };
                    //NonNull::new处理了raw_ptr为零的情况,返回NonNull<u8>,此时内存长度还与T不匹配
                    let ptr = NonNull::new(raw_ptr).ok_or(AllocError)?;
                    //将NonNull<u8>转换为NonNull<[u8]>, NonNull<[u8]>已经是类型T的内存长度。后继可以直接转换为T类型的指针了。这个转换极为重要。
                    Ok(NonNull::slice_from_raw_parts(ptr, size))
                },
            }
        }
        ....
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

基本上,如果* const T/*mut T要跨越函数使用,或作为数据结构体的成员时,应将之转化成NonNull 或Unique T。*const T应该仅仅保持在单一函数内。

NonNull 与MaybeUninit相关函数

NonNull<T>::as_uninit_ref<`a>(&self) -> &`a MaybeUninit<T>NonNull与MaybeUninit的引用基本就是直接转换的关系,一体双面

    pub unsafe fn as_uninit_ref<'a>(&self) -> &'a MaybeUninit<T> {
        // self.cast将NonNull<T>转换为NonNull<MaybeUninit<T>>
        //self.cast.as_ptr将NonNull<MaybeUninit<T>>转换为 *mut MaybeUninit<T>
        unsafe { &*self.cast().as_ptr() }
    }
  • 1
  • 2
  • 3
  • 4
  • 5

NonNull<T>::as_uninit_mut<`a>(&self) -> &`a mut MaybeUninit<T>
NonNull<[T]>::as_uninit_slice<'a>(&self) -> &'a [MaybeUninit<T>]

    pub unsafe fn as_uninit_slice<'a>(&self) -> &'a [MaybeUninit<T>] {
        // 下面的函数调用ptr::slice_from_raw_parts
        unsafe { slice::from_raw_parts(self.cast().as_ptr(), self.len()) }
    }
  • 1
  • 2
  • 3
  • 4

NonNull<[T]>::as_uninit_slice_mut<'a>(&self) -> &'a mut [MaybeUninit<T>]

Unique 代码分析

Unique类型结构定义如下:

    #[repr(transparent)]
    pub struct Unique<T: ?Sized> {
        pointer: *const T,
        _marker: PhantomData<T>,
    }
  • 1
  • 2
  • 3
  • 4
  • 5

和NonNull对比,Unique多了PhantomData类型变量。这个定义使得编译器知晓,Unique拥有了pointer指向的内存的所有权,NonNull没有这个特性。具备所有权后,Unique可以实现Send, Sync等Trait。因为获得了所有权,此块内存无法用于他处,这也是Unique的名字由来原因.
指针在被Unique封装前,必须保证是NonNull的。
对于RUST从堆内存申请的内存块,其指针都是用Unique封装后来作为智能指针结构体内部成员变量,保证智能指针结构体拥有申请出来的内存块的所有权。

Unique模块的函数及代码与NonNull函数代码相类似,此处不分析。
Unique::cast<U>(self)->Unique<U> 类型转换,程序员应该保证T和U的内存布局相同
Unique::<T>::new(* mut T)->Option<Self> 此函数内部判断* mut T是否为0值
Unique::<T>::new_unchecked(* mut T)->Self 封装* mut T, 调用代码应该保证* mut T的安全性
Unique::as_ptr(self)->* mut T
Unique::as_ref(&self)->& T 因为Unique具备所有权,此处&T的生命周期与self相同,不必特别声明声明周期
Unique::as_mut(&mut self)->& mut T 同上

mem模块函数

泛型类型创建

mem::zeroed<T>() -> T 返回一个内存块清零的泛型变量,内存块在栈空间,代码如下:

pub unsafe fn zeroed<T>() -> T {
    // 调用代码必须确认T类型的变量可以取全零值
    unsafe {
        intrinsics::assert_zero_valid::<T>();
        MaybeUninit::zeroed().assume_init()
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

mem::uninitialized<T>() -> T 返回一个未初始化过的泛型变量,内存块在栈空间。

pub unsafe fn uninitialized<T>() -> T {
    // 调用者必须确认T类型的变量允许未初始化的任意值
    unsafe {
        intrinsics::assert_uninit_valid::<T>();
        MaybeUninit::uninit().assume_init()
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

泛型类型拷贝与替换

mem::take<T: Default>(dest: &mut T) -> T 将dest设置为默认内容(不改变所有权),用一个新变量返回dest的内容。

pub fn take<T: Default>(dest: &mut T) -> T {
    //即mem::replace,见下文
    //此处,对于引用类型,编译器禁止用*dest来转移所有权,所以不能用let xxx = *dest; xxx这种形式返回T
    //其他语言简单的事情在RUST中必须用一个较难理解的方式来进行解决。replace()对所有权有仔细的处理
    replace(dest, T::default())
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

mem::replace<T>(dest: &mut T, src: T) -> T 用src的内容赋值dest(不改变所有权),用一个新变量返回dest的内容。replace函数的难点在于了解所有权的转移。

pub const fn replace<T>(dest: &mut T, src: T) -> T {
    unsafe {
        //因为要替换dest, 所以必须对dest原有变量的所有权做处理,因此先用read将*dest的所有权转移到T,交由调用者进行处理, RUST不支持对引用类型做解引用的相等来转移所有权。将一个引用的所有权进行转移的方式只有粗暴的内存浅拷贝这种方法。
        //使用这个函数,调用代码必须了解T类型的情况,T类型有可能需要显式的调用drop函数。ptr::read前文已经分析过。
        let result = ptr::read(dest);
        //ptr::write本身会导致src的所有权转移到dest,后继不允许在src生命周期终止时做drop。ptr::write会用forget(src)做到这一点。
        ptr::write(dest, src);
        result
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

mem::transmute_copy<T, U>(src: &T) -> U 新建类型U的变量,并把src的内容拷贝到U。调用者应保证T类型的内容与U一致,src后继的所有权问题需要做处理。

pub const unsafe fn transmute_copy<T, U>(src: &T) -> U {
    if align_of::<U>() > align_of::<T>() {
        // 如果两个类型字节对齐U 大于 T. 使用read_unaligned
        unsafe { ptr::read_unaligned(src as *const T as *const U) }
    } else {
        //用read即可完成
        unsafe { ptr::read(src as *const T as *const U) }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
所有权转移的底层实现

所有权的转移实际上是两步: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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

输出:
2 3
TestPtr { a: 1, b: 2 }
1 2

其他函数

mem::forget<T>(t:T) 通知RUST不做变量的drop操作

pub const fn forget<T>(t: T) {
    //没有使用intrinsic::forget, 实际上效果一致,这里应该是尽量规避用intrinsic函数
    let _ = ManuallyDrop::new(t);
}
  • 1
  • 2
  • 3
  • 4

mem::forget_unsized<T:Sized?> 对intrinsics::forget的封装
mem::size_of<T>()->usize/mem::min_align_of<T>()->usize/mem::size_of_val<T>(val:& T)->usize/mem::min_align_of_val<T>(val: &T)->usize/mem::needs_drop<T>()->bool 基本就是直接调用intrinsic模块的同名函数
mem::drop<T>(_x:T) 释放内存

RUST堆内存申请及释放

RUST类型系统的内存布局

RUST提供了Layout内存布局类型, 此布局类型结构主要用于做堆内存申请。
Layout的数据结构如下:

pub struct Layout {
    // 类型需占用的内存大小,用字节数目表示
    size_: usize,
    //  按照此字节数目进行类型内存对齐, NonZeroUsize见代码后面文字分析
    align_: NonZeroUsize,
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

NonZeroUsize是一种非0值的usize, 这种类型主要应用于不可取0的值,本结构中, 字节对齐属性变量不能被置0,所以用NonZeroUsize来确保安全性。如果用usize类型,那代码中就可能会把0置给align_,导致bug产生。这是RUST的一个设计规则,所有的约束要在类型定义即显性化,从而使bug在编译中就被发现。

每一个RUST的类型都有自身独特的内存布局Layout。一种类型的Layout可以用intrinsic::<T>::size_of()intrinsic::<T>::min_align_of()获得的类型内存大小和对齐来获得。
RUST的内存布局更详细原理阐述请参考[RUST内存布局] (https://doc.rust-lang.org/nomicon/data.html),
Layout比较有典型意义的函数:

impl Layout {
    ...
    ...

    //array函数是计算n个T类型变量形成的数组所需的Layout,是从代码了解Rust Layout概念的一个好的实例
    //这里主要注意的是T类型的对齐会导致内存申请不是T类型的内存大小*n
    //而且对齐也是数组的算法
    pub fn array<T>(n: usize) -> Result<Self, LayoutError> {
        //获得n个T类型的内存Layout
        let (layout, offset) = Layout::new::<T>().repeat(n)?;
        debug_assert_eq!(offset, mem::size_of::<T>());
        //以完全对齐的大小  ,得出数组的Layout
        Ok(layout.pad_to_align())
    }

    //计算n个T类型需要的内存Layout, 以及成员之间的空间
    pub fn repeat(&self, n: usize) -> Result<(Self, usize), LayoutError> {
        // 所有的成员必须以成员的对齐大小来做内存对齐,首先计算对齐需要的padding空间
        let padded_size = self.size() + self.padding_needed_for(self.align());
        // 计算共需要多少内存空间,如果溢出,返回error
        let alloc_size = padded_size.checked_mul(n).ok_or(LayoutError)?;

        //由已经验证过得原始数据生成Layout,并返回单成员占用的空间
        unsafe { Ok((Layout::from_size_align_unchecked(alloc_size, self.align()), padded_size)) }
    }

    //填充以得到一个与T类型完全对齐的,最小的内存大小的Layout
    pub fn pad_to_align(&self) -> Layout {
        //得到T类型与对齐之间的空间大小
        let pad = self.padding_needed_for(self.align());
        // 完全对齐的大小
        let new_size = self.size() + pad;
        
        //以完全对齐的大小生成新的Layout
        Layout::from_size_align(new_size, self.align()).unwrap()
    }

    //计算T类型长度与完全对齐的差
    pub const fn padding_needed_for(&self, align: usize) -> usize {
        let len = self.size();

        // 实际上相当与C语言的表达式
        //   len_rounded_up = (len + align - 1) & !(align - 1);
        // 就是对对齐大小做除,如果有余数,商加1,是一种常用的方式.
        // 但注意,在rust中C语言的"+"等同于wrapping_add, C语言的“-”等同于
        // wrapping_sub
        let len_rounded_up = len.wrapping_add(align).wrapping_sub(1) & !align.wrapping_sub(1);
        //减去len,得到差值
        len_rounded_up.wrapping_sub(len)
    }

    //不检查输入参数,根据输入参数表示的原始数据生成Layout变量,调用代码应保证安全性
    pub const unsafe fn from_size_align_unchecked(size: usize, align: usize) -> Self {
        // 必须保证align满足不为0.
        Layout { size_: size, align_: unsafe { NonZeroUsize::new_unchecked(align) } }
    }

    //对参数进行检查,生成一个类型的Layout
    pub const fn from_size_align(size: usize, align: usize) -> Result<Self, LayoutError> {
        //必须保证对齐是2的幂次
        if !align.is_power_of_two() {
            return Err(LayoutError);
        }

        //满足下面的表达式,则size将不可能对齐 
        if size > usize::MAX - (align - 1) {
            return Err(LayoutError);
        }

        // 参数已经检查完毕.
        unsafe { Ok(Layout::from_size_align_unchecked(size, align)) }
    }
    ...
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75

#[repr(transparent)]内存布局模式

repr(transparent)用于仅包含一个成员变量的类型,该类型的内存布局与成员变量类型的内存布局完全一致。类型仅仅具备编译阶段的意义,在运行时,类型变量与其成员变量可以认为是一个相同变量,可以相互无障碍类型转换。使用repr(transparent)布局的类型基本是一种封装结构。

#[repr(packed)]内存布局模式

强制类型成员变量以1字节对齐,此种结构在协议分析和结构化二进制数据文件中经常使用

#[repr(align(n))] 内存布局模式

强制类型以2的幂次对齐

#[repr(RUST)]内存布局模式

默认的布局方式,采用此种布局,RUST编译器会根据情况来自行优化内存

#[repr(C)]内存布局模式

采用C语言布局方式, 所有结构变量按照声明的顺序在内存排列。默认4字节对齐。

RUST堆内存申请与释放接口

资深的C/C++程序员都了解,在大型系统开发时,往往需要自行实现内存管理模块,以根据系统的特点优化内存使用及性能,并作出内存跟踪。
对于操作系统,内存管理模块更是核心功能。
对于C/C++小型系统,没有内存管理,仅仅是调用操作系统的内存系统调用,内存管理交给操作系统负责。操作系统内存管理模块接口是内存申请及内存释放的系统调用
对于GC语言,内存管理由虚拟机或语言运行时负责,利用语言提供的new来完成类型结构内存获取。
RUST的内存管理分成了三个界面:

  1. 由智能指针类型提供的类型创建函数,一般有new, 与其他的GC类语言相同,同时增加了一些更直观的函数。
  2. 智能指针使用实现Allocator Trait的类型做内存申请及释放。Allocator使用编译器提供的函数名申请及释放内存。
  3. 实现了GlobalAlloc Trait的类型来完成独立的内存管理模块,并用#[global_allocator]注册入编译器,替代编译器默认的内存申请及释放函数。
    这样,RUST达到了:
  4. 对于小规模的程序,拥有与GC语言相类似的内存获取机制
  5. 对于大型程序和操作系统内核,从语言层面提供了独立的内存管理模块接口,达成了将现代语法与内存管理模块共同存在,相互配合的目的。
    但因为所有权概念的存在,从内存申请到转换为类型系统仍然还存在复杂的工作。
    堆内存申请和释放的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
    }

    //其他方法
    ...
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在内核编程或大的框架系统编程中,开发人员通常开发自定义的堆内存管理模块,模块实现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()) }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

再实现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);

    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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) }
        }
    }
    ...
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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)
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

以上代码可以看到,NonNull<[u8]>可以直接通过cast 转换为NonNull<MaybeUninit>, 这是另一种MaybeUninit的生成方法,直接通过指针类型转换将未初始化的内存转换为MaybeUninit。

小结

本章主要分析了RUST标准库内存相关模块, 内存相关模块代码多数不复杂,主要是要对内存块与类型系统之间的转换要有比较深刻的理解,并能领会在实际编码过程中在那些场景会使用内存相关的代码和API。RUST的内存安全给编码加了非常多的限制,有些时候这些限制只有通过内存API来有效的突破。如将引用指向的变量所有权转移出来的take函数。后继我们会看到几乎每个标准库的模块都大量的使用了ptr, mem模块中的方法和函数。只要是大型系统,不熟悉内存模块的代码,基本上无法做出良好的程序。

后继文章仅粉丝可见,如要阅读请关注

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/702709
推荐阅读
相关标签
  

闽ICP备14008679号