关于rust中内存模块-代码分析(二)

前篇

再议裸指针模块

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

  • ptr::read<T>(src: *const T) -> T
    此函数在MaybeUninit<T>节中已经给出了代码,ptr::read是对所有类型通用的一种复制方法,需要指出,此函数完成浅拷贝,复制后,src指向的变量的所有权会转移至返回值。所以,调用此函数的代码必须保证src指向的变量生命周期结束后不会被编译器自动调用drop,否则可能导致重复drop,出现UB问题。
  • ptr::read_unaligned<T>(src: *const T) -> T
    当数据结构中有未内存对齐的成员变量时,需要用此函数读取内容并转化为内存对齐的变量。否则会引发未定义行为(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() }
   }

例子中,为了从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);
    }
}

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.
  • 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
}

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

pub macro addr_of_mut($place:expr) {
    &raw mut $place
}

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

NonNull<T>代码分析

结构体定义如下:

#[repr(transparent)]
pub struct NonNull<T: ?Sized> {
    pointer: *const T,
}
说明:

属性repr(transparent)实际上表示外部的封装结构在内存中等价于内部的变量。
NonNull<T>在内存中与*const T完全一致。可以直接转化为* const T

裸指针的值因为可以为0,如果敞开来用,会有很多无法控制的代码隐患。按照rust的习惯,标准库定义了非0的指针封装结构NonNull<T>,从而可以用Option<NonNull<T>>来对值可能为0的裸指针做出强制安全代码逻辑。不需要Option的则认为裸指针不会取值为0。

NonNull<T>本身是协变(covarient)类型.
rust中的协变,在rust中,不同的生命周期被视为不同的类型,对于带有生命周期的类型变量做赋值操作时,仅允许子类型赋给基类型(长周期赋给短周期), 为了从基本类型生成复合类型的子类型和基类型的关系,rust引入了协变性。从基本类型到复合类型的协变性有 协变(covarient)/逆变(contracovarient)/不变(invarient)三种**
程序员分析代码时,可以从基本类型之间的生命周期关系及协变性确定复合类型变量之间的生命周期关系,从而做合适的赋值操作。

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

NonNull<T>创建关联方法

创建一个悬垂(dangling)指针, 保证指针满足类型内存对齐要求。该指针可能指向一个正常的变量,所以不能认为指向的内存是未初始化的。dangling实际表示NonNull<T>无意义,与NonNull<T>的本意有些违背,因为这个语义可以用None来实现。

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

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

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

NonNull::<T>::new_unchecked(* mut T)->Self* mut T生成NonNull<T>,不检查* 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))
        }
    }

由From trait创建NonNull<T>

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 } }
    }
}
NonNull<T>类型转换方法

NonNull<T>的方法基本与*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<T>的使用实例

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

    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))
                },
            }
        }
        ....
    }

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

NonNull<T>MaybeUninit<T>相关函数

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() }
    }

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()) }
    }

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

Unique<T> 代码分析

Unique<T>类型结构定义如下:

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

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

Unique<T>模块的函数及代码与NonNull<T>函数代码相类似,此处不分析。
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()
    }
}

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

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

泛型类型拷贝与替换

mem::take<T: Default>(dest: &mut T) -> T 将dest设置为默认内容(不改变所有权),用一个新变量返回dest的内容。这里有一个坑,即任何类型的default()必然能够满足多次drop不会出现内存安全问题。

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

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
    }
}

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) }
    }
}

所有权转移的底层实现

所有权的本质是只能对变量做一次drop操作。变量的drop操作会引起变量结构体内部成员的链式drop。
所以,只要引发了变量的浅拷贝,所有权便被转移。原先放置变量的那块内存就必须被处理,forget及ManuallyDrop是两种典型方案。
不涉及裸指针的代码,一般不必考虑所有权必须人工处理的情况。但一旦涉及到裸指针,那就必须注意看是否出现了一个变量的双份或多份拷贝,每多一次拷贝,意味着编译器会对变量多做一次drop,触发UB。

变量调用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

其他函数

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

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

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,
}

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)) }
    }
    ...
    ...
}

#[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
    }

    //其他方法
    ...
    ...
}

在内核编程或大的框架系统编程中,开发人员通常开发自定义的堆内存管理模块,模块实现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<T>>, 这是另一种MaybeUninit<T>的生成方法,直接通过指针类型转换将未初始化的内存转换为MaybeUninit<T>。

RUST的全局变量内存探讨

RUST支持const 及 static类型的全局变量,且static支持可写操作。所有对static的写操作都是unsafe的。
需要特别注意的,全局变量不支持非Copy trait类型所有权转移,这也很好理解,所有权转移实际上一个内存"move“的操
作。但static变量的内存显然是没有办法"move"的。

static这一性质导致如果要有转移所有权的操作,必须使用mem::replace的方式进行,RUST标准库中很多类型基于mem::replace实现类型自身的replace方法或take方法。

C/C++程序员比较习惯于设计全局变量及其变种静态方法,RUST的全局变量所有权的限制会对这个设计思维有较大的冲击

RUST所有权,生命周期,借用探讨

RUST在定义一个变量时,实际上把变量在逻辑上分成了两个部分,变量的内存块与变量内容。
变量类型定义了内存块内容的格式,变量声明语句定义了一个内存块,变量初始化赋值则在内存块中写入初始化变量内容。
所有权指变量内容的独占性。所有权转移指的是变量内容在不同的内存块之间的转移(浅拷贝)。当变量内容转移到新的内存块,旧的内存块就失去了这个变量内容的所有权。由此可见,变量名实际仅代表一个内存块,内存块的变量内容与变量名是一个暂时关联关系,RUST定义这种关联关系为绑定
设计所有权的目的是保证对变量进行清理操作的正确,如果一个变量内容在多个内存块中有效,变量清理的正确性用静态编译的方法无法保证。
这里有个例外,就是实现Copy trait的类型变量不做所有权转移操作,实现Copy trait的类型可通过栈拷贝完成变量内容赋值,清理也可以仅通过通常的调用栈返回完成。
RUST被设计成自动调用变量类型的drop以完成清理,对变量的生命周期跟踪成为一个必然的选择,在判断变量的生命周期终结的时候调用变量的drop函数。

RUST采用生命周期仅与内存块(变量名)相关联的设计,这样的设计容易对生命周期进行跟踪。没有绑定所有权的内存块在生命周期终结不做任何操作,拥有所有权的内存块生命周期终结会自动触发变量的drop操作。
如果仅仅考虑drop操作,那生命周期的方案不会太复杂,但RUST决定用生命周期同时解决另一个问题,变量引用导致的野指针问题。因为所有权的关系,RUST将变量引用改了一个RUST的名字——借用,意味着对所有权的借用。
用生命周期解决借用导致的野指针问题思路很简单,就是借用的生命周期应该短于所有权的生命周期。但这个简单的思路却需要极为复杂的设计来完成,对这个复杂设计的理解也成了RUST最被人诟病的点。

理想的生命周期方案是完全由编译器搞定,程序员不要参与。但这显然不可能,编译器没办法在所有的情况下都能够完成全部的推断,势必需要程序员在编码中给出提示。生命周期因此成为rust的一个语法部分。

首先,生命周期被设计成一种实现继承语法的类型每一个生命周期都是一个类型,不同的生命周期之间的关系用类型继承语法来完成。生命周期类型的继承具体而言: 假设有两个生命周期类型A和B,如果A完全被B包含在内,那就说B继承于A。A是基类型,B是子类型。从继承的概念,B类型能被转换为A类型,A类型无法转换为B类型。也就是说B类型的值能赋给A类型的变量,A类型的值无法赋给B类型变量。
生命周期是类型这一点与直观感觉有区别,毕竟,一个作用域给人的感觉就应该是个值。但是,用类型这个方案:

  1. 可以利用类型系统来完成生命周期方案,没有给rust编译器增加太大的负担,代码也几乎不受影响。
  2. 利用继承语法,在变量赋值时根据类型能否转换完成生命周期长短的判断,是极为巧妙的,简化的,自然的设计。

因为生命周期仅对内存块有意义,而在转移所有权的操作中,是两个不同的内存块发生的联系,他们的生命周期彼此独立。所以所有权转移时,所有权变量的类型层次上不涉及生命周期类型转换。如果类型成员中有引用,则见下面的内容。
当对一个引用类型变量做赋值时,便出现了生命周期类型转换,举例分析如下:

  1. 当声明一个类型引用的变量时,例如:let a: &i32 实质声明了一个i32类型引用的内存块,这个内存块有一个生命周期泛型, 假设为'a,
  2. 假设要对此变量赋值为另一个变量的引用,例如: let b:i32 = 4; a = &b; &b实质是对b的内存块进行引用,该内存块的生命周期假设为'b,
  3. 赋值实质是将一个&'b i32 类型的变量赋值给&'a i32类型变量。则必然发生类型转换关系,这时,只有当'b是'a的子类型时,即'b长于'a时,这个类型转换才能被编译器认为正确。

以上实际就是生命周期的奥秘所在了,Rust对生命周期设计的关键点就是:

  1. 在变量赋值时捕捉触发生命周期类型转换的情况
  2. 确保类型转换不正确时,给出生命周期不正确的编译错误警告。

但是,还有些其他情况需要考虑。
因为引用类型是泛型的一种,那由泛型派生的类型的赋值也就会出现类型转换的问题,例如:*const T,*mut T,Box<T> ,... 具体的类型可以参考Rust Reference。这时,需要由T的继承关系推断出派生类型的继承关系。这就是变异性Variance特性存在的意义,Variance存在三种情况

  1. 协变covariant,泛型是子类,派生类型也是子类。泛型是父类,派生类型也是父类
  2. 逆变contravariant,泛型是子类,派生类型是父类。泛型是父类,派生类型是子类
  3. 不变invariant, 泛型的子类还是父类都推导不出派生类型是否是子类还是父类。

复合类型之间的继承的关系可根据成员变异性得出。
因为在rust编程中引用派生类型及其赋值操作的广泛性,所以变异性是一个重要的需要被理解的概念。完整的变异性请参考rust Reference。

对生命周期推断的复杂性,rust采用了每个函数自决的方式(推断)。
每一个函数的生命周期类型转换处理正确与否在函数内完成判断(以下为根据逻辑进行的推断,可能不准确):

  1. 函数作用域会有一个生命周期泛型;
  2. 函数的定义会定义函数参数的生命周期泛型,以及这些生命周期泛型之间的继承关系。显然,函数作用域生命周期泛型是所有输入参数生命周期泛型的基类型
  3. 函数的定义会定义输出的生命周期泛型,以及输出的生命周期泛型与输入参数生命周期泛型的继承关系。如果输出是一个借用或由借用派生的类型或者有借用成员的复合类型,则输出的生命周期泛型必须是某一输入生命周期泛型的基类型。
  4. 编译器会分析函数中的作用域,针对每个作用域生成生命周期泛型,并形成这些生命周期泛型之间的继承关系,当然,函数内所有生命周期泛型都是函数作用域生命周期泛型的基类型。
  5. 根据这些生命周期泛型及他们之间的继承关系,处理函数内操作时引发的生命周期泛型类型转换,并对错误的转换做出错警告。
  6. 如果调用了其他函数,则对调用函数的输入参数及输出之间的转换是否正确判断转移至调用函数。

如果一个复合类型内部存在引用类型成员或递归至引用类型成员,则必须明确此复合类型的生命周期泛型与成员生命周期泛型的继承关系。一般复合类型的生命周期应该是基类型。

rust编译器做了很多工作以避免生命周期泛型在代码中出现。这部分的工作仍然在持续进行中。
举几个生命周期的例子:

impl *const T{
    pub const unsafe fn as_ref<'a>(self) -> Option<&'a T> {
        if self.is_null() { None } else { unsafe { Some(&*self) } }
    }
}

因为*const T没有生命周期类型与之相关。所以上面这个函数必须声明一个生命周期泛型用于标注返回的生命周期,此泛型独立存在,不与其他生命周期泛型有关系。因此返回的引用变量的生命周期完全决定于调用此函数的代码定义。因为返回引用的生命周期应短于self指向的内存块的生命周期,这只能由调用此函数的代码即程序员来保证,rust编译器此时无能为力。

rust中,对于申请的堆内存内存块,通常将其与一个位于栈内存空间的智能指针的类型变量相结合。智能指针类型变量生命周期终止时,调用drop方法释放堆内存的内存块。智能指针类型通常会提供leak函数,将堆内存的内存块与智能指针类型的关联切断。这通常是一个中间状态,需要尽快再将堆内存与另一个智能指针类型的变量建立联系,以便其能重新被纳入生命周期的体系中。

小结

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

引用

rust标准库关于内存模块
rust内存布局

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容