毕业论文
您现在的位置: 语言识别 >> 语言识别前景 >> 正文 >> 正文

Rust学习内存安全探秘变量的所有权

来源:语言识别 时间:2024/10/12

作者:京东零售周凯

一.前言

Rust语言由Mozilla开发,最早发布于年9月,是一种高效、可靠的通用高级语言。其高效不仅限于开发效率,它的执行效率也是令人称赞的,是一种少有的兼顾开发效率和执行效率的语言。Rust语言具备如下特性:

?高性能-Rust速度惊人且内存利用率极高。由于没有运行时和垃圾回收,它能够胜任对性能要求特别高的服务,可以在嵌入式设备上运行,还能轻松和其他语言集成。

?可靠性-Rust丰富的类型系统和所有权模型保证了内存安全和线程安全,让您在编译期就能够消除各种各样的错误。

?生产力-Rust拥有出色的文档、友好的编译器和清晰的错误提示信息,还集成了一流的工具——包管理器和构建工具,智能地自动补全和类型检验的多编辑器支持,以及自动格式化代码等等。

Rust最近几年发展非常迅速,广受一线程序员的欢迎,Rust有一个官方维护的模块库(crates.io:RustPackageRegistry),可以通过编译器自带的cargo管理工具方便的引入模块,目前crates.io上面的模块数量已经突破10万个,仍在快速增长,此情此景仿佛过去10年node.js的发展情景再现。

12月11日,LinusTorvalds发布了Linux6.1内核稳定版,并带来一个重磅的新闻,即Linux6.1将包含对Rust语言的原生支持。尽管这一功能仍在构建中,不过这也意味着,在可见的将来,Linux的历史将翻开崭新的一页——除了C之外,开发人员将第一次能够使用另一种语言Rust进行内核开发。

在近几年的讨论中,是否在Linux内核中引入Rust多次成为议题。不过包括Torvalds在内的一众关键人物均对此表示了期待。早在年,AlexGaynor和GeoffreyThomas就曾于LinuxSecuritySummit安全峰会上进行了演讲。他们指出,在Android和Ubuntu中,约有三分之二的内核漏洞被分配到CVE中,这些漏洞都是来自于内存安全问题。原则上,Rust可以通过其typesystem和borrowchecker所提供的更安全的API来完全避免这类错误。简言之,Rust比C更安全。谷歌Android团队的WedsonAlmeidaFilho也曾公开表示:“我们觉得Rust现在已经准备好加入C语言,作为实现内核的实用语言。它可以帮助我们减少特权代码中潜在错误和安全漏洞的数量,同时很好地与核心内核配合并保留其性能特征。”

当前,谷歌在Android中广泛使用Rust。在那里,“目标不是将现有的C/C++转换为Rust,而是随着时间的推移,将新代码的开发转移到内存安全语言”。这一言论也逐渐在实践中得到论证。“随着进入Android的新内存不安全代码的数量减少,内存安全漏洞的数量也在减少。从年到年,相关漏洞占比已从Android总漏洞的76%下降到5%。年,在Android漏洞排行中,内存安全漏洞第一次不再是主因。”

本文将探寻相比于其他语言,Rust是怎样实现内存安全的。Rust针对创建于内存堆上的复杂数据类型,设计了一套独有的内存管理机制,该套机制包含变量的所有权机制、变量的作用域、变量的引用与借用,并专门针对字符串、数组、元组等复杂类型设计了slice类型,下面将具体讲述这些机制与规则。

二.变量的所有权

Rust的核心功能(之一)是所有权(ownership)。虽然该功能很容易解释,但它对语言的其他部分有着深刻的影响。

所有程序都必须管理其运行时使用计算机内存的方式。一些语言中具有垃圾回收机制,在程序运行时有规律地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则,程序都不能编译。在运行时,所有权系统的任何功能都不会减慢程序。

因为所有权对很多程序员来说都是一个新概念,需要一些时间来适应。好消息是随着你对Rust和所有权系统的规则越来越有经验,你就越能自然地编写出安全和高效的代码。持之以恒!

当你理解了所有权,你将有一个坚实的基础来理解那些使Rust独特的功能。在本章中,我们将通过完成一些示例来介绍所有权,这些示例基于一个常用的数据结构:字符串。

栈(Stack)与堆(Heap)在很多语言中,你并不需要经常考虑到栈与堆。不过在像Rust这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的抉择。我们会在本文的稍后部分描述所有权与栈和堆相关的内容,所以这里只是一个用来预热的简要解释。栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈以放入值的顺序存储值并以相反顺序取出值。这也被称作后进先出(lastin,firstout)。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做进栈(pushingontothestack),而移出数据叫做出栈(poppingoffthestack)。栈中的所有数据都必须占用已知且固定的大小。在编译时大小未知或大小可能变化的数据,要改为存储在堆上。堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。内存分配器(memoryallocator)在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针(pointer)。这个过程称作在堆上分配内存(allocatingontheheap),有时简称为“分配”(allocating)。(将数据推入栈中并不被认为是分配)。因为指向放入堆中数据的指针是已知的并且大小是固定的,你可以将该指针存储在栈上,不过当需要实际数据时,必须访问指针。想象一下去餐馆就座吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。入栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。继续类比,假设有一个服务员在餐厅里处理多个桌子的点菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子A听一个菜,接着桌子B听一个菜,然后再桌子A,然后再桌子B这样的流程会更加缓慢。出于同样原因,处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过明白了所有权的主要目的就是为了管理堆数据,能够帮助解释为什么所有权要以这种方式工作。

2.1.所有权规则

首先,让我们看一下所有权的规则。当我们通过举例说明时,请谨记这些规则:

Rust中的每一个值都有一个所有者(owner)。值在任一时刻有且只有一个所有者。当所有者(变量)离开作用域,这个值将被丢弃。

2.2.变量作用域

既然我们已经掌握了基本语法,将不会在之后的例子中包含fnmain(){代码,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个main函数中。这样,例子将显得更加简明,使我们可以

转载请注明:http://www.0431gb208.com/sjsbszl/7765.html