Porting Rust STD
Table of Contents
1. Porting Rust 标准库
1.1. overview
rust 标准库包含 core, alloc, std, 当使用 no_std
时, 只会使用 core, alloc.
core, alloc 是平台无关的, 基本不依赖 libc, 除了 memcpy, memmove, memset, memcmp,
bcmp, strlen 这几个符号. 为了解决这个问题, rust 的 compiler_builtins
包含了这几个符号的实现
core 和 alloc 包含了 vec, string, fmt, ffi, box, rc, option, result 以及
collections 中的 binary_heap
, btree_map
等, 足够开发 bare_metal
的程序. 其中涉及到动态内存分配的功能例如 vec 依赖 app 自己定义的 allocator, 避免了对 libc malloc
的依赖.
linux 下的 rust std 大量依赖 libc.a, 以编译一个空的 rust executable 为例, 输出的 linker map 中引用 lib.a 接近 3000 次. 但这并不表示 libc 之上的 rust std 是平台无关的, rust std 中也有一些平台相关的代码, 例如它的 Platform Abstraction Layer
1.2. 定制 rust toolchain
porting rust 标准库的最终目的是提供一个 toolchain, 包含定制的 libc 以及定制的 rust 标准库
1.2.1. 定制 libc
使用 aarch64_unknown_linux_gnu
target 时, 默认会使用系统自带的
/lib/gcc-cross/aarch64-linux-gnu/
下的 libc, crt{xx}.o, libgcc. 但使用
aarch64_unknown_linux_musl
target 时, 默认会使用 rust toolchain 下
self-contained 里放置的 crt{xx}.o 和 libc.a, 通过使用自定义的基于
aarch64_unknown_linux_musl
的 target, 可以方便的定制自己使用的 libc
1.2.2. 定制 std
rust std 中有一个 PAL (Platform Abstraction Layer) 以及散布在代码各处的 cfg 宏, 会针对 os, feature 等进行条件编译, 只替换 libc 并不能完全定制 std, 例如:
cfg_if::cfg_if! { if #[cfg(any( all(target_os = "windows", not(target_vendor = "win7")), target_os = "linux", target_os = "android", all(target_arch = "wasm32", target_feature = "atomics"), target_os = "freebsd", target_os = "openbsd", target_os = "fuchsia", ))] { mod futex; pub use futex::Parker; // .... } else if #[cfg(target_family = "unix")] { mod pthread; pub use pthread::Parker; } else { mod unsupported; pub use unsupported::Parker; } }
针对 linux 会使用 futex 实现 thread parking, 针对 unix 才会使用 pthread, 因此需要针对 cfg 加一些定制, 例如让 std 里的大部分组件的实现和 linux 一致, 但 thread parking 使用 pthread 实现
通过 cargo 的 -Zbuild-std 可以让它每次都重新编译 rust toolchain 中的 rust std 源码, 而不使用预编译的 std, 例如:
$> cat hello/.cargo/cargo.toml [build] target = "<path_to>/taishan_rust_toolchain/aarch64-unknonw-taishan-musl.json" [unstable] build-std = ["std"]
因此定制一个 rust toolchain 需要:
- 包含修改过的 libc.a
- 包含修改过的 rust std 源码
- 通过 aarch64-unknonw-taishan-musl.json 定制需要的条件编译的配置
1.3. platform 相关接口
有一个简单的方法可以快速识别哪些接口是 platform 相关的: 查找 unsupported 关键字, 因为 unsupported 是 platform 提供的默认的空实现
rust/library/std/src/sys/sync/thread_parking/unsupported.rs rust/library/std/src/sys/anonymous_pipe/unsupported.rs rust/library/std/src/sys/pal/unix/process/process_unsupported.rs rust/library/std/src/sys/pal/unix/process/process_unsupported/wait_status.rs rust/library/std/src/sys/pal/unsupported/args.rs rust/library/std/src/sys/pal/unsupported/common.rs rust/library/std/src/sys/pal/unsupported/env.rs rust/library/std/src/sys/pal/unsupported/fs.rs rust/library/std/src/sys/pal/unsupported/io.rs rust/library/std/src/sys/pal/unsupported/mod.rs rust/library/std/src/sys/pal/unsupported/net.rs rust/library/std/src/sys/pal/unsupported/os.rs rust/library/std/src/sys/pal/unsupported/pipe.rs rust/library/std/src/sys/pal/unsupported/process.rs rust/library/std/src/sys/pal/unsupported/stdio.rs rust/library/std/src/sys/pal/unsupported/thread.rs rust/library/std/src/sys/pal/unsupported/time.rs rust/library/std/src/sys/random/unsupported.rs
1.3.1. thread_parking
1.3.1.1. channel
use std::sync::mpsc::channel; use std::thread; use std::time::Duration; fn main() { let (tx, rx) = channel(); thread::spawn(move || { for i in 1..20 { tx.send(i).unwrap(); } }); thread::sleep(Duration::from_millis(2000)); for i in 1..20 { let x = rx.recv().unwrap(); println!("recv: {:?}", x); } }
默认的 channel (非 sync_channel
) 主要通过线程间共享的一个 block 链表并配合
thread_parking
来实现的
1.3.1.1.1. block 链表
- block 链表由多个 block 组成, 每个 block 包含 32 个 slot, 每个 slot 保存一个消息
- 维护着
head_block
,tail_block
, 分别指向链表的 head 和 tail head_index
保存着head_block
当前的 index, recv 操作会读取head_block
中head_index
对应的 slot.tail_index
保存着tail_block
当前的 index, send 操作会写tail_block
中tail_index
对应的 slot- 默认的 channel 是容量不限的, 所以随着不断的 send, block 链表会不断增长
- 当
head_block
被 recv 读完以后, 当前head_block
会被释放, 而不是采用 ring buffer 的形式重复使用
1.3.1.1.2. 依赖的同步操作
- atomic
channel 是通过 mpmc (multi-producer multi-consumer) 实现的,
head_{block, index}
会被多个 consumer 访问, 通过 atomic 来保证原子操作, 这个通过 compiler intrinsics 就可以支持 thread::yield_now
当 recv 线程发现当前
head_index
已经达到BLOCK_CAP
(31), 或者 send 线程发现tail_index
达到BLOCK_CAP
时, 会通过thread::yield_now
等待其它 recv (或 send) 线程切换head_block
或tail_block
到新的 block.所有 {send, recv} 线程在
{head, tail}_index
到达BLOCK_CAP-1
时会在{head, tail}_index
这个 atomic 上竞争, 获胜者会负责切换新的{head, tail}_block
并重设 index, 这时之前那些 yield 的线程就可以继续工作了yield_now
在taget_family
为 unix 时其实现依赖于 libc 的sched_yield
thread::park{_timeout}
当 recv 时发现
head_index = tail_index
且head_block = tail_block
时, recv 线程需要通过 thread::park 停下来 (send 线程永远不会 park, 因为默认的 channel 容量是无限的)thread::park 在 linux, android, bsd, fuchsia 上使用
futex_wait
实现, 在其它 unix 上使用 libc 的pthread_cond_{timed}wait
实现- thread::unpark
和 park 类似, 依赖于
futex_wake
或pthread_cond_signal