亮网络解锁器,解锁网络数据的无限可能 了解详情
写点什么

发现 Rust:从项目化角度谈效率之变(一)

  • 2024-01-18
    北京
  • 本文字数:5423 字

    阅读完需:约 18 分钟

大小:2.67M时长:15:31
发现Rust:从项目化角度谈效率之变(一)

1、前言


Rust 作为一种安全的系统语言,将语言层面的语义约束与编译器自动化推导深度结合,实现了更加严谨的编程风格和更加安全的编程方式。从 Linux 6.1 内核正式合入 Rust 支持开始,它与 Linux 内核的深度融合就再也不是悬念,未来的发展充满想象。


这种技术趋势的影响毫无疑问将传递到开发者、项目管理者。对于开发者来说,编写一个 Hello world 程序就足以开启 Rust 编程体验之旅。但对于项目管理者来说,是否启动基于 Rust 的项目?什么时候启动?则是比较复杂的问题。


更加深入的问题还有,我们为什么要采用 Rust 来开发项目?毫无疑问,安全性是主要因素。但它真的能给我们带来更好的安全性吗?近期出现的流行语言都是在做减法,但是 Rust 增加了很多陌生的语言要素,会不会门槛太高导致项目推进困难?诸如此类问题,会层出不穷困扰着决策者。


很多事实告诉我们,没有实践作为依据,没有审慎的分析评判,要做出一个明智的决策是非常困难的,在风险与竞争优势之间,决策者陷入两难。


本文试图通过一次完整真实的项目实践,来为上述问题的解答提供一些依据。为了使问题更加聚焦,我们选择从一个全新的项目入手。至于对现有的项目进行迁移,我们后续也将尝试探索和分享。

2、实践


Rust 与 Linux 的结合还在发展中,对于新手来说,选择从应用程序入手,再到内核模块,再到内核,是一个循序渐进的过程。

 

我们的项目是一个 FUSE 文件系统的性能优化项目,涉及到 FUSE 用户态文件系统和 eBPF 动态编程技术(后文称 BPFuse 项目)。我们需要使用 eBPF 动态程序技术优化 FUSE 文件系统的处理流程,以便达成保证文件系统操作正确性,同时稳定、兼容的情况下,提升文件访问性能的目的。


我们还需要跨越多个内核版本,提供一致的功能、性能特性。而在这个版本跨度中,eBPF 自身从指令到基础设施的发展变化很大,因此我们还需要解决 eBPF 指令兼容性等问题。


我们还希望形成长期可持续的技术成果,为垂直虚拟文件系统的开发提供更强的能力支撑和效率提升。我们希望将 eBPF 的开发和业务逻辑的开发融合在一起。我们也希望利用数据与指令同态的特性,使之成为融合开发模式的媒介。


所以,这就形成了我们这个项目的基本技术形态,在内核部分我们对 FUSE 进行了扩展,使之具备 eBPF 动态扩展能力。同时在用户态,实现了一个文件领域的 Unilang 前端解释器(后文称解释器子项目,或解释器项目,或本项目。是本文的主要叙述对象)。向用户提供初步的将业务逻辑开发与 eBPF 开发融合的开发模式。以面向文件系统领域的 Unilang 衍生语言(子语言)作为 eBPF 的前端表达,下层是 Rust 实现的一个 Unilang 解释器,用于生成 eBPF 程序。最后是执行器,将 eBPF 程序加载到内核中,与内核部分进行交互并提供服务。



考虑到这是一个与内核紧密关联的项目,为了尽力提升项目的安全性,解释器部分我们采用了 Rust 语言进行开发。事实证明,这个选择为我们省却了很多麻烦,并且使我们对这个项目充满了信心。

2.1.快速迭代


在解释器项目中,前端是一个 Unilang 的词法、语法分析器,后端是一个通用的语义模块和具体的语义实现,目前实现 VFS 语义,后续可能增加其他语义。因此这是一个具有一定通用性但更多的是面向文件系统这个专用领域的 Unilang 前端解释器。为了快速迭代,本项目并没有引入过于复杂的东西,在设计上满足主要需求同时考虑一定的扩展性即可。因此整体结构并不复杂,是一个树形的模块结构,需要将大致三层的模块集成在一起,同时允许后续模块的扩展。


我们希望能够通过紧密的逻辑关系来划分和组织模块,同时又希望能够在需要拆分、扩展模块时能够灵活、简洁地调整模块组织。我们希望能够让模块间的耦合关系清晰、明确、最小化,同时又希望模块间能够简单方便地引用。我们首先接触到的是 Rust 的模块化开发机制。


Rust 提供了现代的模块化开发的机制,目录结构+说明文件/关键词形成模块的树形结构,程序中通过 use 及层次路径进行引用。


Rust 的模块化机制使用方便,可以方便地定义软件的整体结构,同时也最大化地降低在开发过程中重构代码和改进软件结构带来的额外工作量,非常有利于快速迭代的开发方式。


我们的项目采用的就是快速迭代的开发方式,开发过程中,进行过三两次代码重构和模块拆分。最开始,我们实现的是一个 Unliang 的词、语法解析器,输出的是符号流,是一个简单的 Solo 的转换程序。


待解析功能稳定后,我们进行了第一次模块拆分,同时对代码进行了重构。整个软件分成了最上层的壳和下面的前、后端两个模块。之后我们专注于后端模块部分的开发,生成 eBPF 指令。我们主要参考 V5 的 eBPF 指令集,对指令生成规则进行了封装,将前后端衔接在一起,成功生成第一个由 Unilang 编写的 eBPF 程序。


之后,我们又进行了几次重构,将后端模块进行更精细的拆分,把语义部分拆分出来,以便支持 VFS 之外的语义,为扩展功能做准备。对指令生成部分进行模块拆分,以便同时支持 V4/V5 的 eBPF 指令集。


最后,我们对上层的壳也做了拆分,增加了一层 FFI 封装,对外提供 C 语言风格的动态链接库,方便 C/C++语言程序调用我们的解释器库。


整个过程中,功能的不断添加、模块拆分、代码重构等都进行地非常流畅,开发体验非常不错。这主要得益于 Rust 的完善、灵活的模块化机制,在语言层面设计了 Crates 和 Modules 两级组织机制。在外围,又通过工具扩展了 Packages 和 Workspaces 两级组织机制。对于不同规模的项目都提供了恰到好处的支持。


既提供了适用于局部模块的声明的灵活性,以目录结构为基础,结合链式路径表达,比较符合开发人员的直觉习惯。同时通过 Toml 语言引入外部模块,通过 cargo 方便地进行外部模块的添加、调整。统一的全局资源 crates.io 也提供了全局的库,强大的搜索功能、发布功能,可以方便地找到需要的库,引用也非常简单。

 

Rust 的模块化系统包含了三个层面的机制:文件管理机制、路径管理机制、命名空间机制。文件管理机制,以文件系统为基础,比较符合用户使用习惯;路径管理机制,在文件系统的基础上,通过节点描述文件,将各层级文件串联起来,形成层次化的组织结构。命名空间管理通过 use、pub 等关键词,管理命名空间的合并、元素的剔除等等。

 

Rust 的私有性控制,可以细化到任意结构体元素,说明 Rust 在这方面的表达能力是非常强的,其次 Rust 提供了很多语法糖,在灵活的表达能力基础上,以后可能也会提供更加简洁的复合表达方式,来提升描述复杂结构化私有性布局的效率。

 

Rust 的命名空间管理非常灵活,可以通过 use、pub 关键词实现命名空间最精微的控制,可以实现库实现者与库使用者两个完全不同的视角分别适用不同的命名空间,使库的实现者在内部实现中最大化发挥自己的个性,自由地设计模块的细节,同时在库向外暴露的视图中,可以最大限度地按照使用者的理解方式组织名字空间中的各个要素。通过这种强大的表达能力,充分减少了对于开发者的约束,使开发者更容易上手,更自由地发挥。

2.2.轻量建模


面向对象是一个很好的建模工具,包括 Linux 内核中的代码,也充分应用了面向对象的编程方法,这使得内核代码更易读、更易维护。但是面向对象编程经过数十年发展,已经走向了一个极端,变得非常重。多层抽象、类的嵌套,增加了设计难度,失去了过程编程的直接,并不适合轻量级、快速迭代的开发方式。

 

Rust 语言的抽象工具,主要是 trait,这相当于一个“特性”的集合。提供了一种更加轻量化的语言特性,特别适合快速迭代的开发方式。所谓“特性”(traits),这种抽象方式介于过程式编程和面向对象编程两种模式之间。当一个数据类型具备某一个 trait 的时候,意味着我们可以对这个数据类型的实例使用 trait 中定义的“方法”进行操作。


显然,Rust 摈弃了常见面向对象语言中最关键的类与对象的概念,类与对象是比较重的抽象工具,编译会插入很多相关的代码来维护类与对象,在应用开发领域,这可能是优点,是很多高级特性的基础,但在系统编程领域这些就是累赘。这种基础设计理念的差异,是 Rust 有别于一般面向对象语言的关键特征,也是其成为系统级语言的基础之一,也是它相对于其他面向对象的系统语言、准系统语言的优势之一。


Rust 不是面向对象的,但它也提供了不弱于面向对象编程的抽象能力,它是面向“特性”编程的。从语言层面,面向特性和面向对象,具有同等的抽象能力,却消除了面向对象的运行时开销,从设计层面再到运行层面都更加轻量级和灵活。同时这种设计将 Rust 语言的内涵和优势集中在了编译阶段,通过语言的特性结合编译器的能力,在编译阶段解决设计语言的解读、理解和代码生成。强化语言表达并且在编译阶段解决所有编程问题,这是 Rust 的鲜明特色。


本项目目前的技术构型并不复杂,并不需要建立复杂的类系统,我们希望只在几个关键的层级应用抽象,使大模块之间的关系更加清晰,而内部我们希望使用更加简单直接的实现方式。在面向对象的语言中,由于一切都是对象,很容易导致软件设计时过度抽象,反而使开发效率降低,代码难以阅读。


在本项目中,Rust 为我们提供了恰到好处的抽象工具,我们只在大对象之间建立抽象,实现 Analyser、、InstComposite、Execute 三个抽象 trait,用来描述语言分析、代码生成、加载执行的特性,简化了编程的复杂度。

2.3.生态衔接


Rust 提供了全局化的开发模式,其核心是 crates.io 这个 URI 资源。通过将 Rust 发展过程中,众多参与者协作开发出来的大量 crates 映射到 URI 命名空间中,实现了全球开发者分布式开发,资源集中共享。

 

在这种开发模式下,任何一个 Rust 项目都不是孤立,它从创建开始就被全球的 Rust 资源所支持,同时这个项目的成果也可以通过 crates.io 被全球开发者所共享。

 

一个 Rust 项目的开始,首先就是在 crates.io 上进行搜索,查找相关的项目,然后进行继承和组合。随着项目进展,新需求不断导入,也可以方便地把 crates.io 上更多的项目组合进来,以实现更加复杂的功能。在这种全局开发模式下,关联项目之间的更新同步也变得非常简单,大大简化了软件项目的维护过程。

 

除了 Rust 自身生态的迭代能力之外,Rust 也能够很好地引入 C 语言的生态资源,对于建立一个新兴的有张力的多元化生态系统来说这一点尤其重要。Rust 将对于成长性生态的支持和语言的核心特质结合在了一起,Rust 强大的元编程能力,使的它很容易实现对 C 语言库的封装和继承。

 

Rust 还在发展完善的过程中,尤其是原生库的发展更需要时间来积累。目前暂时的情况是,在很多有用的公共算法或者底层功能方面 Rust 原生库还并不完善。因此,在实现实际项目的时候,我们往往需要调用现有的 C 风格的库来凑手,满足上层功能实现的需要。

 

而访问这类 C 实现的库的时候,避免不了要理解库中使用的数据结构,而这些数据结构都是用 C 语言头文件进行定义的。Rust 不能直接引用 C 语言头文件,那么如何理解其中定义的数据结构格式,并实现准确的数据操作呢?

 

如果参照 C 语言头文件,完全用 Rust 重写一份等价的数据结构定义,无异于愚公移山,是非常枯燥乏味且低收益的。而且依赖完全重写来实现两种不同的语言的互操作,这为上下两个相关的项目引入了很重的依赖,这不是很好的继承方式,不利于后期维护。

 

这个时候,元编程可以很好地解决这个问题。C 语言和 Rust 在词法层面基本一致,这使得我们甚至可以通过元编程实现一个略做简化的 C 语言编译器。当然这会非常复杂和难于理解,但是对于更加简单的任务,通过元编程却是可以轻松且优雅地胜任的。比如,只需要少量的修改,就可以理解 C 语言头文件中定义的数据结构,并且不影响两个关联项目后续的独立发展。

 

在 crates.io 上,可以找到很多这样的对 C 语言库进行封装的非原生库,极大地补足了 Rust 未完善的生态领域。如果遇到未封装的 C 语言库,通过 Rust 强大的封装能力,也可以快速地实现封装。这是 Rust 强大语言设计所赋予的能力。

 

本项目不单纯是一个 Unilang 的解释器,同时也是执行器,执行的目的就是将生成的 eBPF 程序加载到 Linux 内核中。因此,执行器中需要调用 Linux 的系统调用 bpf()来完成功能。但是,目前 Rust 还没有对 bpf 系统调用的封装,因此没法像使用 Rust 原生 std 库那样实现对 bpf 系统函数的调用。

 

因此在本项目中,我们需要自己封装相关的数据结构等定义,来完成 bpf 系统调用。bpf 的系统调用,已经非常清晰地定义在 C 语言头文件中,但是我们无法直接引用 C 头文件,同时又不想用 Rust 重写一遍这些定义,所以我们就利用了 Rust 的强大的封装能力,通过宏将 C 定义转换为 Rust 定义。这样就是实现了对 C 数据定义封装的复用,以后相关的 C 定义变更时,Rust 的定义也会自动更新。

 

至于系统调用方面,则可以使用 crates.io 上已经存在的项目 linux-syscall 来实现,使用方式是完全 Rust 化的,可见 Rust 强大的融合能力。

3、结语


对于有疑虑是否使用 Rust 开发项目的经理等管理角色,需要明确的一个概念是,Rust 是安全的语言,开发人员的编码水平和对语言的熟悉程度,并不会影响项目的质量,这一点和 C 语言项目完全不同,经验丰富的 C 开发人员对于软件质量有至关重要的影响。

 

同时,Rust 的语言、工具和开发方式的设计也融合了现代的开发理念,对于不同规模的软件项目,从模型设计、模块组织、迭代更新、全局开发等方方面面都提供了丰富的支持。而且,Rust 正在快速变得强大和完善。


Rust 是安全的编程语言,使用 Rust 会推动整个开发过程的重心发生偏移,软件调试、测试和后期维护的开销大大降低,软件质量显著提升。因此与一般的观念相反,Rust 特别适合快速迭代式的开发方式,因为 Rust 的安全特性会导致整个研发过程更加紧凑高效、甚至缩短,无疑减小了开发迭代的周期。

作者介绍


钟俊,统信软件研发技术专家,专注于内核与编译器技术,长期在通信、云计算、安全、信创等多个行业从事底层软件技术研发及相关工作。

2024-01-18 16:437817

评论

发布
暂无评论

假如孔乙己是程序员

顿晓

学习 程序员 孔乙己

软件开发生产率改进之我见(二)

清水

软件工程 软件开发 技术管理

ARTS week 2

锈蠢刀

Vol.1 Java初探,新手必看!

pyfn2030

编程 新手指南

突破困局

Neco.W

感悟 工作 创业心态

栀子花,我们应该像你一样静静绽放

小天同学

个人感想 感悟 日常思考

Spring Security 两种资源放行策略,千万别用错了!

江南一点雨

Java spring springboot springsecurity

python实现·十大排序算法之计数排序(Counting Sort)

南风以南

Python 排序算法 计数排序

实现元素等高: Flexbox vs. Grid

寇云

CSS css3

点击劫持:无X-Frame-Options头信息(修复)

唯爱

redis过期策略和内存淘汰机制

wjchenge

Android原生人脸识别Camera2+FaceDetector 快速实现人脸跟踪

sar

管理规划篇

姜戈

团队管理 团队组织

联邦学习与推荐系统

博文视点Broadview

人工智能 大数据 学习 推荐系统

多线程与线程安全(实例讲解)

YoungZY

Java 多线程 线程安全

码农远程办公指北

大伟

数据与广告系列三:合约广告与与衍生的第三方广告数据监控

黄崇远@数据虫巢

数据挖掘 互联网 广告 移动互联网

使用<input>标签实现六个格子验证码输入框

AR7

Java vue.js 大前端

你的团队想做出什么成果?

姜戈

团队管理

一致性算法 Raft 简述

架构精进之路

raft 一致性算法

好的软件工程原则

pydata

健身一周年:持续锻炼带来无法想象的改变

Taylor

学习 职业 专注 健身

你的团队是干什么的?

姜戈

团队管理 团队职能

你真的会用Mac中的Finder吗

Winann

macos 效率 App Mac

100天从 Python 小白到大神最良心的学习资源!

JackTian

Python GitHub 学习 Python-100-Days Python-Core-50-Courses

揭秘神经拟态计算:缘何成为AI界新宠?

最新动态

终于,我也到了和Eclipse说再见的时候,难说再见

程序员小跃

Java eclipse IDEA

提升输入效率第一步——切换双拼

dongh11

效率工具 提升效率 生产力 分享 有趣

你为什么“啃不动”你手中的技术书?

图灵社区

Java Python 算法 HTTP R语言

源码分析 | Mybatis接口没有实现类为什么可以执行增删改查

小傅哥

Java 源码分析 小傅哥 mybatis 编程思维

宕机原因千千万,被雷劈了最无奈

田晓旭

发现Rust:从项目化角度谈效率之变(一)_编程语言_钟俊_InfoQ精选文章