函数转 IPC #
背景 #
Rust 介绍 #
Rust 是一种系统编程语言,旨在提供内存安全、并发性和性能。它的设计目标是避免常见的编程错误,如空指针解引用和数据竞争,同时保持高效的性能。
Rust 模块依赖 #
Rust 使用 Cargo 作为包管理器和构建系统。Cargo 允许开发者轻松地管理项目的依赖关系、构建和测试代码。Rust 的模块系统使得代码组织和重用变得更加简单。
Cargo 使用 Cargo.toml
文件来定义项目的元数据和依赖关系。这个文件包含了项目的名称、版本、作者、许可证等信息,以及所需的依赖库和它们的版本。对于一个程序,需要一个 top 模块,top 模块中会包含一个 main.rs
文件来标识。
Rust 将 crate 作为代码管理的单位,每个 crate 都是一个独立的包,可以包含多个模块。模块可以嵌套在其他模块中,从而形成一个层次结构。Rust 的模块系统允许开发者将代码组织成逻辑上相关的部分,从而提高代码的可读性和可维护性。最上层的模块包含 main.rs
文件。
IPC 介绍 #
IPC(Inter-Process Communication)是指在不同进程之间进行通信的机制。它允许不同的进程共享数据和信息,从而实现协同工作。IPC 的常见方式包括管道、消息队列、共享内存和套接字等。
seL4 的 IPC 是一种高效的通信机制,允许不同的线程在同一地址空间中进行通信。seL4 的 IPC 机制基于消息传递,允许线程之间交换数据和信息。seL4 的 IPC 需要 Endpoint 和 Capability 的支持。Endpoint 是 seL4 中用于实现 IPC 的基本构件,它允许线程之间进行消息传递。Capability 是 seL4 中用于控制访问权限的机制,它允许线程访问特定的资源和服务。
整体思路 #
通过改造 crate 的组织结构与初始化机制,将原本直接调用函数的行为转为通过 seL4 的 IPC 机制进行远程调用,同时保持接口调用尽量无感知(transparent)。对参数进行特殊处理,通过生成中间代码的方式链接 本地(在一个 elf 文件中)函数或者 远程(不在一个 elf 文件中,或者直接是两个进程)中的函数,参数通过 IPC 等方式传递,参数将被序列化后传输,在接收的函数中还原,由此实现只填充中间代码就能改变程序的运行形态。
面临的问题 #
如果需要将函数调用转换为 IPC 调用,需要考虑以下问题。
需要唯一的顶层模块 #
旧的设计
新的设计
从 crate 级别来说,cargo 在编译的时候需要一个唯一的顶层模块,且存在唯一一个 main.rs
文件。在目前的 rel4-linux-kit 中以 lwext4-thread 和 blk-thread 为例,这两个 crate 都是顶层模块,且都包含一个 main.rs
文件。我们需要将这两个 crate 中的 main.rs
文件进行合并,形成一个新的顶层模块。需要将 lwext4-thread 作为顶层模块,blk-thread 作为子模块,将 blk-thread 中的 main.rs 文件重命名为 lib.rs
看起来是整个插件需要完成的事情,其实是一个系统性的工程。需要在编程期间遵守一定的编程规范。
- 将程序分为 lib.rs 和 main.rs
- 在 lib.rs 中提供服务的函数
- 在 main.rs 中提供调用逻辑
在遵循一定规范后,整个系统的调用就有迹可循,可以在两个 main.rs 中使用 IPC 调用,在 main.rs 中调用 lib.rs 中直接定义的调用函数。
一些介绍,如何让一个程序同时作为 Lib 和 main 存在。 https://doc.rust-lang.org/book/ch12-03-improving-error-handling-and-modularity.html#splitting-code-into-a-library-crate
合并初始化的代码 #
在 Rust 中,初始化的代码通常在 main.rs
文件中进行。我们需要将 lwext4-thread 和 blk-thread 中的初始化代码进行合并。由于这两个 crate 都是顶层模块,所以它们的初始化代码是独立的。我们需要将它们的初始化代码进行合并,形成一个新的初始化函数。在原来的独立模块中,他们的名字都是 main 函数,如果不进行处理,就会产生冲突。
在可以使用 contructor 函数的机制,让程序天然拥有多个入口,当合并之后,就会合并入口。需要为入口添加关系或者优先级,保证合并的时候能够正常初始化依赖。
从函数调用到 IPC 调用 #
函数调用时直接调用目的函数的地址,直接跳转到目的函数的地址。函数调用的参数传递是通过栈来实现的。函数调用的返回值是通过寄存器来实现的。 而 IPC 调用是通过消息队列来实现的。IPC 调用的参数传递是通过消息队列来实现的。IPC 调用的返回值是通过消息队列来实现的。
那么如何无感的将函数调用转换为 IPC 调用呢?
我们需要将函数分为定义和实现两部分。函数的定义部分是函数的接口,函数的实现部分是函数的具体实现。我们需要将函数的定义部分和实现部分分开,这样就可以在函数调用的时候让函数调用到伪实现中,从而实现将函数进行转发。
参数的传递 #
函数调用的参数传递是通过栈来实现的。函数调用的返回值是通过寄存器来实现的。而 IPC 调用的参数传递是通过 IPCBuffer 或共享内存来实现的。IPC 调用的返回值也是如此。如何将栈上的数据通过符合IPC的方式发送呢?
我们需要将函数的参数进行序列化,然后通过消息队列进行传递。函数的返回值也需要进行序列化,然后通过消息队列进行传递。可以选择的方案是
rust
中的一个zerocopy
库。这个库可以将数据进行序列化和反序列化。我们需要将函数的参数进行序列化,然后通过共享内存传输。
需要额外的设计 #
如果需要将函数调用和 IPC 调用统一起来,我们就需要确保整个系统的设计是一个稳定的,如何为 函数分配一个特定的标签。
需要额外的信息 #
在函数调用的时候是直接进行调用,只需要知道函数的信息就可以,但是在进行 IPC 调用的时候,我们不仅需要知道函数的信息,同样也需要知道 IPC 的通道,在 sel4 之上,我们还需要知道 IPC 的 Endpoint 和 Capability 的信息。我们需要在初始化的时候创建信息,并在传输的时候使用。
我们在对函数进行标记的时候,在函数的标记上同时填入一个数字,表示使用哪个域(Domain),Domain 会在我们附加的模块中定义,这个 Domain 会包含所需要使用到的信息,后续不管什么形式的信息,都可以通过一个数字进行对应。至于这个信息的初始化,则可以通过 Ctor(Contructor) 进行初始化。
通道复用的时候如何处理 #
在 sel4 之上用户态的时候,可能会出现 Endpoint 和 Notification,甚至是多个 Endpoint 复用一个通道,通过 badge 通信,这个时候如何进行区分,如何进行处理是一个问题。
IPC 发送大小的问题 #
对于直接函数调用,可以直接通过传递参数的方式,在一个地址空间共享内存,所以传输内存没有上限,但是对于 IPC 来说,需要考虑单次 IPC 传输上限的问题。如何平衡这两种结构。
可以参考的信息 #
RPC相关 #
RPC 框架可以提供一些借鉴,
https://github.com/google/tarpc/blob/master/plugins/src/lib.rs
tarpc 中提出了一种方式将参数和结果都序列化 后包裹起来。这部分代码也在 ByteOS-Microkernel
中也使用过这种方法,但是对于一些特殊的类型比如指针之类的需要单独处理,且只能在 Rust 之间流动。
我们需要做的东西与 RPC 是什么关系呢?以及一些可以参考和辩证的东西?
- RPC 主要是通过统一一个路径进行分发,我们需要做的可能有多重路径进行分发。
- RPC 可以通过字符串来判断函数的路径,但是在操作系统中使用字符串来代表需要调用的函数比较慢,所以需要考虑将其转化为数字标签快速判断。
- RPC 或函数调用都只需要考虑一种情况,我们需要考虑在一种环境下将其全部囊括起来,并且能够自由扩展和转换,需要考虑各种问题。
技术路线 #
1. 编写程序将从函数调用的 signature 中生成代码 #
上述的代码我们需要一个工具来实现从函数的定义 signature 中生成特定的代码,将参数和返回结果转换为数据流进行传输。为了方便构建这个模块,我们将这个模块放在一个 crate 中,当函数调用就是直接调用,当是 IPC 调用的时候就是转换为流,然后交给一个特定函数去传输,暂时不考虑复杂环境和 Future 作为参数和返回值。
这种流的转换无法处理包含指针在内的复杂环境,且无法将 async 函数包含在内(Rust 限制 extern “Rust” 不能使用 async 标签)。