rust 内存管理

 

计算机是一个数据处理设备,CPU负责处理数据,内存负责装载数据,外设负责输入输出数据。对于一门计算机语言,关注的是存储数据的内存管理。以前的计算机语言在提高效率和提高安全性之间总是有取舍,而rust在这方面却别具一格,鱼和熊掌兼得之

计算机是一个数据处理设备,CPU负责处理数据,内存负责装载数据,外设负责输入输出数据。对于一门计算机语言,关注的是存储数据的内存管理。以前的计算机语言在提高效率和提高安全性之间总是有取舍,而rust在这方面却别具一格,鱼和熊掌兼得之

内存管理的通用概念

物理内存与虚拟内存

早期CPU直接读写物理内存,但是这种方式不能用于多进程。一个进程使用了 0x100 的地址,另一个相同进程也会用到 0x100 的地址,这样就会起冲突。

随着计算机技术的发展,就出现了虚拟内存,它让每个进程都有自己独立的地址空间;整个虚拟内存由多个页(大小相等的块,一段连续的地址)组成。而且虚拟内存的地址空间分成两部分:用户空间和内核空间(linux是3:1,windows是2:2)

  • 栈(stack)
    • 一种后进先出的数据结构
    • 固定的一端为栈底,运动的一端是栈顶
      • 栈顶由ESP(extened stack point)寄存器保存
      • 栈底由EBP(extened base point)寄存器保存
    • 栈向内存地址减小的方向生长
    • 保存函数调用时的数据 stack frame
      • 函数的返回地址与参数
      • 临时变量
        • 非静态局部变量
        • 编译器的临时变量
      • 上下文
  • main调用函数foo
    1. 把返回地址压栈保存
    2. mainEBP压栈
    3. 进入当前函数foo的栈帧
    4. 预分配参数和临时变量的空间
    5. 运行foo函数
    6. 执行完毕,变量依次弹出
    7. 直到main函数的EBP
    8. 跳回main函数栈

  • 数据结构的堆
    • 完全二叉树
    • 大顶堆
      • 父节点大于两个子节点
    • 小顶堆
      • 父节点小于两个子节点
  • 内存的堆
    • 长久保存在内存中的值
    • 跨函数保存的内容

内存布局

为了CPU读写内存的高效,要内存对齐。CPU在单位时间能处理的一组比特位称为,字包含的比特位数称为字长,字节对齐是对齐CPU的字长。

Rust的资源管理

Rust也遵循上面的基本规定,除此之外,Rust在编译期就可以确定内存的分配与释放

变量与函数

常量是用const定义,没有固定内存地址,会被编译器内联到每个使用它的地方

静态变量用static定义,有固定的内存地址。不在栈中也不在堆中,而是与程序代码一并存储在静态存储区(随程序的二进制文件的生成被分配的)

为了保证内存安全,Rust会对变量进行一系列检查:

  • 检查每个变量是否都初始化了
  • 检查if每条分支是否都完成初始化了变量
    let x: i32;
    if true {
      x = 1;
    }
    println!("{}", x);
    

    else 分支没有完成对x的初始化

  • 检查循环中是否完成初始化了变量
  • 转移所有权会产生未初始化变量
    let y = Box::new(5);
    let y2 = y;
    

    y 是未初始化变量 首先 y 是智能指针,没有实现Copy,y2 = y后y交出了所有权

智能指针与RAII

Rust 内存地址的可以用指针来表示

  • 引用
    • &T&mut T
    • Rust中的普通指针
    • Rust对其安全检查
  • 原始指针
    • *const T*mut T
    • 不进行安全检查的内存地址
  • 智能指针
    • 结构体
      • 值是内存地址
    • 实现了DropDeref
      • Deref重载解引用*
      • Drop析构资源

确定性析构

C++资源管理方式RAII资源获取即初始化,RAII利用析构函数来回收资源。当变量离开作用域后,资源由变量的析构函数drop回收。所以RAII也称作作用域界定的资源管理(Scope-Bound Resource Management, SBRM)

实现了Copy的类型,没有析构函数

内存泄漏与内存安全

  • 未定义内存
    • Rust在编译期可以指出
  • 空指针
    • Safe中,创造不出空指针
      • Rust不支持将整数转为指针
    • Option<T>代表有无情况
  • 悬垂指针
    • 用生命期解决
    • 数组越界检查

内存泄漏是没有对合法的数据进行操作,内存不安全是对不合法的数据进行操作

主动泄漏是因为,Rust通过FFI与外部函数打交道,把数据交由C代码处理,防止Rust调用析构函数引起问题

附录

循环引用

创建一个泛型链表节点

struct Node<T> {
  data: T,
  next: NodePtr<T>,
}

因为NodePtr是指针型,所以应该是Box<Node<T>>,又因为可以为空,所以是Option<Box<Node<T>>>

这样一个循环引用如下:

let mut frist = Box::new(Node {data: 1, next: None});
let mut second = Box::new(Node {data:2, next: None});
frist.next = Some(second);
second.next = Some(frist);

但因为second是指针,没有实现Copy,所以Some(second)会将second这块内存的所有权移到Some()中。

为了能在不同地方都能持有同一块内存,使用Rc共享智能指针,所以NodePtr应该是Option<Rc<Node<T>>>

let frist = Rc::new(Node {data: 1, next: None});
let second = Rc::new(Node {data: 2, next: None});
frist.next = Some(second.clone());
second.next = Some(frist.clone());

但因为frist.next = ...需要frist有改写能力,而Rc<T>是只读共享指针,为了修改内部的数据,必须引入RefCell<T>,这样NodePtr应该是Option<Rc<RefCell<Node<T>>>>

let frist = Rc::new(RefCell::new(
  Node {data: 1, next: None}
));
let second = Rc::new(RefCell::new(
  Node {data: 2, next: None}
));
frist.brrow_mut().next = Some(second.clone());
second.brrow_mut().next = Some(frist.clone());