初级版本实现

函数调用转 IPC 初级版本实现 #

/monolithic/funcs/func2ipc.md 中提到,本项目目前的环境是在 rel4-linux-kit 上,也就是说目前有一个基本的多进程程序(以下称呼为多个组件),且组件之间是通过 IPC 通信,现在需要利用某种方式让他们能够自由的选择 IPC 调用或者函数调用,在使用函数调用的时候就将所有的组件合并为一个进程在一个地址空间中运行,如果选择 IPC 调用就让使用 IPC 通信的双方分布在不同的进程中,在不同的地址空间中。

为了达成这个目的,需要对现有的系统进行改造,这将对现有的程序和编译系统进行一定的修改,并引入一些额外的描述文件来描述模块之间的关系。修改的内容如下:

  • 一个描述模块需要的资源和依赖关系的文件
  • 在编译系统中对描述文件进行整合,修改以编译一个或多个程序
  • 修改源代码,将单个部件的进程程序从一个 main.rs 修改为 lib.rs 和 main.rs 共存
  • 修改源代码,模块之间通信的函数尽量在 trait 中,添加一些通道专属的代码
  • 添加 cfg 并修改模块的 Cargo.toml 和 lib.rs,让程序在选择函数调用和 IPC 的时候能够引入正确的内容

添加描述文件 #

目前本程序的所有模块都使用 Rust 进行编写,使用 Rust 能够获得了一系列的好处,包括丰富的库,Rust 的安全性和可靠性,优秀的包管理系统,不输于 C/C++ 的性能。但是 Rust 同样为过去的工作带来了一些麻烦,由于 Rust 的包管理系统存在,所以在使用模块的时候就需要在包管理模块中描述依赖关系,包之间的描述不能动态的修改(在单纯 Rust 的环境下),且 Rust 使用的 Cargo.toml 中并不能详细的描述在微内核 seL4/reL4 上部件所使用的资源,因此需要引入一个单独的文件来描述部件在微内核系统中使用的资源和 Capability,以及如果程序单独执行的时候需要的编译信息。以便于在选择将某些组件合并的时候可以让上层组件继承被依赖的组件的资源信息和 Capability。

本方案在这个描述文件进行类型选择的时候遵从功能单一,尽量简单,可维护性高的原则,由于本方案配置程序只用来描述程序的资源和依赖关系,不需要很复杂的层级关系,所以在三种预备的配置文件类型(toml、yaml、json)中选择了 toml,且 toml 对于已经使用了 Rust 的 Cargo.toml 系统来说,并不算一个需要花很多时间去了解的负担。

系统的配置文件是在根目录中的一个名为 apps.toml 的文件,在文件中使用 [[tasks]] 来声明一个新的组件。组件使用多个字段来描述信息:

  • name 模块的名称,在单独存在的时候会用来让 root-task 标识
  • file 模块的可执行文件,当模块单独存在的时候,root-task 引入这个文件。这个文件需要在 target 目录下
  • mem 组件需要的设备内存,当组件独立存在的时候会被映射到当前地址空间,模块被合并的时候会归入最后的顶层模块中,格式 [被映射到的地址,设备地址,大小]
  • dma 模块需要的内存,同 mem,可以被继承,格式 [被映射的地址,大小],dma 地址可以为任意可用内存,只需要保证连续即可
  • deps 模块的依赖关系,当前模块依赖哪些模块,当模块没有选择独立的时候可以继承相应的资源
  • cfg 模块被选中作为独立组建的时候,会为 rust 创建一个单独的 cfg,可以在源代码中使用 #[cfg(xxx)] 进行判断,类似 feature 的形式

下面是一个简单的案例:

[[tasks]]
name = "block-thread"
file = "blk-thread"
mem = [["VIRTIO_MMIO_VIRT_ADDR", "VIRTIO_MMIO_ADDR", "0x1000"]]
dma = [["DMA_ADDR_START", "0x2000"]]
cfg = ["blk_ipc"]

[[tasks]]
deps = ["block-thread"]
name = "fs-thread"
file = "lwext4-thread"
cfg = ["fs_ipc"]

在编译系统中对描述文件进行整合,以编译一个或多个程序 #

在 rel4-linux-kit 中使用的编译系统以 Makefile 为主,利用 Makefile 调用 Cargo 的 build 并传递配置参数信息的方式进行,为了快速完成上述描述文件的解析以及根据配置文件生成 Rust 源代码和针对 Cargo 的配置信息,采用 python 程序在 Makefile 之前运行,生成一个 autoconfig.rsautoconfig.mk 文件。Python 具有很多库的良好支持,本方案主要使用了 tomllib 库来解析描述文件,并利用 Python 提供的库生成对应的文件。由于本方案需要和生成的信息比较简单,所以不需要文本模板引擎,可以直接使用文本拼接实现。

本方案的描述文件中描述了程序的组件列表、组件的资源信息以及组件之间的依赖关系,所以解析描述文件的程序就需要实现以下功能:

  1. 根据传递的参数选择需要哪些模块,以及哪些模块是独立的
  2. 在默认的情况下使用函数调用,没有独立的组件需要由上层组件继承资源
  3. 确保在的描述文件中没有环的存在
  4. 自动的判断哪个模块需要独立,由于本方案基于 reL4/seL4 进行,当程序涉及到中断和资源的时候,且两个程序都依赖了一个模块,那么这个模块就需要独立出来。

解析程序需要完成的功能其实就是一个比较简单的图解析算法,将模块之间的依赖关系理解为一个不应该存在环的图,被选择到的模块和他依赖到的模块的入度会 +1,对于手动指定的模块,为了让它们和其他的组件进行区分,需要在程序中手动的为其进行 +1 处理,如果在对一个模块的依赖进行 +1 处理的时候发现其已经独立了,那么 +1 操作不会再对这个已经独立的模块的依赖进行处理。最后会统计入度大于 1 的模块的数量,并将其作为单独的进程运行,将其依赖的资源转移到这个组模块上。所有入度 >= 1 的模块就是在系统中使用的模块。

下面用一个简单的案例来描述模块模块的独立和资源的继承:现有三个模块 A,B,C,且 A 和 B 都依赖 C。A,B 互不依赖。

  • 当仅选择 A 的时候,A,C 就会绑定在一起编译,A 中继承了 C 的资源和信息
  • 当同时选择 A,C 的时候,这个时候就认为 A, C 独立运行,之间通过 IPC 进行通信。
  • 如果选择 A, B 那么由于 A, B 都依赖 C,且 C 的资源无法复制,那么就会产生三个独立的进程 A, B, C

由于描述文件是 Cargo.toml 中依赖信息的补充,所以会尽量与 Cargo.toml 中提供的依赖信息进行对齐。Cargo.toml 要求程序不能存在循环依赖,也就是不能存在环,所以默认现有的描述文件中并不会有环的存在。

在完成整个系统的构建后,将生成资源信息写入 root-task 目录下的 autoconfig.rs 中,这个文件最后会在 config.rs 中被包含,在运行时使用;将配置信息(需要编写哪些单独的进程,需要哪些 cfg 信息)写入 autoconfig.mk 文件中,在 Makefile 中引入,并在编译的时候使用。

下面是完成后的解析程序在整个系统中的使用方式:

# 模块列表使用空格作为分割进行区分,所以尽量不要在模块名称中存在空格
tools/app-parser.py 模块列表

现有的实现代码在: https://github.com/reL4team2/rel4-linux-kit/blob/macros-generate-ipc/tools/app-parser.py

在源代码中让 lib.rsmain.rs 共存 #

本系统在添加函数调用转 IPC 这一套功能之前,组件作为单独的进程运行,由于 Rust 要求单独运行的程序,必须有一个唯一的上层模块声明 main.rs 文件,所以本系统先前实现的程序都是仅声明一个 main.rs,包含模块信息、服务函数和调用逻辑。而仅包含 main.rs 的顶层模块无法自由的作为 Rust 库在 Cargo.toml 中被引用,因此需要对于模块进行一定的修改,以确保模块能既能作为 lib 被依赖,又能单独编译为独立的 elf 文件区运行。让程序同时兼容 lib 模式和独立运行模式,并非是将 main.rs 重命名为 lib.rs 那么简单,而是需要对模块进行拆分,将仅被动提供功能的函数和信息放在 lib.rs 中,将主动调用的逻辑放在 main.rs 中,在main.rs 中使用 extern crate [当前模块名称] 的方式去声明当前的 lib, 之后就可以像引入其他依赖中的部件一样使用 use 去引用。 https://users.rust-lang.org/t/main-rs-and-lib-rs-at-same-level/42499

在分离后在 main.rs 文件中描述了程序的处理逻辑,包括 IPC 消息的接收,中间信息的处理等。当程序编译为单独的逻辑时程序就会包含 IPC 部分的功能。此时模块之间的通信就会使用 IPC 进行发送和解析。