c 多线程map_Rust:一个不再有 C/C++ 的,实现安全实时软件的未来

ee784fbc77a591f401a1b7fd3cf451e6.gif

作者丨lochsh

译者丨马可薇

策划丨王文婧

Rust 作为新兴编程语言深受 Haskell 和 OCaml 等函数式编程语言的影响,使得它在语法上与 C++ 类似,但在语义上则完全不同。Rust 是静态类型语言,同时具有完整类型推断,而不是 C++ 的部分类型推断,它在速度上可与 C++ 媲美的同时,也保证了内存安全。 索引的故事

在详细介绍 Rust 之前,我们先举一个例子。想象你是一个为新房子搭建煤气管道的工人,你的老板想要你去地下室把煤气管连到街上的主煤气管道里,然而你下楼时却发现有个小问题,这个房子并没有地下室。所以,现在你要做什么呢?什么都不做,还是异想天开地妄图通过把煤气主管道连到隔壁办公室的空调进气口来解决问题?不管怎么说,当你向老板汇报任务完成时,你或许会在煤气爆炸的土灰中以刑事疏忽罪起诉。

这就是在某些编程语言中会发生的事。在 C 里是数组,C++ 里可能是向量,当程序试图寻找第 -1 个元素时,什么都有可能发生:或许是每次搜索的结果都不同,让你意识不到这里存在问题。这种被称作是未定义的行为,它发生的可能性并不能完全被杜绝,因为底层的硬件操作从本质上来说并不安全,这些操作在其他的编程语言里可能会被编译器警告,但是 C/C++ 并不会。

在无法保证内存安全的情况下,未定义行为极有可能发生。漏洞 HeartBleed,一个著名的 SSL 安全漏洞,就是因为缺少内存安全防护;Stagefright,同样出名的安卓漏洞,是因为 C++ 里整数溢出造成的未定义行为。

内存安全不止用来提防漏洞,它对应用程序的正确运行和可靠性同样至关重要。可靠性的重要性在于它可以保证程序不会突然崩溃。至于准确性,作者有一个曾经在火箭飞行模拟软件公司工作的朋友,他们发现传递相同的初始化数据,但是使用不同的文件名会导致不同的结果,这是因为有些未初始化的内存被读取,因此模拟器就不同文件名的原因而使用了垃圾数值做基础,可以说他们的这个项目毫无用处。

为什么不用 Python 或 Java 这些可以保障内存安全的语言呢?

Python 和 Java 使用自动垃圾回收来避免内存错误,例如:

  • 释放重引用(Use-After-Free):申请已经被释放的内存。

  • 多次释放(double free):对同一片内存区域释放两次,导致未定义行为。

  • 内存泄漏:内存没有被回收,导致系统可用的内存减少。

自动垃圾收集会作为 JVM 或者 Python 解释器的一部分运行,在程序运行时不断地寻找不再使用的模块,释放他们相对应的内存或者资源。但是这么做的代价很大,垃圾回收不仅速度缓慢还会占用大量内存,而你也永远不会知道下一秒你的程序会不会暂停运行来回收垃圾。

Python 和 Java 的内存安全牺牲了运行速度。C/C++ 的运行速度则是牺牲了内存的安全性。

这种让人无法掌控的垃圾回收让 Python 与 Java 无法应用在实时软件中,因为你必须要保证你的程序可以在一定时间内完成运行。这并不是比拼运行速度,而是保障你的软件在每次运行的时候都可以足够迅速。

当然,C/C++ 如此受欢迎还有其他方面的因素:他们已经存在了足够长的时间来让人们习惯他们了。但是他们同样因为运行速度与运行结果的保障而受到追捧。然而不幸的是,这样的速度是在牺牲内存安全的前提下。更糟糕的是,许多实时软件在保障速度的基础上同样需要注重安全性,例如车辆或者医用机器人中的控制软件。而这些软件用的仍然是这些并不安全的语言。

在很长的一段时间里,二者处于鱼与熊掌不可兼得的状态,要么选择运行速度和不可预知性,要么选择内存安全和可预知性。Rust 则完全颠覆了这一点,这也是它为什么令人激动的原因。

Rust 的设计目标
  • 无需担心数据的并发运算:只要程序中的不同部分可能在不同的时间或者乱序运行,并发就有可能发生。众所周知,数据并发在多线程程序中是一个常见的危险因素,这一点我们稍后再详细描述。

  • 零开销抽象:指编程语言提供的便利与表现力并不会带来额外的负担,也不会降低程序的运行速度。

  • 不需要垃圾回收的内存安全:内存安全和垃圾回收的定义我们已经了解了,接下来我们将详细阐述 Rsut 是如何平衡速度与安全的关系的。

无需垃圾回收就能实现内存安全

Rust 的内存安全保障说简单也很简单,说复杂也是复杂。简单是因为这里只包含了几个非常容易理解的规则。

在 Rust 中,每一个对象有且只有一个所有者(owner),确保任何资源只能有一个绑定。为了避免被限制,在严格的规则下我们可以使用引用。引用在 Rsut 中经常被称作“借用(borrowing)”。

借用规则如下:

  • 任何借用的作用域都能不大于其所有者的。

  • 不能在拥有可变引用的同时拥有不可变引用,但是多个可变引用是可以的。

第一个规则避免了释放重引用的发生,第二个规则排除了数据互斥的可能性。数据互斥会让内存处于未知状态,而它可由这三个行为造成:

  • 两个或更多指针同时访问同一数据。

  • 至少有一个指针被用来写入数据。

  • 没有同步数据访问的机制。

当作者还是嵌入式工程师的时候,堆(heap)还没有出现,于是便在硬件上设置了一个空指针解引用的陷阱,这样一来,很多常见的内存问题就显得不是那么重要了。数据互斥是作者当时最怕的一种 bug;它难以追踪,当你修改了一部分看起来并不重要的代码,或是外部条件发生了微小的改变时,互斥的胜利者也就易位了。Therac-25 事件,就是因为数据互斥使得癌症病人在治疗过程中受到了过量的辐射,因此造成患者死亡或者重伤。

Rust 革新的关键也是它聪明的地方,它可以在编译时强制执行内存安全保障。这些规则对任何接触过数据互斥的人来说都应当不是什么新鲜事。

不安全的 Rust

如作者之前所说,未定义行为发生的可能性是不能完全被清除的,这是由于底层计算机硬件固有的不安全性导致的。Rust 允许在一个存放不安全代码的模块进行不安全操作。C# 和 Ada 应该也有类似禁用安全检查的方案。在进行嵌入式编程操作或者在底层系统编程的时候,就会需要这样的一个块。隔离代码的潜在不安全部分非常有用,这样一来,与内存相关的错误就必定位于这个模块内,而不是整个程序的任意部分。

不安全模块并不会关闭借用检查,用户可以在不安全块中进行解引用裸引针,访问或修改可变静态变量,所有权系统的优点仍然存在。

重温所有权

说起所有权,就不得不提起 C++ 的所有权机制。

C++ 中的所有权在 C++11 发布之后得到了极大的提升,但是它也为向后兼容性问题付出了不小的代价。对于作者来说,C++ 的所有权非常多余,以前简单的值分类被吊打。不管怎么说,对 C++ 这样广泛使用的语言进行大规模优化是一项伟大的成就,但是 Rust 却是将所有权从一开始就当作核心理念进行设计的语言。

C++ 的类型系统不会对对象模型的生命周期进行建模,因此在运行时是无法检查释放后重引用的问题。C++ 的智能指针只是加在旧系统上的一个库,而这个库会以 Rust 中不被允许的方式滥用和误用。

下面是作者在工作中编写的一些经过简化后的代码,代码中存在误用的问题。

#include #include #include std::vector createChecksFromStrings(        std::unique_ptr data,        std::vector<std::string> dataCheckStrs) {    auto createCheck =  &  {        return DataValueCheck(checkStr, std::move(data));    };    std::vector checks;    std::transform(            dataCheckStrs.begin(),            dataCheckStrs.end(),            std::back_inserter(checks),            createCheck);    return checks;}

这段代码的作用是,通过字符串 dataCheckStrs 定义对某些数据的检查,例如一个特定范围内的值,然后再通过解析这个字符串创建一个用于检查对象的向量。

首先创建一个引用捕捉的 lambda 表达式,由 & 标识,这个智能指针(unique_ptr)指向的对象在这个 lambda 内被移动,因此是非法的。

然后用被移动的数据构建的检查填充向量,但问题是它只能完成第一步。unique_ptr 和被指向对象表示一种独自占有的关系,不能被拷贝。所以在 std::transform 的第一个循环之后,unique_ptr 很有可能被清空,官方声明是它会处于一种有效但是未知的状态,但是以作者对 Clang 的经验来看它通常会被清空。

后续使用这个空指针时会导致未定义行为,作者运行之后得到了一个空指针错误,在大多数托管系统的空指针解引用都会报这种错误,因为零内存页面通常会被保留。但当然这种情况并不会百分百发生,这种 bug 在理论上可能会被暂时搁置一段时间,然后等着你的就是程序的突然崩溃。

这里使用 lambda 的方式很大程度上导致了这种危险的发生。编译器在调用时只能看到以一个函数指针,它并不能像标准函数那样检查 lambda。

结合上下文来理解这个 bug 的话,最初使用 shared_ptr 来存储数据,这一部分没有问题。然而我们却错误地将数据存储在了 unique_ptr 里,当我们试图进行更改时就会有问题,它并没有引起注意是因为编译器并没有报错。

这是 C++ 内存安全问题并没有引起重视的真实例子,作者和审核代码的人直到一次测试前都没有注意到这点。不管你有多少年的编程经验,这类 bug 根本躲不开!哪怕是编译器都不能拯救你。这时就需要更好的工具了,不仅仅是为了我们的理智着想,也是为了公众安全,这关乎职业道德。

接下来让我们看一看同样问题在 Rust 中的体现。

在 Rust 中,这种糟糕的 move() 是不会被允许的。

pub fn create_checks_from_strings(        data: Box,        data_check_strs: Vec<String>)    -> Vec{    let create_check = |check_str: &String| DataValueCheck::new(check_str, data);    data_check_strs.iter().map(create_check).collect()}

这是我们第一次看到 Rust 的代码。需要注意的是,默认情况下变量都是不可变的,但可以在变量前加 mut 关键词使其可变,mut 类似于 C/C++ 中的 const 的反义词。

Box 类型则表示我们已经在堆上分配了内存,在这里使用是因为 unique_ptr 同样可以分配到堆。因为 Rust 中每个对象一次有且仅有一个所有者的规则,我们并不需要任何 unique_ptr 类似的东西。接着创建一个闭包,用更高阶的函数 map 转换字符串,类似 C++ 的方式,但并不显得冗长。但当编译的时候还是会报错,下面是错误信息:

error[E0525]: expected a closure that implements the `FnMut` trait, but this closure only implements `FnOnce`   --> bad_move.rs:1:8    |  6 |     let create_check = |check_str: &String| DataValueCheck::new(check_str, data);    |                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^----^    |                        |                                                     |    |                        |                                 closure is `FnOnce` because it moves    |                        |                                 the variable `data` out of its environment    |                        this closure implements `FnOnce`, not `FnMut`  7 |     data_check_strs.iter().map(create_check).collect()    |                            --- the requirement to implement `FnMut` derives from here  error: aborting due to previous error  For more information about this error, try `rustc --explain E0525`.

Rust 社区有一点很棒,它提供给人们的学习资源非常多,也会提供可读性的错误信息,用户甚至可以向编译器询问关于错误的更详细信息,而编译器则会回复一个带有解释的最小示例。

当创建闭包时,由于有且仅有一个所有者的规则,数据是在其内被移动的。接下来编译器推断闭包只能运行一次:没有所有权的原因,多次的运行是非法的。之后 map 函数就会需求一个可以重复调用并且处于可变状态的可调用函数,这就是为什么编译器会失败的原因。

这一段代码显示了 Rust 中类型系统与 C++ 相比有多么强大,同时也体现了在当编译器跟踪对象生命周期时的语言中编程是多么不同。

在示例中的错误信息里提到了特质(trait)。例如:”缺少实现 FnMut 特质的闭包“。特质是一种告诉 Rust 编译器某个特定类型拥有功能的语言特性,特质也是 Rust 多态机制的体现。

多态性

C++ 支持多种形式的多态,作者认为这有助于语言的丰富性。静态多态中有模板、函数和以及操作符重载;动态多态有子类。但这些表达形式也有非常明显的缺点:子类与父类之间的紧密耦合,导致子类过于依赖父类,缺乏独立性;模板则因为其缺乏参数化的特性而导致调试困难。

Rust 中的 trait 则定义了一种指定静态动态接口共享的行为。Trait 类似于其他语言中接口(interface)的功能,但 Rust 中只支持实现(implements)而没有继承(extends)关系,鼓励基于组合的设计而不是实现继承,降低耦合度。

下面来看一个简单又有趣的例子:

trait Rateable {    /// Rate fluff out of 10    /// Ratings above 10 for exceptionally soft bois    fn fluff_rating(&self) -> f32;}struct Alpaca {    days_since_shearing: f32,    age: f32}impl Rateable for Alpaca {    fn fluff_rating(&self) -> f32 {        10.0 * 365.0 / self.days_since_shearing    }}

首先定义一个名为 Rateable 的 trait,然后需要调用函数 fluff_rating 并返回一个浮点数来实现 Rateable。接着就是在 Alpaca 结构体上对 Rateable trait 的实现。下面是使用同样的方法定义 Cat 类型。

enum Coat {    Hairless,    Short,    Medium,    Long}struct Cat {    coat: Coat,    age: f32}impl Rateable for Cat {    fn fluff_rating(&self) -> f32 {        match self.coat {            Coat::Hairless => 0.0,            Coat::Short => 5.0,            Coat::Medium => 7.5,            Coat::Long => 10.0        }    }}

在这段例子中作者使用了 Rust 的另一特性,模式匹配。它与 C 中的 switch 语句用法类似,但在语义上却有很大的区别。switch 块中的 case 只能用来跳转,模式匹配中则要求覆盖全部可能性才能编译成功,但可选的匹配范围和结构则赋予了其灵活性。

下面是这两种类型的实现结合得出的通用函数:

fn pet(boi: T) -> &str {    match boi.fluff_rating() {        0.0...3.5 => "naked alien boi...but precious nonetheless",        3.5...6.5 => "increased floof...increased joy",        6.5...8.5 => "approaching maximum fluff",        _ => "sublime. the softest boi!"}

尖括号中的是类型参数,这一点和 C++ 中相同,但与 C++ 模板的不同之处在于我们可以使函数参数化。“此函数只适用于 Rateable 类型”的说法在 Rust 中是可以的,但在 C++ 中却毫无意义,这带来的后果不仅限于可读性。类型参数上的 trait bound 意味着 Rust 的编译器可以只对函数进行一次类型检查,避免了单独检查每个具体的实现,从而缩短编译时间并简化了编译错误信息。

Trait 也可以动态使用,虽然有的时候是必须的,但是并不推荐,因为会增加运行开销,所以作者在本文中并没有详细提及。Trait 中另一大部分就是它的互通性,例如标准库中的 Display 和 Add trait。实现 add trait 意味着可以重载运算符 +,实现 display trait 则意味着可以格式化输出显示。

Rust 的工具

C/C++ 中并没有用于管理依赖的标准,倒是有不少工具可以提供帮助,但是它们的口碑都不是很好。基础的 Makefiles 用于构建系统非常灵活,但在维护上就是一团垃圾。CMake 减少了维护的负担,但是它的灵活性较弱,又很让人烦恼。

Rust 在这方面就很优秀,Cargo 是唯一 Rust 社区中唯一的可以用来管理包和依赖,同时还可以用来搭建和运行项目。它的地位与 Python 中的 Pipenv 和 Poetry 类似。官方安装包会自带 Cargo,它好用到让人遗憾为什么 C/C++ 中没有类似的工具。

0eafca86e2926676db262d579d1889af.png

我们难道都要转向 Rust 吗?

这个问题没有标准答案,完全取决于用户的应用程序场景,这一点在任何编程语言中都是共通的。Rust 在不同方面都有成功的案例:包括微软的 Azure IoT 项目,Mozilla 也支持 Rust 并将用于部分火狐浏览器中,同样很多人也在使用 Rust。Rust 已经日渐成熟并可以用于生产,但对于某些应用程序来说,它可能还不够成熟或缺乏支持库。

1、嵌入式:在嵌入式的环境中,Rust 的使用体验完全由用户定义用它做什么。Cortex-M 已经资源成熟并可以用于生产了,RISC-V 也有了一个还在发展尚未常熟的工具链。.

x86 和 arm8 架构也发展得不错,其中就有 Raspberry Pi。像是 PIC 和 AVR 这样的老式架构还有些欠缺,但作者认为,对于大多数的新项目来说应该没什么大问题。

交叉编译支持也适用于所有的 LLVM(Low-Level Virtual Machine)的目标,因为 Rust 使用 LLVM 作为其编译器后端。

Rust 在嵌入式中缺少的另一个部分是生产级的 RTOS,在 HAL 的发展也很匮乏。对许多项目来说,这没什么大不了了,但对另一些项目的阻碍依旧存在。在未来几年内,阻碍可能还会继续增加。

2、异步:语言的异步支持还尚在开发阶段,async/await 的语法都还未被确定。

3、互通性:至于与其他语言的互操作性,Rust 有一个 C 的外部函数接口(FFI),无论是 C++ 到 Rust 函数的回调还是将 Rust 对象作为回调,都需要经过这一步。在很多语言中这都是非常普遍的,在这里提到则是因为如果将 Rust 合并到现有的 C++ 项目中会有些麻烦,因为用户需要在 Rust 和 C++ 中添加一个 C 语言层,这毫无疑问会带来很多问题。

写在最后

如果要在工作中从头开始一个项目,那么作者绝对会选择 Rust 编程语言。希望 Rust 可以成为一个更可靠,更安全,也更令人享受的未来编程语言。

原文链接:

https://mcla.ug/blog/rust-a-future-for-real-time-and-safety-critical-software.html

0c640c249a45e631abde285a061e1aca.gif

点个在看少个 bug ?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/270761.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SQLServer中交叉联接的用法介绍

目录 1、交叉联接(cross join)的概念 2、交叉联接的语法格式 3、交叉查询的使用场景 3.1 交叉联接可以查询全部数据 3.2 交叉联接优化查询性能 4、总结 今天给大家介绍SQLServer中交叉联接的用法&#xff0c;希望对大家能有所帮助&#xff01; 1、交叉联接(cross join)的概念 交…

后端技术:SpringBoot配置热加载工具(devtools)笔记

今天给大家介绍IDEA开发工具如何配置devtools热加载工具。1、devtools原理介绍spring-boot-devtools是spring为开发者提供的热加载工具包。主要原理是使用了两个ClassLoader类加载器&#xff0c;一个Classloader加载那些不会改变的类&#xff08;第三方Jar包&#xff09;&#…

服务器不能创建对象教务系统,[转]解决强智教务系统非IE下无法创建对象错误...

最近要弄教务网的模拟登陆&#xff0c;但苦于教务网只兼容IE8以下的浏览器&#xff0c;不能用chrome强大的F12抓包就很烦&#xff0c;然后发现Fly俊大佬弄了一个相当强的插件啊。但是怕Fly俊大佬的博客失效&#xff0c;所以私自留了个档。侵删啊大佬~以下内容均为转载学校教务系…

Java8对List<Integer>的求和

一般都是对对象对某个字段求和&#xff0c;对List<Object>中Object中的某个字段进行求和&#xff0c;就像这样&#xff1a; 如果想要用流对List<Integer>进行求和&#xff0c;如下 long sum receiveTotalPriceList.stream().reduce(Integer::sum).orElse(0);

电脑软件:主流的压缩软件对比,看完你就会选择了

目录 1、WinRAR 2、360压缩 3、7-ZIP 4、Bandizip 5、好压/快压 今天小编给大家介绍一下主流的压缩软件&#xff0c;看完你就会选择了&#xff01; 目前主流的压缩软件主要有WinRAR、360压缩、7-ZIP、Bandzip、好压。现在给大家逐个介绍一下&#xff0c;每款压缩软件的特。 1、…

控制DIV内容滚动的方法,实现不用拖滚动条就可以看到最新消息

三种控制DIV内容滚动的方法&#xff1a; 本人qq群也有许多的技术文档&#xff0c;希望可以为你提供一些帮助(非技术的勿加)。 QQ群&#xff1a; 281442983 (点击链接加入群&#xff1a;http://jq.qq.com/?_wv1027&k29LoD19) 方法一&#xff1a;使用锚标记要滚动到的位置…

电脑软件:推荐10款实用的办公效率神器

目录 1.Everything 搜索神器 2.IDM 下载器 3.Dism 电脑维护工具 4.Captura 录屏神器 5.Hourglass 倒计时软件 6.Ditto剪切板增强工具 7.Snipaste 截图神器 8.ScreenToGif GIF动态图片录制软件 9.QuickLook文档查看工具 10.processlasso 任务管理 1.Everything 搜索神器 Everyth…

数据库:MySQL、SqlServer、Oracle对比

一、MySQL 优点&#xff1a; 软件体积小、速度快、免费开源&#xff1b; 跨平台&#xff1b; 因为是开源数据库&#xff0c;提供的接口支持多种语言连接操作 &#xff1b; MySQL的核心程序是采用完全的多线程编程。并且是轻量级的进程&#xff0c;它可以灵活地为用户提供服务…

软件:分享9款实用电脑软件,值得看一看

目录 01.ArcTime 02.Luminar 4 03.嗨格式数据恢复大师 04. iTools 05.皮皮直连 06.Kodi 07. PhotoZoom 08.AnyDesk 09.射手影音 今天给大家分享9款实用电脑软件&#xff0c;值得看一看。 01.ArcTime 下载地址&#xff1a;https://arctime.org/ 这是一款特别好用的字幕制作软件。…

怎么转化大小写_亚马逊search term被限制,Search Terms只能写一行怎么办?

search term简称ST&#xff0c; 也叫做亚马逊的关键字&#xff0c;由亚马逊为卖家提供的&#xff0c;除了Title标题&#xff0c;Description描述之外的关键字拓展功能&#xff0c;正确的书写5行描述可以提曝光&#xff0c;流量&#xff0c;和转化.关键字建议包含了客户的具体需…

Idea实现WebService实例 转

作者&#xff1a;http://blog.csdn.net/dreamfly88/article/details/52350370 因为工作需要&#xff0c;数据传输部分需要使用webservice实现&#xff0c;经过两天的研究&#xff0c;实现了一个简单的例子&#xff0c;具体方法如下。 首先需要新建一个项目&#xff0c;如图&…

电脑操作系统维护10条实用建议

目录 1、经常进行“磁盘清理”和磁盘碎片整理” 2、移动internet临时文件和临时交换文件 3、开启wondows xp保留的20%带宽 4、删除不必要的系统声音 5、定期清理internet预读文件 6、关于internet explorer 7、优化硬盘参数 8、让系统自动释放系统资源 9、固定自己的DNS和IP 10…

h5 手风琴效果_小程序-实现折叠面板-手风琴效果

背景无论是在小程序还是 h5 网页,折叠菜单,手风琴是一个非常常见的效果,如今也有很多现成的 UI 组件库已经实现了这一效果的,但有时候在写原生小程序时,单单就是一个折叠菜单效果,却要引入整个 UI 库,有点得不偿失以下就自己手动实现一个的实例效果具体实现如下是wxml示例代码&…

oracle数据库连接 ORA-12638:身份证明检索失败

连数据库的时候突然报了一个这个 查找各种办法&#xff0c;发现自己从10g换成了11g&#xff0c;不过这个没有什么关系&#xff0c;跟oracle的安全设置有关系&#xff0c; 首先从开始菜单找到Net Manager 打开&#xff0c;选择本地&#xff0c;概要文件&#xff0c;下拉列表中选…

IntelliJ IDEA 2021.1更新了好多实用功能介绍

目录 1、WSL 2的支持 2、内置的HTML预览器 3、搜索范围的增强 4、增强的Pull Request支持 5、拆分窗口优化 6、JAVA 16的支持 7、更智能的数据检查 IntelliJ IDEA 2021.1 正式版发布了&#xff0c;这个版本最大的更新内容&#xff0c;就是支持WSL 2和JAVA 16了。而且除了支持WS…