一、Rust的内存管理
采用虚拟内存空间在栈和堆上分配内存,这是诸多编程语言通用的内存管理基石,Rust也是一样。然而,与c/c++语言不同的是,Rust不需要开发者显式地通过malloc/new或free/delete之类的函数去分配和回收堆内存。
栈内存的生命周期是短暂的,会随着栈展开(如函数调用)的过程而被自动清理。而堆内容是动态的,其分配和重新分配并不遵循某个固定的模式,所以需要使用指针来对其进行跟踪。
Rust也引入了智能指针来管理内存。智能指针在堆上开辟内存空间,并拥有其所有权,通过存储于栈中的指针来管理堆内存。智能指针的RAII机制利用栈的特点,在栈元素被自动清空时自动调用析构函数,来释放智能指针所管理的堆内存间。
函数的局部变量
在函数中定义的局部变量都会被默认存储到栈中。这和c/c++语言,甚至更多的语言行为都一样,但不同的是,Rust编译器可以检查末初始化的变量,以保证内存安全。
Rust编译器会对代码做基本的静态分支流程分析。
当函数调用完毕时,栈帧会被释放,局部变量会被清空。如果变量指向堆内存,那么Rust会自动清空其指向的已分配堆内存。
Rust中的指针
Rust中的指针大致可以分为三种:引用、原生指针(裸指针)和智能指针。
- 原生指针可以在unsafe块下任意使用,不受Rust的安全检查规则的限制;
- 引用则必须受到编译器安全检查规则的限制;
- 智能指针是对指针的一层封装,提供了一些额外的功能,比如自动释放堆内存。智能指针区别于常规结构体的特性在于,它实现了Deref和Drop这两个trait。 Deref提供了解引用能力,Drop提供了自动析构的能力,正是这两个trait让智能指针拥有了类似指针的行为。比如String和Vec类型就是一种智能指针。
RAII(构造和析构)
RAII使用构造函数来初始化资源,使用析构函数来回收资源。这是指在定义对象的时候实现一个析构函数负责释放资源,在变量作用域结束的时候,编译器会自动帮我们加上对析构函数的调用,我们使用这样的对象时,就不需要手动释放资源,从而实现了资源的自动释放。
RAII与GC最大的不同在于,RAII将资源托管给创建堆内存的指针对象本身来管理,并保证资源在其生命周期内始终有效,一旦生命周期终止,资源马上会被回收。
二、Rust的内存安全
内存不安全的例子
- 空指针
解引用空指针是不安全的。这块地址空间一般是受保护的,对空指针解引用在大部分平台上会产生segfaul。
- 野指针
野指针指的是未初始化的指针。它的值取决于这个位置以前遗留下来的是什么值。所以它可能指向任意一个地方。对它解引用,可能会造成degfault,也可能不会,纯粹看运气。但无论如何,这个行为都不会是你预期内的行为,是一定定会产生bug的。
- 悬空指针
悬空指针指的是内存空间在被释放了之后,继续使用。它跟野指针类似,同样会读写已经不属于这个指针的内容。
- 使用末初始化内存
不只是指针类型,任何一种类型不初始化就直接使用都是危险的,造成的后果我们无法预测。
- 非法释放内存
分配和释放要配对。如果对同一个指针释放两次,会制造出内存错误。如果指针并不是内存分配器返回的值,对其执行释放操作,也是危险的。
- 缓冲区溢出
指针访问越界了,结果也是类似于野指针,会读取或者修改临近内存空间的值,造成危险。
Rust是如何解决内存安全问题的?
- 使用末定义内存
Rust中的变量必须初始化以后才可使用,否则无法通过编译器检查。
- 空指针
开发者没有任何办法去创建一个空指针。Rust中使用Option类型来代替空指针,Option实际是枚举体,包含两个值:Some(T) 和 None,分别代表两种情况,有和无。这就迫使开发者必须对这两种情况都做处理,以保证内存安全。
- 悬空指针
悬空指针指的是内存空间在被释放了之后,继续使用。Rust通过所有权和借用机制解决这个问题。
- 缓冲区溢出
Rust编译器在编译期就能检查出数据越界的问题,从而完美地避免了缓冲区溢出。
- 非法释放末分配的指针或已经释放过的指针
Rust中不会出现未分配的指针,所以也不存在非法释放的情况。同时,Rust的所有权机制严格地保证了析构函数只会调用一次,所以也不会出现非法释放已释放内存的情况。
三、所有权系统