關(guān)于rust中內(nèi)存模塊-代碼分析(二)

前篇

再議裸指針模塊

有了MaybeUnint<T>做基礎(chǔ)后,可以對裸指針其他至關(guān)重要的標準庫函數(shù)做出分析

  • ptr::read<T>(src: *const T) -> T
    此函數(shù)在MaybeUninit<T>節(jié)中已經(jīng)給出了代碼,ptr::read是對所有類型通用的一種復(fù)制方法,需要指出,此函數(shù)完成淺拷貝,復(fù)制后,src指向的變量的所有權(quán)會轉(zhuǎn)移至返回值。所以,調(diào)用此函數(shù)的代碼必須保證src指向的變量生命周期結(jié)束后不會被編譯器自動調(diào)用drop,否則可能導致重復(fù)drop,出現(xiàn)UB問題。
  • ptr::read_unaligned<T>(src: *const T) -> T
    當數(shù)據(jù)結(jié)構(gòu)中有未內(nèi)存對齊的成員變量時,需要用此函數(shù)讀取內(nèi)容并轉(zhuǎn)化為內(nèi)存對齊的變量。否則會引發(fā)未定義行為(undefined behaiver) 如下例:
    /// 從字節(jié)數(shù)組中讀一個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,因為不確定字節(jié)是否對齊
       unsafe { ptr.read_unaligned() }
   }

例子中,為了從byte串中讀取一個usize,需要用read_unaligned來獲取值,不能象C語言那樣通過指針類型轉(zhuǎn)換直接獲取值。

  • 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);
        //必須調(diào)用forget,這里所有權(quán)已經(jīng)轉(zhuǎn)移。不允許再對src做drop操作
        intrinsics::forget(src);
    }
}

write函數(shù)本質(zhì)上就是一個所有權(quán)轉(zhuǎn)移的操作。完成src到dst的淺拷貝,然后調(diào)用了forget(src), 這使得src的Drop不再被調(diào)用。從而將所有權(quán)轉(zhuǎn)移到dst。此函數(shù)是mem::replace, mem::transmute_copy的基礎(chǔ)。底層由intrisic::copy_no_overlapping支持。
這個函數(shù)中,如果dst已經(jīng)初始化過,那原dst變量的所有權(quán)將被丟失掉,有可能引發(fā)內(nèi)存泄漏。

  • ptr::write_unaligned<T>(dst: *mut T, src: T)
    與read_unaligned相對應(yīng)。
    舉例如下:
    #[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.
    // 對于結(jié)構(gòu)中字節(jié)沒有按照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)
    由于用&獲得引用必須是字節(jié)按照2的冪次對齊的地址,所以用這個宏獲取非地址對齊的變量地址
pub macro addr_of($place:expr) {
    //關(guān)鍵字是&raw const,這個是rust的原始引用語義,但目前還沒有在官方做公開。
    //區(qū)別與&, &要求地址必須滿足字節(jié)對齊和初始化,&raw 則沒有這個問題
    &raw const $place
}

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

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

指針的通用函數(shù)請參考Rust庫函數(shù)參考

NonNull<T>代碼分析

結(jié)構(gòu)體定義如下:

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

屬性repr(transparent)實際上表示外部的封裝結(jié)構(gòu)在內(nèi)存中等價于內(nèi)部的變量。
NonNull<T>在內(nèi)存中與*const T完全一致。可以直接轉(zhuǎn)化為* const T。

裸指針的值因為可以為0,如果敞開來用,會有很多無法控制的代碼隱患。按照rust的習慣,標準庫定義了非0的指針封裝結(jié)構(gòu)NonNull<T>,從而可以用Option<NonNull<T>>來對值可能為0的裸指針做出強制安全代碼邏輯。不需要Option的則認為裸指針不會取值為0。

NonNull<T>本身是協(xié)變(covarient)類型.
rust中的協(xié)變,在rust中,不同的生命周期被視為不同的類型,對于帶有生命周期的類型變量做賦值操作時,僅允許子類型賦給基類型(長周期賦給短周期), 為了從基本類型生成復(fù)合類型的子類型和基類型的關(guān)系,rust引入了協(xié)變性。從基本類型到復(fù)合類型的協(xié)變性有 協(xié)變(covarient)/逆變(contracovarient)/不變(invarient)三種**
程序員分析代碼時,可以從基本類型之間的生命周期關(guān)系及協(xié)變性確定復(fù)合類型變量之間的生命周期關(guān)系,從而做合適的賦值操作。

因為NonNull<T>實際上是封裝* mut T類型,但* mut TNonNull<T>的協(xié)變性不同,所以程序員如果不能確定需要協(xié)變類型,就不要使用NonNull<T>

NonNull<T>創(chuàng)建關(guān)聯(lián)方法

創(chuàng)建一個懸垂(dangling)指針, 保證指針滿足類型內(nèi)存對齊要求。該指針可能指向一個正常的變量,所以不能認為指向的內(nèi)存是未初始化的。dangling實際表示NonNull<T>無意義,與NonNull<T>的本意有些違背,因為這個語義可以用None來實現(xiàn)。

    pub const fn dangling() -> Self {
        unsafe {
            //取內(nèi)存對齊地址作為裸指針的地址。調(diào)用者應(yīng)保證不對此內(nèi)存地址進行讀寫
            let ptr = mem::align_of::<T>() as *mut T;
            NonNull::new_unchecked(ptr)
        }
    }

new函數(shù),由輸入的*mut T裸指針創(chuàng)建NonNull<T>。代碼如下:

    pub fn new(ptr: *mut T) -> Option<Self> {
        if !ptr.is_null() {
            //ptr的安全性已經(jīng)檢查完畢
            Some(unsafe { Self::new_unchecked(ptr) })
        } else {
            None
        }
    }

NonNull::<T>::new_unchecked(* mut T)->Self* mut T生成NonNull<T>,不檢查* mut T是否為0,調(diào)用者應(yīng)保證* mut T不為0。
from_raw_parts函數(shù),類似裸指針的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創(chuàng)建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也可以接收不可變引用,不能后繼將這個變量轉(zhuǎn)換為可變引用
        unsafe { NonNull { pointer: reference as *const T } }
    }
}
NonNull<T>類型轉(zhuǎn)換方法

NonNull<T>的方法基本與*const T/* mut T相同,也容易理解,下文僅做羅列和簡單說明

  • NonNull::<T>::as_ptr(self)->* mut T
    返回內(nèi)部的pointer 裸指針
  • NonNull::<T>::as_ref<'a>(&self)->&'a T
    返回的引用的生命周期與引用指向的變量生命周期無關(guān),調(diào)用者應(yīng)保證返回的引用的生命周期符合安全性要求
  • NonNull::<T>::as_mut<'a>(&mut self)->&'a mut T
    與 as_ref類似,但返回可變引用。
  • NonNull::<T>::cast<U>(self)->NonNull<U>
    指針類型轉(zhuǎn)換,程序員應(yīng)該保證T和U的內(nèi)存布局相同
NonNull<[T]> 方法
  • NonNull::<[T]>::slice_from_raw_parts(data: NonNull<T>, len: usize) -> Self
    將類型指針轉(zhuǎn)化為類型的切片類型指針,實質(zhì)是ptr::slice_from_raw_parts的一種包裝。
  • NonNull::<[T]>::as_non_null_ptr(self) -> NonNull<T>
    • const [T]::as_ptr的NonNull版本
NonNull<T>的使用實例

以下的實例展示了 NonNull<T>在動態(tài)申請堆內(nèi)存的使用:

    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>,此時內(nèi)存長度還與T不匹配
                    let ptr = NonNull::new(raw_ptr).ok_or(AllocError)?;
                    //將NonNull<u8>轉(zhuǎn)換為NonNull<[u8]>, NonNull<[u8]>已經(jīng)是類型T的內(nèi)存長度。后繼可以直接轉(zhuǎn)換為T類型的指針了。這個轉(zhuǎn)換極為重要。
                    Ok(NonNull::slice_from_raw_parts(ptr, size))
                },
            }
        }
        ....
    }

基本上,如果* const T/*mut T要跨越函數(shù)使用,或作為數(shù)據(jù)結(jié)構(gòu)體的成員時,應(yīng)將之轉(zhuǎn)化成NonNull<T>Unique<T>。*const T應(yīng)該僅僅保持在單一函數(shù)內(nèi)。

NonNull<T>MaybeUninit<T>相關(guān)函數(shù)

NonNull<T>::as_uninit_ref<`a>(&self) -> &`a MaybeUninit<T> NonNull與MaybeUninit的引用基本就是直接轉(zhuǎn)換的關(guān)系,一體雙面

    pub unsafe fn as_uninit_ref<'a>(&self) -> &'a MaybeUninit<T> {
        // self.cast將NonNull<T>轉(zhuǎn)換為NonNull<MaybeUninit<T>>
        //self.cast.as_ptr將NonNull<MaybeUninit<T>>轉(zhuǎn)換為 *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>] {
        // 下面的函數(shù)調(diào)用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>類型結(jié)構(gòu)定義如下:

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

NonNull<T>對比,Unique<T>多了PhantomData<T>類型成員。這個定義使得編譯器知曉,Unique<T>擁有了pointer指向的內(nèi)存的所有權(quán),NonNull<T>沒有這個特性。具備所有權(quán)后,Unique<T>可以實現(xiàn)Send, Sync等trait。因為獲得了所有權(quán),此塊內(nèi)存無法用于他處,這也是Unique的名字由來原因.
指針在被Unique<T>封裝前,必須保證是NonNull的。
對于rust從堆內(nèi)存申請的內(nèi)存塊,其指針都是用Unique<T>封裝后來作為智能指針結(jié)構(gòu)體內(nèi)部成員變量,保證智能指針結(jié)構(gòu)體擁有申請出來的內(nèi)存塊的所有權(quán)。

Unique<T>模塊的函數(shù)及代碼與NonNull<T>函數(shù)代碼相類似,此處不分析。
Unique::cast<U>(self)->Unique<U> 類型轉(zhuǎn)換,程序員應(yīng)該保證T和U的內(nèi)存布局相同
Unique::<T>::new(* mut T)->Option<Self> 此函數(shù)內(nèi)部判斷* mut T是否為0值
Unique::<T>::new_unchecked(* mut T)->Self 封裝* mut T, 調(diào)用代碼應(yīng)該保證* mut T的安全性
Unique::as_ptr(self)->* mut T
Unique::as_ref(&self)->& T 因為Unique具備所有權(quán),此處&T的生命周期與self相同,不必特別聲明聲明周期
Unique::as_mut(&mut self)->& mut T 同上

mem模塊函數(shù)

泛型類型創(chuàng)建

mem::zeroed<T>() -> T 返回一個內(nèi)存塊清零的泛型變量,內(nèi)存塊在??臻g,代碼如下:

pub unsafe fn zeroed<T>() -> T {
    // 調(diào)用代碼必須確認T類型的變量可以取全零值
    unsafe {
        intrinsics::assert_zero_valid::<T>();
        MaybeUninit::zeroed().assume_init()
    }
}

mem::uninitialized<T>() -> T 返回一個未初始化過的泛型變量,內(nèi)存塊在棧空間。

pub unsafe fn uninitialized<T>() -> T {
    // 調(diào)用者必須確認T類型的變量允許未初始化的任意值
    unsafe {
        intrinsics::assert_uninit_valid::<T>();
        MaybeUninit::uninit().assume_init()
    }
}

泛型類型拷貝與替換

mem::take<T: Default>(dest: &mut T) -> T 將dest設(shè)置為默認內(nèi)容(不改變所有權(quán)),用一個新變量返回dest的內(nèi)容。這里有一個坑,即任何類型的default()必然能夠滿足多次drop不會出現(xiàn)內(nèi)存安全問題。

pub fn take<T: Default>(dest: &mut T) -> T {
    //即mem::replace,見下文
    //此處,對于引用類型,編譯器禁止用*dest來轉(zhuǎn)移所有權(quán),所以不能用let xxx = *dest; xxx這種形式返回T
    //其他語言簡單的事情在rust中必須用一個較難理解的方式來進行解決。replace()對所有權(quán)有仔細的處理
    replace(dest, T::default())
}

mem::replace<T>(dest: &mut T, src: T) -> T 用src的內(nèi)容賦值dest(不改變所有權(quán)),用一個新變量返回dest的內(nèi)容。replace函數(shù)的難點在于了解所有權(quán)的轉(zhuǎn)移。

pub const fn replace<T>(dest: &mut T, src: T) -> T {
    unsafe {
        //因為要替換dest, 所以必須對dest原有變量的所有權(quán)做處理,因此先用read將*dest的所有權(quán)轉(zhuǎn)移到T,交由調(diào)用者進行處理,rust不支持對引用類型做解引用的相等來轉(zhuǎn)移所有權(quán)。將一個引用的所有權(quán)進行轉(zhuǎn)移的方式只有粗暴的內(nèi)存淺拷貝這種方法。
        //使用這個函數(shù),調(diào)用代碼必須了解T類型的情況,T類型有可能需要顯式的調(diào)用drop函數(shù)。ptr::read前文已經(jīng)分析過。
        let result = ptr::read(dest);
        //ptr::write本身會導致src的所有權(quán)轉(zhuǎn)移到dest,后繼不允許在src生命周期終止時做drop。ptr::write會用forget(src)做到這一點。
        ptr::write(dest, src);
        result
    }
}

mem::transmute_copy<T, U>(src: &T) -> U 新建類型U的變量,并把src的內(nèi)容拷貝到U。調(diào)用者應(yīng)保證T類型的內(nèi)容與U一致,src后繼的所有權(quán)問題需要做處理。

pub const unsafe fn transmute_copy<T, U>(src: &T) -> U {
    if align_of::<U>() > align_of::<T>() {
        // 如果兩個類型字節(jié)對齊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) }
    }
}

所有權(quán)轉(zhuǎn)移的底層實現(xiàn)

所有權(quán)的本質(zhì)是只能對變量做一次drop操作。變量的drop操作會引起變量結(jié)構(gòu)體內(nèi)部成員的鏈式drop。
所以,只要引發(fā)了變量的淺拷貝,所有權(quán)便被轉(zhuǎn)移。原先放置變量的那塊內(nèi)存就必須被處理,forget及ManuallyDrop是兩種典型方案。
不涉及裸指針的代碼,一般不必考慮所有權(quán)必須人工處理的情況。但一旦涉及到裸指針,那就必須注意看是否出現(xiàn)了一個變量的雙份或多份拷貝,每多一次拷貝,意味著編譯器會對變量多做一次drop,觸發(fā)UB。

變量調(diào)用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擁有所有權(quán)的變量,然后再給test2賦值。代碼后的輸出會給出證據(jù)
   //將test1的所有權(quán)轉(zhuǎn)移給test2,無疑代表著test2現(xiàn)有的所有權(quán)會在后繼無法訪問,因此drop被立即調(diào)用。
   test2 = test1;
   println!("{:?}", test2);
}

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

其他函數(shù)

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

pub const fn forget<T>(t: T) {
    //沒有使用intrinsic::forget, 實際上效果一致,這里應(yīng)該是盡量規(guī)避用intrinsic函數(shù)
    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 基本就是直接調(diào)用intrinsic模塊的同名函數(shù)
mem::drop<T>(_x:T) 釋放內(nèi)存

Rust堆內(nèi)存申請及釋放

Rust類型系統(tǒng)的內(nèi)存布局

Rust提供了Layout內(nèi)存布局類型, 此布局類型結(jié)構(gòu)主要用于做堆內(nèi)存申請。
Layout的數(shù)據(jù)結(jié)構(gòu)如下:

pub struct Layout {
    // 類型需占用的內(nèi)存大小,用字節(jié)數(shù)目表示
    size_: usize,
    //  按照此字節(jié)數(shù)目進行類型內(nèi)存對齊, NonZeroUsize見代碼后面文字分析
    align_: NonZeroUsize,
}

NonZeroUsize是一種非0值的usize, 這種類型主要應(yīng)用于不可取0的值,本結(jié)構(gòu)中, 字節(jié)對齊屬性變量不能被置0,所以用NonZeroUsize來確保安全性。如果用usize類型,那代碼中就可能會把0置給align_,導致bug產(chǎn)生。這是rust的一個設(shè)計規(guī)則,所有的約束要在類型定義即顯性化,從而使bug在編譯中就被發(fā)現(xiàn)。

每一個rust的類型都有自身獨特的內(nèi)存布局Layout。一種類型的Layout可以用intrinsic::<T>::size_of()intrinsic::<T>::min_align_of()獲得的類型內(nèi)存大小和對齊來獲得。
rust的內(nèi)存布局更詳細原理闡述請參考[rust內(nèi)存布局] (https://doc.rust-lang.org/nomicon/data.html),
Layout比較有典型意義的函數(shù):

impl Layout {
    ...
    ...
    //array函數(shù)是計算n個T類型變量形成的數(shù)組所需的Layout,是從代碼了解Rust Layout概念的一個好的實例
    //這里主要注意的是T類型的對齊會導致內(nèi)存申請不是T類型的內(nèi)存大小*n
    //而且對齊也是數(shù)組的算法
    pub fn array<T>(n: usize) -> Result<Self, LayoutError> {
        //獲得n個T類型的內(nèi)存Layout
        let (layout, offset) = Layout::new::<T>().repeat(n)?;
        debug_assert_eq!(offset, mem::size_of::<T>());
        //以完全對齊的大小  ,得出數(shù)組的Layout
        Ok(layout.pad_to_align())
    }

    //計算n個T類型需要的內(nèi)存Layout, 以及成員之間的空間
    pub fn repeat(&self, n: usize) -> Result<(Self, usize), LayoutError> {
        // 所有的成員必須以成員的對齊大小來做內(nèi)存對齊,首先計算對齊需要的padding空間
        let padded_size = self.size() + self.padding_needed_for(self.align());
        // 計算共需要多少內(nèi)存空間,如果溢出,返回error
        let alloc_size = padded_size.checked_mul(n).ok_or(LayoutError)?;

        //由已經(jīng)驗證過得原始數(shù)據(jù)生成Layout,并返回單成員占用的空間
        unsafe { Ok((Layout::from_size_align_unchecked(alloc_size, self.align()), padded_size)) }
    }

    //填充以得到一個與T類型完全對齊的,最小的內(nèi)存大小的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);
        // 就是對對齊大小做除,如果有余數(shù),商加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)
    }

    //不檢查輸入?yún)?shù),根據(jù)輸入?yún)?shù)表示的原始數(shù)據(jù)生成Layout變量,調(diào)用代碼應(yīng)保證安全性
    pub const unsafe fn from_size_align_unchecked(size: usize, align: usize) -> Self {
        // 必須保證align滿足不為0.
        Layout { size_: size, align_: unsafe { NonZeroUsize::new_unchecked(align) } }
    }

    //對參數(shù)進行檢查,生成一個類型的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);
        }

        // 參數(shù)已經(jīng)檢查完畢.
        unsafe { Ok(Layout::from_size_align_unchecked(size, align)) }
    }
    ...
    ...
}

#[repr(transparent)]內(nèi)存布局模式

repr(transparent)用于僅包含一個成員變量的類型,該類型的內(nèi)存布局與成員變量類型的內(nèi)存布局完全一致。類型僅僅具備編譯階段的意義,在運行時,類型變量與其成員變量可以認為是一個相同變量,可以相互無障礙類型轉(zhuǎn)換。使用repr(transparent)布局的類型基本是一種封裝結(jié)構(gòu)。

#[repr(packed)]內(nèi)存布局模式

強制類型成員變量以1字節(jié)對齊,此種結(jié)構(gòu)在協(xié)議分析和結(jié)構(gòu)化二進制數(shù)據(jù)文件中經(jīng)常使用

#[repr(align(n))] 內(nèi)存布局模式

強制類型以2的冪次對齊

#[repr(RUST)]內(nèi)存布局模式

默認的布局方式,采用此種布局,RUST編譯器會根據(jù)情況來自行優(yōu)化內(nèi)存

#[repr(C)]內(nèi)存布局模式

采用C語言布局方式, 所有結(jié)構(gòu)變量按照聲明的順序在內(nèi)存排列。默認4字節(jié)對齊。

RUST堆內(nèi)存申請與釋放接口

資深的C/C++程序員都了解,在大型系統(tǒng)開發(fā)時,往往需要自行實現(xiàn)內(nèi)存管理模塊,以根據(jù)系統(tǒng)的特點優(yōu)化內(nèi)存使用及性能,并作出內(nèi)存跟蹤。
對于操作系統(tǒng),內(nèi)存管理模塊更是核心功能。
對于C/C++小型系統(tǒng),沒有內(nèi)存管理,僅僅是調(diào)用操作系統(tǒng)的內(nèi)存系統(tǒng)調(diào)用,內(nèi)存管理交給操作系統(tǒng)負責。操作系統(tǒng)內(nèi)存管理模塊接口是內(nèi)存申請及內(nèi)存釋放的系統(tǒng)調(diào)用
對于GC語言,內(nèi)存管理由虛擬機或語言運行時負責,利用語言提供的new來完成類型結(jié)構(gòu)內(nèi)存獲取。
RUST的內(nèi)存管理分成了三個界面:

  1. 由智能指針類型提供的類型創(chuàng)建函數(shù),一般有new, 與其他的GC類語言相同,同時增加了一些更直觀的函數(shù)。
  2. 智能指針使用實現(xiàn)Allocator Trait的類型做內(nèi)存申請及釋放。Allocator使用編譯器提供的函數(shù)名申請及釋放內(nèi)存。
  3. 實現(xiàn)了GlobalAlloc Trait的類型來完成獨立的內(nèi)存管理模塊,并用#[global_allocator]注冊入編譯器,替代編譯器默認的內(nèi)存申請及釋放函數(shù)。
    這樣,RUST達到了:
  4. 對于小規(guī)模的程序,擁有與GC語言相類似的內(nèi)存獲取機制
  5. 對于大型程序和操作系統(tǒng)內(nèi)核,從語言層面提供了獨立的內(nèi)存管理模塊接口,達成了將現(xiàn)代語法與內(nèi)存管理模塊共同存在,相互配合的目的。
    但因為所有權(quán)概念的存在,從內(nèi)存申請到轉(zhuǎn)換為類型系統(tǒng)仍然還存在復(fù)雜的工作。
    堆內(nèi)存申請和釋放的Trait GlobalAlloc定義如下:
pub unsafe trait GlobalAlloc {
    //申請內(nèi)存,因為Layout中內(nèi)存大小不為0,所以,alloc不會申請大小為0的內(nèi)存
    unsafe fn alloc(&self, layout: Layout) -> *mut u8;
    //釋放內(nèi)存
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout);
    
    //申請后的內(nèi)存應(yīng)初始化為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,確保每個字節(jié)都清零
            unsafe { ptr::write_bytes(ptr, 0, size) };
        }
        ptr
    }

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

在內(nèi)核編程或大的框架系統(tǒng)編程中,開發(fā)人員通常開發(fā)自定義的堆內(nèi)存管理模塊,模塊實現(xiàn)GlobalAlloc Trait并添加#[global_allocator]標識。對于用戶態(tài),RUST標準庫有默認的GlobalAlloc實現(xiàn)。

extern "Rust" {
    // 編譯器會將實現(xiàn)了GlobalAlloc Trait,并標記 #[global_allocator]的四個方法自動轉(zhuǎn)化為以下的函數(shù)
    #[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()) }
}

再實現(xiàn)Allocator Trait,對以上四個函數(shù)做封裝處理。作為RUST其他模塊對堆內(nèi)存的申請和釋放接口。

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
        // 復(fù)雜的類型轉(zhuǎn)換,實際是調(diào)用 *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 實現(xiàn)了 Allocator Trait。Rust大部分alloc庫數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)使用Global作為Allocator。

unsafe impl Allocator for Global {
    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        //上文已經(jīng)給出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接口獲取內(nèi)存,然后將GlobalAlloc申請到的* mut u8轉(zhuǎn)換為確定大小的單一指針NonNull<[u8]>, 并處理申請內(nèi)存可能出現(xiàn)的不成功。NonNull<[u8]>此時內(nèi)存布局與 T的內(nèi)存布局已經(jīng)相同,后繼可以轉(zhuǎn)換為真正需要的T的指針并進一步轉(zhuǎn)化為相關(guān)類型的引用,從而符合RUST類型系統(tǒng)安全并進行后繼的處理。
以上是堆內(nèi)存的申請和釋放。 基于泛型,RUST也巧妙實現(xiàn)了棧內(nèi)存的申請和釋放機制 mem::MaybeUninit<T>

用Box的內(nèi)存申請做綜合舉例:

    //此處A是一個A:Allocator類型
    pub fn try_new_uninit_in(alloc: A) -> Result<Box<mem::MaybeUninit<T>, A>, AllocError> {
        //實質(zhì)是T類型的內(nèi)存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指向的變量的所有權(quán)
        Box(unsafe { Unique::new_unchecked(raw) }, alloc)
    }

以上代碼可以看到,NonNull<[u8]>可以直接通過cast 轉(zhuǎn)換為NonNull<MaybeUninit<T>>, 這是另一種MaybeUninit<T>的生成方法,直接通過指針類型轉(zhuǎn)換將未初始化的內(nèi)存轉(zhuǎn)換為MaybeUninit<T>。

RUST的全局變量內(nèi)存探討

RUST支持const 及 static類型的全局變量,且static支持可寫操作。所有對static的寫操作都是unsafe的。
需要特別注意的,全局變量不支持非Copy trait類型所有權(quán)轉(zhuǎn)移,這也很好理解,所有權(quán)轉(zhuǎn)移實際上一個內(nèi)存"move“的操
作。但static變量的內(nèi)存顯然是沒有辦法"move"的。

static這一性質(zhì)導致如果要有轉(zhuǎn)移所有權(quán)的操作,必須使用mem::replace的方式進行,RUST標準庫中很多類型基于mem::replace實現(xiàn)類型自身的replace方法或take方法。

C/C++程序員比較習慣于設(shè)計全局變量及其變種靜態(tài)方法,RUST的全局變量所有權(quán)的限制會對這個設(shè)計思維有較大的沖擊

RUST所有權(quán),生命周期,借用探討

RUST在定義一個變量時,實際上把變量在邏輯上分成了兩個部分,變量的內(nèi)存塊與變量內(nèi)容。
變量類型定義了內(nèi)存塊內(nèi)容的格式,變量聲明語句定義了一個內(nèi)存塊,變量初始化賦值則在內(nèi)存塊中寫入初始化變量內(nèi)容。
所有權(quán)指變量內(nèi)容的獨占性。所有權(quán)轉(zhuǎn)移指的是變量內(nèi)容在不同的內(nèi)存塊之間的轉(zhuǎn)移(淺拷貝)。當變量內(nèi)容轉(zhuǎn)移到新的內(nèi)存塊,舊的內(nèi)存塊就失去了這個變量內(nèi)容的所有權(quán)。由此可見,變量名實際僅代表一個內(nèi)存塊,內(nèi)存塊的變量內(nèi)容與變量名是一個暫時關(guān)聯(lián)關(guān)系,RUST定義這種關(guān)聯(lián)關(guān)系為綁定。
設(shè)計所有權(quán)的目的是保證對變量進行清理操作的正確,如果一個變量內(nèi)容在多個內(nèi)存塊中有效,變量清理的正確性用靜態(tài)編譯的方法無法保證。
這里有個例外,就是實現(xiàn)Copy trait的類型變量不做所有權(quán)轉(zhuǎn)移操作,實現(xiàn)Copy trait的類型可通過??截愅瓿勺兞績?nèi)容賦值,清理也可以僅通過通常的調(diào)用棧返回完成。
RUST被設(shè)計成自動調(diào)用變量類型的drop以完成清理,對變量的生命周期跟蹤成為一個必然的選擇,在判斷變量的生命周期終結(jié)的時候調(diào)用變量的drop函數(shù)。

RUST采用生命周期僅與內(nèi)存塊(變量名)相關(guān)聯(lián)的設(shè)計,這樣的設(shè)計容易對生命周期進行跟蹤。沒有綁定所有權(quán)的內(nèi)存塊在生命周期終結(jié)不做任何操作,擁有所有權(quán)的內(nèi)存塊生命周期終結(jié)會自動觸發(fā)變量的drop操作。
如果僅僅考慮drop操作,那生命周期的方案不會太復(fù)雜,但RUST決定用生命周期同時解決另一個問題,變量引用導致的野指針問題。因為所有權(quán)的關(guān)系,RUST將變量引用改了一個RUST的名字——借用,意味著對所有權(quán)的借用。
用生命周期解決借用導致的野指針問題思路很簡單,就是借用的生命周期應(yīng)該短于所有權(quán)的生命周期。但這個簡單的思路卻需要極為復(fù)雜的設(shè)計來完成,對這個復(fù)雜設(shè)計的理解也成了RUST最被人詬病的點。

理想的生命周期方案是完全由編譯器搞定,程序員不要參與。但這顯然不可能,編譯器沒辦法在所有的情況下都能夠完成全部的推斷,勢必需要程序員在編碼中給出提示。生命周期因此成為rust的一個語法部分。

首先,生命周期被設(shè)計成一種實現(xiàn)繼承語法的類型,每一個生命周期都是一個類型,不同的生命周期之間的關(guān)系用類型繼承語法來完成。生命周期類型的繼承具體而言: 假設(shè)有兩個生命周期類型A和B,如果A完全被B包含在內(nèi),那就說B繼承于A。A是基類型,B是子類型。從繼承的概念,B類型能被轉(zhuǎn)換為A類型,A類型無法轉(zhuǎn)換為B類型。也就是說B類型的值能賦給A類型的變量,A類型的值無法賦給B類型變量。
生命周期是類型這一點與直觀感覺有區(qū)別,畢竟,一個作用域給人的感覺就應(yīng)該是個值。但是,用類型這個方案:

  1. 可以利用類型系統(tǒng)來完成生命周期方案,沒有給rust編譯器增加太大的負擔,代碼也幾乎不受影響。
  2. 利用繼承語法,在變量賦值時根據(jù)類型能否轉(zhuǎn)換完成生命周期長短的判斷,是極為巧妙的,簡化的,自然的設(shè)計。

因為生命周期僅對內(nèi)存塊有意義,而在轉(zhuǎn)移所有權(quán)的操作中,是兩個不同的內(nèi)存塊發(fā)生的聯(lián)系,他們的生命周期彼此獨立。所以所有權(quán)轉(zhuǎn)移時,所有權(quán)變量的類型層次上不涉及生命周期類型轉(zhuǎn)換。如果類型成員中有引用,則見下面的內(nèi)容。
當對一個引用類型變量做賦值時,便出現(xiàn)了生命周期類型轉(zhuǎn)換,舉例分析如下:

  1. 當聲明一個類型引用的變量時,例如:let a: &i32 實質(zhì)聲明了一個i32類型引用的內(nèi)存塊,這個內(nèi)存塊有一個生命周期泛型, 假設(shè)為'a,
  2. 假設(shè)要對此變量賦值為另一個變量的引用,例如: let b:i32 = 4; a = &b; &b實質(zhì)是對b的內(nèi)存塊進行引用,該內(nèi)存塊的生命周期假設(shè)為'b,
  3. 賦值實質(zhì)是將一個&'b i32 類型的變量賦值給&'a i32類型變量。則必然發(fā)生類型轉(zhuǎn)換關(guān)系,這時,只有當'b是'a的子類型時,即'b長于'a時,這個類型轉(zhuǎn)換才能被編譯器認為正確。

以上實際就是生命周期的奧秘所在了,Rust對生命周期設(shè)計的關(guān)鍵點就是:

  1. 在變量賦值時捕捉觸發(fā)生命周期類型轉(zhuǎn)換的情況
  2. 確保類型轉(zhuǎn)換不正確時,給出生命周期不正確的編譯錯誤警告。

但是,還有些其他情況需要考慮。
因為引用類型是泛型的一種,那由泛型派生的類型的賦值也就會出現(xiàn)類型轉(zhuǎn)換的問題,例如:*const T,*mut T,Box<T> ,... 具體的類型可以參考Rust Reference。這時,需要由T的繼承關(guān)系推斷出派生類型的繼承關(guān)系。這就是變異性Variance特性存在的意義,Variance存在三種情況

  1. 協(xié)變covariant,泛型是子類,派生類型也是子類。泛型是父類,派生類型也是父類
  2. 逆變contravariant,泛型是子類,派生類型是父類。泛型是父類,派生類型是子類
  3. 不變invariant, 泛型的子類還是父類都推導不出派生類型是否是子類還是父類。

復(fù)合類型之間的繼承的關(guān)系可根據(jù)成員變異性得出。
因為在rust編程中引用派生類型及其賦值操作的廣泛性,所以變異性是一個重要的需要被理解的概念。完整的變異性請參考rust Reference。

對生命周期推斷的復(fù)雜性,rust采用了每個函數(shù)自決的方式(推斷)。
每一個函數(shù)的生命周期類型轉(zhuǎn)換處理正確與否在函數(shù)內(nèi)完成判斷(以下為根據(jù)邏輯進行的推斷,可能不準確):

  1. 函數(shù)作用域會有一個生命周期泛型;
  2. 函數(shù)的定義會定義函數(shù)參數(shù)的生命周期泛型,以及這些生命周期泛型之間的繼承關(guān)系。顯然,函數(shù)作用域生命周期泛型是所有輸入?yún)?shù)生命周期泛型的基類型
  3. 函數(shù)的定義會定義輸出的生命周期泛型,以及輸出的生命周期泛型與輸入?yún)?shù)生命周期泛型的繼承關(guān)系。如果輸出是一個借用或由借用派生的類型或者有借用成員的復(fù)合類型,則輸出的生命周期泛型必須是某一輸入生命周期泛型的基類型。
  4. 編譯器會分析函數(shù)中的作用域,針對每個作用域生成生命周期泛型,并形成這些生命周期泛型之間的繼承關(guān)系,當然,函數(shù)內(nèi)所有生命周期泛型都是函數(shù)作用域生命周期泛型的基類型。
  5. 根據(jù)這些生命周期泛型及他們之間的繼承關(guān)系,處理函數(shù)內(nèi)操作時引發(fā)的生命周期泛型類型轉(zhuǎn)換,并對錯誤的轉(zhuǎn)換做出錯警告。
  6. 如果調(diào)用了其他函數(shù),則對調(diào)用函數(shù)的輸入?yún)?shù)及輸出之間的轉(zhuǎn)換是否正確判斷轉(zhuǎn)移至調(diào)用函數(shù)。

如果一個復(fù)合類型內(nèi)部存在引用類型成員或遞歸至引用類型成員,則必須明確此復(fù)合類型的生命周期泛型與成員生命周期泛型的繼承關(guān)系。一般復(fù)合類型的生命周期應(yīng)該是基類型。

rust編譯器做了很多工作以避免生命周期泛型在代碼中出現(xiàn)。這部分的工作仍然在持續(xù)進行中。
舉幾個生命周期的例子:

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沒有生命周期類型與之相關(guān)。所以上面這個函數(shù)必須聲明一個生命周期泛型用于標注返回的生命周期,此泛型獨立存在,不與其他生命周期泛型有關(guān)系。因此返回的引用變量的生命周期完全決定于調(diào)用此函數(shù)的代碼定義。因為返回引用的生命周期應(yīng)短于self指向的內(nèi)存塊的生命周期,這只能由調(diào)用此函數(shù)的代碼即程序員來保證,rust編譯器此時無能為力。

rust中,對于申請的堆內(nèi)存內(nèi)存塊,通常將其與一個位于棧內(nèi)存空間的智能指針的類型變量相結(jié)合。智能指針類型變量生命周期終止時,調(diào)用drop方法釋放堆內(nèi)存的內(nèi)存塊。智能指針類型通常會提供leak函數(shù),將堆內(nèi)存的內(nèi)存塊與智能指針類型的關(guān)聯(lián)切斷。這通常是一個中間狀態(tài),需要盡快再將堆內(nèi)存與另一個智能指針類型的變量建立聯(lián)系,以便其能重新被納入生命周期的體系中。

小結(jié)

本章主要分析了rust標準庫內(nèi)存相關(guān)模塊, 內(nèi)存相關(guān)模塊代碼多數(shù)不復(fù)雜,主要是要對內(nèi)存塊與類型系統(tǒng)之間的轉(zhuǎn)換要有比較深刻的理解,并能領(lǐng)會在實際編碼過程中在那些場景會使用內(nèi)存相關(guān)的代碼和API。rust的內(nèi)存安全給編碼加了非常多的限制,有些時候這些限制只有通過內(nèi)存API來有效的突破。如將引用指向的變量所有權(quán)轉(zhuǎn)移出來的take函數(shù)。后繼我們會看到幾乎每個標準庫的模塊都大量的使用了ptr, mem模塊中的方法和函數(shù)。只要是大型系統(tǒng),不熟悉內(nèi)存模塊的代碼,基本上無法做出良好的程序。

引用

rust標準庫關(guān)于內(nèi)存模塊
rust內(nèi)存布局

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容